mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
quickshell and hyprland additions
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
readonly property int rounding: floating ? 0 : Appearance.rounding.normal
|
||||
|
||||
property alias floating: session.floating
|
||||
property alias active: session.active
|
||||
property alias navExpanded: session.navExpanded
|
||||
|
||||
readonly property Session session: Session {
|
||||
id: session
|
||||
|
||||
root: root
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
}
|
||||
|
||||
implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio
|
||||
implicitHeight: screen.height * Config.controlCenter.sizes.heightMult
|
||||
|
||||
GridLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
rowSpacing: 0
|
||||
columnSpacing: 0
|
||||
rows: root.floating ? 2 : 1
|
||||
columns: 2
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 2
|
||||
|
||||
active: root.floating
|
||||
visible: active
|
||||
|
||||
sourceComponent: WindowTitle {
|
||||
screen: root.screen
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillHeight: true
|
||||
|
||||
topLeftRadius: root.rounding
|
||||
bottomLeftRadius: root.rounding
|
||||
implicitWidth: navRail.implicitWidth
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
CustomMouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
// Prevent tab switching during initial opening animation to avoid blank pages
|
||||
if (!panes.initialOpeningComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.angleDelta.y < 0)
|
||||
root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1);
|
||||
else if (event.angleDelta.y > 0)
|
||||
root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
NavRail {
|
||||
id: navRail
|
||||
|
||||
screen: root.screen
|
||||
session: root.session
|
||||
initialOpeningComplete: root.initialOpeningComplete
|
||||
}
|
||||
}
|
||||
|
||||
Panes {
|
||||
id: panes
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
topRightRadius: root.rounding
|
||||
bottomRightRadius: root.rounding
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool initialOpeningComplete: panes.initialOpeningComplete
|
||||
}
|
||||
231
.config/quickshell/caelestia/modules/controlcenter/NavRail.qml
Normal file
231
.config/quickshell/caelestia/modules/controlcenter/NavRail.qml
Normal file
@@ -0,0 +1,231 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.controlcenter
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property Session session
|
||||
required property bool initialOpeningComplete
|
||||
|
||||
implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.padding.larger * 2
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.session.navExpanded
|
||||
|
||||
PropertyChanges {
|
||||
layout.spacing: Appearance.spacing.small
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "spacing"
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
active: !root.session.floating
|
||||
visible: active
|
||||
|
||||
sourceComponent: StyledRect {
|
||||
readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2
|
||||
|
||||
implicitWidth: nonAnimWidth
|
||||
implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth
|
||||
|
||||
color: Colours.palette.m3primaryContainer
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
StateLayer {
|
||||
id: normalWinState
|
||||
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.root.close();
|
||||
WindowFactory.create(null, {
|
||||
active: root.session.active,
|
||||
navExpanded: root.session.navExpanded
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: normalWinIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
|
||||
text: "select_window"
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: normalWinLabel
|
||||
|
||||
anchors.left: normalWinIcon.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.spacing.normal
|
||||
|
||||
text: qsTr("Float window")
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
opacity: root.session.navExpanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: PaneRegistry.count
|
||||
|
||||
NavItem {
|
||||
required property int index
|
||||
Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0
|
||||
icon: PaneRegistry.getByIndex(index).icon
|
||||
label: PaneRegistry.getByIndex(index).label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NavItem: Item {
|
||||
id: item
|
||||
|
||||
required property string icon
|
||||
required property string label
|
||||
readonly property bool active: root.session.active === label
|
||||
|
||||
implicitWidth: background.implicitWidth
|
||||
implicitHeight: background.implicitHeight + smallLabel.implicitHeight + smallLabel.anchors.topMargin
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.session.navExpanded
|
||||
|
||||
PropertyChanges {
|
||||
expandedLabel.opacity: 1
|
||||
smallLabel.opacity: 0
|
||||
background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth
|
||||
background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
item.implicitHeight: background.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: background
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0)
|
||||
|
||||
implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small
|
||||
|
||||
StateLayer {
|
||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
// Prevent tab switching during initial opening animation to avoid blank pages
|
||||
if (!root.initialOpeningComplete) {
|
||||
return;
|
||||
}
|
||||
root.session.active = item.label;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
|
||||
text: item.icon
|
||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: item.active ? 1 : 0
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: expandedLabel
|
||||
|
||||
anchors.left: icon.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.spacing.normal
|
||||
|
||||
opacity: 0
|
||||
text: item.label
|
||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
font.capitalization: Font.Capitalize
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: smallLabel
|
||||
|
||||
anchors.horizontalCenter: icon.horizontalCenter
|
||||
anchors.top: icon.bottom
|
||||
anchors.topMargin: Appearance.spacing.small / 2
|
||||
|
||||
text: item.label
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.capitalization: Font.Capitalize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property list<QtObject> panes: [
|
||||
QtObject {
|
||||
readonly property string id: "network"
|
||||
readonly property string label: "network"
|
||||
readonly property string icon: "router"
|
||||
readonly property string component: "network/NetworkingPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "bluetooth"
|
||||
readonly property string label: "bluetooth"
|
||||
readonly property string icon: "settings_bluetooth"
|
||||
readonly property string component: "bluetooth/BtPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "audio"
|
||||
readonly property string label: "audio"
|
||||
readonly property string icon: "volume_up"
|
||||
readonly property string component: "audio/AudioPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "appearance"
|
||||
readonly property string label: "appearance"
|
||||
readonly property string icon: "palette"
|
||||
readonly property string component: "appearance/AppearancePane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "taskbar"
|
||||
readonly property string label: "taskbar"
|
||||
readonly property string icon: "task_alt"
|
||||
readonly property string component: "taskbar/TaskbarPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "launcher"
|
||||
readonly property string label: "launcher"
|
||||
readonly property string icon: "apps"
|
||||
readonly property string component: "launcher/LauncherPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "dashboard"
|
||||
readonly property string label: "dashboard"
|
||||
readonly property string icon: "dashboard"
|
||||
readonly property string component: "dashboard/DashboardPane.qml"
|
||||
}
|
||||
]
|
||||
|
||||
readonly property int count: panes.length
|
||||
|
||||
readonly property var labels: {
|
||||
const result = [];
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
result.push(panes[i].label);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getByIndex(index: int): QtObject {
|
||||
if (index >= 0 && index < panes.length) {
|
||||
return panes[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getIndexByLabel(label: string): int {
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
if (panes[i].label === label) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function getByLabel(label: string): QtObject {
|
||||
const index = getIndexByLabel(label);
|
||||
return getByIndex(index);
|
||||
}
|
||||
|
||||
function getById(id: string): QtObject {
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
if (panes[i].id === id) {
|
||||
return panes[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
175
.config/quickshell/caelestia/modules/controlcenter/Panes.qml
Normal file
175
.config/quickshell/caelestia/modules/controlcenter/Panes.qml
Normal file
@@ -0,0 +1,175 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "bluetooth"
|
||||
import "network"
|
||||
import "audio"
|
||||
import "appearance"
|
||||
import "taskbar"
|
||||
import "launcher"
|
||||
import "dashboard"
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.controlcenter
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ClippingRectangle {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
readonly property bool initialOpeningComplete: layout.initialOpeningComplete
|
||||
|
||||
color: "transparent"
|
||||
clip: true
|
||||
focus: false
|
||||
activeFocusOnTab: false
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onPressed: function (mouse) {
|
||||
root.focus = true;
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session
|
||||
|
||||
function onActiveIndexChanged(): void {
|
||||
root.focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
spacing: 0
|
||||
y: -root.session.activeIndex * root.height
|
||||
clip: true
|
||||
|
||||
property bool animationComplete: true
|
||||
property bool initialOpeningComplete: false
|
||||
|
||||
Timer {
|
||||
id: animationDelayTimer
|
||||
interval: Appearance.anim.durations.normal
|
||||
onTriggered: {
|
||||
layout.animationComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: initialOpeningTimer
|
||||
interval: Appearance.anim.durations.large
|
||||
running: true
|
||||
onTriggered: {
|
||||
layout.initialOpeningComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: PaneRegistry.count
|
||||
|
||||
Pane {
|
||||
required property int index
|
||||
paneIndex: index
|
||||
componentPath: PaneRegistry.getByIndex(index).component
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session
|
||||
function onActiveIndexChanged(): void {
|
||||
layout.animationComplete = false;
|
||||
animationDelayTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Pane: Item {
|
||||
id: pane
|
||||
|
||||
required property int paneIndex
|
||||
required property string componentPath
|
||||
|
||||
implicitWidth: root.width
|
||||
implicitHeight: root.height
|
||||
|
||||
property bool hasBeenLoaded: false
|
||||
|
||||
function updateActive(): void {
|
||||
const diff = Math.abs(root.session.activeIndex - pane.paneIndex);
|
||||
const isActivePane = diff === 0;
|
||||
let shouldBeActive = false;
|
||||
|
||||
if (!layout.initialOpeningComplete) {
|
||||
shouldBeActive = isActivePane;
|
||||
} else {
|
||||
if (diff <= 1) {
|
||||
shouldBeActive = true;
|
||||
} else if (pane.hasBeenLoaded) {
|
||||
shouldBeActive = true;
|
||||
} else {
|
||||
shouldBeActive = layout.animationComplete;
|
||||
}
|
||||
}
|
||||
|
||||
loader.active = shouldBeActive;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(pane.updateActive);
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && !pane.hasBeenLoaded) {
|
||||
pane.hasBeenLoaded = true;
|
||||
}
|
||||
|
||||
if (active && !item) {
|
||||
loader.setSource(pane.componentPath, {
|
||||
"session": root.session
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onItemChanged: {
|
||||
if (item) {
|
||||
pane.hasBeenLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session
|
||||
function onActiveIndexChanged(): void {
|
||||
pane.updateActive();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: layout
|
||||
function onInitialOpeningCompleteChanged(): void {
|
||||
pane.updateActive();
|
||||
}
|
||||
function onAnimationCompleteChanged(): void {
|
||||
pane.updateActive();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import "./state"
|
||||
import qs.modules.controlcenter
|
||||
|
||||
QtObject {
|
||||
readonly property list<string> panes: PaneRegistry.labels
|
||||
|
||||
required property var root
|
||||
property bool floating: false
|
||||
property string active: "network"
|
||||
property int activeIndex: 0
|
||||
property bool navExpanded: false
|
||||
|
||||
readonly property BluetoothState bt: BluetoothState {}
|
||||
readonly property NetworkState network: NetworkState {}
|
||||
readonly property EthernetState ethernet: EthernetState {}
|
||||
readonly property LauncherState launcher: LauncherState {}
|
||||
readonly property VpnState vpn: VpnState {}
|
||||
|
||||
onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active))
|
||||
onActiveIndexChanged: if (panes[activeIndex])
|
||||
active = panes[activeIndex]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function create(parent: Item, props: var): void {
|
||||
controlCenter.createObject(parent ?? dummy, props);
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: dummy
|
||||
}
|
||||
|
||||
Component {
|
||||
id: controlCenter
|
||||
|
||||
FloatingWindow {
|
||||
id: win
|
||||
|
||||
property alias active: cc.active
|
||||
property alias navExpanded: cc.navExpanded
|
||||
|
||||
color: Colours.tPalette.m3surface
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
destroy();
|
||||
}
|
||||
|
||||
implicitWidth: cc.implicitWidth
|
||||
implicitHeight: cc.implicitHeight
|
||||
|
||||
minimumSize.width: implicitWidth
|
||||
minimumSize.height: implicitHeight
|
||||
maximumSize.width: implicitWidth
|
||||
maximumSize.height: implicitHeight
|
||||
|
||||
title: qsTr("Caelestia Settings - %1").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1))
|
||||
|
||||
ControlCenter {
|
||||
id: cc
|
||||
|
||||
anchors.fill: parent
|
||||
screen: win.screen
|
||||
floating: true
|
||||
|
||||
function close(): void {
|
||||
win.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property Session session
|
||||
|
||||
implicitHeight: text.implicitHeight + Appearance.padding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
StyledText {
|
||||
id: text
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
text: qsTr("Caelestia Settings - %1").arg(root.session.active)
|
||||
font.capitalization: Font.Capitalize
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: closeIcon.implicitHeight + Appearance.padding.small
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
QsWindow.window.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: closeIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "./sections"
|
||||
import "../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.components.images
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia.Models
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1
|
||||
property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded"
|
||||
property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF"
|
||||
property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik"
|
||||
property real fontSizeScale: Config.appearance.font.size.scale ?? 1
|
||||
property real paddingScale: Config.appearance.padding.scale ?? 1
|
||||
property real roundingScale: Config.appearance.rounding.scale ?? 1
|
||||
property real spacingScale: Config.appearance.spacing.scale ?? 1
|
||||
property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false
|
||||
property real transparencyBase: Config.appearance.transparency.base ?? 0.85
|
||||
property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4
|
||||
property real borderRounding: Config.border.rounding ?? 1
|
||||
property real borderThickness: Config.border.thickness ?? 1
|
||||
|
||||
property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false
|
||||
property real desktopClockScale: Config.background.desktopClock.scale ?? 1
|
||||
property string desktopClockPosition: Config.background.desktopClock.position ?? "bottom-right"
|
||||
property bool desktopClockShadowEnabled: Config.background.desktopClock.shadow.enabled ?? true
|
||||
property real desktopClockShadowOpacity: Config.background.desktopClock.shadow.opacity ?? 0.7
|
||||
property real desktopClockShadowBlur: Config.background.desktopClock.shadow.blur ?? 0.4
|
||||
property bool desktopClockBackgroundEnabled: Config.background.desktopClock.background.enabled ?? false
|
||||
property real desktopClockBackgroundOpacity: Config.background.desktopClock.background.opacity ?? 0.7
|
||||
property bool desktopClockBackgroundBlur: Config.background.desktopClock.background.blur ?? false
|
||||
property bool desktopClockInvertColors: Config.background.desktopClock.invertColors ?? false
|
||||
property bool backgroundEnabled: Config.background.enabled ?? true
|
||||
property bool wallpaperEnabled: Config.background.wallpaperEnabled ?? true
|
||||
property bool visualiserEnabled: Config.background.visualiser.enabled ?? false
|
||||
property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true
|
||||
property real visualiserRounding: Config.background.visualiser.rounding ?? 1
|
||||
property real visualiserSpacing: Config.background.visualiser.spacing ?? 1
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function saveConfig() {
|
||||
Config.appearance.anim.durations.scale = root.animDurationsScale;
|
||||
|
||||
Config.appearance.font.family.material = root.fontFamilyMaterial;
|
||||
Config.appearance.font.family.mono = root.fontFamilyMono;
|
||||
Config.appearance.font.family.sans = root.fontFamilySans;
|
||||
Config.appearance.font.size.scale = root.fontSizeScale;
|
||||
|
||||
Config.appearance.padding.scale = root.paddingScale;
|
||||
Config.appearance.rounding.scale = root.roundingScale;
|
||||
Config.appearance.spacing.scale = root.spacingScale;
|
||||
|
||||
Config.appearance.transparency.enabled = root.transparencyEnabled;
|
||||
Config.appearance.transparency.base = root.transparencyBase;
|
||||
Config.appearance.transparency.layers = root.transparencyLayers;
|
||||
|
||||
Config.background.desktopClock.enabled = root.desktopClockEnabled;
|
||||
Config.background.enabled = root.backgroundEnabled;
|
||||
Config.background.desktopClock.scale = root.desktopClockScale;
|
||||
Config.background.desktopClock.position = root.desktopClockPosition;
|
||||
Config.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled;
|
||||
Config.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity;
|
||||
Config.background.desktopClock.shadow.blur = root.desktopClockShadowBlur;
|
||||
Config.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled;
|
||||
Config.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity;
|
||||
Config.background.desktopClock.background.blur = root.desktopClockBackgroundBlur;
|
||||
Config.background.desktopClock.invertColors = root.desktopClockInvertColors;
|
||||
|
||||
Config.background.wallpaperEnabled = root.wallpaperEnabled;
|
||||
|
||||
Config.background.visualiser.enabled = root.visualiserEnabled;
|
||||
Config.background.visualiser.autoHide = root.visualiserAutoHide;
|
||||
Config.background.visualiser.rounding = root.visualiserRounding;
|
||||
Config.background.visualiser.spacing = root.visualiserSpacing;
|
||||
|
||||
Config.border.rounding = root.borderRounding;
|
||||
Config.border.thickness = root.borderThickness;
|
||||
|
||||
Config.save();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appearanceRightContentComponent
|
||||
|
||||
Item {
|
||||
id: rightAppearanceFlickable
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Appearance.spacing.normal
|
||||
text: qsTr("Wallpaper")
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 600
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: wallpaperLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.bottomMargin: -Appearance.padding.large * 2
|
||||
|
||||
active: {
|
||||
const isActive = root.session.activeIndex === 3;
|
||||
const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1;
|
||||
const splitLayout = root.children[0];
|
||||
const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null;
|
||||
const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent);
|
||||
return shouldActivate;
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
console.error("[AppearancePane] Wallpaper loader error!");
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: WallpaperGrid {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SplitPaneLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
|
||||
StyledFlickable {
|
||||
id: sidebarFlickable
|
||||
readonly property var rootPane: root
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: sidebarLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: sidebarFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: sidebarLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
readonly property var rootPane: sidebarFlickable.rootPane
|
||||
|
||||
readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Appearance")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more"
|
||||
type: IconButton.Text
|
||||
label.animate: true
|
||||
onClicked: {
|
||||
const shouldExpand = !sidebarLayout.allSectionsExpanded;
|
||||
themeModeSection.expanded = shouldExpand;
|
||||
colorVariantSection.expanded = shouldExpand;
|
||||
colorSchemeSection.expanded = shouldExpand;
|
||||
animationsSection.expanded = shouldExpand;
|
||||
fontsSection.expanded = shouldExpand;
|
||||
scalesSection.expanded = shouldExpand;
|
||||
transparencySection.expanded = shouldExpand;
|
||||
borderSection.expanded = shouldExpand;
|
||||
backgroundSection.expanded = shouldExpand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ThemeModeSection {
|
||||
id: themeModeSection
|
||||
}
|
||||
|
||||
ColorVariantSection {
|
||||
id: colorVariantSection
|
||||
}
|
||||
|
||||
ColorSchemeSection {
|
||||
id: colorSchemeSection
|
||||
}
|
||||
|
||||
AnimationsSection {
|
||||
id: animationsSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
FontsSection {
|
||||
id: fontsSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
ScalesSection {
|
||||
id: scalesSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
TransparencySection {
|
||||
id: transparencySection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
BorderSection {
|
||||
id: borderSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
BackgroundSection {
|
||||
id: backgroundSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: appearanceRightContentComponent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Animations")
|
||||
showBackground: true
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Animation duration scale")
|
||||
value: rootPane.animDurationsScale
|
||||
from: 0.1
|
||||
to: 5.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 5.0
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.animDurationsScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Background")
|
||||
showBackground: true
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Background enabled")
|
||||
checked: rootPane.backgroundEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.backgroundEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Wallpaper enabled")
|
||||
checked: rootPane.wallpaperEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.wallpaperEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Desktop Clock")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Desktop Clock enabled")
|
||||
checked: rootPane.desktopClockEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
id: posContainer
|
||||
|
||||
contentSpacing: Appearance.spacing.small
|
||||
z: 1
|
||||
|
||||
readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-')
|
||||
readonly property string currentV: pos[0]
|
||||
readonly property string currentH: pos[1]
|
||||
|
||||
function updateClockPos(v, h) {
|
||||
rootPane.desktopClockPosition = v + "-" + h;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Positioning")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SplitButtonRow {
|
||||
label: qsTr("Vertical Position")
|
||||
enabled: rootPane.desktopClockEnabled
|
||||
|
||||
menuItems: [
|
||||
MenuItem {
|
||||
text: qsTr("Top")
|
||||
icon: "vertical_align_top"
|
||||
property string val: "top"
|
||||
},
|
||||
MenuItem {
|
||||
text: qsTr("Middle")
|
||||
icon: "vertical_align_center"
|
||||
property string val: "middle"
|
||||
},
|
||||
MenuItem {
|
||||
text: qsTr("Bottom")
|
||||
icon: "vertical_align_bottom"
|
||||
property string val: "bottom"
|
||||
}
|
||||
]
|
||||
|
||||
Component.onCompleted: {
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
if (menuItems[i].val === posContainer.currentV)
|
||||
active = menuItems[i];
|
||||
}
|
||||
}
|
||||
|
||||
// The signal from SplitButtonRow
|
||||
onSelected: item => posContainer.updateClockPos(item.val, posContainer.currentH)
|
||||
}
|
||||
|
||||
SplitButtonRow {
|
||||
label: qsTr("Horizontal Position")
|
||||
enabled: rootPane.desktopClockEnabled
|
||||
expandedZ: 99
|
||||
|
||||
menuItems: [
|
||||
MenuItem {
|
||||
text: qsTr("Left")
|
||||
icon: "align_horizontal_left"
|
||||
property string val: "left"
|
||||
},
|
||||
MenuItem {
|
||||
text: qsTr("Center")
|
||||
icon: "align_horizontal_center"
|
||||
property string val: "center"
|
||||
},
|
||||
MenuItem {
|
||||
text: qsTr("Right")
|
||||
icon: "align_horizontal_right"
|
||||
property string val: "right"
|
||||
}
|
||||
]
|
||||
|
||||
Component.onCompleted: {
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
if (menuItems[i].val === posContainer.currentH)
|
||||
active = menuItems[i];
|
||||
}
|
||||
}
|
||||
|
||||
onSelected: item => posContainer.updateClockPos(posContainer.currentV, item.val)
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Invert colors")
|
||||
checked: rootPane.desktopClockInvertColors
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockInvertColors = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Shadow")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: rootPane.desktopClockShadowEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockShadowEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Opacity")
|
||||
value: rootPane.desktopClockShadowOpacity * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.desktopClockShadowOpacity = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Blur")
|
||||
value: rootPane.desktopClockShadowBlur * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.desktopClockShadowBlur = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Background")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: rootPane.desktopClockBackgroundEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockBackgroundEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Blur enabled")
|
||||
checked: rootPane.desktopClockBackgroundBlur
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockBackgroundBlur = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Opacity")
|
||||
value: rootPane.desktopClockBackgroundOpacity * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.desktopClockBackgroundOpacity = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Visualiser")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Visualiser enabled")
|
||||
checked: rootPane.visualiserEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.visualiserEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Visualiser auto hide")
|
||||
checked: rootPane.visualiserAutoHide
|
||||
onToggled: checked => {
|
||||
rootPane.visualiserAutoHide = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Visualiser rounding")
|
||||
value: rootPane.visualiserRounding
|
||||
from: 0
|
||||
to: 10
|
||||
stepSize: 1
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 10
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.visualiserRounding = Math.round(newValue);
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Visualiser spacing")
|
||||
value: rootPane.visualiserSpacing
|
||||
from: 0
|
||||
to: 2
|
||||
validator: DoubleValidator {
|
||||
bottom: 0
|
||||
top: 2
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.visualiserSpacing = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Border")
|
||||
showBackground: true
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Border rounding")
|
||||
value: rootPane.borderRounding
|
||||
from: 0.1
|
||||
to: 100
|
||||
decimals: 1
|
||||
suffix: "px"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 100
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.borderRounding = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Border thickness")
|
||||
value: rootPane.borderThickness
|
||||
from: 0.1
|
||||
to: 100
|
||||
decimals: 1
|
||||
suffix: "px"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 100
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.borderThickness = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
title: qsTr("Color scheme")
|
||||
description: qsTr("Available color schemes")
|
||||
showBackground: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
Repeater {
|
||||
model: Schemes.list
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
readonly property string schemeKey: `${modelData.name} ${modelData.flavour}`
|
||||
readonly property bool isCurrent: schemeKey === Schemes.currentScheme
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
const name = modelData.name;
|
||||
const flavour = modelData.flavour;
|
||||
const schemeKey = `${name} ${flavour}`;
|
||||
|
||||
Schemes.currentScheme = schemeKey;
|
||||
Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]);
|
||||
|
||||
Qt.callLater(() => {
|
||||
reloadTimer.restart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reloadTimer
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
Schemes.reload();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: schemeRow
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
id: preview
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
border.width: 1
|
||||
border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5)
|
||||
|
||||
color: `#${modelData.colours?.surface}`
|
||||
radius: Appearance.rounding.full
|
||||
implicitWidth: iconPlaceholder.implicitWidth
|
||||
implicitHeight: iconPlaceholder.implicitWidth
|
||||
|
||||
MaterialIcon {
|
||||
id: iconPlaceholder
|
||||
visible: false
|
||||
text: "circle"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitWidth: parent.implicitWidth / 2
|
||||
clip: true
|
||||
|
||||
StyledRect {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitWidth: preview.implicitWidth
|
||||
color: `#${modelData.colours?.primary}`
|
||||
radius: Appearance.rounding.full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: modelData.flavour ?? ""
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.name ?? ""
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3outline
|
||||
|
||||
elide: Text.ElideRight
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
title: qsTr("Color variant")
|
||||
description: qsTr("Material theme variant")
|
||||
showBackground: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
Repeater {
|
||||
model: M3Variants.list
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: modelData.variant === Schemes.currentVariant ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
const variant = modelData.variant;
|
||||
|
||||
Schemes.currentVariant = variant;
|
||||
Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]);
|
||||
|
||||
Qt.callLater(() => {
|
||||
reloadTimer.restart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reloadTimer
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
Schemes.reload();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: variantRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: modelData.icon
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.variant === Schemes.currentVariant ? 1 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.name
|
||||
font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
visible: modelData.variant === Schemes.currentVariant
|
||||
text: "check"
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Fonts")
|
||||
showBackground: true
|
||||
|
||||
CollapsibleSection {
|
||||
id: materialFontSection
|
||||
title: qsTr("Material font family")
|
||||
expanded: true
|
||||
showBackground: true
|
||||
nested: true
|
||||
|
||||
Loader {
|
||||
id: materialFontLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
|
||||
active: materialFontSection.expanded
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: materialFontList
|
||||
property alias contentHeight: materialFontList.contentHeight
|
||||
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
model: Qt.fontFamilies()
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: materialFontList
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
rootPane.fontFamilyMaterial = modelData;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fontFamilyMaterialRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: monoFontSection
|
||||
title: qsTr("Monospace font family")
|
||||
expanded: false
|
||||
showBackground: true
|
||||
nested: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
|
||||
active: monoFontSection.expanded
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: monoFontList
|
||||
property alias contentHeight: monoFontList.contentHeight
|
||||
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
model: Qt.fontFamilies()
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: monoFontList
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
readonly property bool isCurrent: modelData === rootPane.fontFamilyMono
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
rootPane.fontFamilyMono = modelData;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fontFamilyMonoRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: sansFontSection
|
||||
title: qsTr("Sans-serif font family")
|
||||
expanded: false
|
||||
showBackground: true
|
||||
nested: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
|
||||
active: sansFontSection.expanded
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: sansFontList
|
||||
property alias contentHeight: sansFontList.contentHeight
|
||||
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
model: Qt.fontFamilies()
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: sansFontList
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
readonly property bool isCurrent: modelData === rootPane.fontFamilySans
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
rootPane.fontFamilySans = modelData;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fontFamilySansRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Font size scale")
|
||||
value: rootPane.fontSizeScale
|
||||
from: 0.7
|
||||
to: 1.5
|
||||
decimals: 2
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.7
|
||||
top: 1.5
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.fontSizeScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Scales")
|
||||
showBackground: true
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Padding scale")
|
||||
value: rootPane.paddingScale
|
||||
from: 0.5
|
||||
to: 2.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.5
|
||||
top: 2.0
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.paddingScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Rounding scale")
|
||||
value: rootPane.roundingScale
|
||||
from: 0.1
|
||||
to: 5.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 5.0
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.roundingScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Spacing scale")
|
||||
value: rootPane.spacingScale
|
||||
from: 0.1
|
||||
to: 2.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 2.0
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.spacingScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
CollapsibleSection {
|
||||
title: qsTr("Theme mode")
|
||||
description: qsTr("Light or dark theme")
|
||||
showBackground: true
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Dark mode")
|
||||
checked: !Colours.currentLight
|
||||
onToggled: checked => {
|
||||
Colours.setMode(checked ? "dark" : "light");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Transparency")
|
||||
showBackground: true
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Transparency enabled")
|
||||
checked: rootPane.transparencyEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.transparencyEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Transparency base")
|
||||
value: rootPane.transparencyBase * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.transparencyBase = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Transparency layers")
|
||||
value: rootPane.transparencyLayers * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.transparencyLayers = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
SplitPaneLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
|
||||
StyledFlickable {
|
||||
id: leftAudioFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: leftContent.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: leftAudioFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: leftContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Audio")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: outputDevicesSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Output devices")
|
||||
expanded: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Devices (%1)").arg(Audio.sinks.length)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("All available output devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
Repeater {
|
||||
Layout.fillWidth: true
|
||||
model: Audio.sinks
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
Audio.setAudioSink(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: outputRowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: Audio.sink?.id === modelData.id ? 1 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.description || qsTr("Unknown")
|
||||
font.weight: Audio.sink?.id === modelData.id ? 500 : 400
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: inputDevicesSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Input devices")
|
||||
expanded: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Devices (%1)").arg(Audio.sources.length)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("All available input devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
Repeater {
|
||||
Layout.fillWidth: true
|
||||
model: Audio.sources
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
Audio.setAudioSource(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: inputRowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: "mic"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: Audio.source?.id === modelData.id ? 1 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.description || qsTr("Unknown")
|
||||
font.weight: Audio.source?.id === modelData.id ? 500 : 400
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: Component {
|
||||
StyledFlickable {
|
||||
id: rightAudioFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: contentLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: rightAudioFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "volume_up"
|
||||
title: qsTr("Audio Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Output volume")
|
||||
description: qsTr("Control the volume of your output device")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Volume")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: outputVolumeInput
|
||||
Layout.preferredWidth: 70
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
enabled: !Audio.muted
|
||||
|
||||
Component.onCompleted: {
|
||||
text = Math.round(Audio.volume * 100).toString();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Audio
|
||||
function onVolumeChanged() {
|
||||
if (!outputVolumeInput.hasFocus) {
|
||||
outputVolumeInput.text = Math.round(Audio.volume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: text => {
|
||||
if (hasFocus) {
|
||||
const val = parseInt(text);
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) {
|
||||
Audio.setVolume(val / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = parseInt(text);
|
||||
if (isNaN(val) || val < 0 || val > 100) {
|
||||
text = Math.round(Audio.volume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "%"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
opacity: Audio.muted ? 0.5 : 1
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (Audio.sink?.audio) {
|
||||
Audio.sink.audio.muted = !Audio.sink.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: muteIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Audio.muted ? "volume_off" : "volume_up"
|
||||
color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: outputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
value: Audio.volume
|
||||
enabled: !Audio.muted
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onMoved: {
|
||||
Audio.setVolume(value);
|
||||
if (!outputVolumeInput.hasFocus) {
|
||||
outputVolumeInput.text = Math.round(value * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Input volume")
|
||||
description: qsTr("Control the volume of your input device")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Volume")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: inputVolumeInput
|
||||
Layout.preferredWidth: 70
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
enabled: !Audio.sourceMuted
|
||||
|
||||
Component.onCompleted: {
|
||||
text = Math.round(Audio.sourceVolume * 100).toString();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Audio
|
||||
function onSourceVolumeChanged() {
|
||||
if (!inputVolumeInput.hasFocus) {
|
||||
inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: text => {
|
||||
if (hasFocus) {
|
||||
const val = parseInt(text);
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) {
|
||||
Audio.setSourceVolume(val / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = parseInt(text);
|
||||
if (isNaN(val) || val < 0 || val > 100) {
|
||||
text = Math.round(Audio.sourceVolume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "%"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
opacity: Audio.sourceMuted ? 0.5 : 1
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (Audio.source?.audio) {
|
||||
Audio.source.audio.muted = !Audio.source.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: muteInputIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "mic_off"
|
||||
color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: inputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
value: Audio.sourceVolume
|
||||
enabled: !Audio.sourceMuted
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onMoved: {
|
||||
Audio.setSourceVolume(value);
|
||||
if (!inputVolumeInput.hasFocus) {
|
||||
inputVolumeInput.text = Math.round(value * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Applications")
|
||||
description: qsTr("Control volume for individual applications")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Repeater {
|
||||
model: Audio.streams
|
||||
Layout.fillWidth: true
|
||||
|
||||
delegate: ColumnLayout {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: "apps"
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
fill: 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
text: Audio.getStreamName(modelData)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: streamVolumeInput
|
||||
Layout.preferredWidth: 70
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
enabled: !Audio.getStreamMuted(modelData)
|
||||
|
||||
Component.onCompleted: {
|
||||
text = Math.round(Audio.getStreamVolume(modelData) * 100).toString();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: modelData
|
||||
function onAudioChanged() {
|
||||
if (!streamVolumeInput.hasFocus && modelData?.audio) {
|
||||
streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: text => {
|
||||
if (hasFocus) {
|
||||
const val = parseInt(text);
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) {
|
||||
Audio.setStreamVolume(modelData, val / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = parseInt(text);
|
||||
if (isNaN(val) || val < 0 || val > 100) {
|
||||
text = Math.round(Audio.getStreamVolume(modelData) * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "%"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData));
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: streamMuteIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Audio.getStreamMuted(modelData) ? "volume_off" : "volume_up"
|
||||
color: Audio.getStreamMuted(modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
value: Audio.getStreamVolume(modelData)
|
||||
enabled: !Audio.getStreamMuted(modelData)
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onMoved: {
|
||||
Audio.setStreamVolume(modelData, value);
|
||||
if (!streamVolumeInput.hasFocus) {
|
||||
streamVolumeInput.text = Math.round(value * 100).toString();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: modelData
|
||||
function onAudioChanged() {
|
||||
if (modelData?.audio) {
|
||||
value = modelData.audio.volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
visible: Audio.streams.length === 0
|
||||
text: qsTr("No applications currently playing audio")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
|
||||
SplitPaneWithDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
activeItem: session.bt.active
|
||||
paneIdGenerator: function (item) {
|
||||
return item ? (item.address || "") : "";
|
||||
}
|
||||
|
||||
leftContent: Component {
|
||||
StyledFlickable {
|
||||
id: leftFlickable
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: deviceList.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: leftFlickable
|
||||
}
|
||||
|
||||
DeviceList {
|
||||
id: deviceList
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightDetailsComponent: Component {
|
||||
Details {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightSettingsComponent: Component {
|
||||
StyledFlickable {
|
||||
id: settingsFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: settingsFlickable
|
||||
}
|
||||
|
||||
Settings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledFlickable {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property BluetoothDevice device: session.bt.active
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: detailsWrapper.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: root
|
||||
}
|
||||
|
||||
Item {
|
||||
id: detailsWrapper
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitHeight: details.implicitHeight
|
||||
|
||||
DeviceDetails {
|
||||
id: details
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
session: root.session
|
||||
device: root.device
|
||||
|
||||
headerComponent: Component {
|
||||
SettingsHeader {
|
||||
icon: Icons.getBluetoothIcon(root.device?.icon ?? "")
|
||||
title: root.device?.name ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Connection status")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Connection settings for this device")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceStatus
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Connected")
|
||||
checked: root.device?.connected ?? false
|
||||
toggle.onToggled: root.device.connected = checked
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Paired")
|
||||
checked: root.device?.paired ?? false
|
||||
toggle.onToggled: {
|
||||
if (root.device.paired)
|
||||
root.device.forget();
|
||||
else
|
||||
root.device.pair();
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Blocked")
|
||||
checked: root.device?.blocked ?? false
|
||||
toggle.onToggled: root.device.blocked = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Device properties")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Additional settings")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceProps
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Item {
|
||||
id: renameDevice
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.spacing.small
|
||||
|
||||
implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "editingDeviceName"
|
||||
when: root.session.bt.editingDeviceName
|
||||
|
||||
AnchorChanges {
|
||||
target: deviceNameEdit
|
||||
anchors.top: renameDevice.top
|
||||
}
|
||||
PropertyChanges {
|
||||
renameDevice.implicitHeight: deviceNameEdit.implicitHeight
|
||||
renameLabel.opacity: 0
|
||||
deviceNameEdit.padding: Appearance.padding.normal
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
Anim {
|
||||
properties: "implicitHeight,opacity,padding"
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: renameLabel
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: qsTr("Device name")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: deviceNameEdit
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: renameLabel.bottom
|
||||
anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal
|
||||
|
||||
text: root.device?.name ?? ""
|
||||
readOnly: !root.session.bt.editingDeviceName
|
||||
onAccepted: {
|
||||
root.session.bt.editingDeviceName = false;
|
||||
root.device.name = text;
|
||||
}
|
||||
|
||||
leftPadding: Appearance.padding.normal
|
||||
rightPadding: Appearance.padding.normal
|
||||
|
||||
background: StyledRect {
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 2
|
||||
border.color: Colours.palette.m3primary
|
||||
opacity: root.session.bt.editingDeviceName ? 1 : 0
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.small
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
opacity: root.session.bt.editingDeviceName ? 1 : 0
|
||||
scale: root.session.bt.editingDeviceName ? 1 : 0.5
|
||||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
disabled: !root.session.bt.editingDeviceName
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingDeviceName = false;
|
||||
deviceNameEdit.text = Qt.binding(() => root.device?.name ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: cancelEditIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: "cancel"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
|
||||
color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName;
|
||||
if (root.session.bt.editingDeviceName)
|
||||
deviceNameEdit.forceActiveFocus();
|
||||
else
|
||||
deviceNameEdit.accepted();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: editIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: root.session.bt.editingDeviceName ? "check_circle" : "edit"
|
||||
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Trusted")
|
||||
checked: root.device?.trusted ?? false
|
||||
toggle.onToggled: root.device.trusted = checked
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Wake allowed")
|
||||
checked: root.device?.wakeAllowed ?? false
|
||||
toggle.onToggled: root.device.wakeAllowed = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Device information")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Information about this device")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceInfo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: batteryPercent
|
||||
Layout.topMargin: Appearance.spacing.small / 2
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Appearance.padding.smaller
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
|
||||
StyledRect {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: parent.height * 0.25
|
||||
|
||||
implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Dbus path")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.dbusPath ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("MAC address")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.address ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Bonded")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.bonded ? qsTr("Yes") : qsTr("No")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("System name")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.deviceName ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.right: fabRoot.right
|
||||
anchors.bottom: fabRoot.top
|
||||
anchors.bottomMargin: Appearance.padding.normal
|
||||
|
||||
Repeater {
|
||||
id: fabMenu
|
||||
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
name: "trust"
|
||||
icon: "handshake"
|
||||
}
|
||||
ListElement {
|
||||
name: "block"
|
||||
icon: "block"
|
||||
}
|
||||
ListElement {
|
||||
name: "pair"
|
||||
icon: "missing_controller"
|
||||
}
|
||||
ListElement {
|
||||
name: "connect"
|
||||
icon: "bluetooth_connected"
|
||||
}
|
||||
}
|
||||
|
||||
StyledClippingRect {
|
||||
id: fabMenuItem
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
implicitHeight: fabMenuItemInner.implicitHeight + Appearance.padding.larger * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primaryContainer
|
||||
|
||||
opacity: 0
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.session.bt.fabMenuOpen
|
||||
|
||||
PropertyChanges {
|
||||
fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Appearance.padding.large * 2
|
||||
fabMenuItem.opacity: 1
|
||||
fabMenuItemInner.opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
to: "visible"
|
||||
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: (fabMenu.count - 1 - fabMenuItem.index) * Appearance.anim.durations.small / 8
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "implicitWidth"
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "visible"
|
||||
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: fabMenuItem.index * Appearance.anim.durations.small / 8
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "implicitWidth"
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.session.bt.fabMenuOpen = false;
|
||||
|
||||
const name = fabMenuItem.modelData.name;
|
||||
if (fabMenuItem.modelData.name !== "pair")
|
||||
root.device[`${name}ed`] = !root.device[`${name}ed`];
|
||||
else if (root.device.paired)
|
||||
root.device.forget();
|
||||
else
|
||||
root.device.pair();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fabMenuItemInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
opacity: 0
|
||||
|
||||
MaterialIcon {
|
||||
text: fabMenuItem.modelData.icon
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
fill: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
animate: true
|
||||
text: (root.device && root.device[`${fabMenuItem.modelData.name}ed`] ? fabMenuItem.modelData.name === "connect" ? "dis" : "un" : "") + fabMenuItem.modelData.name
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
font.capitalization: Font.Capitalize
|
||||
Layout.preferredWidth: implicitWidth
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fabRoot
|
||||
|
||||
x: root.contentX + root.width - width
|
||||
y: root.contentY + root.height - height
|
||||
width: 64
|
||||
height: 64
|
||||
z: 10000
|
||||
|
||||
StyledRect {
|
||||
id: fabBg
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
implicitWidth: 64
|
||||
implicitHeight: 64
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.session.bt.fabMenuOpen
|
||||
|
||||
PropertyChanges {
|
||||
fabBg.implicitWidth: 48
|
||||
fabBg.implicitHeight: 48
|
||||
fabBg.radius: 48 / 2
|
||||
fab.font.pointSize: Appearance.font.size.larger
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
properties: "radius,font.pointSize"
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
level: fabState.containsMouse && !fabState.pressed ? 4 : 3
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: fabState
|
||||
|
||||
color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: fab
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: root.session.bt.fabMenuOpen ? "close" : "settings"
|
||||
color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Toggle: RowLayout {
|
||||
required property string label
|
||||
property alias checked: toggle.checked
|
||||
property alias toggle: toggle
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: parent.label
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
id: toggle
|
||||
|
||||
cLayer: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceList {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property bool smallDiscoverable: width <= 540
|
||||
readonly property bool smallPairable: width <= 480
|
||||
|
||||
title: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length)
|
||||
description: qsTr("All available bluetooth devices")
|
||||
activeItem: session.bt.active
|
||||
|
||||
model: ScriptModel {
|
||||
id: deviceModel
|
||||
|
||||
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Bluetooth")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.enabled ?? false
|
||||
icon: "power"
|
||||
accent: "Tertiary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Toggle Bluetooth")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.enabled = !adapter.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.discoverable ?? false
|
||||
icon: root.smallDiscoverable ? "group_search" : ""
|
||||
label: root.smallDiscoverable ? "" : qsTr("Discoverable")
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Make discoverable")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discoverable = !adapter.discoverable;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.pairable ?? false
|
||||
icon: "missing_controller"
|
||||
label: root.smallPairable ? "" : qsTr("Pairable")
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Make pairable")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.pairable = !adapter.pairable;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.discovering ?? false
|
||||
icon: "bluetooth_searching"
|
||||
accent: "Secondary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Scan for devices")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discovering = !adapter.discovering;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.bt.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Bluetooth settings")
|
||||
|
||||
onClicked: {
|
||||
if (root.session.bt.active)
|
||||
root.session.bt.active = null;
|
||||
else {
|
||||
root.session.bt.active = root.model.values[0] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
id: device
|
||||
|
||||
required property BluetoothDevice modelData
|
||||
readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting)
|
||||
readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
function onClicked(): void {
|
||||
if (device.modelData)
|
||||
root.session.bt.active = device.modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: deviceInner
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "")
|
||||
color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: device.connected ? 1 : 0
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: device.modelData ? device.modelData.name : qsTr("Unknown")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: connectBtn
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0)
|
||||
|
||||
CircularIndicator {
|
||||
anchors.fill: parent
|
||||
running: device.loading
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
disabled: device.loading
|
||||
|
||||
function onClicked(): void {
|
||||
if (device.loading)
|
||||
return;
|
||||
|
||||
if (device.connected) {
|
||||
device.modelData.connected = false;
|
||||
} else {
|
||||
if (device.modelData.bonded) {
|
||||
device.modelData.connected = true;
|
||||
} else {
|
||||
device.modelData.pair();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: device.connected ? "link_off" : "link"
|
||||
color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
opacity: device.loading ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelected: item => session.bt.active = item
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "bluetooth"
|
||||
title: qsTr("Bluetooth Settings")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Adapter status")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("General adapter settings")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: adapterStatus.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: adapterStatus
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Powered")
|
||||
checked: Bluetooth.defaultAdapter?.enabled ?? false
|
||||
toggle.onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.enabled = checked;
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Discoverable")
|
||||
checked: Bluetooth.defaultAdapter?.discoverable ?? false
|
||||
toggle.onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discoverable = checked;
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Pairable")
|
||||
checked: Bluetooth.defaultAdapter?.pairable ?? false
|
||||
toggle.onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.pairable = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Adapter properties")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Per-adapter settings")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: adapterSettings.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: adapterSettings
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Current adapter")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: adapterPickerButton
|
||||
|
||||
property bool expanded
|
||||
|
||||
implicitWidth: adapterPicker.implicitWidth + Appearance.padding.normal * 2
|
||||
implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
function onClicked(): void {
|
||||
adapterPickerButton.expanded = !adapterPickerButton.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: adapterPicker
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.topMargin: Appearance.padding.smaller
|
||||
anchors.bottomMargin: Appearance.padding.smaller
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.leftMargin: Appearance.padding.small
|
||||
text: Bluetooth.defaultAdapter?.name ?? qsTr("None")
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "expand_more"
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: adapterListBg
|
||||
radius: adapterListBg.radius
|
||||
opacity: adapterPickerButton.expanded ? 1 : 0
|
||||
scale: adapterPickerButton.expanded ? 1 : 0.7
|
||||
level: 2
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledClippingRect {
|
||||
id: adapterListBg
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight
|
||||
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
radius: Appearance.rounding.small
|
||||
opacity: adapterPickerButton.expanded ? 1 : 0
|
||||
scale: adapterPickerButton.expanded ? 1 : 0.7
|
||||
|
||||
ColumnLayout {
|
||||
id: adapterList
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: Bluetooth.adapters
|
||||
|
||||
Item {
|
||||
id: adapter
|
||||
|
||||
required property BluetoothAdapter modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
StateLayer {
|
||||
disabled: !adapterPickerButton.expanded
|
||||
|
||||
function onClicked(): void {
|
||||
adapterPickerButton.expanded = false;
|
||||
root.session.bt.currentAdapter = adapter.modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: adapterInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Appearance.padding.small
|
||||
text: adapter.modelData.name
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
visible: adapter.modelData === root.session.bt.currentAdapter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Discoverable timeout")
|
||||
}
|
||||
|
||||
CustomSpinBox {
|
||||
min: 0
|
||||
value: root.session.bt.currentAdapter?.discoverableTimeout ?? 0
|
||||
onValueModified: value => {
|
||||
if (root.session.bt.currentAdapter) {
|
||||
root.session.bt.currentAdapter.discoverableTimeout = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Item {
|
||||
id: renameAdapter
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.spacing.small
|
||||
|
||||
implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "editingAdapterName"
|
||||
when: root.session.bt.editingAdapterName
|
||||
|
||||
AnchorChanges {
|
||||
target: adapterNameEdit
|
||||
anchors.top: renameAdapter.top
|
||||
}
|
||||
PropertyChanges {
|
||||
renameAdapter.implicitHeight: adapterNameEdit.implicitHeight
|
||||
renameLabel.opacity: 0
|
||||
adapterNameEdit.padding: Appearance.padding.normal
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
Anim {
|
||||
properties: "implicitHeight,opacity,padding"
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: renameLabel
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: qsTr("Rename adapter (currently does not work)")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: adapterNameEdit
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: renameLabel.bottom
|
||||
anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal
|
||||
|
||||
text: root.session.bt.currentAdapter?.name ?? ""
|
||||
readOnly: !root.session.bt.editingAdapterName
|
||||
onAccepted: {
|
||||
root.session.bt.editingAdapterName = false;
|
||||
}
|
||||
|
||||
leftPadding: Appearance.padding.normal
|
||||
rightPadding: Appearance.padding.normal
|
||||
|
||||
background: StyledRect {
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 2
|
||||
border.color: Colours.palette.m3primary
|
||||
opacity: root.session.bt.editingAdapterName ? 1 : 0
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.small
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
opacity: root.session.bt.editingAdapterName ? 1 : 0
|
||||
scale: root.session.bt.editingAdapterName ? 1 : 0.5
|
||||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
disabled: !root.session.bt.editingAdapterName
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingAdapterName = false;
|
||||
adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: cancelEditIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: "cancel"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: root.session.bt.editingAdapterName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
|
||||
color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName;
|
||||
if (root.session.bt.editingAdapterName)
|
||||
adapterNameEdit.forceActiveFocus();
|
||||
else
|
||||
adapterNameEdit.accepted();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: editIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: root.session.bt.editingAdapterName ? "check_circle" : "edit"
|
||||
color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Adapter information")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Information about the default adapter")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: adapterInfo.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: adapterInfo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Adapter state")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Dbus path")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Bluetooth.defaultAdapter?.dbusPath ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Adapter id")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Bluetooth.defaultAdapter?.adapterId ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Toggle: RowLayout {
|
||||
required property string label
|
||||
property alias checked: toggle.checked
|
||||
property alias toggle: toggle
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: parent.label
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
id: toggle
|
||||
|
||||
cLayer: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
property var options: [] // Array of {label: string, propertyName: string, onToggled: function}
|
||||
property var rootItem: null // The root item that contains the properties we want to bind to
|
||||
property string title: "" // Optional title text
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
visible: root.title !== ""
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: buttonRow
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: root.options
|
||||
|
||||
delegate: TextButton {
|
||||
id: button
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
text: modelData.label
|
||||
|
||||
property bool _checked: false
|
||||
|
||||
checked: _checked
|
||||
toggle: false
|
||||
type: TextButton.Tonal
|
||||
|
||||
// Create binding in Component.onCompleted
|
||||
Component.onCompleted: {
|
||||
if (root.rootItem && modelData.propertyName) {
|
||||
const propName = modelData.propertyName;
|
||||
const rootItem = root.rootItem;
|
||||
_checked = Qt.binding(function () {
|
||||
return rootItem[propName] ?? false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Match utilities Toggles radius styling
|
||||
// Each button has full rounding (not connected) since they have spacing
|
||||
radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal
|
||||
|
||||
// Match utilities Toggles inactive color
|
||||
inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)
|
||||
|
||||
// Adjust width similar to utilities toggles
|
||||
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)
|
||||
|
||||
onClicked: {
|
||||
if (modelData.onToggled && root.rootItem && modelData.propertyName) {
|
||||
const currentValue = root.rootItem[modelData.propertyName] ?? false;
|
||||
modelData.onToggled(!currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property Session session
|
||||
property var device: null
|
||||
|
||||
property Component headerComponent: null
|
||||
property list<Component> sections: []
|
||||
|
||||
property Component topContent: null
|
||||
property Component bottomContent: null
|
||||
|
||||
implicitWidth: layout.implicitWidth
|
||||
implicitHeight: layout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Loader {
|
||||
id: headerLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.headerComponent
|
||||
visible: root.headerComponent !== null
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: topContentLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.topContent
|
||||
visible: root.topContent !== null
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.sections
|
||||
|
||||
Loader {
|
||||
required property Component modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: modelData
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: bottomContentLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.bottomContent
|
||||
visible: root.bottomContent !== null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property Session session: null
|
||||
property var model: null
|
||||
property Component delegate: null
|
||||
|
||||
property string title: ""
|
||||
property string description: ""
|
||||
property var activeItem: null
|
||||
property Component headerComponent: null
|
||||
property Component titleSuffix: null
|
||||
property bool showHeader: true
|
||||
|
||||
signal itemSelected(var item)
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Loader {
|
||||
id: headerLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.headerComponent
|
||||
visible: root.headerComponent !== null && root.showHeader
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: root.headerComponent ? 0 : 0
|
||||
spacing: Appearance.spacing.small
|
||||
visible: root.title !== "" || root.description !== ""
|
||||
|
||||
StyledText {
|
||||
visible: root.title !== ""
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Loader {
|
||||
sourceComponent: root.titleSuffix
|
||||
visible: root.titleSuffix !== null
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
property alias view: view
|
||||
|
||||
StyledText {
|
||||
visible: root.description !== ""
|
||||
Layout.fillWidth: true
|
||||
text: root.description
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledListView {
|
||||
id: view
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentHeight
|
||||
|
||||
model: root.model
|
||||
delegate: root.delegate
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
interactive: false
|
||||
clip: false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
SequentialAnimation {
|
||||
id: root
|
||||
|
||||
required property Item target
|
||||
property list<PropertyAction> propertyActions
|
||||
|
||||
property real scaleFrom: 1.0
|
||||
property real scaleTo: 0.8
|
||||
property real opacityFrom: 1.0
|
||||
property real opacityTo: 0.0
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "opacity"
|
||||
from: root.opacityFrom
|
||||
to: root.opacityTo
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "scale"
|
||||
from: root.scaleFrom
|
||||
to: root.scaleTo
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
for (let i = 0; i < root.propertyActions.length; i++) {
|
||||
const action = root.propertyActions[i];
|
||||
if (action.target && action.property !== undefined) {
|
||||
action.target[action.property] = action.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "opacity"
|
||||
from: root.opacityTo
|
||||
to: root.opacityFrom
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "scale"
|
||||
from: root.scaleTo
|
||||
to: root.scaleFrom
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
property real from: 0
|
||||
property real to: 100
|
||||
property string suffix: ""
|
||||
property bool readonly: false
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
visible: root.label !== ""
|
||||
text: root.label
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
visible: root.readonly
|
||||
text: "lock"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1)
|
||||
opacity: root.readonly ? 0.5 : 1.0
|
||||
|
||||
StyledRect {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * ((root.value - root.from) / (root.to - root.from))
|
||||
radius: parent.radius
|
||||
color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string icon
|
||||
required property string title
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: column.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.icon
|
||||
font.pointSize: Appearance.font.size.extraLarge * 3
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
property real from: 0
|
||||
property real to: 100
|
||||
property real stepSize: 0
|
||||
property var validator: null
|
||||
property string suffix: "" // Optional suffix text (e.g., "×", "px")
|
||||
property int decimals: 1 // Number of decimal places to show (default: 1)
|
||||
property var formatValueFunction: null // Optional custom format function
|
||||
property var parseValueFunction: null // Optional custom parse function
|
||||
|
||||
function formatValue(val: real): string {
|
||||
if (formatValueFunction) {
|
||||
return formatValueFunction(val);
|
||||
}
|
||||
// Default format function
|
||||
// Check if it's an IntValidator (IntValidator doesn't have a 'decimals' property)
|
||||
if (validator && validator.bottom !== undefined && validator.decimals === undefined) {
|
||||
return Math.round(val).toString();
|
||||
}
|
||||
// For DoubleValidator or no validator, use the decimals property
|
||||
return val.toFixed(root.decimals);
|
||||
}
|
||||
|
||||
function parseValue(text: string): real {
|
||||
if (parseValueFunction) {
|
||||
return parseValueFunction(text);
|
||||
}
|
||||
// Default parse function
|
||||
if (validator && validator.bottom !== undefined) {
|
||||
// Check if it's an integer validator
|
||||
if (validator.top !== undefined && validator.top === Math.floor(validator.top)) {
|
||||
return parseInt(text);
|
||||
}
|
||||
}
|
||||
return parseFloat(text);
|
||||
}
|
||||
|
||||
signal valueModified(real newValue)
|
||||
|
||||
property bool _initialized: false
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Component.onCompleted: {
|
||||
// Set initialized flag after a brief delay to allow component to fully load
|
||||
Qt.callLater(() => {
|
||||
_initialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
visible: root.label !== ""
|
||||
text: root.label
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: inputField
|
||||
Layout.preferredWidth: 70
|
||||
validator: root.validator
|
||||
|
||||
Component.onCompleted: {
|
||||
// Initialize text without triggering valueModified signal
|
||||
text = root.formatValue(root.value);
|
||||
}
|
||||
|
||||
onTextEdited: text => {
|
||||
if (hasFocus) {
|
||||
const val = root.parseValue(text);
|
||||
if (!isNaN(val)) {
|
||||
// Validate against validator bounds if available
|
||||
let isValid = true;
|
||||
if (root.validator) {
|
||||
if (root.validator.bottom !== undefined && val < root.validator.bottom) {
|
||||
isValid = false;
|
||||
}
|
||||
if (root.validator.top !== undefined && val > root.validator.top) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
root.valueModified(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = root.parseValue(text);
|
||||
let isValid = true;
|
||||
if (root.validator) {
|
||||
if (root.validator.bottom !== undefined && val < root.validator.bottom) {
|
||||
isValid = false;
|
||||
}
|
||||
if (root.validator.top !== undefined && val > root.validator.top) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNaN(val) || !isValid) {
|
||||
text = root.formatValue(root.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.suffix !== ""
|
||||
text: root.suffix
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: slider
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
from: root.from
|
||||
to: root.to
|
||||
stepSize: root.stepSize
|
||||
|
||||
// Use Binding to allow slider to move freely during dragging
|
||||
Binding {
|
||||
target: slider
|
||||
property: "value"
|
||||
value: root.value
|
||||
when: !slider.pressed
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
// Update input field text in real-time as slider moves during dragging
|
||||
// Always update when slider value changes (during dragging or external updates)
|
||||
if (!inputField.hasFocus) {
|
||||
const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;
|
||||
inputField.text = root.formatValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
onMoved: {
|
||||
const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;
|
||||
root.valueModified(newValue);
|
||||
if (!inputField.hasFocus) {
|
||||
inputField.text = root.formatValue(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update input field when value changes externally (slider is already bound)
|
||||
onValueChanged: {
|
||||
// Only update if component is initialized to avoid issues during creation
|
||||
if (root._initialized && !inputField.hasFocus) {
|
||||
inputField.text = root.formatValue(root.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
spacing: 0
|
||||
|
||||
property Component leftContent: null
|
||||
property Component rightContent: null
|
||||
|
||||
property real leftWidthRatio: 0.4
|
||||
property int leftMinimumWidth: 420
|
||||
property var leftLoaderProperties: ({})
|
||||
property var rightLoaderProperties: ({})
|
||||
|
||||
property alias leftLoader: leftLoader
|
||||
property alias rightLoader: rightLoader
|
||||
|
||||
Item {
|
||||
id: leftPane
|
||||
|
||||
Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio)
|
||||
Layout.minimumWidth: root.leftMinimumWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
ClippingRectangle {
|
||||
id: leftClippingRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal / 2
|
||||
|
||||
radius: leftBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: leftLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
|
||||
|
||||
sourceComponent: root.leftContent
|
||||
|
||||
Component.onCompleted: {
|
||||
for (const key in root.leftLoaderProperties) {
|
||||
leftLoader[key] = root.leftLoaderProperties[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: leftBorder
|
||||
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal / 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightPane
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ClippingRectangle {
|
||||
id: rightClippingRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal / 2
|
||||
|
||||
radius: rightBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: rightLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large * 2
|
||||
|
||||
sourceComponent: root.rightContent
|
||||
|
||||
Component.onCompleted: {
|
||||
for (const key in root.rightLoaderProperties) {
|
||||
rightLoader[key] = root.rightLoaderProperties[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: rightBorder
|
||||
|
||||
leftThickness: Appearance.padding.normal / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Component leftContent
|
||||
required property Component rightDetailsComponent
|
||||
required property Component rightSettingsComponent
|
||||
|
||||
property var activeItem: null
|
||||
property var paneIdGenerator: function (item) {
|
||||
return item ? String(item) : "";
|
||||
}
|
||||
|
||||
property Component overlayComponent: null
|
||||
|
||||
SplitPaneLayout {
|
||||
id: splitLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: root.leftContent
|
||||
|
||||
rightContent: Component {
|
||||
Item {
|
||||
id: rightPaneItem
|
||||
|
||||
property var pane: root.activeItem
|
||||
property string paneId: root.paneIdGenerator(pane)
|
||||
property Component targetComponent: root.rightSettingsComponent
|
||||
property Component nextComponent: root.rightSettingsComponent
|
||||
|
||||
function getComponentForPane() {
|
||||
return pane ? root.rightDetailsComponent : root.rightSettingsComponent;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
targetComponent = getComponentForPane();
|
||||
nextComponent = targetComponent;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: rightLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
transformOrigin: Item.Center
|
||||
|
||||
clip: false
|
||||
sourceComponent: rightPaneItem.targetComponent
|
||||
}
|
||||
|
||||
Behavior on paneId {
|
||||
PaneTransition {
|
||||
target: rightLoader
|
||||
propertyActions: [
|
||||
PropertyAction {
|
||||
target: rightPaneItem
|
||||
property: "targetComponent"
|
||||
value: rightPaneItem.nextComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onPaneChanged: {
|
||||
nextComponent = getComponentForPane();
|
||||
paneId = root.paneIdGenerator(pane);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overlayLoader
|
||||
|
||||
anchors.fill: parent
|
||||
z: 1000
|
||||
sourceComponent: root.overlayComponent
|
||||
active: root.overlayComponent !== null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.images
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Caelestia.Models
|
||||
import QtQuick
|
||||
|
||||
GridView {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
readonly property int minCellWidth: 200 + Appearance.spacing.normal
|
||||
readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth))
|
||||
|
||||
cellWidth: width / columnsCount
|
||||
cellHeight: 140 + Appearance.spacing.normal
|
||||
|
||||
model: Wallpapers.list
|
||||
|
||||
clip: true
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: root
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: root.cellWidth
|
||||
height: root.cellHeight
|
||||
|
||||
readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent
|
||||
readonly property real itemMargin: Appearance.spacing.normal / 2
|
||||
readonly property real itemRadius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: itemMargin
|
||||
anchors.rightMargin: itemMargin
|
||||
anchors.topMargin: itemMargin
|
||||
anchors.bottomMargin: itemMargin
|
||||
radius: itemRadius
|
||||
|
||||
function onClicked(): void {
|
||||
Wallpapers.setWallpaper(modelData.path);
|
||||
}
|
||||
}
|
||||
|
||||
StyledClippingRect {
|
||||
id: image
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: itemMargin
|
||||
anchors.rightMargin: itemMargin
|
||||
anchors.topMargin: itemMargin
|
||||
anchors.bottomMargin: itemMargin
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
radius: itemRadius
|
||||
antialiasing: true
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
|
||||
CachingImage {
|
||||
id: cachingImage
|
||||
|
||||
path: modelData.path
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
sourceSize: Qt.size(width, height)
|
||||
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if CachingImage fails to load
|
||||
Image {
|
||||
id: fallbackImage
|
||||
|
||||
anchors.fill: parent
|
||||
source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : ""
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
sourceSize: Qt.size(width, height)
|
||||
|
||||
opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fallbackTimer
|
||||
|
||||
property bool triggered: false
|
||||
interval: 800
|
||||
running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null
|
||||
onTriggered: triggered = true
|
||||
}
|
||||
|
||||
// Gradient overlay for filename
|
||||
Rectangle {
|
||||
id: filenameOverlay
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5
|
||||
radius: 0
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.3
|
||||
color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.7)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.6
|
||||
color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.9)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.95)
|
||||
}
|
||||
}
|
||||
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: itemMargin
|
||||
anchors.rightMargin: itemMargin
|
||||
anchors.topMargin: itemMargin
|
||||
anchors.bottomMargin: itemMargin
|
||||
color: "transparent"
|
||||
radius: itemRadius + border.width
|
||||
border.width: isCurrent ? 2 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Appearance.padding.small
|
||||
|
||||
visible: isCurrent
|
||||
text: "check_circle"
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: filenameText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
|
||||
anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
|
||||
anchors.bottomMargin: Appearance.padding.normal
|
||||
|
||||
text: modelData.name
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
font.weight: 500
|
||||
color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
// General Settings
|
||||
property bool enabled: Config.dashboard.enabled ?? true
|
||||
property bool showOnHover: Config.dashboard.showOnHover ?? true
|
||||
property int updateInterval: Config.dashboard.updateInterval ?? 1000
|
||||
property int dragThreshold: Config.dashboard.dragThreshold ?? 50
|
||||
|
||||
// Performance Resources
|
||||
property bool showBattery: Config.dashboard.performance.showBattery ?? false
|
||||
property bool showGpu: Config.dashboard.performance.showGpu ?? true
|
||||
property bool showCpu: Config.dashboard.performance.showCpu ?? true
|
||||
property bool showMemory: Config.dashboard.performance.showMemory ?? true
|
||||
property bool showStorage: Config.dashboard.performance.showStorage ?? true
|
||||
property bool showNetwork: Config.dashboard.performance.showNetwork ?? true
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function saveConfig() {
|
||||
Config.dashboard.enabled = root.enabled;
|
||||
Config.dashboard.showOnHover = root.showOnHover;
|
||||
Config.dashboard.updateInterval = root.updateInterval;
|
||||
Config.dashboard.dragThreshold = root.dragThreshold;
|
||||
Config.dashboard.performance.showBattery = root.showBattery;
|
||||
Config.dashboard.performance.showGpu = root.showGpu;
|
||||
Config.dashboard.performance.showCpu = root.showCpu;
|
||||
Config.dashboard.performance.showMemory = root.showMemory;
|
||||
Config.dashboard.performance.showStorage = root.showStorage;
|
||||
Config.dashboard.performance.showNetwork = root.showNetwork;
|
||||
// Note: sizes properties are readonly and cannot be modified
|
||||
Config.save();
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: dashboardClippingRect
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
radius: dashboardBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: dashboardLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large
|
||||
|
||||
sourceComponent: dashboardContentComponent
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: dashboardBorder
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal
|
||||
}
|
||||
|
||||
Component {
|
||||
id: dashboardContentComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: dashboardFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: dashboardLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: dashboardFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: dashboardLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Dashboard")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
// General Settings Section
|
||||
GeneralSection {
|
||||
rootItem: root
|
||||
}
|
||||
|
||||
// Performance Resources Section
|
||||
PerformanceSection {
|
||||
rootItem: root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
SectionContainer {
|
||||
id: root
|
||||
|
||||
required property var rootItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("General Settings")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: root.rootItem.enabled
|
||||
onToggled: checked => {
|
||||
root.rootItem.enabled = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Show on hover")
|
||||
checked: root.rootItem.showOnHover
|
||||
onToggled: checked => {
|
||||
root.rootItem.showOnHover = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Update interval")
|
||||
value: root.rootItem.updateInterval
|
||||
from: 100
|
||||
to: 10000
|
||||
stepSize: 100
|
||||
suffix: "ms"
|
||||
validator: IntValidator { bottom: 100; top: 10000 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
root.rootItem.updateInterval = Math.round(newValue);
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Drag threshold")
|
||||
value: root.rootItem.dragThreshold
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "px"
|
||||
validator: IntValidator { bottom: 0; top: 100 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
root.rootItem.dragThreshold = Math.round(newValue);
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import ".."
|
||||
import "../components"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.UPower
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SectionContainer {
|
||||
id: root
|
||||
|
||||
required property var rootItem
|
||||
// GPU toggle is hidden when gpuType is "NONE" (no GPU data available)
|
||||
readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE"
|
||||
// Battery toggle is hidden when no laptop battery is present
|
||||
readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery
|
||||
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Performance Resources")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root.rootItem
|
||||
options: {
|
||||
let opts = [];
|
||||
if (root.batteryAvailable)
|
||||
opts.push({
|
||||
"label": qsTr("Battery"),
|
||||
"propertyName": "showBattery",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showBattery = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
});
|
||||
|
||||
if (root.gpuAvailable)
|
||||
opts.push({
|
||||
"label": qsTr("GPU"),
|
||||
"propertyName": "showGpu",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showGpu = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
});
|
||||
|
||||
opts.push({
|
||||
"label": qsTr("CPU"),
|
||||
"propertyName": "showCpu",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showCpu = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}, {
|
||||
"label": qsTr("Memory"),
|
||||
"propertyName": "showMemory",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showMemory = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}, {
|
||||
"label": qsTr("Storage"),
|
||||
"propertyName": "showStorage",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showStorage = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}, {
|
||||
"label": qsTr("Network"),
|
||||
"propertyName": "showNetwork",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showNetwork = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,658 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../../utils/scripts/fuzzysort.js" as Fuzzy
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property var selectedApp: root.session.launcher.active
|
||||
property bool hideFromLauncherChecked: false
|
||||
property bool favouriteChecked: false
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onSelectedAppChanged: {
|
||||
root.session.launcher.active = root.selectedApp;
|
||||
updateToggleState();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session.launcher
|
||||
function onActiveChanged() {
|
||||
root.selectedApp = root.session.launcher.active;
|
||||
updateToggleState();
|
||||
}
|
||||
}
|
||||
|
||||
function updateToggleState() {
|
||||
if (!root.selectedApp) {
|
||||
root.hideFromLauncherChecked = false;
|
||||
root.favouriteChecked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const appId = root.selectedApp.id || root.selectedApp.entry?.id;
|
||||
|
||||
root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId);
|
||||
root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId);
|
||||
}
|
||||
|
||||
function saveHiddenApps(isHidden) {
|
||||
if (!root.selectedApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appId = root.selectedApp.id || root.selectedApp.entry?.id;
|
||||
|
||||
const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
|
||||
|
||||
if (isHidden) {
|
||||
if (!hiddenApps.includes(appId)) {
|
||||
hiddenApps.push(appId);
|
||||
}
|
||||
} else {
|
||||
const index = hiddenApps.indexOf(appId);
|
||||
if (index !== -1) {
|
||||
hiddenApps.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Config.launcher.hiddenApps = hiddenApps;
|
||||
Config.save();
|
||||
}
|
||||
|
||||
AppDb {
|
||||
id: allAppsDb
|
||||
|
||||
path: `${Paths.state}/apps.sqlite`
|
||||
favouriteApps: Config.launcher.favouriteApps
|
||||
entries: DesktopEntries.applications.values
|
||||
}
|
||||
|
||||
property string searchText: ""
|
||||
|
||||
function filterApps(search: string): list<var> {
|
||||
if (!search || search.trim() === "") {
|
||||
const apps = [];
|
||||
for (let i = 0; i < allAppsDb.apps.length; i++) {
|
||||
apps.push(allAppsDb.apps[i]);
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
if (!allAppsDb.apps || allAppsDb.apps.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const preparedApps = [];
|
||||
for (let i = 0; i < allAppsDb.apps.length; i++) {
|
||||
const app = allAppsDb.apps[i];
|
||||
const name = app.name || app.entry?.name || "";
|
||||
preparedApps.push({
|
||||
_item: app,
|
||||
name: Fuzzy.prepare(name)
|
||||
});
|
||||
}
|
||||
|
||||
const results = Fuzzy.go(search, preparedApps, {
|
||||
all: true,
|
||||
keys: ["name"],
|
||||
scoreFn: r => r[0].score
|
||||
});
|
||||
|
||||
return results.sort((a, b) => b._score - a._score).map(r => r.obj._item);
|
||||
}
|
||||
|
||||
property list<var> filteredApps: []
|
||||
|
||||
function updateFilteredApps() {
|
||||
filteredApps = filterApps(searchText);
|
||||
}
|
||||
|
||||
onSearchTextChanged: {
|
||||
updateFilteredApps();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateFilteredApps();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: allAppsDb
|
||||
function onAppsChanged() {
|
||||
updateFilteredApps();
|
||||
}
|
||||
}
|
||||
|
||||
SplitPaneLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
|
||||
ColumnLayout {
|
||||
id: leftLauncherLayout
|
||||
anchors.fill: parent
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Launcher")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.launcher.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Launcher settings")
|
||||
|
||||
onClicked: {
|
||||
if (root.session.launcher.active) {
|
||||
root.session.launcher.active = null;
|
||||
} else {
|
||||
if (root.filteredApps.length > 0) {
|
||||
root.session.launcher.active = root.filteredApps[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("All applications available in the launcher")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.bottomMargin: Appearance.spacing.small
|
||||
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight)
|
||||
|
||||
MaterialIcon {
|
||||
id: searchIcon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
|
||||
text: "search"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: searchField
|
||||
|
||||
anchors.left: searchIcon.right
|
||||
anchors.right: clearIcon.left
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
|
||||
topPadding: Appearance.padding.normal
|
||||
bottomPadding: Appearance.padding.normal
|
||||
|
||||
placeholderText: qsTr("Search applications...")
|
||||
|
||||
onTextChanged: {
|
||||
root.searchText = text;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: clearIcon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
width: searchField.text ? implicitWidth : implicitWidth / 2
|
||||
opacity: {
|
||||
if (!searchField.text)
|
||||
return 0;
|
||||
if (clearMouse.pressed)
|
||||
return 0.7;
|
||||
if (clearMouse.containsMouse)
|
||||
return 0.8;
|
||||
return 1;
|
||||
}
|
||||
|
||||
text: "close"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
|
||||
MouseArea {
|
||||
id: clearMouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: searchField.text ? Qt.PointingHandCursor : undefined
|
||||
|
||||
onClicked: searchField.text = ""
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: appsListLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
asynchronous: true
|
||||
active: true
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: appsListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: root.filteredApps
|
||||
spacing: Appearance.spacing.small / 2
|
||||
clip: true
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: parent
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
width: parent ? parent.width : 0
|
||||
implicitHeight: 40
|
||||
|
||||
readonly property bool isSelected: root.selectedApp === modelData
|
||||
|
||||
color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.session.launcher.active = modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitSize: 32
|
||||
source: {
|
||||
const entry = modelData.entry;
|
||||
return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing";
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.name || modelData.entry?.name || qsTr("Unknown")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false
|
||||
readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false
|
||||
active: isHidden || isFav
|
||||
|
||||
sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: hiddenIcon
|
||||
MaterialIcon {
|
||||
text: "visibility_off"
|
||||
fill: 1
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: favouriteIcon
|
||||
MaterialIcon {
|
||||
text: "favorite"
|
||||
fill: 1
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: Component {
|
||||
Item {
|
||||
id: rightLauncherPane
|
||||
|
||||
property var pane: root.session.launcher.active
|
||||
property string paneId: pane ? (pane.id || pane.entry?.id || "") : ""
|
||||
property Component targetComponent: settings
|
||||
property Component nextComponent: settings
|
||||
property var displayedApp: null
|
||||
|
||||
function getComponentForPane() {
|
||||
return pane ? appDetails : settings;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
displayedApp = pane;
|
||||
targetComponent = getComponentForPane();
|
||||
nextComponent = targetComponent;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: rightLauncherLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
transformOrigin: Item.Center
|
||||
clip: false
|
||||
|
||||
sourceComponent: rightLauncherPane.targetComponent
|
||||
active: true
|
||||
|
||||
property var displayedApp: rightLauncherPane.displayedApp
|
||||
|
||||
onItemChanged: {
|
||||
if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) {
|
||||
rightLauncherPane.displayedApp = rightLauncherPane.pane;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on paneId {
|
||||
PaneTransition {
|
||||
target: rightLauncherLoader
|
||||
propertyActions: [
|
||||
PropertyAction {
|
||||
target: rightLauncherPane
|
||||
property: "displayedApp"
|
||||
value: rightLauncherPane.pane
|
||||
},
|
||||
PropertyAction {
|
||||
target: rightLauncherLoader
|
||||
property: "active"
|
||||
value: false
|
||||
},
|
||||
PropertyAction {
|
||||
target: rightLauncherPane
|
||||
property: "targetComponent"
|
||||
value: rightLauncherPane.nextComponent
|
||||
},
|
||||
PropertyAction {
|
||||
target: rightLauncherLoader
|
||||
property: "active"
|
||||
value: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onPaneChanged: {
|
||||
nextComponent = getComponentForPane();
|
||||
paneId = pane ? (pane.id || pane.entry?.id || "") : "";
|
||||
}
|
||||
|
||||
onDisplayedAppChanged: {
|
||||
if (displayedApp) {
|
||||
const appId = displayedApp.id || displayedApp.entry?.id;
|
||||
root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId);
|
||||
root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId);
|
||||
} else {
|
||||
root.hideFromLauncherChecked = false;
|
||||
root.favouriteChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: settings
|
||||
|
||||
StyledFlickable {
|
||||
id: settingsFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: settingsFlickable
|
||||
}
|
||||
|
||||
Settings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appDetails
|
||||
|
||||
ColumnLayout {
|
||||
id: appDetailsLayout
|
||||
anchors.fill: parent
|
||||
|
||||
readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
Layout.leftMargin: Appearance.padding.large * 2
|
||||
Layout.rightMargin: Appearance.padding.large * 2
|
||||
Layout.topMargin: Appearance.padding.large * 2
|
||||
visible: displayedApp === null
|
||||
icon: "apps"
|
||||
title: qsTr("Launcher Applications")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.leftMargin: Appearance.padding.large * 2
|
||||
Layout.rightMargin: Appearance.padding.large * 2
|
||||
Layout.topMargin: Appearance.padding.large * 2
|
||||
visible: displayedApp !== null
|
||||
implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth)
|
||||
implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
IconImage {
|
||||
id: appIconImage
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitSize: Appearance.font.size.extraLarge * 3 * 2
|
||||
source: {
|
||||
const app = appDetailsLayout.displayedApp;
|
||||
if (!app)
|
||||
return "image-missing";
|
||||
const entry = app.entry;
|
||||
if (entry && entry.icon) {
|
||||
return Quickshell.iconPath(entry.icon, "image-missing");
|
||||
}
|
||||
return "image-missing";
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: appTitleText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : ""
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
Layout.leftMargin: Appearance.padding.large * 2
|
||||
Layout.rightMargin: Appearance.padding.large * 2
|
||||
|
||||
StyledFlickable {
|
||||
id: detailsFlickable
|
||||
anchors.fill: parent
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: debugLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: parent
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: debugLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SwitchRow {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
visible: appDetailsLayout.displayedApp !== null
|
||||
label: qsTr("Mark as favourite")
|
||||
checked: root.favouriteChecked
|
||||
// disabled if:
|
||||
// * app is hidden
|
||||
// * app isn't in favouriteApps array but marked as favourite anyway
|
||||
// ^^^ This means that this app is favourited because of a regex check
|
||||
// this button can not toggle regexed apps
|
||||
enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (Config.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked)
|
||||
opacity: enabled ? 1 : 0.6
|
||||
onToggled: checked => {
|
||||
root.favouriteChecked = checked;
|
||||
const app = appDetailsLayout.displayedApp;
|
||||
if (app) {
|
||||
const appId = app.id || app.entry?.id;
|
||||
const favouriteApps = Config.launcher.favouriteApps ? [...Config.launcher.favouriteApps] : [];
|
||||
if (checked) {
|
||||
if (!favouriteApps.includes(appId)) {
|
||||
favouriteApps.push(appId);
|
||||
}
|
||||
} else {
|
||||
const index = favouriteApps.indexOf(appId);
|
||||
if (index !== -1) {
|
||||
favouriteApps.splice(index, 1);
|
||||
}
|
||||
}
|
||||
Config.launcher.favouriteApps = favouriteApps;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
SwitchRow {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
visible: appDetailsLayout.displayedApp !== null
|
||||
label: qsTr("Hide from launcher")
|
||||
checked: root.hideFromLauncherChecked
|
||||
// disabled if:
|
||||
// * app is favourited
|
||||
// * app isn't in hiddenApps array but marked as hidden anyway
|
||||
// ^^^ This means that this app is hidden because of a regex check
|
||||
// this button can not toggle regexed apps
|
||||
enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (Config.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked)
|
||||
opacity: enabled ? 1 : 0.6
|
||||
onToggled: checked => {
|
||||
root.hideFromLauncherChecked = checked;
|
||||
const app = appDetailsLayout.displayedApp;
|
||||
if (app) {
|
||||
const appId = app.id || app.entry?.id;
|
||||
const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
|
||||
if (checked) {
|
||||
if (!hiddenApps.includes(appId)) {
|
||||
hiddenApps.push(appId);
|
||||
}
|
||||
} else {
|
||||
const index = hiddenApps.indexOf(appId);
|
||||
if (index !== -1) {
|
||||
hiddenApps.splice(index, 1);
|
||||
}
|
||||
}
|
||||
Config.launcher.hiddenApps = hiddenApps;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "apps"
|
||||
title: qsTr("Launcher Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("General")
|
||||
description: qsTr("General launcher settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: Config.launcher.enabled
|
||||
toggle.onToggled: {
|
||||
Config.launcher.enabled = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Show on hover")
|
||||
checked: Config.launcher.showOnHover
|
||||
toggle.onToggled: {
|
||||
Config.launcher.showOnHover = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Vim keybinds")
|
||||
checked: Config.launcher.vimKeybinds
|
||||
toggle.onToggled: {
|
||||
Config.launcher.vimKeybinds = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Enable dangerous actions")
|
||||
checked: Config.launcher.enableDangerousActions
|
||||
toggle.onToggled: {
|
||||
Config.launcher.enableDangerousActions = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Display")
|
||||
description: qsTr("Display and appearance settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Max shown items")
|
||||
value: qsTr("%1").arg(Config.launcher.maxShown)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Max wallpapers")
|
||||
value: qsTr("%1").arg(Config.launcher.maxWallpapers)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Drag threshold")
|
||||
value: qsTr("%1 px").arg(Config.launcher.dragThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Prefixes")
|
||||
description: qsTr("Command prefix settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Special prefix")
|
||||
value: Config.launcher.specialPrefix || qsTr("None")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Action prefix")
|
||||
value: Config.launcher.actionPrefix || qsTr("None")
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Fuzzy search")
|
||||
description: qsTr("Fuzzy search settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Apps")
|
||||
checked: Config.launcher.useFuzzy.apps
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.apps = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Actions")
|
||||
checked: Config.launcher.useFuzzy.actions
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.actions = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Schemes")
|
||||
checked: Config.launcher.useFuzzy.schemes
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.schemes = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Variants")
|
||||
checked: Config.launcher.useFuzzy.variants
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.variants = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Wallpapers")
|
||||
checked: Config.launcher.useFuzzy.wallpapers
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.wallpapers = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Sizes")
|
||||
description: qsTr("Size settings for launcher items")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Item width")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Item height")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Wallpaper width")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Wallpaper height")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Hidden apps")
|
||||
description: qsTr("Applications hidden from launcher")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Total hidden")
|
||||
value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property var ethernetDevice: root.session.ethernet.active
|
||||
|
||||
device: ethernetDevice
|
||||
|
||||
Component.onCompleted: {
|
||||
if (ethernetDevice && ethernetDevice.interface) {
|
||||
Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
|
||||
}
|
||||
}
|
||||
|
||||
onEthernetDeviceChanged: {
|
||||
if (ethernetDevice && ethernetDevice.interface) {
|
||||
Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
|
||||
} else {
|
||||
Nmcli.ethernetDeviceDetails = null;
|
||||
}
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
ConnectionHeader {
|
||||
icon: "cable"
|
||||
title: root.ethernetDevice?.interface ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection status")
|
||||
description: qsTr("Connection settings for this device")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Connected")
|
||||
checked: root.ethernetDevice?.connected ?? false
|
||||
toggle.onToggled: {
|
||||
if (checked) {
|
||||
Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {});
|
||||
} else {
|
||||
if (root.ethernetDevice?.connection) {
|
||||
Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Device properties")
|
||||
description: qsTr("Additional information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Interface")
|
||||
value: root.ethernetDevice?.interface ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Connection")
|
||||
value: root.ethernetDevice?.connection || qsTr("Not connected")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("State")
|
||||
value: root.ethernetDevice?.state ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection information")
|
||||
description: qsTr("Network connection details")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ConnectionInfoSection {
|
||||
deviceDetails: Nmcli.ethernetDeviceDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceList {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
title: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length)
|
||||
description: qsTr("All available ethernet devices")
|
||||
activeItem: session.ethernet.active
|
||||
|
||||
model: Nmcli.ethernetDevices
|
||||
|
||||
headerComponent: Component {
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Settings")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.ethernet.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
if (root.session.ethernet.active)
|
||||
root.session.ethernet.active = null;
|
||||
else {
|
||||
root.session.ethernet.active = root.view.model.get(0)?.modelData ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
id: ethernetItem
|
||||
|
||||
required property var modelData
|
||||
readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.ethernet.active = modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Qt.alpha(modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "cable"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.connected ? 1 : 0
|
||||
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.interface || qsTr("Unknown")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected")
|
||||
color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: modelData.connected ? 500 : 400
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: connectBtn
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
if (modelData.connected && modelData.connection) {
|
||||
Nmcli.disconnectEthernet(modelData.connection, () => {});
|
||||
} else {
|
||||
Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: modelData.connected ? "link_off" : "link"
|
||||
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelected: function (item) {
|
||||
session.ethernet.active = item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
SplitPaneWithDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
activeItem: session.ethernet.active
|
||||
paneIdGenerator: function (item) {
|
||||
return item ? (item.interface || "") : "";
|
||||
}
|
||||
|
||||
leftContent: Component {
|
||||
EthernetList {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightDetailsComponent: Component {
|
||||
EthernetDetails {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightSettingsComponent: Component {
|
||||
StyledFlickable {
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
clip: true
|
||||
|
||||
EthernetSettings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "cable"
|
||||
title: qsTr("Ethernet settings")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Ethernet devices")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Available ethernet devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: ethernetInfo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Total devices")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("%1").arg(Nmcli.ethernetDevices.length)
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Connected devices")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "router"
|
||||
title: qsTr("Network Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Ethernet")
|
||||
description: qsTr("Ethernet device information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Total devices")
|
||||
value: qsTr("%1").arg(Nmcli.ethernetDevices.length)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Connected devices")
|
||||
value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Wireless")
|
||||
description: qsTr("WiFi network settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("WiFi enabled")
|
||||
checked: Nmcli.wifiEnabled
|
||||
toggle.onToggled: {
|
||||
Nmcli.enableWifi(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("VPN")
|
||||
description: qsTr("VPN provider settings")
|
||||
visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("VPN enabled")
|
||||
checked: Config.utilities.vpn.enabled
|
||||
toggle.onToggled: {
|
||||
Config.utilities.vpn.enabled = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Providers")
|
||||
value: qsTr("%1").arg(Config.utilities.vpn.provider.length)
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
text: qsTr("⚙ Manage VPN Providers")
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
|
||||
onClicked: {
|
||||
vpnSettingsDialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Current connection")
|
||||
description: qsTr("Active network connection information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Network")
|
||||
value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected"))
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
visible: Nmcli.active !== null
|
||||
label: qsTr("Signal strength")
|
||||
value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
visible: Nmcli.active !== null
|
||||
label: qsTr("Security")
|
||||
value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
visible: Nmcli.active !== null
|
||||
label: qsTr("Frequency")
|
||||
value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: vpnSettingsDialog
|
||||
|
||||
parent: Overlay.overlay
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(600, parent.width - Appearance.padding.large * 2)
|
||||
height: Math.min(700, parent.height - Appearance.padding.large * 2)
|
||||
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: StyledRect {
|
||||
color: Colours.palette.m3surface
|
||||
radius: Appearance.rounding.large
|
||||
}
|
||||
|
||||
StyledFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large * 1.5
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: vpnSettingsContent.height
|
||||
clip: true
|
||||
|
||||
VpnSettings {
|
||||
id: vpnSettingsContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
SplitPaneLayout {
|
||||
id: splitLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
StyledFlickable {
|
||||
id: leftFlickable
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: leftContent.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: leftFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: leftContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Network")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.wifiEnabled
|
||||
icon: "wifi"
|
||||
accent: "Tertiary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Toggle WiFi")
|
||||
|
||||
onClicked: {
|
||||
Nmcli.toggleWifi(null);
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.scanning
|
||||
icon: "wifi_find"
|
||||
accent: "Secondary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Scan for networks")
|
||||
|
||||
onClicked: {
|
||||
Nmcli.rescanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.ethernet.active && !root.session.network.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Network settings")
|
||||
|
||||
onClicked: {
|
||||
if (root.session.ethernet.active || root.session.network.active) {
|
||||
root.session.ethernet.active = null;
|
||||
root.session.network.active = null;
|
||||
} else {
|
||||
if (Nmcli.ethernetDevices.length > 0) {
|
||||
root.session.ethernet.active = Nmcli.ethernetDevices[0];
|
||||
} else if (Nmcli.networks.length > 0) {
|
||||
root.session.network.active = Nmcli.networks[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: vpnListSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("VPN")
|
||||
expanded: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Component {
|
||||
VpnList {
|
||||
session: root.session
|
||||
showHeader: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: ethernetListSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Ethernet")
|
||||
expanded: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Component {
|
||||
EthernetList {
|
||||
session: root.session
|
||||
showHeader: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: wirelessListSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Wireless")
|
||||
expanded: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Component {
|
||||
WirelessList {
|
||||
session: root.session
|
||||
showHeader: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: Component {
|
||||
Item {
|
||||
id: rightPaneItem
|
||||
|
||||
property var vpnPane: root.session && root.session.vpn ? root.session.vpn.active : null
|
||||
property var ethernetPane: root.session && root.session.ethernet ? root.session.ethernet.active : null
|
||||
property var wirelessPane: root.session && root.session.network ? root.session.network.active : null
|
||||
property var pane: vpnPane || ethernetPane || wirelessPane
|
||||
property string paneId: vpnPane ? ("vpn:" + (vpnPane.name || "")) : (ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings"))
|
||||
property Component targetComponent: settingsComponent
|
||||
property Component nextComponent: settingsComponent
|
||||
|
||||
function getComponentForPane() {
|
||||
if (vpnPane)
|
||||
return vpnDetailsComponent;
|
||||
if (ethernetPane)
|
||||
return ethernetDetailsComponent;
|
||||
if (wirelessPane)
|
||||
return wirelessDetailsComponent;
|
||||
return settingsComponent;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
targetComponent = getComponentForPane();
|
||||
nextComponent = targetComponent;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session && root.session.vpn ? root.session.vpn : null
|
||||
enabled: target !== null
|
||||
|
||||
function onActiveChanged() {
|
||||
// Clear others when VPN is selected
|
||||
if (root.session && root.session.vpn && root.session.vpn.active) {
|
||||
if (root.session.ethernet && root.session.ethernet.active)
|
||||
root.session.ethernet.active = null;
|
||||
if (root.session.network && root.session.network.active)
|
||||
root.session.network.active = null;
|
||||
}
|
||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session && root.session.ethernet ? root.session.ethernet : null
|
||||
enabled: target !== null
|
||||
|
||||
function onActiveChanged() {
|
||||
// Clear others when ethernet is selected
|
||||
if (root.session && root.session.ethernet && root.session.ethernet.active) {
|
||||
if (root.session.vpn && root.session.vpn.active)
|
||||
root.session.vpn.active = null;
|
||||
if (root.session.network && root.session.network.active)
|
||||
root.session.network.active = null;
|
||||
}
|
||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session && root.session.network ? root.session.network : null
|
||||
enabled: target !== null
|
||||
|
||||
function onActiveChanged() {
|
||||
// Clear others when wireless is selected
|
||||
if (root.session && root.session.network && root.session.network.active) {
|
||||
if (root.session.vpn && root.session.vpn.active)
|
||||
root.session.vpn.active = null;
|
||||
if (root.session.ethernet && root.session.ethernet.active)
|
||||
root.session.ethernet.active = null;
|
||||
}
|
||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: rightLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
transformOrigin: Item.Center
|
||||
clip: false
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: rightPaneItem.targetComponent
|
||||
}
|
||||
|
||||
Behavior on paneId {
|
||||
PaneTransition {
|
||||
target: rightLoader
|
||||
propertyActions: [
|
||||
PropertyAction {
|
||||
target: rightPaneItem
|
||||
property: "targetComponent"
|
||||
value: rightPaneItem.nextComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: settingsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: settingsFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: settingsFlickable
|
||||
}
|
||||
|
||||
NetworkSettings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: ethernetDetailsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: ethernetFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: ethernetDetailsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: ethernetFlickable
|
||||
}
|
||||
|
||||
EthernetDetails {
|
||||
id: ethernetDetailsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: wirelessDetailsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: wirelessFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: wirelessDetailsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: wirelessFlickable
|
||||
}
|
||||
|
||||
WirelessDetails {
|
||||
id: wirelessDetailsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: vpnDetailsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: vpnFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: vpnDetailsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: vpnFlickable
|
||||
}
|
||||
|
||||
VpnDetails {
|
||||
id: vpnDetailsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WirelessPasswordDialog {
|
||||
anchors.fill: parent
|
||||
session: root.session
|
||||
z: 1000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property var vpnProvider: root.session.vpn.active
|
||||
readonly property bool providerEnabled: {
|
||||
if (!vpnProvider || vpnProvider.index === undefined)
|
||||
return false;
|
||||
const provider = Config.utilities.vpn.provider[vpnProvider.index];
|
||||
return provider && typeof provider === "object" && provider.enabled === true;
|
||||
}
|
||||
|
||||
device: vpnProvider
|
||||
|
||||
headerComponent: Component {
|
||||
ConnectionHeader {
|
||||
icon: "vpn_key"
|
||||
title: root.vpnProvider?.displayName ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection status")
|
||||
description: qsTr("VPN connection settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Enable this provider")
|
||||
checked: root.providerEnabled
|
||||
toggle.onToggled: {
|
||||
if (!root.vpnProvider)
|
||||
return;
|
||||
const providers = [];
|
||||
const index = root.vpnProvider.index;
|
||||
|
||||
// Copy providers and update enabled state
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
const p = Config.utilities.vpn.provider[i];
|
||||
if (typeof p === "object") {
|
||||
const newProvider = {
|
||||
name: p.name,
|
||||
displayName: p.displayName,
|
||||
interface: p.interface
|
||||
};
|
||||
|
||||
if (checked) {
|
||||
// Enable this one, disable others
|
||||
newProvider.enabled = (i === index);
|
||||
} else {
|
||||
// Just disable this one
|
||||
newProvider.enabled = (i === index) ? false : (p.enabled !== false);
|
||||
}
|
||||
|
||||
providers.push(newProvider);
|
||||
} else {
|
||||
providers.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
visible: root.providerEnabled
|
||||
enabled: !VPN.connecting
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
text: VPN.connected ? qsTr("Disconnect") : qsTr("Connect")
|
||||
|
||||
onClicked: {
|
||||
VPN.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Edit Provider")
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
|
||||
onClicked: {
|
||||
editVpnDialog.editIndex = root.vpnProvider.index;
|
||||
editVpnDialog.providerName = root.vpnProvider.name;
|
||||
editVpnDialog.displayName = root.vpnProvider.displayName;
|
||||
editVpnDialog.interfaceName = root.vpnProvider.interface;
|
||||
editVpnDialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Delete Provider")
|
||||
inactiveColour: Colours.palette.m3errorContainer
|
||||
inactiveOnColour: Colours.palette.m3onErrorContainer
|
||||
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i !== root.vpnProvider.index) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
}
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
root.session.vpn.active = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Provider details")
|
||||
description: qsTr("VPN provider information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Provider")
|
||||
value: root.vpnProvider?.name ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Display name")
|
||||
value: root.vpnProvider?.displayName ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Interface")
|
||||
value: root.vpnProvider?.interface || qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Status")
|
||||
value: {
|
||||
if (!root.providerEnabled)
|
||||
return qsTr("Disabled");
|
||||
if (VPN.connecting)
|
||||
return qsTr("Connecting...");
|
||||
if (VPN.connected)
|
||||
return qsTr("Connected");
|
||||
return qsTr("Enabled (Not connected)");
|
||||
}
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Enabled")
|
||||
value: root.providerEnabled ? qsTr("Yes") : qsTr("No")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// Edit VPN Dialog
|
||||
Popup {
|
||||
id: editVpnDialog
|
||||
|
||||
property int editIndex: -1
|
||||
property string providerName: ""
|
||||
property string displayName: ""
|
||||
property string interfaceName: ""
|
||||
|
||||
parent: Overlay.overlay
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(400, parent.width - Appearance.padding.large * 2)
|
||||
padding: Appearance.padding.large * 1.5
|
||||
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
opacity: 0
|
||||
scale: 0.7
|
||||
|
||||
enter: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
from: 0.7
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.7
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
function closeWithAnimation(): void {
|
||||
close();
|
||||
}
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity)
|
||||
}
|
||||
|
||||
background: StyledRect {
|
||||
color: Colours.palette.m3surfaceContainerHigh
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
level: 3
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Edit VPN Provider")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Display Name")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: displayNameField
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: editVpnDialog.displayName
|
||||
onTextChanged: editVpnDialog.displayName = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Interface (e.g., wg0, torguard)")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: interfaceNameField
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: editVpnDialog.interfaceName
|
||||
onTextChanged: editVpnDialog.interfaceName = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cancel")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: editVpnDialog.closeWithAnimation()
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Save")
|
||||
enabled: editVpnDialog.interfaceName.length > 0
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex];
|
||||
const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true;
|
||||
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i === editVpnDialog.editIndex) {
|
||||
providers.push({
|
||||
name: editVpnDialog.providerName,
|
||||
displayName: editVpnDialog.displayName || editVpnDialog.interfaceName,
|
||||
interface: editVpnDialog.interfaceName,
|
||||
enabled: wasEnabled
|
||||
});
|
||||
} else {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
editVpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
property bool showHeader: true
|
||||
property int pendingSwitchIndex: -1
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Connections {
|
||||
target: VPN
|
||||
function onConnectedChanged() {
|
||||
if (!VPN.connected && root.pendingSwitchIndex >= 0) {
|
||||
const targetIndex = root.pendingSwitchIndex;
|
||||
root.pendingSwitchIndex = -1;
|
||||
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
const p = Config.utilities.vpn.provider[i];
|
||||
if (typeof p === "object") {
|
||||
const newProvider = {
|
||||
name: p.name,
|
||||
displayName: p.displayName,
|
||||
interface: p.interface,
|
||||
enabled: (i === targetIndex)
|
||||
};
|
||||
providers.push(newProvider);
|
||||
} else {
|
||||
providers.push(p);
|
||||
}
|
||||
}
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
|
||||
Qt.callLater(function () {
|
||||
VPN.toggle();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("+ Add VPN Provider")
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
onClicked: {
|
||||
vpnDialog.showProviderSelection();
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
|
||||
interactive: false
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
model: ScriptModel {
|
||||
values: Config.utilities.vpn.provider.map((provider, index) => {
|
||||
const isObject = typeof provider === "object";
|
||||
const name = isObject ? (provider.name || "custom") : String(provider);
|
||||
const displayName = isObject ? (provider.displayName || name) : name;
|
||||
const iface = isObject ? (provider.interface || "") : "";
|
||||
const enabled = isObject ? (provider.enabled === true) : false;
|
||||
|
||||
return {
|
||||
index: index,
|
||||
name: name,
|
||||
displayName: displayName,
|
||||
interface: iface,
|
||||
provider: provider,
|
||||
enabled: enabled
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (root.session && root.session.vpn) {
|
||||
root.session.vpn.active = modelData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.enabled && VPN.connected ? 1 : 0
|
||||
color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.displayName || qsTr("Unknown")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
if (modelData.enabled && VPN.connected)
|
||||
return qsTr("Connected");
|
||||
if (modelData.enabled && VPN.connecting)
|
||||
return qsTr("Connecting...");
|
||||
if (modelData.enabled)
|
||||
return qsTr("Enabled");
|
||||
return qsTr("Disabled");
|
||||
}
|
||||
color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: modelData.enabled && VPN.connected ? 500 : 400
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
enabled: !VPN.connecting
|
||||
function onClicked(): void {
|
||||
const clickedIndex = modelData.index;
|
||||
|
||||
if (modelData.enabled) {
|
||||
VPN.toggle();
|
||||
} else {
|
||||
if (VPN.connected) {
|
||||
root.pendingSwitchIndex = clickedIndex;
|
||||
VPN.toggle();
|
||||
} else {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
const p = Config.utilities.vpn.provider[i];
|
||||
if (typeof p === "object") {
|
||||
const newProvider = {
|
||||
name: p.name,
|
||||
displayName: p.displayName,
|
||||
interface: p.interface,
|
||||
enabled: (i === clickedIndex)
|
||||
};
|
||||
providers.push(newProvider);
|
||||
} else {
|
||||
providers.push(p);
|
||||
}
|
||||
}
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
|
||||
Qt.callLater(function () {
|
||||
VPN.toggle();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: VPN.connected && modelData.enabled ? "link_off" : "link"
|
||||
color: VPN.connected && modelData.enabled ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: "transparent"
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i !== modelData.index) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
}
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: deleteIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "delete"
|
||||
color: Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: vpnDialog
|
||||
|
||||
property string currentState: "selection"
|
||||
property int editIndex: -1
|
||||
property string providerName: ""
|
||||
property string displayName: ""
|
||||
property string interfaceName: ""
|
||||
|
||||
parent: Overlay.overlay
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2)
|
||||
padding: Appearance.padding.large * 1.5
|
||||
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
opacity: 0
|
||||
scale: 0.7
|
||||
|
||||
enter: Transition {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
from: 0.7
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.7
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showProviderSelection(): void {
|
||||
currentState = "selection";
|
||||
open();
|
||||
}
|
||||
|
||||
function closeWithAnimation(): void {
|
||||
close();
|
||||
}
|
||||
|
||||
function showAddForm(providerType: string, defaultDisplayName: string): void {
|
||||
editIndex = -1;
|
||||
providerName = providerType;
|
||||
displayName = defaultDisplayName;
|
||||
interfaceName = "";
|
||||
|
||||
if (currentState === "selection") {
|
||||
transitionToForm.start();
|
||||
} else {
|
||||
currentState = "form";
|
||||
isClosing = false;
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
function showEditForm(index: int): void {
|
||||
const provider = Config.utilities.vpn.provider[index];
|
||||
const isObject = typeof provider === "object";
|
||||
|
||||
editIndex = index;
|
||||
providerName = isObject ? (provider.name || "custom") : String(provider);
|
||||
displayName = isObject ? (provider.displayName || providerName) : providerName;
|
||||
interfaceName = isObject ? (provider.interface || "") : "";
|
||||
|
||||
currentState = "form";
|
||||
open();
|
||||
}
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity)
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
currentState = "selection";
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: transitionToForm
|
||||
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: selectionContent
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
vpnDialog.currentState = "form";
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: formContent
|
||||
property: "opacity"
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: StyledRect {
|
||||
color: Colours.palette.m3surfaceContainerHigh
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
level: 3
|
||||
z: -1
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
implicitHeight: vpnDialog.currentState === "selection" ? selectionContent.implicitHeight : formContent.implicitHeight
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: selectionContent
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
visible: vpnDialog.currentState === "selection"
|
||||
opacity: vpnDialog.currentState === "selection" ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Add VPN Provider")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Choose a provider to add")
|
||||
wrapMode: Text.WordWrap
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("NetBird")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
providers.push({
|
||||
name: "netbird",
|
||||
displayName: "NetBird",
|
||||
interface: "wt0"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
vpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Tailscale")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
providers.push({
|
||||
name: "tailscale",
|
||||
displayName: "Tailscale",
|
||||
interface: "tailscale0"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
vpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cloudflare WARP")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
providers.push({
|
||||
name: "warp",
|
||||
displayName: "Cloudflare WARP",
|
||||
interface: "CloudflareWARP"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
vpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("WireGuard (Custom)")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
vpnDialog.showAddForm("wireguard", "WireGuard");
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cancel")
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
onClicked: vpnDialog.closeWithAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: formContent
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
visible: vpnDialog.currentState === "form"
|
||||
opacity: vpnDialog.currentState === "form" ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName)
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Display Name")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: displayNameField
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: vpnDialog.displayName
|
||||
onTextChanged: vpnDialog.displayName = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Interface (e.g., wg0, torguard)")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: interfaceNameField
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: vpnDialog.interfaceName
|
||||
onTextChanged: vpnDialog.interfaceName = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cancel")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: vpnDialog.closeWithAnimation()
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Save")
|
||||
enabled: vpnDialog.interfaceName.length > 0
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
const newProvider = {
|
||||
name: vpnDialog.providerName,
|
||||
displayName: vpnDialog.displayName || vpnDialog.interfaceName,
|
||||
interface: vpnDialog.interfaceName
|
||||
};
|
||||
|
||||
if (vpnDialog.editIndex >= 0) {
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i === vpnDialog.editIndex) {
|
||||
providers.push(newProvider);
|
||||
} else {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
providers.push(newProvider);
|
||||
}
|
||||
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
vpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "vpn_key"
|
||||
title: qsTr("VPN Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("General")
|
||||
description: qsTr("VPN configuration")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("VPN enabled")
|
||||
checked: Config.utilities.vpn.enabled
|
||||
toggle.onToggled: {
|
||||
Config.utilities.vpn.enabled = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Providers")
|
||||
description: qsTr("Manage VPN providers")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
|
||||
interactive: false
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
model: ScriptModel {
|
||||
values: Config.utilities.vpn.provider.map((provider, index) => {
|
||||
const isObject = typeof provider === "object";
|
||||
const name = isObject ? (provider.name || "custom") : String(provider);
|
||||
const displayName = isObject ? (provider.displayName || name) : name;
|
||||
const iface = isObject ? (provider.interface || "") : "";
|
||||
|
||||
return {
|
||||
index: index,
|
||||
name: name,
|
||||
displayName: displayName,
|
||||
interface: iface,
|
||||
provider: provider,
|
||||
isActive: index === 0
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
color: Colours.tPalette.m3surfaceContainerHigh
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: modelData.isActive ? "vpn_key" : "vpn_key_off"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: modelData.displayName
|
||||
font.weight: modelData.isActive ? 500 : 400
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface"))
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: modelData.isActive ? "arrow_downward" : "arrow_upward"
|
||||
visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1
|
||||
onClicked: {
|
||||
if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) {
|
||||
// Move down
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
const temp = providers[index];
|
||||
providers[index] = providers[index + 1];
|
||||
providers[index + 1] = temp;
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
} else if (!modelData.isActive) {
|
||||
// Make active (move to top)
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
const provider = providers.splice(index, 1)[0];
|
||||
providers.unshift(provider);
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "delete"
|
||||
onClicked: {
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
providers.splice(index, 1);
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: 60
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("+ Add Provider")
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
onClicked: {
|
||||
addProviderDialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Quick Add")
|
||||
description: qsTr("Add common VPN providers")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.smaller
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("+ Add NetBird")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
providers.push({
|
||||
name: "netbird",
|
||||
displayName: "NetBird",
|
||||
interface: "wt0"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("+ Add Tailscale")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
providers.push({
|
||||
name: "tailscale",
|
||||
displayName: "Tailscale",
|
||||
interface: "tailscale0"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("+ Add Cloudflare WARP")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
providers.push({
|
||||
name: "warp",
|
||||
displayName: "Cloudflare WARP",
|
||||
interface: "CloudflareWARP"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property var network: root.session.network.active
|
||||
|
||||
device: network
|
||||
|
||||
Component.onCompleted: {
|
||||
updateDeviceDetails();
|
||||
checkSavedProfile();
|
||||
}
|
||||
|
||||
onNetworkChanged: {
|
||||
connectionUpdateTimer.stop();
|
||||
if (network && network.ssid) {
|
||||
connectionUpdateTimer.start();
|
||||
}
|
||||
updateDeviceDetails();
|
||||
checkSavedProfile();
|
||||
}
|
||||
|
||||
function checkSavedProfile(): void {
|
||||
if (network && network.ssid) {
|
||||
Nmcli.loadSavedConnections(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Nmcli
|
||||
function onActiveChanged() {
|
||||
updateDeviceDetails();
|
||||
}
|
||||
function onWirelessDeviceDetailsChanged() {
|
||||
if (network && network.ssid) {
|
||||
const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
|
||||
if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) {
|
||||
connectionUpdateTimer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionUpdateTimer
|
||||
interval: 500
|
||||
repeat: true
|
||||
running: network && network.ssid
|
||||
onTriggered: {
|
||||
if (network) {
|
||||
const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
|
||||
if (isActive) {
|
||||
if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) {
|
||||
Nmcli.getWirelessDeviceDetails("", () => {});
|
||||
} else {
|
||||
connectionUpdateTimer.stop();
|
||||
}
|
||||
} else {
|
||||
if (Nmcli.wirelessDeviceDetails !== null) {
|
||||
Nmcli.wirelessDeviceDetails = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDeviceDetails(): void {
|
||||
if (network && network.ssid) {
|
||||
const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
|
||||
if (isActive) {
|
||||
Nmcli.getWirelessDeviceDetails("");
|
||||
} else {
|
||||
Nmcli.wirelessDeviceDetails = null;
|
||||
}
|
||||
} else {
|
||||
Nmcli.wirelessDeviceDetails = null;
|
||||
}
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
ConnectionHeader {
|
||||
icon: root.network?.isSecure ? "lock" : "wifi"
|
||||
title: root.network?.ssid ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection status")
|
||||
description: qsTr("Connection settings for this network")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Connected")
|
||||
checked: root.network?.active ?? false
|
||||
toggle.onToggled: {
|
||||
if (checked) {
|
||||
NetworkConnection.handleConnect(root.network, root.session, null);
|
||||
} else {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
visible: {
|
||||
if (!root.network || !root.network.ssid) {
|
||||
return false;
|
||||
}
|
||||
return Nmcli.hasSavedProfile(root.network.ssid);
|
||||
}
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
text: qsTr("Forget Network")
|
||||
|
||||
onClicked: {
|
||||
if (root.network && root.network.ssid) {
|
||||
if (root.network.active) {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
}
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Network properties")
|
||||
description: qsTr("Additional information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("SSID")
|
||||
value: root.network?.ssid ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("BSSID")
|
||||
value: root.network?.bssid ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Signal strength")
|
||||
value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Frequency")
|
||||
value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Security")
|
||||
value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection information")
|
||||
description: qsTr("Network connection details")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ConnectionInfoSection {
|
||||
deviceDetails: Nmcli.wirelessDeviceDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceList {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
title: qsTr("Networks (%1)").arg(Nmcli.networks.length)
|
||||
description: qsTr("All available WiFi networks")
|
||||
activeItem: session.network.active
|
||||
|
||||
titleSuffix: Component {
|
||||
StyledText {
|
||||
visible: Nmcli.scanning
|
||||
text: qsTr("Scanning...")
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Nmcli.networks].sort((a, b) => {
|
||||
if (a.active !== b.active)
|
||||
return b.active - a.active;
|
||||
return b.strength - a.strength;
|
||||
})
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Settings")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.wifiEnabled
|
||||
icon: "wifi"
|
||||
accent: "Tertiary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
Nmcli.toggleWifi(null);
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.scanning
|
||||
icon: "wifi_find"
|
||||
accent: "Secondary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
Nmcli.rescanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.network.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
if (root.session.network.active)
|
||||
root.session.network.active = null;
|
||||
else {
|
||||
root.session.network.active = root.view.model.get(0)?.modelData ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
required property var modelData
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.session.network.active = modelData;
|
||||
if (modelData && modelData.ssid) {
|
||||
root.checkSavedProfileForNetwork(modelData.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure)
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.active ? 1 : 0
|
||||
color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.ssid || qsTr("Unknown")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
if (modelData.active)
|
||||
return qsTr("Connected");
|
||||
if (modelData.isSecure && modelData.security && modelData.security.length > 0) {
|
||||
return modelData.security;
|
||||
}
|
||||
if (modelData.isSecure)
|
||||
return qsTr("Secured");
|
||||
return qsTr("Open");
|
||||
}
|
||||
color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: modelData.active ? 500 : 400
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (modelData.active) {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
} else {
|
||||
NetworkConnection.handleConnect(modelData, root.session, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: modelData.active ? "link_off" : "link"
|
||||
color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelected: function (item) {
|
||||
session.network.active = item;
|
||||
if (item && item.ssid) {
|
||||
checkSavedProfileForNetwork(item.ssid);
|
||||
}
|
||||
}
|
||||
|
||||
function checkSavedProfileForNetwork(ssid: string): void {
|
||||
if (ssid && ssid.length > 0) {
|
||||
Nmcli.loadSavedConnections(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
SplitPaneWithDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
activeItem: session.network.active
|
||||
paneIdGenerator: function (item) {
|
||||
return item ? (item.ssid || item.bssid || "") : "";
|
||||
}
|
||||
|
||||
leftContent: Component {
|
||||
WirelessList {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightDetailsComponent: Component {
|
||||
WirelessDetails {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightSettingsComponent: Component {
|
||||
StyledFlickable {
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
clip: true
|
||||
|
||||
WirelessSettings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
overlayComponent: Component {
|
||||
WirelessPasswordDialog {
|
||||
anchors.fill: parent
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
readonly property var network: {
|
||||
if (session.network.pendingNetwork) {
|
||||
return session.network.pendingNetwork;
|
||||
}
|
||||
if (session.network.active) {
|
||||
return session.network.active;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
property bool isClosing: false
|
||||
visible: session.network.showPasswordDialog || isClosing
|
||||
enabled: session.network.showPasswordDialog && !isClosing
|
||||
focus: enabled
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: dialog
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surface
|
||||
opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0
|
||||
scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: root.isClosing
|
||||
onFinished: {
|
||||
if (root.isClosing) {
|
||||
root.session.network.showPasswordDialog = false;
|
||||
root.isClosing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: dialog
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: dialog
|
||||
property: "scale"
|
||||
to: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: closeDialog()
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "lock"
|
||||
font.pointSize: Appearance.font.size.extraLarge * 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Enter password")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: statusText
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
visible: connectButton.connecting || connectButton.hasError
|
||||
text: {
|
||||
if (connectButton.hasError) {
|
||||
return qsTr("Connection failed. Please check your password and try again.");
|
||||
}
|
||||
if (connectButton.connecting) {
|
||||
return qsTr("Connecting...");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: 400
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.maximumWidth: parent.width - Appearance.padding.large * 2
|
||||
}
|
||||
|
||||
Item {
|
||||
id: passwordContainer
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)
|
||||
|
||||
focus: true
|
||||
Keys.onPressed: event => {
|
||||
if (!activeFocus) {
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
if (connectButton.hasError && event.text && event.text.length > 0) {
|
||||
connectButton.hasError = false;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
if (connectButton.enabled) {
|
||||
connectButton.clicked();
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
passwordBuffer = "";
|
||||
} else {
|
||||
passwordBuffer = passwordBuffer.slice(0, -1);
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.text && event.text.length > 0) {
|
||||
passwordBuffer += event.text;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
property string passwordBuffer: ""
|
||||
|
||||
Connections {
|
||||
target: root.session.network
|
||||
function onShowPasswordDialogChanged(): void {
|
||||
if (root.session.network.showPasswordDialog) {
|
||||
Qt.callLater(() => {
|
||||
passwordContainer.forceActiveFocus();
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.hasError = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onVisibleChanged(): void {
|
||||
if (root.visible) {
|
||||
Qt.callLater(() => {
|
||||
passwordContainer.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.normal
|
||||
color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer
|
||||
border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0)
|
||||
border.color: {
|
||||
if (connectButton.hasError) {
|
||||
return Colours.palette.m3error;
|
||||
}
|
||||
if (passwordContainer.activeFocus) {
|
||||
return Colours.palette.m3primary;
|
||||
}
|
||||
return root.visible ? Colours.palette.m3outline : "transparent";
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
hoverEnabled: false
|
||||
cursorShape: Qt.IBeamCursor
|
||||
|
||||
function onClicked(): void {
|
||||
passwordContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeholder
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Password")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.family: Appearance.font.family.mono
|
||||
opacity: passwordContainer.passwordBuffer ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: charList
|
||||
|
||||
readonly property int fullWidth: count * (implicitHeight + spacing) - spacing
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: fullWidth
|
||||
implicitHeight: Appearance.font.size.normal
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Appearance.spacing.small / 2
|
||||
interactive: false
|
||||
|
||||
model: ScriptModel {
|
||||
values: passwordContainer.passwordBuffer.split("")
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
id: ch
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: charList.implicitHeight
|
||||
|
||||
color: Colours.palette.m3onSurface
|
||||
radius: Appearance.rounding.small / 2
|
||||
|
||||
opacity: 0
|
||||
scale: 0
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
scale = 1;
|
||||
}
|
||||
ListView.onRemove: removeAnim.start()
|
||||
|
||||
SequentialAnimation {
|
||||
id: removeAnim
|
||||
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: true
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: ch
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: ch
|
||||
property: "scale"
|
||||
to: 0.5
|
||||
}
|
||||
}
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
id: cancelButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.closeDialog()
|
||||
}
|
||||
|
||||
TextButton {
|
||||
id: connectButton
|
||||
|
||||
property bool connecting: false
|
||||
property bool hasError: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3primary
|
||||
inactiveOnColour: Colours.palette.m3onPrimary
|
||||
text: qsTr("Connect")
|
||||
enabled: passwordContainer.passwordBuffer.length > 0 && !connecting
|
||||
|
||||
onClicked: {
|
||||
if (!root.network || connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = passwordContainer.passwordBuffer;
|
||||
if (!password || password.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasError = false;
|
||||
connecting = true;
|
||||
enabled = false;
|
||||
text = qsTr("Connecting...");
|
||||
|
||||
NetworkConnection.connectWithPassword(root.network, password, result => {
|
||||
if (result && result.success) {} else if (result && result.needsPassword) {
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
} else {
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connectionMonitor.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkConnectionStatus(): void {
|
||||
if (!root.visible || !connectButton.connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
|
||||
if (isConnected) {
|
||||
connectionSuccessTimer.start();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Nmcli.pendingConnection === null && connectButton.connecting) {
|
||||
if (connectionMonitor.repeatCount > 10) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionMonitor
|
||||
interval: 1000
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
property int repeatCount: 0
|
||||
|
||||
onTriggered: {
|
||||
repeatCount++;
|
||||
checkConnectionStatus();
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
repeatCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionSuccessTimer
|
||||
interval: 500
|
||||
onTriggered: {
|
||||
if (root.visible && Nmcli.active && Nmcli.active.ssid) {
|
||||
const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
if (stillConnected) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Nmcli
|
||||
function onActiveChanged() {
|
||||
if (root.visible) {
|
||||
checkConnectionStatus();
|
||||
}
|
||||
}
|
||||
function onConnectionFailed(ssid: string) {
|
||||
if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
Nmcli.forgetNetwork(ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog(): void {
|
||||
if (isClosing) {
|
||||
return;
|
||||
}
|
||||
|
||||
isClosing = true;
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
connectionMonitor.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "wifi"
|
||||
title: qsTr("Network settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("WiFi status")
|
||||
description: qsTr("General WiFi settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("WiFi enabled")
|
||||
checked: Nmcli.wifiEnabled
|
||||
toggle.onToggled: {
|
||||
Nmcli.enableWifi(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Network information")
|
||||
description: qsTr("Current network connection")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Connected network")
|
||||
value: Nmcli.active ? Nmcli.active.ssid : qsTr("Not connected")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Signal strength")
|
||||
value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Security")
|
||||
value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Frequency")
|
||||
value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property BluetoothDevice active: null
|
||||
property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter
|
||||
property bool editingAdapterName: false
|
||||
property bool fabMenuOpen: false
|
||||
property bool editingDeviceName: false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var active: null
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var active: null
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var active: null
|
||||
property bool showPasswordDialog: false
|
||||
property var pendingNetwork: null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
property var active: null
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property bool clockShowIcon: Config.bar.clock.showIcon ?? true
|
||||
property bool persistent: Config.bar.persistent ?? true
|
||||
property bool showOnHover: Config.bar.showOnHover ?? true
|
||||
property int dragThreshold: Config.bar.dragThreshold ?? 20
|
||||
property bool showAudio: Config.bar.status.showAudio ?? true
|
||||
property bool showMicrophone: Config.bar.status.showMicrophone ?? true
|
||||
property bool showKbLayout: Config.bar.status.showKbLayout ?? false
|
||||
property bool showNetwork: Config.bar.status.showNetwork ?? true
|
||||
property bool showWifi: Config.bar.status.showWifi ?? true
|
||||
property bool showBluetooth: Config.bar.status.showBluetooth ?? true
|
||||
property bool showBattery: Config.bar.status.showBattery ?? true
|
||||
property bool showLockStatus: Config.bar.status.showLockStatus ?? true
|
||||
property bool trayBackground: Config.bar.tray.background ?? false
|
||||
property bool trayCompact: Config.bar.tray.compact ?? false
|
||||
property bool trayRecolour: Config.bar.tray.recolour ?? false
|
||||
property int workspacesShown: Config.bar.workspaces.shown ?? 5
|
||||
property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true
|
||||
property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false
|
||||
property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false
|
||||
property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true
|
||||
property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true
|
||||
property bool scrollVolume: Config.bar.scrollActions.volume ?? true
|
||||
property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true
|
||||
property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true
|
||||
property bool popoutTray: Config.bar.popouts.tray ?? true
|
||||
property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Component.onCompleted: {
|
||||
if (Config.bar.entries) {
|
||||
entriesModel.clear();
|
||||
for (let i = 0; i < Config.bar.entries.length; i++) {
|
||||
const entry = Config.bar.entries[i];
|
||||
entriesModel.append({
|
||||
id: entry.id,
|
||||
enabled: entry.enabled !== false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig(entryIndex, entryEnabled) {
|
||||
Config.bar.clock.showIcon = root.clockShowIcon;
|
||||
Config.bar.persistent = root.persistent;
|
||||
Config.bar.showOnHover = root.showOnHover;
|
||||
Config.bar.dragThreshold = root.dragThreshold;
|
||||
Config.bar.status.showAudio = root.showAudio;
|
||||
Config.bar.status.showMicrophone = root.showMicrophone;
|
||||
Config.bar.status.showKbLayout = root.showKbLayout;
|
||||
Config.bar.status.showNetwork = root.showNetwork;
|
||||
Config.bar.status.showWifi = root.showWifi;
|
||||
Config.bar.status.showBluetooth = root.showBluetooth;
|
||||
Config.bar.status.showBattery = root.showBattery;
|
||||
Config.bar.status.showLockStatus = root.showLockStatus;
|
||||
Config.bar.tray.background = root.trayBackground;
|
||||
Config.bar.tray.compact = root.trayCompact;
|
||||
Config.bar.tray.recolour = root.trayRecolour;
|
||||
Config.bar.workspaces.shown = root.workspacesShown;
|
||||
Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator;
|
||||
Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg;
|
||||
Config.bar.workspaces.showWindows = root.workspacesShowWindows;
|
||||
Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor;
|
||||
Config.bar.scrollActions.workspaces = root.scrollWorkspaces;
|
||||
Config.bar.scrollActions.volume = root.scrollVolume;
|
||||
Config.bar.scrollActions.brightness = root.scrollBrightness;
|
||||
Config.bar.popouts.activeWindow = root.popoutActiveWindow;
|
||||
Config.bar.popouts.tray = root.popoutTray;
|
||||
Config.bar.popouts.statusIcons = root.popoutStatusIcons;
|
||||
|
||||
const entries = [];
|
||||
for (let i = 0; i < entriesModel.count; i++) {
|
||||
const entry = entriesModel.get(i);
|
||||
let enabled = entry.enabled;
|
||||
if (entryIndex !== undefined && i === entryIndex) {
|
||||
enabled = entryEnabled;
|
||||
}
|
||||
entries.push({
|
||||
id: entry.id,
|
||||
enabled: enabled
|
||||
});
|
||||
}
|
||||
Config.bar.entries = entries;
|
||||
Config.save();
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: entriesModel
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: taskbarClippingRect
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
radius: taskbarBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: taskbarLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large
|
||||
|
||||
sourceComponent: taskbarContentComponent
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: taskbarBorder
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal
|
||||
}
|
||||
|
||||
Component {
|
||||
id: taskbarContentComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: sidebarFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: sidebarLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: sidebarFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: sidebarLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Taskbar")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Status Icons")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root
|
||||
|
||||
options: [
|
||||
{
|
||||
label: qsTr("Speakers"),
|
||||
propertyName: "showAudio",
|
||||
onToggled: function (checked) {
|
||||
root.showAudio = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Microphone"),
|
||||
propertyName: "showMicrophone",
|
||||
onToggled: function (checked) {
|
||||
root.showMicrophone = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Keyboard"),
|
||||
propertyName: "showKbLayout",
|
||||
onToggled: function (checked) {
|
||||
root.showKbLayout = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Network"),
|
||||
propertyName: "showNetwork",
|
||||
onToggled: function (checked) {
|
||||
root.showNetwork = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Wifi"),
|
||||
propertyName: "showWifi",
|
||||
onToggled: function (checked) {
|
||||
root.showWifi = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Bluetooth"),
|
||||
propertyName: "showBluetooth",
|
||||
onToggled: function (checked) {
|
||||
root.showBluetooth = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Battery"),
|
||||
propertyName: "showBattery",
|
||||
onToggled: function (checked) {
|
||||
root.showBattery = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Capslock"),
|
||||
propertyName: "showLockStatus",
|
||||
onToggled: function (checked) {
|
||||
root.showLockStatus = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: mainRowLayout
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
id: leftColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Workspaces")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesShownRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Shown")
|
||||
}
|
||||
|
||||
CustomSpinBox {
|
||||
min: 1
|
||||
max: 20
|
||||
value: root.workspacesShown
|
||||
onValueModified: value => {
|
||||
root.workspacesShown = value;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesActiveIndicatorRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Active indicator")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesActiveIndicator
|
||||
onToggled: {
|
||||
root.workspacesActiveIndicator = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesOccupiedBgRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Occupied background")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesOccupiedBg
|
||||
onToggled: {
|
||||
root.workspacesOccupiedBg = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesShowWindowsRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Show windows")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesShowWindows
|
||||
onToggled: {
|
||||
root.workspacesShowWindows = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesPerMonitorRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Per monitor workspaces")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesPerMonitor
|
||||
onToggled: {
|
||||
root.workspacesPerMonitor = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Scroll Actions")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root
|
||||
|
||||
options: [
|
||||
{
|
||||
label: qsTr("Workspaces"),
|
||||
propertyName: "scrollWorkspaces",
|
||||
onToggled: function (checked) {
|
||||
root.scrollWorkspaces = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Volume"),
|
||||
propertyName: "scrollVolume",
|
||||
onToggled: function (checked) {
|
||||
root.scrollVolume = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Brightness"),
|
||||
propertyName: "scrollBrightness",
|
||||
onToggled: function (checked) {
|
||||
root.scrollBrightness = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: middleColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Clock")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Show clock icon")
|
||||
checked: root.clockShowIcon
|
||||
onToggled: checked => {
|
||||
root.clockShowIcon = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Bar Behavior")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Persistent")
|
||||
checked: root.persistent
|
||||
onToggled: checked => {
|
||||
root.persistent = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Show on hover")
|
||||
checked: root.showOnHover
|
||||
onToggled: checked => {
|
||||
root.showOnHover = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Drag threshold")
|
||||
value: root.dragThreshold
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "px"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
root.dragThreshold = Math.round(newValue);
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: rightColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Popouts")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Active window")
|
||||
checked: root.popoutActiveWindow
|
||||
onToggled: checked => {
|
||||
root.popoutActiveWindow = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Tray")
|
||||
checked: root.popoutTray
|
||||
onToggled: checked => {
|
||||
root.popoutTray = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Status icons")
|
||||
checked: root.popoutStatusIcons
|
||||
onToggled: checked => {
|
||||
root.popoutStatusIcons = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Tray Settings")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root
|
||||
|
||||
options: [
|
||||
{
|
||||
label: qsTr("Background"),
|
||||
propertyName: "trayBackground",
|
||||
onToggled: function (checked) {
|
||||
root.trayBackground = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Compact"),
|
||||
propertyName: "trayCompact",
|
||||
onToggled: function (checked) {
|
||||
root.trayCompact = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Recolour"),
|
||||
propertyName: "trayRecolour",
|
||||
onToggled: function (checked) {
|
||||
root.trayRecolour = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user