quickshell and hyprland additions

This commit is contained in:
2026-03-15 13:56:00 +02:00
parent c9c27d1554
commit 1ad06b82a6
509 changed files with 68371 additions and 19 deletions

View File

@@ -0,0 +1,56 @@
import qs.config
import Caelestia
import Quickshell
import Quickshell.Services.UPower
import QtQuick
Scope {
id: root
readonly property list<var> warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level)
Connections {
target: UPower
function onOnBatteryChanged(): void {
if (UPower.onBattery) {
if (Config.utilities.toasts.chargingChanged)
Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off");
} else {
if (Config.utilities.toasts.chargingChanged)
Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power");
for (const level of root.warnLevels)
level.warned = false;
}
}
}
Connections {
target: UPower.displayDevice
function onPercentageChanged(): void {
if (!UPower.onBattery)
return;
const p = UPower.displayDevice.percentage * 100;
for (const level of root.warnLevels) {
if (p <= level.level && !level.warned) {
level.warned = true;
Toaster.toast(level.title ?? qsTr("Battery warning"), level.message ?? qsTr("Battery level is low"), level.icon ?? "battery_android_alert", level.critical ? Toast.Error : Toast.Warning);
}
}
if (!hibernateTimer.running && p <= Config.general.battery.criticalLevel) {
Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error);
hibernateTimer.start();
}
}
}
Timer {
id: hibernateTimer
interval: 5000
onTriggered: Quickshell.execDetached(["systemctl", "hibernate"])
}
}

View File

@@ -0,0 +1,51 @@
pragma ComponentBehavior: Bound
import "lock"
import qs.config
import qs.services
import Caelestia.Internal
import Quickshell
import Quickshell.Wayland
Scope {
id: root
required property Lock lock
readonly property bool enabled: !Config.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying)
function handleIdleAction(action: var): void {
if (!action)
return;
if (action === "lock")
lock.lock.locked = true;
else if (action === "unlock")
lock.lock.locked = false;
else if (typeof action === "string")
Hypr.dispatch(action);
else
Quickshell.execDetached(action);
}
LogindManager {
onAboutToSleep: {
if (Config.general.idle.lockBeforeSleep)
root.lock.lock.locked = true;
}
onLockRequested: root.lock.lock.locked = true
onUnlockRequested: root.lock.lock.unlock()
}
Variants {
model: Config.general.idle.timeouts
IdleMonitor {
required property var modelData
enabled: root.enabled && (modelData.enabled ?? true)
timeout: modelData.timeout
respectInhibitors: modelData.respectInhibitors ?? true
onIsIdleChanged: root.handleIdleAction(isIdle ? modelData.idleAction : modelData.returnAction)
}
}
}

View File

@@ -0,0 +1,142 @@
import qs.components.misc
import qs.modules.controlcenter
import qs.services
import Caelestia
import Quickshell
import Quickshell.Io
Scope {
id: root
property bool launcherInterrupted
readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false
CustomShortcut {
name: "controlCenter"
description: "Open control center"
onPressed: WindowFactory.create()
}
CustomShortcut {
name: "showall"
description: "Toggle launcher, dashboard and osd"
onPressed: {
if (root.hasFullscreen)
return;
const v = Visibilities.getForActive();
v.launcher = v.dashboard = v.osd = v.utilities = !(v.launcher || v.dashboard || v.osd || v.utilities);
}
}
CustomShortcut {
name: "dashboard"
description: "Toggle dashboard"
onPressed: {
if (root.hasFullscreen)
return;
const visibilities = Visibilities.getForActive();
visibilities.dashboard = !visibilities.dashboard;
}
}
CustomShortcut {
name: "session"
description: "Toggle session menu"
onPressed: {
if (root.hasFullscreen)
return;
const visibilities = Visibilities.getForActive();
visibilities.session = !visibilities.session;
}
}
CustomShortcut {
name: "launcher"
description: "Toggle launcher"
onPressed: root.launcherInterrupted = false
onReleased: {
if (!root.launcherInterrupted && !root.hasFullscreen) {
const visibilities = Visibilities.getForActive();
visibilities.launcher = !visibilities.launcher;
}
root.launcherInterrupted = false;
}
}
CustomShortcut {
name: "launcherInterrupt"
description: "Interrupt launcher keybind"
onPressed: root.launcherInterrupted = true
}
CustomShortcut {
name: "sidebar"
description: "Toggle sidebar"
onPressed: {
if (root.hasFullscreen)
return;
const visibilities = Visibilities.getForActive();
visibilities.sidebar = !visibilities.sidebar;
}
}
CustomShortcut {
name: "utilities"
description: "Toggle utilities"
onPressed: {
if (root.hasFullscreen)
return;
const visibilities = Visibilities.getForActive();
visibilities.utilities = !visibilities.utilities;
}
}
IpcHandler {
target: "drawers"
function toggle(drawer: string): void {
if (list().split("\n").includes(drawer)) {
if (root.hasFullscreen && ["launcher", "session", "dashboard"].includes(drawer))
return;
const visibilities = Visibilities.getForActive();
visibilities[drawer] = !visibilities[drawer];
} else {
console.warn(`[IPC] Drawer "${drawer}" does not exist`);
}
}
function list(): string {
const visibilities = Visibilities.getForActive();
return Object.keys(visibilities).filter(k => typeof visibilities[k] === "boolean").join("\n");
}
}
IpcHandler {
target: "controlCenter"
function open(): void {
WindowFactory.create();
}
}
IpcHandler {
target: "toaster"
function info(title: string, message: string, icon: string): void {
Toaster.toast(title, message, icon, Toast.Info);
}
function success(title: string, message: string, icon: string): void {
Toaster.toast(title, message, icon, Toast.Success);
}
function warn(title: string, message: string, icon: string): void {
Toaster.toast(title, message, icon, Toast.Warning);
}
function error(title: string, message: string, icon: string): void {
Toaster.toast(title, message, icon, Toast.Error);
}
}
}

View File

@@ -0,0 +1,124 @@
pragma ComponentBehavior: Bound
import qs.components.containers
import qs.components.misc
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
Scope {
LazyLoader {
id: root
property bool freeze
property bool closing
property bool clipboardOnly
Variants {
model: Quickshell.screens
StyledWindow {
id: win
required property ShellScreen modelData
screen: modelData
name: "area-picker"
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: root.closing ? WlrKeyboardFocus.None : WlrKeyboardFocus.Exclusive
mask: root.closing ? empty : null
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
Region {
id: empty
}
Picker {
loader: root
screen: win.modelData
}
}
}
}
IpcHandler {
target: "picker"
function open(): void {
root.freeze = false;
root.closing = false;
root.clipboardOnly = false;
root.activeAsync = true;
}
function openFreeze(): void {
root.freeze = true;
root.closing = false;
root.clipboardOnly = false;
root.activeAsync = true;
}
function openClip(): void {
root.freeze = false;
root.closing = false;
root.clipboardOnly = true;
root.activeAsync = true;
}
function openFreezeClip(): void {
root.freeze = true;
root.closing = false;
root.clipboardOnly = true;
root.activeAsync = true;
}
}
CustomShortcut {
name: "screenshot"
description: "Open screenshot tool"
onPressed: {
root.freeze = false;
root.closing = false;
root.clipboardOnly = false;
root.activeAsync = true;
}
}
CustomShortcut {
name: "screenshotFreeze"
description: "Open screenshot tool (freeze mode)"
onPressed: {
root.freeze = true;
root.closing = false;
root.clipboardOnly = false;
root.activeAsync = true;
}
}
CustomShortcut {
name: "screenshotClip"
description: "Open screenshot tool (clipboard)"
onPressed: {
root.freeze = false;
root.closing = false;
root.clipboardOnly = true;
root.activeAsync = true;
}
}
CustomShortcut {
name: "screenshotFreezeClip"
description: "Open screenshot tool (freeze mode, clipboard)"
onPressed: {
root.freeze = true;
root.closing = false;
root.clipboardOnly = true;
root.activeAsync = true;
}
}
}

View File

@@ -0,0 +1,300 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import Caelestia
import Quickshell
import Quickshell.Wayland
import QtQuick
import QtQuick.Effects
MouseArea {
id: root
required property LazyLoader loader
required property ShellScreen screen
property bool onClient
property real realBorderWidth: onClient ? (Hypr.options["general:border_size"] ?? 1) : 2
property real realRounding: onClient ? (Hypr.options["decoration:rounding"] ?? 0) : 0
property real ssx
property real ssy
property real sx: 0
property real sy: 0
property real ex: screen.width
property real ey: screen.height
property real rsx: Math.min(sx, ex)
property real rsy: Math.min(sy, ey)
property real sw: Math.abs(sx - ex)
property real sh: Math.abs(sy - ey)
property list<var> clients: {
const mon = Hypr.monitorFor(screen);
if (!mon)
return [];
const special = mon.lastIpcObject.specialWorkspace;
const wsId = special.name ? special.id : mon.activeWorkspace.id;
return Hypr.toplevels.values.filter(c => c.workspace?.id === wsId).sort((a, b) => {
// Pinned first, then fullscreen, then floating, then any other
const ac = a.lastIpcObject;
const bc = b.lastIpcObject;
return (bc.pinned - ac.pinned) || ((bc.fullscreen !== 0) - (ac.fullscreen !== 0)) || (bc.floating - ac.floating);
});
}
function checkClientRects(x: real, y: real): void {
for (const client of clients) {
if (!client)
continue;
let {
at: [cx, cy],
size: [cw, ch]
} = client.lastIpcObject;
cx -= screen.x;
cy -= screen.y;
if (cx <= x && cy <= y && cx + cw >= x && cy + ch >= y) {
onClient = true;
sx = cx;
sy = cy;
ex = cx + cw;
ey = cy + ch;
break;
}
}
}
function save(): void {
const tmpfile = Qt.resolvedUrl(`/tmp/caelestia-picker-${Quickshell.processId}-${Date.now()}.png`);
CUtils.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => {
if (root.loader.clipboardOnly) {
Quickshell.execDetached(["sh", "-c", "wl-copy --type image/png < " + path]);
Quickshell.execDetached(["notify-send", "-a", "caelestia-cli", "-i", path, "Screenshot taken", "Screenshot copied to clipboard"]);
} else {
Quickshell.execDetached(["swappy", "-f", path]);
}
closeAnim.start();
});
}
onClientsChanged: checkClientRects(mouseX, mouseY)
anchors.fill: parent
opacity: 0
hoverEnabled: true
cursorShape: Qt.CrossCursor
Component.onCompleted: {
Hypr.extras.refreshOptions();
// Break binding if frozen
if (loader.freeze)
clients = clients;
opacity = 1;
const c = clients[0];
if (c) {
const cx = c.lastIpcObject.at[0] - screen.x;
const cy = c.lastIpcObject.at[1] - screen.y;
onClient = true;
sx = cx;
sy = cy;
ex = cx + c.lastIpcObject.size[0];
ey = cy + c.lastIpcObject.size[1];
} else {
sx = screen.width / 2 - 100;
sy = screen.height / 2 - 100;
ex = screen.width / 2 + 100;
ey = screen.height / 2 + 100;
}
}
onPressed: event => {
ssx = event.x;
ssy = event.y;
}
onReleased: {
if (closeAnim.running)
return;
if (root.loader.freeze) {
save();
} else {
overlay.visible = border.visible = false;
screencopy.visible = false;
screencopy.active = true;
}
}
onPositionChanged: event => {
const x = event.x;
const y = event.y;
if (pressed) {
onClient = false;
sx = ssx;
sy = ssy;
ex = x;
ey = y;
} else {
checkClientRects(x, y);
}
}
focus: true
Keys.onEscapePressed: closeAnim.start()
SequentialAnimation {
id: closeAnim
PropertyAction {
target: root.loader
property: "closing"
value: true
}
ParallelAnimation {
Anim {
target: root
property: "opacity"
to: 0
duration: Appearance.anim.durations.large
}
ExAnim {
target: root
properties: "rsx,rsy"
to: 0
}
ExAnim {
target: root
property: "sw"
to: root.screen.width
}
ExAnim {
target: root
property: "sh"
to: root.screen.height
}
}
PropertyAction {
target: root.loader
property: "activeAsync"
value: false
}
}
Loader {
id: screencopy
anchors.fill: parent
active: root.loader.freeze
sourceComponent: ScreencopyView {
captureSource: root.screen
onHasContentChanged: {
if (hasContent && !root.loader.freeze) {
overlay.visible = border.visible = true;
root.save();
}
}
}
}
StyledRect {
id: overlay
anchors.fill: parent
color: Colours.palette.m3secondaryContainer
opacity: 0.3
layer.enabled: true
layer.effect: MultiEffect {
maskSource: selectionWrapper
maskEnabled: true
maskInverted: true
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
}
Item {
id: selectionWrapper
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
id: selectionRect
radius: root.realRounding
x: root.rsx
y: root.rsy
implicitWidth: root.sw
implicitHeight: root.sh
}
}
Rectangle {
id: border
color: "transparent"
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
border.width: root.realBorderWidth
border.color: Colours.palette.m3primary
x: selectionRect.x - root.realBorderWidth
y: selectionRect.y - root.realBorderWidth
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
Behavior on border.color {
CAnim {}
}
}
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.large
}
}
Behavior on rsx {
enabled: !root.pressed
ExAnim {}
}
Behavior on rsy {
enabled: !root.pressed
ExAnim {}
}
Behavior on sw {
enabled: !root.pressed
ExAnim {}
}
Behavior on sh {
enabled: !root.pressed
ExAnim {}
}
component ExAnim: Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}

View File

@@ -0,0 +1,153 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.containers
import qs.services
import qs.config
import Quickshell
import Quickshell.Wayland
import QtQuick
Loader {
active: Config.background.enabled
sourceComponent: Variants {
model: Quickshell.screens
StyledWindow {
id: win
required property ShellScreen modelData
screen: modelData
name: "background"
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom
color: Config.background.wallpaperEnabled ? "black" : "transparent"
surfaceFormat.opaque: false
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
Item {
id: behindClock
anchors.fill: parent
Loader {
id: wallpaper
anchors.fill: parent
active: Config.background.wallpaperEnabled
sourceComponent: Wallpaper {}
}
Visualiser {
anchors.fill: parent
screen: win.modelData
wallpaper: wallpaper
}
}
Loader {
id: clockLoader
active: Config.background.desktopClock.enabled
anchors.margins: Appearance.padding.large * 2
anchors.leftMargin: Appearance.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Appearance.padding.smaller, Config.border.thickness)
state: Config.background.desktopClock.position
states: [
State {
name: "top-left"
AnchorChanges {
target: clockLoader
anchors.top: parent.top
anchors.left: parent.left
}
},
State {
name: "top-center"
AnchorChanges {
target: clockLoader
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
}
},
State {
name: "top-right"
AnchorChanges {
target: clockLoader
anchors.top: parent.top
anchors.right: parent.right
}
},
State {
name: "middle-left"
AnchorChanges {
target: clockLoader
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
}
},
State {
name: "middle-center"
AnchorChanges {
target: clockLoader
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
}
},
State {
name: "middle-right"
AnchorChanges {
target: clockLoader
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
}
},
State {
name: "bottom-left"
AnchorChanges {
target: clockLoader
anchors.bottom: parent.bottom
anchors.left: parent.left
}
},
State {
name: "bottom-center"
AnchorChanges {
target: clockLoader
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
}
},
State {
name: "bottom-right"
AnchorChanges {
target: clockLoader
anchors.bottom: parent.bottom
anchors.right: parent.right
}
}
]
transitions: Transition {
AnchorAnimation {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
sourceComponent: DesktopClock {
wallpaper: behindClock
absX: clockLoader.x
absY: clockLoader.y
}
}
}
}
}

View File

@@ -0,0 +1,169 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
Item {
id: root
required property Item wallpaper
required property real absX
required property real absY
property real scale: Config.background.desktopClock.scale
readonly property bool bgEnabled: Config.background.desktopClock.background.enabled
readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur && !GameMode.enabled
readonly property bool invertColors: Config.background.desktopClock.invertColors
readonly property bool useLightSet: Colours.light ? !invertColors : invertColors
readonly property color safePrimary: useLightSet ? Colours.palette.m3primaryContainer : Colours.palette.m3primary
readonly property color safeSecondary: useLightSet ? Colours.palette.m3secondaryContainer : Colours.palette.m3secondary
readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary
implicitWidth: layout.implicitWidth + (Appearance.padding.large * 4 * root.scale)
implicitHeight: layout.implicitHeight + (Appearance.padding.large * 2 * root.scale)
Item {
id: clockContainer
anchors.fill: parent
layer.enabled: Config.background.desktopClock.shadow.enabled
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: Colours.palette.m3shadow
shadowOpacity: Config.background.desktopClock.shadow.opacity
shadowBlur: Config.background.desktopClock.shadow.blur
}
Loader {
anchors.fill: parent
active: root.blurEnabled
sourceComponent: MultiEffect {
source: ShaderEffectSource {
sourceItem: root.wallpaper
sourceRect: Qt.rect(root.absX, root.absY, root.width, root.height)
}
maskSource: backgroundPlate
maskEnabled: true
blurEnabled: true
blur: 1
blurMax: 64
autoPaddingEnabled: false
}
}
StyledRect {
id: backgroundPlate
visible: root.bgEnabled
anchors.fill: parent
radius: Appearance.rounding.large * root.scale
opacity: Config.background.desktopClock.background.opacity
color: Colours.palette.m3surface
layer.enabled: root.blurEnabled
}
RowLayout {
id: layout
anchors.centerIn: parent
spacing: Appearance.spacing.larger * root.scale
RowLayout {
spacing: Appearance.spacing.small
StyledText {
text: Time.hourStr
font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale
font.weight: Font.Bold
color: root.safePrimary
}
StyledText {
text: ":"
font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale
color: root.safeTertiary
opacity: 0.8
Layout.topMargin: -Appearance.padding.large * 1.5 * root.scale
}
StyledText {
text: Time.minuteStr
font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale
font.weight: Font.Bold
color: root.safeSecondary
}
Loader {
Layout.alignment: Qt.AlignTop
Layout.topMargin: Appearance.padding.large * 1.4 * root.scale
active: Config.services.useTwelveHourClock
visible: active
sourceComponent: StyledText {
text: Time.amPmStr
font.pointSize: Appearance.font.size.large * root.scale
color: root.safeSecondary
}
}
}
StyledRect {
Layout.fillHeight: true
Layout.preferredWidth: 4 * root.scale
Layout.topMargin: Appearance.spacing.larger * root.scale
Layout.bottomMargin: Appearance.spacing.larger * root.scale
radius: Appearance.rounding.full
color: root.safePrimary
opacity: 0.8
}
ColumnLayout {
spacing: 0
StyledText {
text: Time.format("MMMM").toUpperCase()
font.pointSize: Appearance.font.size.large * root.scale
font.letterSpacing: 4
font.weight: Font.Bold
color: root.safeSecondary
}
StyledText {
text: Time.format("dd")
font.pointSize: Appearance.font.size.extraLarge * root.scale
font.letterSpacing: 2
font.weight: Font.Medium
color: root.safePrimary
}
StyledText {
text: Time.format("dddd")
font.pointSize: Appearance.font.size.larger * root.scale
font.letterSpacing: 2
color: root.safeSecondary
}
}
}
}
Behavior on scale {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
Behavior on implicitWidth {
Anim {
duration: Appearance.anim.durations.small
}
}
}

View File

@@ -0,0 +1,151 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import Caelestia.Services
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Effects
Item {
id: root
required property ShellScreen screen
required property Item wallpaper
readonly property bool shouldBeActive: Config.background.visualiser.enabled && (!Config.background.visualiser.autoHide || (Hypr.monitorFor(screen)?.activeWorkspace?.toplevels?.values.every(t => t.lastIpcObject?.floating) ?? true))
property real offset: shouldBeActive ? 0 : screen.height * 0.2
opacity: shouldBeActive ? 1 : 0
Loader {
anchors.fill: parent
active: root.opacity > 0 && Config.background.visualiser.blur
sourceComponent: MultiEffect {
source: root.wallpaper
maskSource: wrapper
maskEnabled: true
blurEnabled: true
blur: 1
blurMax: 32
autoPaddingEnabled: false
}
}
Item {
id: wrapper
anchors.fill: parent
layer.enabled: true
Loader {
anchors.fill: parent
anchors.topMargin: root.offset
anchors.bottomMargin: -root.offset
active: root.opacity > 0
sourceComponent: Item {
ServiceRef {
service: Audio.cava
}
Item {
id: content
anchors.fill: parent
anchors.margins: Config.border.thickness
anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing
Side {
content: content
}
Side {
content: content
isRight: true
}
Behavior on anchors.leftMargin {
Anim {}
}
}
}
}
}
Behavior on offset {
Anim {}
}
Behavior on opacity {
Anim {}
}
component Side: Repeater {
id: side
required property Item content
property bool isRight
model: Config.services.visualiserBars
ClippingRectangle {
id: bar
required property int modelData
property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1]))
clip: true
x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0)
implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing
y: side.content.height - height
implicitHeight: bar.value * side.content.height * 0.4
color: "transparent"
topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding
topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding
Rectangle {
topLeftRadius: parent.topLeftRadius
topRightRadius: parent.topRightRadius
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Qt.alpha(Colours.palette.m3primary, 0.7)
Behavior on color {
CAnim {}
}
}
GradientStop {
position: 1
color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7)
Behavior on color {
CAnim {}
}
}
}
anchors.left: parent.left
anchors.right: parent.right
y: parent.height - height
implicitHeight: side.content.height * 0.4
}
Behavior on value {
Anim {
duration: Appearance.anim.durations.small
}
}
}
}
}

View File

@@ -0,0 +1,145 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.images
import qs.components.filedialog
import qs.services
import qs.config
import qs.utils
import QtQuick
Item {
id: root
property string source: Wallpapers.current
property Image current: one
onSourceChanged: {
if (!source)
current = null;
else if (current === one)
two.update();
else
one.update();
}
Component.onCompleted: {
if (source)
Qt.callLater(() => one.update());
}
Loader {
anchors.fill: parent
active: !root.source
sourceComponent: StyledRect {
color: Colours.palette.m3surfaceContainer
Row {
anchors.centerIn: parent
spacing: Appearance.spacing.large
MaterialIcon {
text: "sentiment_stressed"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.extraLarge * 5
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.small
StyledText {
text: qsTr("Wallpaper missing?")
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.extraLarge * 2
font.bold: true
}
StyledRect {
implicitWidth: selectWallText.implicitWidth + Appearance.padding.large * 2
implicitHeight: selectWallText.implicitHeight + Appearance.padding.small * 2
radius: Appearance.rounding.full
color: Colours.palette.m3primary
FileDialog {
id: dialog
title: qsTr("Select a wallpaper")
filterLabel: qsTr("Image files")
filters: Images.validImageExtensions
onAccepted: path => Wallpapers.setWallpaper(path)
}
StateLayer {
radius: parent.radius
color: Colours.palette.m3onPrimary
function onClicked(): void {
dialog.open();
}
}
StyledText {
id: selectWallText
anchors.centerIn: parent
text: qsTr("Set it now!")
color: Colours.palette.m3onPrimary
font.pointSize: Appearance.font.size.large
}
}
}
}
}
}
Img {
id: one
}
Img {
id: two
}
component Img: CachingImage {
id: img
function update(): void {
if (path === root.source)
root.current = this;
else
path = root.source;
}
anchors.fill: parent
opacity: 0
scale: Wallpapers.showPreview ? 1 : 0.8
onStatusChanged: {
if (status === Image.Ready)
root.current = this;
}
states: State {
name: "visible"
when: root.current === img
PropertyChanges {
img.opacity: 1
img.scale: 1
}
}
transitions: Transition {
Anim {
target: img
properties: "opacity,scale"
}
}
}
}

View File

@@ -0,0 +1,205 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.config
import "popouts" as BarPopouts
import "components"
import "components/workspaces"
import Quickshell
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
required property ShellScreen screen
required property PersistentProperties visibilities
required property BarPopouts.Wrapper popouts
readonly property int vPadding: Appearance.padding.large
function closeTray(): void {
if (!Config.bar.tray.compact)
return;
for (let i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i);
if (item?.enabled && item.id === "tray") {
item.item.expanded = false;
}
}
}
function checkPopout(y: real): void {
const ch = childAt(width / 2, y) as WrappedLoader;
if (ch?.id !== "tray")
closeTray();
if (!ch) {
popouts.hasCurrent = false;
return;
}
const id = ch.id;
const top = ch.y;
const item = ch.item;
const itemHeight = item.implicitHeight;
if (id === "statusIcons" && Config.bar.popouts.statusIcons) {
const items = item.items;
const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y);
if (icon) {
popouts.currentName = icon.name;
popouts.currentCenter = Qt.binding(() => icon.mapToItem(root, 0, icon.implicitHeight / 2).y);
popouts.hasCurrent = true;
}
} else if (id === "tray" && Config.bar.popouts.tray) {
if (!Config.bar.tray.compact || (item.expanded && !item.expandIcon.contains(mapToItem(item.expandIcon, item.implicitWidth / 2, y)))) {
const index = Math.floor(((y - top - item.padding * 2 + item.spacing) / item.layout.implicitHeight) * item.items.count);
const trayItem = item.items.itemAt(index);
if (trayItem) {
popouts.currentName = `traymenu${index}`;
popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y);
popouts.hasCurrent = true;
} else {
popouts.hasCurrent = false;
}
} else {
popouts.hasCurrent = false;
item.expanded = true;
}
} else if (id === "activeWindow" && Config.bar.popouts.activeWindow) {
popouts.currentName = id.toLowerCase();
popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y;
popouts.hasCurrent = true;
}
}
function handleWheel(y: real, angleDelta: point): void {
const ch = childAt(width / 2, y) as WrappedLoader;
if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) {
// Workspace scroll
const mon = (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor);
const specialWs = mon?.lastIpcObject.specialWorkspace.name;
if (specialWs?.length > 0)
Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`);
else if (angleDelta.y < 0 || (Config.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1)
Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`);
} else if (y < screen.height / 2 && Config.bar.scrollActions.volume) {
// Volume scroll on top half
if (angleDelta.y > 0)
Audio.incrementVolume();
else if (angleDelta.y < 0)
Audio.decrementVolume();
} else if (Config.bar.scrollActions.brightness) {
// Brightness scroll on bottom half
const monitor = Brightness.getMonitorForScreen(screen);
if (angleDelta.y > 0)
monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement);
else if (angleDelta.y < 0)
monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement);
}
}
spacing: Appearance.spacing.normal
Repeater {
id: repeater
model: Config.bar.entries
DelegateChooser {
role: "id"
DelegateChoice {
roleValue: "spacer"
delegate: WrappedLoader {
Layout.fillHeight: enabled
}
}
DelegateChoice {
roleValue: "logo"
delegate: WrappedLoader {
sourceComponent: OsIcon {}
}
}
DelegateChoice {
roleValue: "workspaces"
delegate: WrappedLoader {
sourceComponent: Workspaces {
screen: root.screen
}
}
}
DelegateChoice {
roleValue: "activeWindow"
delegate: WrappedLoader {
sourceComponent: ActiveWindow {
bar: root
monitor: Brightness.getMonitorForScreen(root.screen)
}
}
}
DelegateChoice {
roleValue: "tray"
delegate: WrappedLoader {
sourceComponent: Tray {}
}
}
DelegateChoice {
roleValue: "clock"
delegate: WrappedLoader {
sourceComponent: Clock {}
}
}
DelegateChoice {
roleValue: "statusIcons"
delegate: WrappedLoader {
sourceComponent: StatusIcons {}
}
}
DelegateChoice {
roleValue: "power"
delegate: WrappedLoader {
sourceComponent: Power {
visibilities: root.visibilities
}
}
}
}
}
component WrappedLoader: Loader {
required property bool enabled
required property string id
required property int index
function findFirstEnabled(): Item {
const count = repeater.count;
for (let i = 0; i < count; i++) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
}
function findLastEnabled(): Item {
for (let i = repeater.count - 1; i >= 0; i--) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
}
Layout.alignment: Qt.AlignHCenter
// Cursed ahh thing to add padding to first and last enabled components
Layout.topMargin: findFirstEnabled() === this ? root.vPadding : 0
Layout.bottomMargin: findLastEnabled() === this ? root.vPadding : 0
visible: enabled
active: enabled
}
}

View File

@@ -0,0 +1,87 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.config
import "popouts" as BarPopouts
import Quickshell
import QtQuick
Item {
id: root
required property ShellScreen screen
required property PersistentProperties visibilities
required property BarPopouts.Wrapper popouts
required property bool disabled
readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness)
readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2
readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness
readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered)
property bool isHovered
function closeTray(): void {
content.item?.closeTray();
}
function checkPopout(y: real): void {
content.item?.checkPopout(y);
}
function handleWheel(y: real, angleDelta: point): void {
content.item?.handleWheel(y, angleDelta);
}
visible: width > Config.border.thickness
implicitWidth: Config.border.thickness
states: State {
name: "visible"
when: root.shouldBeVisible
PropertyChanges {
root.implicitWidth: root.contentWidth
}
}
transitions: [
Transition {
from: ""
to: "visible"
Anim {
target: root
property: "implicitWidth"
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
},
Transition {
from: "visible"
to: ""
Anim {
target: root
property: "implicitWidth"
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
]
Loader {
id: content
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
active: root.shouldBeVisible || root.visible
sourceComponent: Bar {
width: root.contentWidth
screen: root.screen
visibilities: root.visibilities
popouts: root.popouts
}
}
}

View File

@@ -0,0 +1,100 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.utils
import qs.config
import QtQuick
Item {
id: root
required property var bar
required property Brightness.Monitor monitor
property color colour: Colours.palette.m3primary
readonly property int maxHeight: {
const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer");
const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0);
// Length - 2 cause repeater counts as a child
return bar.height - otherHeight - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2;
}
property Title current: text1
clip: true
implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight)
implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin
MaterialIcon {
id: icon
anchors.horizontalCenter: parent.horizontalCenter
animate: true
text: Icons.getAppCategoryIcon(Hypr.activeToplevel?.lastIpcObject.class, "desktop_windows")
color: root.colour
}
Title {
id: text1
}
Title {
id: text2
}
TextMetrics {
id: metrics
text: Hypr.activeToplevel?.title ?? qsTr("Desktop")
font.pointSize: Appearance.font.size.smaller
font.family: Appearance.font.family.mono
elide: Qt.ElideRight
elideWidth: root.maxHeight - icon.height
onTextChanged: {
const next = root.current === text1 ? text2 : text1;
next.text = elidedText;
root.current = next;
}
onElideWidthChanged: root.current.text = elidedText
}
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
component Title: StyledText {
id: text
anchors.horizontalCenter: icon.horizontalCenter
anchors.top: icon.bottom
anchors.topMargin: Appearance.spacing.small
font.pointSize: metrics.font.pointSize
font.family: metrics.font.family
color: root.colour
opacity: root.current === this ? 1 : 0
transform: [
Translate {
x: Config.bar.activeWindow.inverted ? -implicitWidth + text.implicitHeight : 0
},
Rotation {
angle: Config.bar.activeWindow.inverted ? 270 : 90
origin.x: text.implicitHeight / 2
origin.y: text.implicitHeight / 2
}
]
width: implicitHeight
height: implicitWidth
Behavior on opacity {
Anim {}
}
}
}

View File

@@ -0,0 +1,38 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import QtQuick
Column {
id: root
property color colour: Colours.palette.m3tertiary
spacing: Appearance.spacing.small
Loader {
anchors.horizontalCenter: parent.horizontalCenter
active: Config.bar.clock.showIcon
visible: active
sourceComponent: MaterialIcon {
text: "calendar_month"
color: root.colour
}
}
StyledText {
id: text
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: StyledText.AlignHCenter
text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm")
font.pointSize: Appearance.font.size.smaller
font.family: Appearance.font.family.mono
color: root.colour
}
}

View File

@@ -0,0 +1,46 @@
import qs.components.effects
import qs.services
import qs.config
import qs.utils
import QtQuick
import qs.components
Item {
id: root
implicitWidth: Appearance.font.size.large * 1.2
implicitHeight: Appearance.font.size.large * 1.2
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const visibilities = Visibilities.getForActive();
visibilities.launcher = !visibilities.launcher;
}
}
Loader {
anchors.centerIn: parent
sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon
}
Component {
id: caelestiaLogo
Logo {
implicitWidth: Appearance.font.size.large * 1.8
implicitHeight: Appearance.font.size.large * 1.8
}
}
Component {
id: distroIcon
ColouredIcon {
source: SysInfo.osLogo
implicitSize: Appearance.font.size.large * 1.2
colour: Colours.palette.m3tertiary
}
}
}

View File

@@ -0,0 +1,40 @@
import qs.components
import qs.services
import qs.config
import Quickshell
import QtQuick
Item {
id: root
required property PersistentProperties visibilities
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
implicitHeight: icon.implicitHeight
StateLayer {
// Cursed workaround to make the height larger than the parent
anchors.fill: undefined
anchors.centerIn: parent
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
radius: Appearance.rounding.full
function onClicked(): void {
root.visibilities.session = !root.visibilities.session;
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
text: "power_settings_new"
color: Colours.palette.m3error
font.bold: true
font.pointSize: Appearance.font.size.normal
}
}

View File

@@ -0,0 +1,41 @@
import qs.components
import qs.modules.controlcenter
import qs.services
import qs.config
import Quickshell
import QtQuick
Item {
id: root
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
implicitHeight: icon.implicitHeight
StateLayer {
// Cursed workaround to make the height larger than the parent
anchors.fill: undefined
anchors.centerIn: parent
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
radius: Appearance.rounding.full
function onClicked(): void {
WindowFactory.create(null, {
active: "network"
});
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
text: "settings"
color: Colours.palette.m3onSurface
font.bold: true
font.pointSize: Appearance.font.size.normal
}
}

View File

@@ -0,0 +1,41 @@
import qs.components
import qs.modules.controlcenter
import qs.services
import qs.config
import Quickshell
import QtQuick
Item {
id: root
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
implicitHeight: icon.implicitHeight
StateLayer {
// Cursed workaround to make the height larger than the parent
anchors.fill: undefined
anchors.centerIn: parent
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
radius: Appearance.rounding.full
function onClicked(): void {
WindowFactory.create(null, {
active: "network"
});
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -1
text: "settings"
color: Colours.palette.m3onSurface
font.bold: true
font.pointSize: Appearance.font.size.normal
}
}

View File

@@ -0,0 +1,270 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.utils
import qs.config
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Services.UPower
import QtQuick
import QtQuick.Layouts
StyledRect {
id: root
property color colour: Colours.palette.m3secondary
readonly property alias items: iconColumn
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.full
clip: true
implicitWidth: Config.bar.sizes.innerWidth
implicitHeight: iconColumn.implicitHeight + Appearance.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0)
ColumnLayout {
id: iconColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: Appearance.padding.normal
spacing: Appearance.spacing.smaller / 2
// Lock keys status
WrappedLoader {
name: "lockstatus"
active: Config.bar.status.showLockStatus
sourceComponent: ColumnLayout {
spacing: 0
Item {
implicitWidth: capslockIcon.implicitWidth
implicitHeight: Hypr.capsLock ? capslockIcon.implicitHeight : 0
MaterialIcon {
id: capslockIcon
anchors.centerIn: parent
scale: Hypr.capsLock ? 1 : 0.5
opacity: Hypr.capsLock ? 1 : 0
text: "keyboard_capslock_badge"
color: root.colour
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
}
Behavior on implicitHeight {
Anim {}
}
}
Item {
Layout.topMargin: Hypr.capsLock && Hypr.numLock ? iconColumn.spacing : 0
implicitWidth: numlockIcon.implicitWidth
implicitHeight: Hypr.numLock ? numlockIcon.implicitHeight : 0
MaterialIcon {
id: numlockIcon
anchors.centerIn: parent
scale: Hypr.numLock ? 1 : 0.5
opacity: Hypr.numLock ? 1 : 0
text: "looks_one"
color: root.colour
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
}
Behavior on implicitHeight {
Anim {}
}
}
}
}
// Audio icon
WrappedLoader {
name: "audio"
active: Config.bar.status.showAudio
sourceComponent: MaterialIcon {
animate: true
text: Icons.getVolumeIcon(Audio.volume, Audio.muted)
color: root.colour
}
}
// Microphone icon
WrappedLoader {
name: "audio"
active: Config.bar.status.showMicrophone
sourceComponent: MaterialIcon {
animate: true
text: Icons.getMicVolumeIcon(Audio.sourceVolume, Audio.sourceMuted)
color: root.colour
}
}
// Keyboard layout icon
WrappedLoader {
name: "kblayout"
active: Config.bar.status.showKbLayout
sourceComponent: StyledText {
animate: true
text: Hypr.kbLayout
color: root.colour
font.family: Appearance.font.family.mono
}
}
// Network icon
WrappedLoader {
name: "network"
active: Config.bar.status.showNetwork && (!Nmcli.activeEthernet || Config.bar.status.showWifi)
sourceComponent: MaterialIcon {
animate: true
text: Nmcli.active ? Icons.getNetworkIcon(Nmcli.active.strength ?? 0) : "wifi_off"
color: root.colour
}
}
// Ethernet icon
WrappedLoader {
name: "ethernet"
active: Config.bar.status.showNetwork && Nmcli.activeEthernet
sourceComponent: MaterialIcon {
animate: true
text: "cable"
color: root.colour
}
}
// Bluetooth section
WrappedLoader {
Layout.preferredHeight: implicitHeight
name: "bluetooth"
active: Config.bar.status.showBluetooth
sourceComponent: ColumnLayout {
spacing: Appearance.spacing.smaller / 2
// Bluetooth icon
MaterialIcon {
animate: true
text: {
if (!Bluetooth.defaultAdapter?.enabled)
return "bluetooth_disabled";
if (Bluetooth.devices.values.some(d => d.connected))
return "bluetooth_connected";
return "bluetooth";
}
color: root.colour
}
// Connected bluetooth devices
Repeater {
model: ScriptModel {
values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected)
}
MaterialIcon {
id: device
required property BluetoothDevice modelData
animate: true
text: Icons.getBluetoothIcon(modelData?.icon)
color: root.colour
fill: 1
SequentialAnimation on opacity {
running: device.modelData?.state !== BluetoothDeviceState.Connected
alwaysRunToEnd: true
loops: Animation.Infinite
Anim {
from: 1
to: 0
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.standardAccel
}
Anim {
from: 0
to: 1
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
}
}
}
Behavior on Layout.preferredHeight {
Anim {}
}
}
// Battery icon
WrappedLoader {
name: "battery"
active: Config.bar.status.showBattery
sourceComponent: MaterialIcon {
animate: true
text: {
if (!UPower.displayDevice.isLaptopBattery) {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
if (PowerProfiles.profile === PowerProfile.Performance)
return "rocket_launch";
return "balance";
}
const perc = UPower.displayDevice.percentage;
const charging = [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state);
if (perc === 1)
return charging ? "battery_charging_full" : "battery_full";
let level = Math.floor(perc * 7);
if (charging && (level === 4 || level === 1))
level--;
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
}
color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error
fill: 1
}
}
}
component WrappedLoader: Loader {
required property string name
Layout.alignment: Qt.AlignHCenter
visible: active
}
}

View File

@@ -0,0 +1,121 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import Quickshell
import Quickshell.Services.SystemTray
import QtQuick
StyledRect {
id: root
readonly property alias layout: layout
readonly property alias items: items
readonly property alias expandIcon: expandIcon
readonly property int padding: Config.bar.tray.background ? Appearance.padding.normal : Appearance.padding.small
readonly property int spacing: Config.bar.tray.background ? Appearance.spacing.small : 0
property bool expanded
readonly property real nonAnimHeight: {
if (!Config.bar.tray.compact)
return layout.implicitHeight + padding * 2;
return (expanded ? expandIcon.implicitHeight + layout.implicitHeight + spacing : expandIcon.implicitHeight) + padding * 2;
}
clip: true
visible: height > 0
implicitWidth: Config.bar.sizes.innerWidth
implicitHeight: nonAnimHeight
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.full
Column {
id: layout
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: root.padding
spacing: Appearance.spacing.small
opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0
add: Transition {
Anim {
properties: "scale"
from: 0
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
move: Transition {
Anim {
properties: "scale"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
properties: "x,y"
}
}
Repeater {
id: items
model: ScriptModel {
values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id))
}
TrayItem {}
}
Behavior on opacity {
Anim {}
}
}
Loader {
id: expandIcon
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
active: Config.bar.tray.compact && items.count > 0
sourceComponent: Item {
implicitWidth: expandIconInner.implicitWidth
implicitHeight: expandIconInner.implicitHeight - Appearance.padding.small * 2
MaterialIcon {
id: expandIconInner
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Config.bar.tray.background ? Appearance.padding.small : -Appearance.padding.small
text: "expand_less"
font.pointSize: Appearance.font.size.large
rotation: root.expanded ? 180 : 0
Behavior on rotation {
Anim {}
}
Behavior on anchors.bottomMargin {
Anim {}
}
}
}
}
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}

View File

@@ -0,0 +1,34 @@
pragma ComponentBehavior: Bound
import qs.components.effects
import qs.services
import qs.config
import qs.utils
import Quickshell.Services.SystemTray
import QtQuick
MouseArea {
id: root
required property SystemTrayItem modelData
acceptedButtons: Qt.LeftButton | Qt.RightButton
implicitWidth: Appearance.font.size.small * 2
implicitHeight: Appearance.font.size.small * 2
onClicked: event => {
if (event.button === Qt.LeftButton)
modelData.activate();
else
modelData.secondaryActivate();
}
ColouredIcon {
id: icon
anchors.fill: parent
source: Icons.getTrayIcon(root.modelData.id, root.modelData.icon)
colour: Colours.palette.m3secondary
layer.enabled: Config.bar.tray.recolour
}
}

View File

@@ -0,0 +1,98 @@
import qs.components
import qs.components.effects
import qs.services
import qs.config
import QtQuick
StyledRect {
id: root
required property int activeWsId
required property Repeater workspaces
required property Item mask
readonly property int currentWsIdx: {
let i = activeWsId - 1;
while (i < 0)
i += Config.bar.workspaces.shown;
return i % Config.bar.workspaces.shown;
}
property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0
property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0
property real currentSize: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.size ?? 0 : 0
property real offset: Math.min(leading, trailing)
property real size: {
const s = Math.abs(leading - trailing) + currentSize;
if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) {
const ws = workspaces.itemAt(lastWs);
// console.log(ws, lastWs);
return ws ? Math.min(ws.y + ws.size - offset, s) : 0;
}
return s;
}
property int cWs
property int lastWs
onCurrentWsIdxChanged: {
lastWs = cWs;
cWs = currentWsIdx;
}
clip: true
y: offset + mask.y
implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2
implicitHeight: size
radius: Appearance.rounding.full
color: Colours.palette.m3primary
Colouriser {
source: root.mask
sourceColor: Colours.palette.m3onSurface
colorizationColor: Colours.palette.m3onPrimary
x: 0
y: -parent.offset
implicitWidth: root.mask.implicitWidth
implicitHeight: root.mask.implicitHeight
anchors.horizontalCenter: parent.horizontalCenter
}
Behavior on leading {
enabled: Config.bar.workspaces.activeTrail
EAnim {}
}
Behavior on trailing {
enabled: Config.bar.workspaces.activeTrail
EAnim {
duration: Appearance.anim.durations.normal * 2
}
}
Behavior on currentSize {
enabled: Config.bar.workspaces.activeTrail
EAnim {}
}
Behavior on offset {
enabled: !Config.bar.workspaces.activeTrail
EAnim {}
}
Behavior on size {
enabled: !Config.bar.workspaces.activeTrail
EAnim {}
}
component EAnim: Anim {
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}

View File

@@ -0,0 +1,103 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import Quickshell
import QtQuick
Item {
id: root
required property Repeater workspaces
required property var occupied
required property int groupOffset
property list<var> pills: []
onOccupiedChanged: {
if (!occupied)
return;
let count = 0;
const start = groupOffset;
const end = start + Config.bar.workspaces.shown;
for (const [ws, occ] of Object.entries(occupied)) {
if (ws > start && ws <= end && occ) {
const isFirstInGroup = Number(ws) === start + 1;
const isLastInGroup = Number(ws) === end;
if (isFirstInGroup || !occupied[ws - 1]) {
if (pills[count])
pills[count].start = ws;
else
pills.push(pillComp.createObject(root, {
start: ws
}));
count++;
}
if ((isLastInGroup || !occupied[ws + 1]) && pills[count - 1])
pills[count - 1].end = ws;
}
}
if (pills.length > count)
pills.splice(count, pills.length - count).forEach(p => p.destroy());
}
Repeater {
model: ScriptModel {
values: root.pills.filter(p => p)
}
StyledRect {
id: rect
required property var modelData
readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null
readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null
function getWsIdx(ws: int): int {
let i = ws - 1;
while (i < 0)
i += Config.bar.workspaces.shown;
return i % Config.bar.workspaces.shown;
}
anchors.horizontalCenter: root.horizontalCenter
y: (start?.y ?? 0) - 1
implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + 2
implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0
color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
radius: Appearance.rounding.full
scale: 0
Component.onCompleted: scale = 1
Behavior on scale {
Anim {
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
Behavior on y {
Anim {}
}
Behavior on implicitHeight {
Anim {}
}
}
}
component Pill: QtObject {
property int start
property int end
}
Component {
id: pillComp
Pill {}
}
}

View File

@@ -0,0 +1,359 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.effects
import qs.services
import qs.utils
import qs.config
import Quickshell
import Quickshell.Hyprland
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property ShellScreen screen
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name ?? ""
layer.enabled: true
layer.effect: OpacityMask {
maskSource: mask
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
radius: Appearance.rounding.full
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop {
position: 0
color: Qt.rgba(0, 0, 0, 0)
}
GradientStop {
position: 0.3
color: Qt.rgba(0, 0, 0, 1)
}
GradientStop {
position: 0.7
color: Qt.rgba(0, 0, 0, 1)
}
GradientStop {
position: 1
color: Qt.rgba(0, 0, 0, 0)
}
}
}
Rectangle {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
radius: Appearance.rounding.full
implicitHeight: parent.height / 2
opacity: view.contentY > 0 ? 0 : 1
Behavior on opacity {
Anim {}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
radius: Appearance.rounding.full
implicitHeight: parent.height / 2
opacity: view.contentY < view.contentHeight - parent.height + Appearance.padding.small ? 0 : 1
Behavior on opacity {
Anim {}
}
}
}
ListView {
id: view
anchors.fill: parent
spacing: Appearance.spacing.normal
interactive: false
currentIndex: model.values.findIndex(w => w.name === root.activeSpecial)
onCurrentIndexChanged: currentIndex = Qt.binding(() => model.values.findIndex(w => w.name === root.activeSpecial))
model: ScriptModel {
values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!Config.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor))
}
preferredHighlightBegin: 0
preferredHighlightEnd: height
highlightRangeMode: ListView.StrictlyEnforceRange
highlightFollowsCurrentItem: false
highlight: Item {
y: view.currentItem?.y ?? 0
implicitHeight: view.currentItem?.size ?? 0
Behavior on y {
Anim {}
}
}
delegate: ColumnLayout {
id: ws
required property HyprlandWorkspace modelData
readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0)
property int wsId
property string icon
property bool hasWindows
anchors.left: view.contentItem.left
anchors.right: view.contentItem.right
spacing: 0
Component.onCompleted: {
wsId = modelData.id;
icon = Icons.getSpecialWsIcon(modelData.name);
hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0;
}
// Hacky thing cause modelData gets destroyed before the remove anim finishes
Connections {
target: ws.modelData
function onIdChanged(): void {
if (ws.modelData)
ws.wsId = ws.modelData.id;
}
function onNameChanged(): void {
if (ws.modelData)
ws.icon = Icons.getSpecialWsIcon(ws.modelData.name);
}
function onLastIpcObjectChanged(): void {
if (ws.modelData)
ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0;
}
}
Connections {
target: Config.bar.workspaces
function onShowWindowsOnSpecialWorkspacesChanged(): void {
if (ws.modelData)
ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0;
}
}
Loader {
id: label
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2
sourceComponent: ws.icon.length === 1 ? letterComp : iconComp
Component {
id: iconComp
MaterialIcon {
fill: 1
text: ws.icon
verticalAlignment: Qt.AlignVCenter
}
}
Component {
id: letterComp
StyledText {
text: ws.icon
verticalAlignment: Qt.AlignVCenter
}
}
}
Loader {
id: windows
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.preferredHeight: implicitHeight
visible: active
active: ws.hasWindows
sourceComponent: Column {
spacing: 0
add: Transition {
Anim {
properties: "scale"
from: 0
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
move: Transition {
Anim {
properties: "scale"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
properties: "x,y"
}
}
Repeater {
model: ScriptModel {
values: Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId)
}
MaterialIcon {
required property var modelData
grade: 0
text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal")
color: Colours.palette.m3onSurfaceVariant
}
}
}
Behavior on Layout.preferredHeight {
Anim {}
}
}
}
add: Transition {
Anim {
properties: "scale"
from: 0
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
remove: Transition {
Anim {
property: "scale"
to: 0.5
duration: Appearance.anim.durations.small
}
Anim {
property: "opacity"
to: 0
duration: Appearance.anim.durations.small
}
}
move: Transition {
Anim {
properties: "scale"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
properties: "x,y"
}
}
displaced: Transition {
Anim {
properties: "scale"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
properties: "x,y"
}
}
}
Loader {
active: Config.bar.workspaces.activeIndicator
anchors.fill: parent
sourceComponent: Item {
StyledClippingRect {
id: indicator
anchors.left: parent.left
anchors.right: parent.right
y: (view.currentItem?.y ?? 0) - view.contentY
implicitHeight: view.currentItem?.size ?? 0
color: Colours.palette.m3tertiary
radius: Appearance.rounding.full
Colouriser {
source: view
sourceColor: Colours.palette.m3onSurface
colorizationColor: Colours.palette.m3onTertiary
anchors.horizontalCenter: parent.horizontalCenter
x: 0
y: -indicator.y
implicitWidth: view.width
implicitHeight: view.height
}
Behavior on y {
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
Anim {
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
}
}
MouseArea {
property real startY
anchors.fill: view
drag.target: view.contentItem
drag.axis: Drag.YAxis
drag.maximumY: 0
drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small)
onPressed: event => startY = event.y
onClicked: event => {
if (Math.abs(event.y - startY) > drag.threshold)
return;
const ws = view.itemAt(event.x, event.y);
if (ws?.modelData)
Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`);
else
Hypr.dispatch("togglespecialworkspace special");
}
}
}

View File

@@ -0,0 +1,107 @@
import qs.components
import qs.services
import qs.utils
import qs.config
import Quickshell
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
required property int index
required property int activeWsId
required property var occupied
required property int groupOffset
readonly property bool isWorkspace: true // Flag for finding workspace children
// Unanimated prop for others to use as reference
readonly property int size: implicitHeight + (hasWindows ? Appearance.padding.small : 0)
readonly property int ws: groupOffset + index + 1
readonly property bool isOccupied: occupied[ws] ?? false
readonly property bool hasWindows: isOccupied && Config.bar.workspaces.showWindows
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: size
spacing: 0
StyledText {
id: indicator
Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2
animate: true
text: {
const ws = Hypr.workspaces.values.find(w => w.id === root.ws);
const wsName = !ws || ws.name == root.ws ? root.ws : ws.name[0];
let displayName = wsName.toString();
if (Config.bar.workspaces.capitalisation.toLowerCase() === "upper") {
displayName = displayName.toUpperCase();
} else if (Config.bar.workspaces.capitalisation.toLowerCase() === "lower") {
displayName = displayName.toLowerCase();
}
const label = Config.bar.workspaces.label || displayName;
const occupiedLabel = Config.bar.workspaces.occupiedLabel || label;
const activeLabel = Config.bar.workspaces.activeLabel || (root.isOccupied ? occupiedLabel : label);
return root.activeWsId === root.ws ? activeLabel : root.isOccupied ? occupiedLabel : label;
}
color: Config.bar.workspaces.occupiedBg || root.isOccupied || root.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.layer(Colours.palette.m3outlineVariant, 2)
verticalAlignment: Qt.AlignVCenter
}
Loader {
id: windows
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.topMargin: -Config.bar.sizes.innerWidth / 10
visible: active
active: root.hasWindows
sourceComponent: Column {
spacing: 0
add: Transition {
Anim {
properties: "scale"
from: 0
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
}
move: Transition {
Anim {
properties: "scale"
to: 1
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
Anim {
properties: "x,y"
}
}
Repeater {
model: ScriptModel {
values: Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws)
}
MaterialIcon {
required property var modelData
grade: 0
text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal")
color: Colours.palette.m3onSurfaceVariant
}
}
}
}
Behavior on Layout.preferredHeight {
Anim {}
}
}

View File

@@ -0,0 +1,137 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.config
import qs.components
import Quickshell
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
StyledClippingRect {
id: root
required property ShellScreen screen
readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name !== ""
readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId
readonly property var occupied: Hypr.workspaces.values.reduce((acc, curr) => {
acc[curr.id] = curr.lastIpcObject.windows > 0;
return acc;
}, {})
readonly property int groupOffset: Math.floor((activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown
property real blur: onSpecial ? 1 : 0
implicitWidth: Config.bar.sizes.innerWidth
implicitHeight: layout.implicitHeight + Appearance.padding.small * 2
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.full
Item {
anchors.fill: parent
scale: root.onSpecial ? 0.8 : 1
opacity: root.onSpecial ? 0.5 : 1
layer.enabled: root.blur > 0
layer.effect: MultiEffect {
blurEnabled: true
blur: root.blur
blurMax: 32
}
Loader {
active: Config.bar.workspaces.occupiedBg
anchors.fill: parent
anchors.margins: Appearance.padding.small
sourceComponent: OccupiedBg {
workspaces: workspaces
occupied: root.occupied
groupOffset: root.groupOffset
}
}
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: Math.floor(Appearance.spacing.small / 2)
Repeater {
id: workspaces
model: Config.bar.workspaces.shown
Workspace {
activeWsId: root.activeWsId
occupied: root.occupied
groupOffset: root.groupOffset
}
}
}
Loader {
anchors.horizontalCenter: parent.horizontalCenter
active: Config.bar.workspaces.activeIndicator
sourceComponent: ActiveIndicator {
activeWsId: root.activeWsId
workspaces: workspaces
mask: layout
}
}
MouseArea {
anchors.fill: layout
onClicked: event => {
const ws = layout.childAt(event.x, event.y).ws;
if (Hypr.activeWsId !== ws)
Hypr.dispatch(`workspace ${ws}`);
else
Hypr.dispatch("togglespecialworkspace special");
}
}
Behavior on scale {
Anim {}
}
Behavior on opacity {
Anim {}
}
}
Loader {
id: specialWs
anchors.fill: parent
anchors.margins: Appearance.padding.small
active: opacity > 0
scale: root.onSpecial ? 1 : 0.5
opacity: root.onSpecial ? 1 : 0
sourceComponent: SpecialWorkspaces {
screen: root.screen
}
Behavior on scale {
Anim {}
}
Behavior on opacity {
Anim {}
}
}
Behavior on blur {
Anim {
duration: Appearance.anim.durations.small
}
}
}

View File

@@ -0,0 +1,102 @@
import qs.components
import qs.services
import qs.utils
import qs.config
import Quickshell.Widgets
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property Item wrapper
implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2
implicitHeight: child.implicitHeight
Column {
id: child
anchors.centerIn: parent
spacing: Appearance.spacing.normal
RowLayout {
id: detailsRow
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.spacing.normal
IconImage {
id: icon
Layout.alignment: Qt.AlignVCenter
implicitSize: details.implicitHeight
source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? "", "image-missing")
}
ColumnLayout {
id: details
spacing: 0
Layout.fillWidth: true
StyledText {
Layout.fillWidth: true
text: Hypr.activeToplevel?.title ?? ""
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
text: Hypr.activeToplevel?.lastIpcObject.class ?? ""
color: Colours.palette.m3onSurfaceVariant
elide: Text.ElideRight
}
}
Item {
implicitWidth: expandIcon.implicitHeight + Appearance.padding.small * 2
implicitHeight: expandIcon.implicitHeight + Appearance.padding.small * 2
Layout.alignment: Qt.AlignVCenter
StateLayer {
radius: Appearance.rounding.normal
function onClicked(): void {
root.wrapper.detach("winfo");
}
}
MaterialIcon {
id: expandIcon
anchors.centerIn: parent
anchors.horizontalCenterOffset: font.pointSize * 0.05
text: "chevron_right"
font.pointSize: Appearance.font.size.large
}
}
}
ClippingWrapperRectangle {
color: "transparent"
radius: Appearance.rounding.small
ScreencopyView {
id: preview
captureSource: Hypr.activeToplevel?.wayland ?? null
live: visible
constraintSize.width: Config.bar.sizes.windowPreviewSize
constraintSize.height: Config.bar.sizes.windowPreviewSize
}
}
}
}

View File

@@ -0,0 +1,120 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
import qs.services
import qs.config
import Quickshell
import Quickshell.Services.Pipewire
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "../../controlcenter/network"
Item {
id: root
required property var wrapper
implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2
implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2
ButtonGroup {
id: sinks
}
ButtonGroup {
id: sources
}
ColumnLayout {
id: layout
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Output device")
font.weight: 500
}
Repeater {
model: Audio.sinks
StyledRadioButton {
id: control
required property PwNode modelData
ButtonGroup.group: sinks
checked: Audio.sink?.id === modelData.id
onClicked: Audio.setAudioSink(modelData)
text: modelData.description
}
}
StyledText {
Layout.topMargin: Appearance.spacing.smaller
text: qsTr("Input device")
font.weight: 500
}
Repeater {
model: Audio.sources
StyledRadioButton {
required property PwNode modelData
ButtonGroup.group: sources
checked: Audio.source?.id === modelData.id
onClicked: Audio.setAudioSource(modelData)
text: modelData.description
}
}
StyledText {
Layout.topMargin: Appearance.spacing.smaller
Layout.bottomMargin: -Appearance.spacing.small / 2
text: qsTr("Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`)
font.weight: 500
}
CustomMouseArea {
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
onWheel: event => {
if (event.angleDelta.y > 0)
Audio.incrementVolume();
else if (event.angleDelta.y < 0)
Audio.decrementVolume();
}
StyledSlider {
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: parent.implicitHeight
value: Audio.volume
onMoved: Audio.setVolume(value)
Behavior on value {
Anim {}
}
}
}
IconTextButton {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
inactiveColour: Colours.palette.m3primaryContainer
inactiveOnColour: Colours.palette.m3onPrimaryContainer
verticalPadding: Appearance.padding.small
text: qsTr("Open settings")
icon: "settings"
onClicked: root.wrapper.detach("audio")
}
}
}

View File

@@ -0,0 +1,73 @@
import qs.components
import qs.services
import qs.config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
required property bool invertBottomRounding
readonly property real rounding: wrapper.isDetached ? Appearance.rounding.normal : Config.border.rounding
readonly property bool flatten: wrapper.width < rounding * 2
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
property real ibr: invertBottomRounding ? -1 : 1
property real sideRounding: startX > 0 ? -1 : 1
strokeWidth: -1
fillColor: Colours.palette.m3surface
PathArc {
relativeX: root.roundingX
relativeY: root.rounding * root.sideRounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.roundingX * 2
relativeY: 0
}
PathArc {
relativeX: root.roundingX
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 2
}
PathArc {
relativeX: -root.roundingX * root.ibr
relativeY: root.rounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise
}
PathLine {
relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr)
relativeY: 0
}
PathArc {
relativeX: -root.roundingX
relativeY: root.rounding * root.sideRounding
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
}
Behavior on fillColor {
CAnim {}
}
Behavior on ibr {
Anim {}
}
Behavior on sideRounding {
Anim {}
}
}

View File

@@ -0,0 +1,230 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import Quickshell.Services.UPower
import QtQuick
Column {
id: root
spacing: Appearance.spacing.normal
width: Config.bar.sizes.batteryWidth
StyledText {
text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected")
}
StyledText {
function formatSeconds(s: int, fallback: string): string {
const day = Math.floor(s / 86400);
const hr = Math.floor(s / 3600) % 60;
const min = Math.floor(s / 60) % 60;
let comps = [];
if (day > 0)
comps.push(`${day} days`);
if (hr > 0)
comps.push(`${hr} hours`);
if (min > 0)
comps.push(`${min} mins`);
return comps.join(", ") || fallback;
}
text: UPower.displayDevice.isLaptopBattery ? qsTr("Time %1: %2").arg(UPower.onBattery ? "remaining" : "until charged").arg(UPower.onBattery ? formatSeconds(UPower.displayDevice.timeToEmpty, "Calculating...") : formatSeconds(UPower.displayDevice.timeToFull, "Fully charged!")) : qsTr("Power profile: %1").arg(PowerProfile.toString(PowerProfiles.profile))
}
Loader {
anchors.horizontalCenter: parent.horizontalCenter
active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None
height: active ? (item?.implicitHeight ?? 0) : 0
sourceComponent: StyledRect {
implicitWidth: child.implicitWidth + Appearance.padding.normal * 2
implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2
color: Colours.palette.m3error
radius: Appearance.rounding.normal
Column {
id: child
anchors.centerIn: parent
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Appearance.spacing.small
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -font.pointSize / 10
text: "warning"
color: Colours.palette.m3onError
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Performance Degraded")
color: Colours.palette.m3onError
font.family: Appearance.font.family.mono
font.weight: 500
}
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -font.pointSize / 10
text: "warning"
color: Colours.palette.m3onError
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Reason: %1").arg(PerformanceDegradationReason.toString(PowerProfiles.degradationReason))
color: Colours.palette.m3onError
}
}
}
}
StyledRect {
id: profiles
property string current: {
const p = PowerProfiles.profile;
if (p === PowerProfile.PowerSaver)
return saver.icon;
if (p === PowerProfile.Performance)
return perf.icon;
return balance.icon;
}
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2
implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.full
StyledRect {
id: indicator
color: Colours.palette.m3primary
radius: Appearance.rounding.full
state: profiles.current
states: [
State {
name: saver.icon
Fill {
item: saver
}
},
State {
name: balance.icon
Fill {
item: balance
}
},
State {
name: perf.icon
Fill {
item: perf
}
}
]
transitions: Transition {
AnchorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
}
Profile {
id: saver
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.small
profile: PowerProfile.PowerSaver
icon: "energy_savings_leaf"
}
Profile {
id: balance
anchors.centerIn: parent
profile: PowerProfile.Balanced
icon: "balance"
}
Profile {
id: perf
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: Appearance.padding.small
profile: PowerProfile.Performance
icon: "rocket_launch"
}
}
component Fill: AnchorChanges {
required property Item item
target: indicator
anchors.left: item.left
anchors.right: item.right
anchors.top: item.top
anchors.bottom: item.bottom
}
component Profile: Item {
required property string icon
required property int profile
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
StateLayer {
radius: Appearance.rounding.full
color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
function onClicked(): void {
PowerProfiles.profile = parent.profile;
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
text: parent.icon
font.pointSize: Appearance.font.size.large
color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
fill: profiles.current === text ? 1 : 0
Behavior on fill {
Anim {}
}
}
}
}

View File

@@ -0,0 +1,197 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
import qs.services
import qs.config
import qs.utils
import Quickshell
import Quickshell.Bluetooth
import QtQuick
import QtQuick.Layouts
import "../../controlcenter/network"
ColumnLayout {
id: root
required property Item wrapper
spacing: Appearance.spacing.small
StyledText {
Layout.topMargin: Appearance.padding.normal
Layout.rightMargin: Appearance.padding.small
text: qsTr("Bluetooth")
font.weight: 500
}
Toggle {
label: qsTr("Enabled")
checked: Bluetooth.defaultAdapter?.enabled ?? false
toggle.onToggled: {
const adapter = Bluetooth.defaultAdapter;
if (adapter)
adapter.enabled = checked;
}
}
Toggle {
label: qsTr("Discovering")
checked: Bluetooth.defaultAdapter?.discovering ?? false
toggle.onToggled: {
const adapter = Bluetooth.defaultAdapter;
if (adapter)
adapter.discovering = checked;
}
}
StyledText {
Layout.topMargin: Appearance.spacing.small
Layout.rightMargin: Appearance.padding.small
text: {
const devices = Bluetooth.devices.values;
let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s");
const connected = devices.filter(d => d.connected).length;
if (connected > 0)
available += qsTr(" (%1 connected)").arg(connected);
return available;
}
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
Repeater {
model: ScriptModel {
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5)
}
RowLayout {
id: device
required property BluetoothDevice modelData
readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
spacing: Appearance.spacing.small
opacity: 0
scale: 0.7
Component.onCompleted: {
opacity = 1;
scale = 1;
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
MaterialIcon {
text: Icons.getBluetoothIcon(device.modelData.icon)
}
StyledText {
Layout.leftMargin: Appearance.spacing.small / 2
Layout.rightMargin: Appearance.spacing.small / 2
Layout.fillWidth: true
text: device.modelData.name
}
StyledRect {
id: connectBtn
implicitWidth: implicitHeight
implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
radius: Appearance.rounding.full
color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0)
CircularIndicator {
anchors.fill: parent
running: device.loading
}
StateLayer {
color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
disabled: device.loading
function onClicked(): void {
device.modelData.connected = !device.modelData.connected;
}
}
MaterialIcon {
id: connectIcon
anchors.centerIn: parent
animate: true
text: device.modelData.connected ? "link_off" : "link"
color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
opacity: device.loading ? 0 : 1
Behavior on opacity {
Anim {}
}
}
}
Loader {
active: device.modelData.bonded
sourceComponent: Item {
implicitWidth: connectBtn.implicitWidth
implicitHeight: connectBtn.implicitHeight
StateLayer {
radius: Appearance.rounding.full
function onClicked(): void {
device.modelData.forget();
}
}
MaterialIcon {
anchors.centerIn: parent
text: "delete"
}
}
}
}
}
IconTextButton {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.normal
inactiveColour: Colours.palette.m3primaryContainer
inactiveOnColour: Colours.palette.m3onPrimaryContainer
verticalPadding: Appearance.padding.small
text: qsTr("Open settings")
icon: "settings"
onClicked: root.wrapper.detach("bluetooth")
}
component Toggle: RowLayout {
required property string label
property alias checked: toggle.checked
property alias toggle: toggle
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
spacing: Appearance.spacing.normal
StyledText {
Layout.fillWidth: true
text: parent.label
}
StyledSwitch {
id: toggle
}
}
}

View File

@@ -0,0 +1,222 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.config
import Quickshell
import Quickshell.Services.SystemTray
import QtQuick
import "./kblayout"
Item {
id: root
required property Item wrapper
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
readonly property Item current: currentPopout?.item ?? null
anchors.centerIn: parent
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2
Item {
id: content
anchors.fill: parent
anchors.margins: Appearance.padding.large
Popout {
name: "activewindow"
sourceComponent: ActiveWindow {
wrapper: root.wrapper
}
}
Popout {
id: networkPopout
name: "network"
sourceComponent: Network {
wrapper: root.wrapper
view: "wireless"
}
}
Popout {
name: "ethernet"
sourceComponent: Network {
wrapper: root.wrapper
view: "ethernet"
}
}
Popout {
id: passwordPopout
name: "wirelesspassword"
sourceComponent: WirelessPassword {
id: passwordComponent
wrapper: root.wrapper
network: networkPopout.item?.passwordNetwork ?? null
}
Connections {
target: root.wrapper
function onCurrentNameChanged() {
// Update network immediately when password popout becomes active
if (root.wrapper.currentName === "wirelesspassword") {
// Set network immediately if available
if (networkPopout.item && networkPopout.item.passwordNetwork) {
if (passwordPopout.item) {
passwordPopout.item.network = networkPopout.item.passwordNetwork;
}
}
// Also try after a short delay in case networkPopout.item wasn't ready
Qt.callLater(() => {
if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) {
passwordPopout.item.network = networkPopout.item.passwordNetwork;
}
}, 100);
}
}
}
Connections {
target: networkPopout
function onItemChanged() {
// When network popout loads, update password popout if it's active
if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) {
Qt.callLater(() => {
if (networkPopout.item && networkPopout.item.passwordNetwork) {
passwordPopout.item.network = networkPopout.item.passwordNetwork;
}
});
}
}
}
}
Popout {
name: "bluetooth"
sourceComponent: Bluetooth {
wrapper: root.wrapper
}
}
Popout {
name: "battery"
sourceComponent: Battery {}
}
Popout {
name: "audio"
sourceComponent: Audio {
wrapper: root.wrapper
}
}
Popout {
name: "kblayout"
sourceComponent: KbLayout {
wrapper: root.wrapper
}
}
Popout {
name: "lockstatus"
sourceComponent: LockStatus {}
}
Repeater {
model: ScriptModel {
values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id))
}
Popout {
id: trayMenu
required property SystemTrayItem modelData
required property int index
name: `traymenu${index}`
sourceComponent: trayMenuComp
Connections {
target: root.wrapper
function onHasCurrentChanged(): void {
if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) {
trayMenu.sourceComponent = null;
trayMenu.sourceComponent = trayMenuComp;
}
}
}
Component {
id: trayMenuComp
TrayMenu {
popouts: root.wrapper
trayItem: trayMenu.modelData.menu
}
}
}
}
}
component Popout: Loader {
id: popout
required property string name
readonly property bool shouldBeActive: root.wrapper.currentName === name
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
opacity: 0
scale: 0.8
active: false
states: State {
name: "active"
when: popout.shouldBeActive
PropertyChanges {
popout.active: true
popout.opacity: 1
popout.scale: 1
}
}
transitions: [
Transition {
from: "active"
to: ""
SequentialAnimation {
Anim {
properties: "opacity,scale"
duration: Appearance.anim.durations.small
}
PropertyAction {
target: popout
property: "active"
}
}
},
Transition {
from: ""
to: "active"
SequentialAnimation {
PropertyAction {
target: popout
property: "active"
}
Anim {
properties: "opacity,scale"
}
}
}
]
}
}

View File

@@ -0,0 +1,16 @@
import qs.components
import qs.services
import qs.config
import QtQuick.Layouts
ColumnLayout {
spacing: Appearance.spacing.small
StyledText {
text: qsTr("Capslock: %1").arg(Hypr.capsLock ? "Enabled" : "Disabled")
}
StyledText {
text: qsTr("Numlock: %1").arg(Hypr.numLock ? "Enabled" : "Disabled")
}
}

View File

@@ -0,0 +1,388 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
import qs.services
import qs.config
import qs.utils
import Quickshell
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
required property Item wrapper
property string connectingToSsid: ""
property string view: "wireless" // "wireless" or "ethernet"
property var passwordNetwork: null
property bool showPasswordDialog: false
spacing: Appearance.spacing.small
width: Config.bar.sizes.networkWidth
// Wireless section
StyledText {
visible: root.view === "wireless"
Layout.preferredHeight: visible ? implicitHeight : 0
Layout.topMargin: visible ? Appearance.padding.normal : 0
Layout.rightMargin: Appearance.padding.small
text: qsTr("Wireless")
font.weight: 500
}
Toggle {
visible: root.view === "wireless"
Layout.preferredHeight: visible ? implicitHeight : 0
label: qsTr("Enabled")
checked: Nmcli.wifiEnabled
toggle.onToggled: Nmcli.enableWifi(checked)
}
StyledText {
visible: root.view === "wireless"
Layout.preferredHeight: visible ? implicitHeight : 0
Layout.topMargin: visible ? Appearance.spacing.small : 0
Layout.rightMargin: Appearance.padding.small
text: qsTr("%1 networks available").arg(Nmcli.networks.length)
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
Repeater {
visible: root.view === "wireless"
model: ScriptModel {
values: [...Nmcli.networks].sort((a, b) => {
if (a.active !== b.active)
return b.active - a.active;
return b.strength - a.strength;
}).slice(0, 8)
}
RowLayout {
id: networkItem
required property Nmcli.AccessPoint modelData
readonly property bool isConnecting: root.connectingToSsid === modelData.ssid
readonly property bool loading: networkItem.isConnecting
visible: root.view === "wireless"
Layout.preferredHeight: visible ? implicitHeight : 0
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
spacing: Appearance.spacing.small
opacity: 0
scale: 0.7
Component.onCompleted: {
opacity = 1;
scale = 1;
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
MaterialIcon {
text: Icons.getNetworkIcon(networkItem.modelData.strength)
color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
}
MaterialIcon {
visible: networkItem.modelData.isSecure
text: "lock"
font.pointSize: Appearance.font.size.small
}
StyledText {
Layout.leftMargin: Appearance.spacing.small / 2
Layout.rightMargin: Appearance.spacing.small / 2
Layout.fillWidth: true
text: networkItem.modelData.ssid
elide: Text.ElideRight
font.weight: networkItem.modelData.active ? 500 : 400
color: networkItem.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurface
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small
radius: Appearance.rounding.full
color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0)
CircularIndicator {
anchors.fill: parent
running: networkItem.loading
}
StateLayer {
color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
disabled: networkItem.loading || !Nmcli.wifiEnabled
function onClicked(): void {
if (networkItem.modelData.active) {
Nmcli.disconnectFromNetwork();
} else {
root.connectingToSsid = networkItem.modelData.ssid;
NetworkConnection.handleConnect(networkItem.modelData, null, network => {
// Password is required - show password dialog
root.passwordNetwork = network;
root.showPasswordDialog = true;
root.wrapper.currentName = "wirelesspassword";
});
// Clear connecting state if connection succeeds immediately (saved profile)
// This is handled by the onActiveChanged connection below
}
}
}
MaterialIcon {
id: wirelessConnectIcon
anchors.centerIn: parent
animate: true
text: networkItem.modelData.active ? "link_off" : "link"
color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
opacity: networkItem.loading ? 0 : 1
Behavior on opacity {
Anim {}
}
}
}
}
}
StyledRect {
visible: root.view === "wireless"
Layout.preferredHeight: visible ? implicitHeight : 0
Layout.topMargin: visible ? Appearance.spacing.small : 0
Layout.fillWidth: true
implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2
radius: Appearance.rounding.full
color: Colours.palette.m3primaryContainer
StateLayer {
color: Colours.palette.m3onPrimaryContainer
disabled: Nmcli.scanning || !Nmcli.wifiEnabled
function onClicked(): void {
Nmcli.rescanWifi();
}
}
RowLayout {
id: rescanBtn
anchors.centerIn: parent
spacing: Appearance.spacing.small
opacity: Nmcli.scanning ? 0 : 1
MaterialIcon {
id: scanIcon
Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575)
animate: true
text: "wifi_find"
color: Colours.palette.m3onPrimaryContainer
}
StyledText {
Layout.topMargin: -Math.round(scanIcon.fontInfo.pointSize * 0.0575)
text: qsTr("Rescan networks")
color: Colours.palette.m3onPrimaryContainer
}
Behavior on opacity {
Anim {}
}
}
CircularIndicator {
anchors.centerIn: parent
strokeWidth: Appearance.padding.small / 2
bgColour: "transparent"
implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2
running: Nmcli.scanning
}
}
// Ethernet section
StyledText {
visible: root.view === "ethernet"
Layout.preferredHeight: visible ? implicitHeight : 0
Layout.topMargin: visible ? Appearance.padding.normal : 0
Layout.rightMargin: Appearance.padding.small
text: qsTr("Ethernet")
font.weight: 500
}
StyledText {
visible: root.view === "ethernet"
Layout.preferredHeight: visible ? implicitHeight : 0
Layout.topMargin: visible ? Appearance.spacing.small : 0
Layout.rightMargin: Appearance.padding.small
text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length)
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
Repeater {
visible: root.view === "ethernet"
model: ScriptModel {
values: [...Nmcli.ethernetDevices].sort((a, b) => {
if (a.connected !== b.connected)
return b.connected - a.connected;
return (a.interface || "").localeCompare(b.interface || "");
}).slice(0, 8)
}
RowLayout {
id: ethernetItem
required property var modelData
readonly property bool loading: false
visible: root.view === "ethernet"
Layout.preferredHeight: visible ? implicitHeight : 0
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
spacing: Appearance.spacing.small
opacity: 0
scale: 0.7
Component.onCompleted: {
opacity = 1;
scale = 1;
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
MaterialIcon {
text: "cable"
color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
}
StyledText {
Layout.leftMargin: Appearance.spacing.small / 2
Layout.rightMargin: Appearance.spacing.small / 2
Layout.fillWidth: true
text: ethernetItem.modelData.interface || qsTr("Unknown")
elide: Text.ElideRight
font.weight: ethernetItem.modelData.connected ? 500 : 400
color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface
}
StyledRect {
implicitWidth: implicitHeight
implicitHeight: connectIcon.implicitHeight + Appearance.padding.small
radius: Appearance.rounding.full
color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0)
CircularIndicator {
anchors.fill: parent
running: ethernetItem.loading
}
StateLayer {
color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
disabled: ethernetItem.loading
function onClicked(): void {
if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) {
Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {});
} else {
Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {});
}
}
}
MaterialIcon {
id: connectIcon
anchors.centerIn: parent
animate: true
text: ethernetItem.modelData.connected ? "link_off" : "link"
color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
opacity: ethernetItem.loading ? 0 : 1
Behavior on opacity {
Anim {}
}
}
}
}
}
Connections {
target: Nmcli
function onActiveChanged(): void {
if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) {
root.connectingToSsid = "";
// Close password dialog if we successfully connected
if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) {
root.showPasswordDialog = false;
root.passwordNetwork = null;
if (root.wrapper.currentName === "wirelesspassword") {
root.wrapper.currentName = "network";
}
}
}
}
function onScanningChanged(): void {
if (!Nmcli.scanning)
scanIcon.rotation = 0;
}
}
Connections {
target: root.wrapper
function onCurrentNameChanged(): void {
// Clear password network when leaving password dialog
if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) {
root.showPasswordDialog = false;
root.passwordNetwork = null;
}
}
}
component Toggle: RowLayout {
required property string label
property alias checked: toggle.checked
property alias toggle: toggle
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
spacing: Appearance.spacing.normal
StyledText {
Layout.fillWidth: true
text: parent.label
}
StyledSwitch {
id: toggle
}
}
}

View File

@@ -0,0 +1,225 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
StackView {
id: root
required property Item popouts
required property QsMenuHandle trayItem
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
initialItem: SubMenu {
handle: root.trayItem
}
pushEnter: NoAnim {}
pushExit: NoAnim {}
popEnter: NoAnim {}
popExit: NoAnim {}
component NoAnim: Transition {
NumberAnimation {
duration: 0
}
}
component SubMenu: Column {
id: menu
required property QsMenuHandle handle
property bool isSubMenu
property bool shown
padding: Appearance.padding.smaller
spacing: Appearance.spacing.small
opacity: shown ? 1 : 0
scale: shown ? 1 : 0.8
Component.onCompleted: shown = true
StackView.onActivating: shown = true
StackView.onDeactivating: shown = false
StackView.onRemoved: destroy()
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
QsMenuOpener {
id: menuOpener
menu: menu.handle
}
Repeater {
model: menuOpener.children
StyledRect {
id: item
required property QsMenuEntry modelData
implicitWidth: Config.bar.sizes.trayMenuWidth
implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight
radius: Appearance.rounding.full
color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent"
Loader {
id: children
anchors.left: parent.left
anchors.right: parent.right
active: !item.modelData.isSeparator
sourceComponent: Item {
implicitHeight: label.implicitHeight
StateLayer {
anchors.margins: -Appearance.padding.small / 2
anchors.leftMargin: -Appearance.padding.smaller
anchors.rightMargin: -Appearance.padding.smaller
radius: item.radius
disabled: !item.modelData.enabled
function onClicked(): void {
const entry = item.modelData;
if (entry.hasChildren)
root.push(subMenuComp.createObject(null, {
handle: entry,
isSubMenu: true
}));
else {
item.modelData.triggered();
root.popouts.hasCurrent = false;
}
}
}
Loader {
id: icon
anchors.left: parent.left
active: item.modelData.icon !== ""
sourceComponent: IconImage {
implicitSize: label.implicitHeight
source: item.modelData.icon
}
}
StyledText {
id: label
anchors.left: icon.right
anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0
text: labelMetrics.elidedText
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
}
TextMetrics {
id: labelMetrics
text: item.modelData.text
font.pointSize: label.font.pointSize
font.family: label.font.family
elide: Text.ElideRight
elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0)
}
Loader {
id: expand
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
active: item.modelData.hasChildren
sourceComponent: MaterialIcon {
text: "chevron_right"
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
}
}
}
}
}
}
Loader {
active: menu.isSubMenu
sourceComponent: Item {
implicitWidth: back.implicitWidth
implicitHeight: back.implicitHeight + Appearance.spacing.small / 2
Item {
anchors.bottom: parent.bottom
implicitWidth: back.implicitWidth
implicitHeight: back.implicitHeight
StyledRect {
anchors.fill: parent
anchors.margins: -Appearance.padding.small / 2
anchors.leftMargin: -Appearance.padding.smaller
anchors.rightMargin: -Appearance.padding.smaller * 2
radius: Appearance.rounding.full
color: Colours.palette.m3secondaryContainer
StateLayer {
radius: parent.radius
color: Colours.palette.m3onSecondaryContainer
function onClicked(): void {
root.pop();
}
}
}
Row {
id: back
anchors.verticalCenter: parent.verticalCenter
MaterialIcon {
anchors.verticalCenter: parent.verticalCenter
text: "chevron_left"
color: Colours.palette.m3onSecondaryContainer
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Back")
color: Colours.palette.m3onSecondaryContainer
}
}
}
}
}
}
Component {
id: subMenuComp
SubMenu {}
}
}

View File

@@ -0,0 +1,605 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
import qs.services
import qs.config
import qs.utils
import Quickshell
import QtQuick
import QtQuick.Layouts
ColumnLayout {
id: root
required property Item wrapper
property var network: null
property bool isClosing: false
readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword"
Connections {
target: root.wrapper
function onCurrentNameChanged() {
if (root.wrapper.currentName === "wirelesspassword") {
// Update network when popout becomes active
Qt.callLater(() => {
// Try to get network from parent Content's networkPopout
const content = root.parent?.parent?.parent;
if (content) {
const networkPopout = content.children.find(c => c.name === "network");
if (networkPopout && networkPopout.item) {
root.network = networkPopout.item.passwordNetwork;
}
}
// Force focus to password container when popout becomes active
// Use Timer for actual delay to ensure dialog is fully rendered
focusTimer.start();
});
}
}
}
Timer {
id: focusTimer
interval: 150
onTriggered: {
root.forceActiveFocus();
passwordContainer.forceActiveFocus();
}
}
spacing: Appearance.spacing.normal
implicitWidth: 400
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
visible: shouldBeVisible || isClosing
enabled: shouldBeVisible && !isClosing
focus: enabled
Component.onCompleted: {
if (shouldBeVisible) {
// Use Timer for actual delay to ensure dialog is fully rendered
focusTimer.start();
}
}
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
// Use Timer for actual delay to ensure dialog is fully rendered
focusTimer.start();
}
}
Keys.onEscapePressed: closeDialog()
StyledRect {
Layout.fillWidth: true
Layout.preferredWidth: 400
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
radius: Appearance.rounding.normal
color: Colours.tPalette.m3surfaceContainer
visible: root.shouldBeVisible || root.isClosing
opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0
scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
ParallelAnimation {
running: root.isClosing
onFinished: {
if (root.isClosing) {
root.isClosing = false;
}
}
Anim {
target: parent
property: "opacity"
to: 0
}
Anim {
target: parent
property: "scale"
to: 0.7
}
}
Keys.onEscapePressed: root.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 {
id: networkNameText
Layout.alignment: Qt.AlignHCenter
text: {
if (root.network) {
const ssid = root.network.ssid;
if (ssid && ssid.length > 0) {
return qsTr("Network: %1").arg(ssid);
}
}
return qsTr("Network: Unknown");
}
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
}
Timer {
interval: 50
running: root.shouldBeVisible && (!root.network || !root.network.ssid)
repeat: true
property int attempts: 0
onTriggered: {
attempts++;
// Keep trying to get network from Network component
const content = root.parent?.parent?.parent;
if (content) {
const networkPopout = content.children.find(c => c.name === "network");
if (networkPopout && networkPopout.item && networkPopout.item.passwordNetwork) {
root.network = networkPopout.item.passwordNetwork;
}
}
// Stop if we got it or after 20 attempts (1 second)
if ((root.network && root.network.ssid) || attempts >= 20) {
stop();
attempts = 0;
}
}
onRunningChanged: {
if (!running) {
attempts = 0;
}
}
}
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
}
FocusScope {
id: passwordContainer
objectName: "passwordContainer"
Layout.topMargin: Appearance.spacing.large
Layout.fillWidth: true
implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)
focus: true
activeFocusOnTab: true
property string passwordBuffer: ""
Keys.onPressed: event => {
// Ensure we have focus when receiving keyboard input
if (!activeFocus) {
forceActiveFocus();
}
// Clear error when user starts typing
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;
}
}
Connections {
target: root
function onShouldBeVisibleChanged(): void {
if (root.shouldBeVisible) {
// Use Timer for actual delay to ensure focus works correctly
passwordFocusTimer.start();
passwordContainer.passwordBuffer = "";
connectButton.hasError = false;
}
}
}
Timer {
id: passwordFocusTimer
interval: 50
onTriggered: {
passwordContainer.forceActiveFocus();
}
}
Component.onCompleted: {
if (root.shouldBeVisible) {
// Use Timer for actual delay to ensure focus works correctly
passwordFocusTimer.start();
}
}
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.shouldBeVisible ? 1 : 0)
border.color: {
if (connectButton.hasError) {
return Colours.palette.m3error;
}
if (passwordContainer.activeFocus) {
return Colours.palette.m3primary;
}
return root.shouldBeVisible ? Colours.palette.m3outline : "transparent";
}
Behavior on border.color {
CAnim {}
}
Behavior on border.width {
CAnim {}
}
Behavior on color {
CAnim {}
}
}
StateLayer {
hoverEnabled: false
cursorShape: Qt.IBeamCursor
radius: Appearance.rounding.normal
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;
}
// Clear any previous error
hasError = false;
// Set connecting state
connecting = true;
enabled = false;
text = qsTr("Connecting...");
// Connect to network
NetworkConnection.connectWithPassword(root.network, password, result => {
if (result && result.success)
// Connection successful, monitor will handle the rest
{} else if (result && result.needsPassword) {
// Shouldn't happen since we provided password
connectionMonitor.stop();
connecting = false;
hasError = true;
enabled = true;
text = qsTr("Connect");
passwordContainer.passwordBuffer = "";
// Delete the failed connection
if (root.network && root.network.ssid) {
Nmcli.forgetNetwork(root.network.ssid);
}
} else {
// Connection failed immediately - show error
connectionMonitor.stop();
connecting = false;
hasError = true;
enabled = true;
text = qsTr("Connect");
passwordContainer.passwordBuffer = "";
// Delete the failed connection
if (root.network && root.network.ssid) {
Nmcli.forgetNetwork(root.network.ssid);
}
}
});
// Start monitoring connection
connectionMonitor.start();
}
}
}
}
}
function checkConnectionStatus(): void {
if (!root.shouldBeVisible || !connectButton.connecting) {
return;
}
// Check if we're connected to the target network (case-insensitive SSID comparison)
const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
if (isConnected) {
// Successfully connected - give it a moment for network list to update
// Use Timer for actual delay
connectionSuccessTimer.start();
return;
}
// Check for connection failures - if pending connection was cleared but we're not connected
if (Nmcli.pendingConnection === null && connectButton.connecting) {
// Wait a bit more before giving up (allow time for connection to establish)
if (connectionMonitor.repeatCount > 10) {
connectionMonitor.stop();
connectButton.connecting = false;
connectButton.hasError = true;
connectButton.enabled = true;
connectButton.text = qsTr("Connect");
passwordContainer.passwordBuffer = "";
// Delete the failed connection
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++;
root.checkConnectionStatus();
}
onRunningChanged: {
if (!running) {
repeatCount = 0;
}
}
}
Timer {
id: connectionSuccessTimer
interval: 500
onTriggered: {
// Double-check connection is still active
if (root.shouldBeVisible && 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");
// Return to network popout on successful connection
if (root.wrapper.currentName === "wirelesspassword") {
root.wrapper.currentName = "network";
}
closeDialog();
}
}
}
}
Connections {
target: Nmcli
function onActiveChanged() {
if (root.shouldBeVisible) {
root.checkConnectionStatus();
}
}
function onConnectionFailed(ssid: string) {
if (root.shouldBeVisible && 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 = "";
// Delete the failed connection
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();
// Return to network popout
if (root.wrapper.currentName === "wirelesspassword") {
root.wrapper.currentName = "network";
}
}
}

View File

@@ -0,0 +1,215 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.services
import qs.config
import qs.modules.windowinfo
import qs.modules.controlcenter
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import QtQuick
Item {
id: root
required property ShellScreen screen
readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0
readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight
readonly property Item current: content.item?.current ?? null
property string currentName
property real currentCenter
property bool hasCurrent
property string detachedMode
property string queuedMode
readonly property bool isDetached: detachedMode.length > 0
property int animLength: Appearance.anim.durations.normal
property list<real> animCurve: Appearance.anim.curves.emphasized
function detach(mode: string): void {
animLength = Appearance.anim.durations.large;
if (mode === "winfo") {
detachedMode = mode;
} else {
queuedMode = mode;
detachedMode = "any";
}
focus = true;
}
function close(): void {
hasCurrent = false;
animCurve = Appearance.anim.curves.emphasizedAccel;
animLength = Appearance.anim.durations.normal;
detachedMode = "";
animCurve = Appearance.anim.curves.emphasized;
}
visible: width > 0 && height > 0
clip: true
implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
focus: hasCurrent
Keys.onEscapePressed: {
// Forward escape to password popout if active, otherwise close
if (currentName === "wirelesspassword" && content.item) {
const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword");
if (passwordPopout && passwordPopout.item) {
passwordPopout.item.closeDialog();
return;
}
}
close();
}
Keys.onPressed: event => {
// Don't intercept keys when password popout is active - let it handle them
if (currentName === "wirelesspassword") {
event.accepted = false;
}
}
HyprlandFocusGrab {
active: root.isDetached
windows: [QsWindow.window]
onCleared: root.close()
}
Binding {
when: root.isDetached
target: QsWindow.window
property: "WlrLayershell.keyboardFocus"
value: WlrKeyboardFocus.OnDemand
}
Binding {
when: root.hasCurrent && root.currentName === "wirelesspassword"
target: QsWindow.window
property: "WlrLayershell.keyboardFocus"
value: WlrKeyboardFocus.OnDemand
}
Comp {
id: content
shouldBeActive: root.hasCurrent && !root.detachedMode
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
sourceComponent: Content {
wrapper: root
}
}
Comp {
shouldBeActive: root.detachedMode === "winfo"
anchors.centerIn: parent
sourceComponent: WindowInfo {
screen: root.screen
client: Hypr.activeToplevel
}
}
Comp {
shouldBeActive: root.detachedMode === "any"
anchors.centerIn: parent
sourceComponent: ControlCenter {
screen: root.screen
active: root.queuedMode
function close(): void {
root.close();
}
}
}
Behavior on x {
Anim {
duration: root.animLength
easing.bezierCurve: root.animCurve
}
}
Behavior on y {
enabled: root.implicitWidth > 0
Anim {
duration: root.animLength
easing.bezierCurve: root.animCurve
}
}
Behavior on implicitWidth {
Anim {
duration: root.animLength
easing.bezierCurve: root.animCurve
}
}
Behavior on implicitHeight {
enabled: root.implicitWidth > 0
Anim {
duration: root.animLength
easing.bezierCurve: root.animCurve
}
}
component Comp: Loader {
id: comp
property bool shouldBeActive
active: false
opacity: 0
states: State {
name: "active"
when: comp.shouldBeActive
PropertyChanges {
comp.opacity: 1
comp.active: true
}
}
transitions: [
Transition {
from: ""
to: "active"
SequentialAnimation {
PropertyAction {
property: "active"
}
Anim {
property: "opacity"
}
}
},
Transition {
from: "active"
to: ""
SequentialAnimation {
Anim {
property: "opacity"
}
PropertyAction {
property: "active"
}
}
}
]
}
}

View File

@@ -0,0 +1,211 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.components
import qs.components.controls
import qs.services
import qs.config
import qs.utils
import "."
ColumnLayout {
id: root
required property Item wrapper
spacing: Appearance.spacing.small
width: Config.bar.sizes.kbLayoutWidth
KbLayoutModel {
id: kb
}
function refresh() {
kb.refresh();
}
Component.onCompleted: kb.start()
StyledText {
Layout.topMargin: Appearance.padding.normal
Layout.rightMargin: Appearance.padding.small
text: qsTr("Keyboard Layouts")
font.weight: 500
}
ListView {
id: list
model: kb.visibleModel
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
Layout.topMargin: Appearance.spacing.small
clip: true
interactive: true
implicitHeight: Math.min(contentHeight, 320)
visible: kb.visibleModel.count > 0
spacing: Appearance.spacing.small
add: Transition {
NumberAnimation {
properties: "opacity"
from: 0
to: 1
duration: 140
}
NumberAnimation {
properties: "y"
duration: 180
easing.type: Easing.OutCubic
}
}
remove: Transition {
NumberAnimation {
properties: "opacity"
to: 0
duration: 100
}
}
move: Transition {
NumberAnimation {
properties: "y"
duration: 180
easing.type: Easing.OutCubic
}
}
displaced: Transition {
NumberAnimation {
properties: "y"
duration: 180
easing.type: Easing.OutCubic
}
}
delegate: Item {
required property int layoutIndex
required property string label
width: list.width
height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2)
readonly property bool isDisabled: layoutIndex > 3
StateLayer {
id: layer
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: parent.height - 4
radius: Appearance.rounding.full
enabled: !isDisabled
function onClicked(): void {
if (!isDisabled)
kb.switchTo(layoutIndex);
}
}
StyledText {
id: rowText
anchors.verticalCenter: layer.verticalCenter
anchors.left: layer.left
anchors.right: layer.right
anchors.leftMargin: Appearance.padding.small
anchors.rightMargin: Appearance.padding.small
text: label
elide: Text.ElideRight
opacity: isDisabled ? 0.4 : 1.0
}
ToolTip.visible: isDisabled && layer.containsMouse
ToolTip.text: "XKB limitation: maximum 4 layouts allowed"
}
}
Rectangle {
visible: kb.activeLabel.length > 0
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
Layout.topMargin: Appearance.spacing.small
height: 1
color: Colours.palette.m3onSurfaceVariant
opacity: 0.35
}
RowLayout {
id: activeRow
visible: kb.activeLabel.length > 0
Layout.fillWidth: true
Layout.rightMargin: Appearance.padding.small
Layout.topMargin: Appearance.spacing.small
spacing: Appearance.spacing.small
opacity: 1
scale: 1
MaterialIcon {
text: "keyboard"
color: Colours.palette.m3primary
}
StyledText {
Layout.fillWidth: true
text: kb.activeLabel
elide: Text.ElideRight
font.weight: 500
color: Colours.palette.m3primary
}
Connections {
target: kb
function onActiveLabelChanged() {
if (!activeRow.visible)
return;
popIn.restart();
}
}
SequentialAnimation {
id: popIn
running: false
ParallelAnimation {
NumberAnimation {
target: activeRow
property: "opacity"
to: 0.0
duration: 70
}
NumberAnimation {
target: activeRow
property: "scale"
to: 0.92
duration: 70
}
}
ParallelAnimation {
NumberAnimation {
target: activeRow
property: "opacity"
to: 1.0
duration: 160
easing.type: Easing.OutCubic
}
NumberAnimation {
target: activeRow
property: "scale"
to: 1.0
duration: 220
easing.type: Easing.OutBack
}
}
}
}
}

View File

@@ -0,0 +1,216 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.config
import Caelestia
Item {
id: model
visible: false
ListModel {
id: _visibleModel
}
property alias visibleModel: _visibleModel
property string activeLabel: ""
property int activeIndex: -1
function start() {
_xkbXmlBase.running = true;
_getKbLayoutOpt.running = true;
}
function refresh() {
_notifiedLimit = false;
_getKbLayoutOpt.running = true;
}
function switchTo(idx) {
_switchProc.command = ["hyprctl", "switchxkblayout", "all", String(idx)];
_switchProc.running = true;
}
ListModel {
id: _layoutsModel
}
property var _xkbMap: ({})
property bool _notifiedLimit: false
Process {
id: _xkbXmlBase
command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"]
stdout: StdioCollector {
onStreamFinished: _buildXmlMap(text)
}
onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0)
_xkbXmlEvdev.running = true
}
Process {
id: _xkbXmlEvdev
command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"]
stdout: StdioCollector {
onStreamFinished: _buildXmlMap(text)
}
}
function _buildXmlMap(xml) {
const map = {};
const re = /<name>\s*([^<]+?)\s*<\/name>[\s\S]*?<description>\s*([^<]+?)\s*<\/description>/g;
let m;
while ((m = re.exec(xml)) !== null) {
const code = (m[1] || "").trim();
const desc = (m[2] || "").trim();
if (!code || !desc)
continue;
map[code] = _short(desc);
}
if (Object.keys(map).length === 0)
return;
_xkbMap = map;
if (_layoutsModel.count > 0) {
const tmp = [];
for (let i = 0; i < _layoutsModel.count; i++) {
const it = _layoutsModel.get(i);
tmp.push({
layoutIndex: it.layoutIndex,
token: it.token,
label: _pretty(it.token)
});
}
_layoutsModel.clear();
tmp.forEach(t => _layoutsModel.append(t));
_fetchActiveLayouts.running = true;
}
}
function _short(desc) {
const m = desc.match(/^(.*)\((.*)\)$/);
if (!m)
return desc;
const lang = m[1].trim();
const region = m[2].trim();
const code = (region.split(/[,\s-]/)[0] || region).slice(0, 2).toUpperCase();
return `${lang} (${code})`;
}
Process {
id: _getKbLayoutOpt
command: ["hyprctl", "-j", "getoption", "input:kb_layout"]
stdout: StdioCollector {
onStreamFinished: {
try {
const j = JSON.parse(text);
const raw = (j?.str || j?.value || "").toString().trim();
if (raw.length) {
_setLayouts(raw);
_fetchActiveLayouts.running = true;
return;
}
} catch (e) {}
_fetchLayoutsFromDevices.running = true;
}
}
}
Process {
id: _fetchLayoutsFromDevices
command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector {
onStreamFinished: {
try {
const dev = JSON.parse(text);
const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0];
const raw = (kb?.layout || "").trim();
if (raw.length)
_setLayouts(raw);
} catch (e) {}
_fetchActiveLayouts.running = true;
}
}
}
Process {
id: _fetchActiveLayouts
command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector {
onStreamFinished: {
try {
const dev = JSON.parse(text);
const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0];
const idx = kb?.active_layout_index ?? -1;
activeIndex = idx >= 0 ? idx : -1;
activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : "";
} catch (e) {
activeIndex = -1;
activeLabel = "";
}
_rebuildVisible();
}
}
}
Process {
id: _switchProc
onRunningChanged: if (!running)
_fetchActiveLayouts.running = true
}
function _setLayouts(raw) {
const parts = raw.split(",").map(s => s.trim()).filter(Boolean);
_layoutsModel.clear();
const seen = new Set();
let idx = 0;
for (const p of parts) {
if (seen.has(p))
continue;
seen.add(p);
_layoutsModel.append({
layoutIndex: idx,
token: p,
label: _pretty(p)
});
idx++;
}
}
function _rebuildVisible() {
_visibleModel.clear();
let arr = [];
for (let i = 0; i < _layoutsModel.count; i++)
arr.push(_layoutsModel.get(i));
arr = arr.filter(i => i.layoutIndex !== activeIndex);
arr.forEach(i => _visibleModel.append(i));
if (!Config.utilities.toasts.kbLimit)
return;
if (_layoutsModel.count > 4) {
Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning");
}
}
function _pretty(token) {
const code = token.replace(/\(.*\)$/, "").trim();
if (_xkbMap[code])
return code.toUpperCase() + " - " + _xkbMap[code];
return code.toUpperCase() + " - " + code;
}
}

View File

@@ -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
}

View 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
}
}
}
}

View File

@@ -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;
}
}

View 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();
}
}
}
}

View File

@@ -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]
}

View File

@@ -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 {}
}
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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
}
}
}
}
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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
}
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}
}
}
}
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}
}
}
]
}

View File

@@ -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;
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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();
}
}
}
}
}
}

View File

@@ -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();
}
}
}
}
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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
}
}
}
}
]
}

View File

@@ -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(() => {});
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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();
}
}

View File

@@ -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")
}
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,7 @@
import QtQuick
QtObject {
id: root
property var active: null
}

View File

@@ -0,0 +1,7 @@
import QtQuick
QtObject {
id: root
property var active: null
}

View File

@@ -0,0 +1,9 @@
import QtQuick
QtObject {
id: root
property var active: null
property bool showPasswordDialog: false
property var pendingNetwork: null
}

View File

@@ -0,0 +1,5 @@
import QtQuick
QtObject {
property var active: null
}

View File

@@ -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();
}
}
]
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,66 @@
import qs.components
import qs.services
import qs.config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
required property Wrapper wrapper
readonly property real rounding: Config.border.rounding
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
strokeWidth: -1
fillColor: Colours.palette.m3surface
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
relativeX: root.rounding
relativeY: root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
relativeX: root.rounding
relativeY: -root.roundingY
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
}
Behavior on fillColor {
CAnim {}
}
}

View File

@@ -0,0 +1,152 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.filedialog
import qs.config
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
Item {
id: root
required property PersistentProperties visibilities
required property PersistentProperties state
required property FileDialog facePicker
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2
implicitWidth: nonAnimWidth
implicitHeight: nonAnimHeight
Tabs {
id: tabs
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Appearance.padding.normal
anchors.margins: Appearance.padding.large
nonAnimWidth: root.nonAnimWidth - anchors.margins * 2
state: root.state
}
ClippingRectangle {
id: viewWrapper
anchors.top: tabs.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.large
radius: Appearance.rounding.normal
color: "transparent"
Flickable {
id: view
readonly property int currentIndex: root.state.currentTab
readonly property Item currentItem: row.children[currentIndex]
anchors.fill: parent
flickableDirection: Flickable.HorizontalFlick
implicitWidth: currentItem.implicitWidth
implicitHeight: currentItem.implicitHeight
contentX: currentItem.x
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight
onContentXChanged: {
if (!moving)
return;
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 2)
root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 2)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
}
onDragEnded: {
const x = contentX - currentItem.x;
if (x > currentItem.implicitWidth / 10)
root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1);
else if (x < -currentItem.implicitWidth / 10)
root.state.currentTab = Math.max(root.state.currentTab - 1, 0);
else
contentX = Qt.binding(() => currentItem.x);
}
RowLayout {
id: row
Pane {
index: 0
sourceComponent: Dash {
visibilities: root.visibilities
state: root.state
facePicker: root.facePicker
}
}
Pane {
index: 1
sourceComponent: Media {
visibilities: root.visibilities
}
}
Pane {
index: 2
sourceComponent: Performance {}
}
Pane {
index: 3
sourceComponent: Weather {}
}
}
Behavior on contentX {
Anim {}
}
}
}
Behavior on implicitWidth {
Anim {
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on implicitHeight {
Anim {
duration: Appearance.anim.durations.large
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
component Pane: Loader {
id: pane
required property int index
Layout.alignment: Qt.AlignTop
Component.onCompleted: active = Qt.binding(() => {
// Always keep current tab loaded
if (pane.index === view.currentIndex)
return true;
const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth);
const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth);
return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth);
})
}
}

View File

@@ -0,0 +1,105 @@
import qs.components
import qs.components.filedialog
import qs.services
import qs.config
import "dash"
import Quickshell
import QtQuick.Layouts
GridLayout {
id: root
required property PersistentProperties visibilities
required property PersistentProperties state
required property FileDialog facePicker
rowSpacing: Appearance.spacing.normal
columnSpacing: Appearance.spacing.normal
Rect {
Layout.column: 2
Layout.columnSpan: 3
Layout.preferredWidth: user.implicitWidth
Layout.preferredHeight: user.implicitHeight
radius: Appearance.rounding.large
User {
id: user
visibilities: root.visibilities
state: root.state
facePicker: root.facePicker
}
}
Rect {
Layout.row: 0
Layout.columnSpan: 2
Layout.preferredWidth: Config.dashboard.sizes.weatherWidth
Layout.fillHeight: true
radius: Appearance.rounding.large * 1.5
Weather {}
}
Rect {
Layout.row: 1
Layout.preferredWidth: dateTime.implicitWidth
Layout.fillHeight: true
radius: Appearance.rounding.normal
DateTime {
id: dateTime
}
}
Rect {
Layout.row: 1
Layout.column: 1
Layout.columnSpan: 3
Layout.fillWidth: true
Layout.preferredHeight: calendar.implicitHeight
radius: Appearance.rounding.large
Calendar {
id: calendar
state: root.state
}
}
Rect {
Layout.row: 1
Layout.column: 4
Layout.preferredWidth: resources.implicitWidth
Layout.fillHeight: true
radius: Appearance.rounding.normal
Resources {
id: resources
}
}
Rect {
Layout.row: 0
Layout.column: 5
Layout.rowSpan: 2
Layout.preferredWidth: media.implicitWidth
Layout.fillHeight: true
radius: Appearance.rounding.large * 2
Media {
id: media
}
}
component Rect: StyledRect {
color: Colours.tPalette.m3surfaceContainer
}
}

View File

@@ -0,0 +1,403 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.components.controls
import qs.services
import qs.utils
import qs.config
import Caelestia.Services
import Quickshell
import Quickshell.Services.Mpris
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
Item {
id: root
required property PersistentProperties visibilities
property real playerProgress: {
const active = Players.active;
return active?.length ? active.position / active.length : 0;
}
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
const hours = Math.floor(length / 3600);
const mins = Math.floor((length % 3600) / 60);
const secs = Math.floor(length % 60).toString().padStart(2, "0");
if (hours > 0)
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2
implicitHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2
Behavior on playerProgress {
Anim {
duration: Appearance.anim.durations.large
}
}
Timer {
running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged()
}
ServiceRef {
service: Audio.cava
}
ServiceRef {
service: Audio.beatTracker
}
Shape {
id: visualiser
readonly property real centerX: width / 2
readonly property real centerY: height / 2
readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small
readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small
property color colour: Colours.palette.m3primary
anchors.fill: cover
anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize
asynchronous: true
preferredRendererType: Shape.CurveRenderer
data: visualiserBars.instances
}
Variants {
id: visualiserBars
model: Array.from({
length: Config.services.visualiserBars
}, (_, i) => i)
ShapePath {
id: visualiserBar
required property int modelData
readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData]))
readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars
readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize
readonly property real cos: Math.cos(angle)
readonly property real sin: Math.sin(angle)
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
strokeWidth: 360 / Config.services.visualiserBars - Appearance.spacing.small / 4
strokeColor: Colours.palette.m3primary
startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos
startY: visualiser.centerY + (visualiser.innerY + strokeWidth / 2) * sin
PathLine {
x: visualiser.centerX + (visualiser.innerX + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.cos
y: visualiser.centerY + (visualiser.innerY + visualiserBar.strokeWidth / 2 + visualiserBar.magnitude) * visualiserBar.sin
}
Behavior on strokeColor {
CAnim {}
}
}
}
StyledClippingRect {
id: cover
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaVisualiserSize
implicitWidth: Config.dashboard.sizes.mediaCoverArtSize
implicitHeight: Config.dashboard.sizes.mediaCoverArtSize
color: Colours.tPalette.m3surfaceContainerHigh
radius: Infinity
MaterialIcon {
anchors.centerIn: parent
grade: 200
text: "art_track"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: (parent.width * 0.4) || 1
}
Image {
id: image
anchors.fill: parent
source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: width
sourceSize.height: height
}
}
ColumnLayout {
id: details
anchors.verticalCenter: parent.verticalCenter
anchors.left: visualiser.right
anchors.leftMargin: Appearance.spacing.normal
spacing: Appearance.spacing.small
StyledText {
id: title
Layout.fillWidth: true
Layout.maximumWidth: parent.implicitWidth
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
}
StyledText {
id: album
Layout.fillWidth: true
Layout.maximumWidth: parent.implicitWidth
animate: true
horizontalAlignment: Text.AlignHCenter
visible: !!Players.active
text: Players.active?.trackAlbum || qsTr("Unknown album")
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.small
elide: Text.ElideRight
}
StyledText {
id: artist
Layout.fillWidth: true
Layout.maximumWidth: parent.implicitWidth
animate: true
horizontalAlignment: Text.AlignHCenter
text: (Players.active?.trackArtist ?? qsTr("Play some music for stuff to show up here!")) || qsTr("Unknown artist")
color: Players.active ? Colours.palette.m3secondary : Colours.palette.m3outline
elide: Text.ElideRight
wrapMode: Players.active ? Text.NoWrap : Text.WordWrap
}
RowLayout {
id: controls
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Appearance.spacing.small
Layout.bottomMargin: Appearance.spacing.smaller
spacing: Appearance.spacing.small
PlayerControl {
type: IconButton.Text
icon: "skip_previous"
font.pointSize: Math.round(Appearance.font.size.large * 1.5)
disabled: !Players.active?.canGoPrevious
onClicked: Players.active?.previous()
}
PlayerControl {
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
label.animate: true
toggle: true
padding: Appearance.padding.small / 2
checked: Players.active?.isPlaying ?? false
font.pointSize: Math.round(Appearance.font.size.large * 1.5)
disabled: !Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
}
PlayerControl {
type: IconButton.Text
icon: "skip_next"
font.pointSize: Math.round(Appearance.font.size.large * 1.5)
disabled: !Players.active?.canGoNext
onClicked: Players.active?.next()
}
}
StyledSlider {
id: slider
enabled: !!Players.active
implicitWidth: 280
implicitHeight: Appearance.padding.normal * 3
onMoved: {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: slider
property: "value"
value: root.playerProgress
when: !slider.pressed
}
CustomMouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
function onWheel(event: WheelEvent) {
const active = Players.active;
if (!active?.canSeek || !active?.positionSupported)
return;
event.accepted = true;
const delta = event.angleDelta.y > 0 ? 10 : -10; // Time 10 seconds
Qt.callLater(() => {
active.position = Math.max(0, Math.min(active.length, active.position + delta));
});
}
}
}
Item {
Layout.fillWidth: true
implicitHeight: Math.max(position.implicitHeight, length.implicitHeight)
StyledText {
id: position
anchors.left: parent.left
text: root.lengthStr(Players.active?.position ?? -1)
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
StyledText {
id: length
anchors.right: parent.right
text: root.lengthStr(Players.active?.length ?? -1)
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Appearance.spacing.small
PlayerControl {
type: IconButton.Text
icon: "move_up"
inactiveOnColour: Colours.palette.m3secondary
padding: Appearance.padding.small
font.pointSize: Appearance.font.size.large
disabled: !Players.active?.canRaise
onClicked: {
Players.active?.raise();
root.visibilities.dashboard = false;
}
}
SplitButton {
id: playerSelector
disabled: !Players.list.length
active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null
menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData
menuItems: playerList.instances
fallbackIcon: "music_off"
fallbackText: qsTr("No players")
label.Layout.maximumWidth: slider.implicitWidth * 0.28
label.elide: Text.ElideRight
stateLayer.disabled: true
menuOnTop: true
Variants {
id: playerList
model: Players.list
PlayerItem {}
}
}
PlayerControl {
type: IconButton.Text
icon: "delete"
inactiveOnColour: Colours.palette.m3error
padding: Appearance.padding.small
font.pointSize: Appearance.font.size.large
disabled: !Players.active?.canQuit
onClicked: Players.active?.quit()
}
}
}
Item {
id: bongocat
anchors.verticalCenter: parent.verticalCenter
anchors.left: details.right
anchors.leftMargin: Appearance.spacing.normal
implicitWidth: visualiser.width
implicitHeight: visualiser.height
AnimatedImage {
anchors.centerIn: parent
width: visualiser.width * 0.75
height: visualiser.height * 0.75
playing: Players.active?.isPlaying ?? false
speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type
source: Paths.absolutePath(Config.paths.mediaGif)
asynchronous: true
fillMode: AnimatedImage.PreserveAspectFit
}
}
component PlayerItem: MenuItem {
required property MprisPlayer modelData
icon: modelData === Players.active ? "check" : ""
text: Players.getIdentity(modelData)
activeIcon: "animated_images"
}
component PlayerControl: IconButton {
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)
radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2
radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial
radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
Behavior on Layout.preferredWidth {
Anim {
duration: Appearance.anim.durations.expressiveFastSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
}
}
}
}

View File

@@ -0,0 +1,967 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.UPower
import qs.components
import qs.components.misc
import qs.config
import qs.services
Item {
id: root
readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2
function displayTemp(temp: real): string {
return `${Math.ceil(Config.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheitPerformance ? "F" : "C"}`;
}
implicitWidth: Math.max(minWidth, content.implicitWidth)
implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight
StyledRect {
id: placeholder
anchors.centerIn: parent
width: 400
height: 350
radius: Appearance.rounding.large
color: Colours.tPalette.m3surfaceContainer
visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery)
ColumnLayout {
anchors.centerIn: parent
spacing: Appearance.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "tune"
font.pointSize: Appearance.font.size.extraLarge * 2
color: Colours.palette.m3onSurfaceVariant
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("No widgets enabled")
font.pointSize: Appearance.font.size.large
color: Colours.palette.m3onSurface
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Enable widgets in dashboard settings")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
}
}
RowLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.spacing.normal
visible: !placeholder.visible
Ref {
service: SystemUsage
}
ColumnLayout {
id: mainColumn
Layout.fillWidth: true
spacing: Appearance.spacing.normal
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE")
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
visible: Config.dashboard.performance.showCpu
icon: "memory"
title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU")
mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%`
mainLabel: qsTr("Usage")
secondaryValue: root.displayTemp(SystemUsage.cpuTemp)
secondaryLabel: qsTr("Temp")
usage: SystemUsage.cpuPerc
temperature: SystemUsage.cpuTemp
accentColor: Colours.palette.m3primary
}
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
icon: "desktop_windows"
title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU")
mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%`
mainLabel: qsTr("Usage")
secondaryValue: root.displayTemp(SystemUsage.gpuTemp)
secondaryLabel: qsTr("Temp")
usage: SystemUsage.gpuPerc
temperature: SystemUsage.gpuTemp
accentColor: Colours.palette.m3secondary
}
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork
GaugeCard {
Layout.minimumWidth: 250
Layout.preferredHeight: 220
Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork
icon: "memory_alt"
title: qsTr("Memory")
percentage: SystemUsage.memPerc
subtitle: {
const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed);
const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
accentColor: Colours.palette.m3tertiary
visible: Config.dashboard.performance.showMemory
}
StorageGaugeCard {
Layout.minimumWidth: 250
Layout.preferredHeight: 220
Layout.fillWidth: !Config.dashboard.performance.showNetwork
visible: Config.dashboard.performance.showStorage
}
NetworkCard {
Layout.fillWidth: true
Layout.minimumWidth: 200
Layout.preferredHeight: 220
visible: Config.dashboard.performance.showNetwork
}
}
}
BatteryTank {
Layout.preferredWidth: 120
Layout.preferredHeight: mainColumn.implicitHeight
visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery
}
}
component BatteryTank: StyledClippingRect {
id: batteryTank
property real percentage: UPower.displayDevice.percentage
property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging
property color accentColor: Colours.palette.m3primary
property real animatedPercentage: 0
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
// Background Fill
StyledRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: parent.height * batteryTank.animatedPercentage
color: Qt.alpha(batteryTank.accentColor, 0.15)
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
// Header Section
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
text: {
if (!UPower.displayDevice.isLaptopBattery) {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
if (PowerProfiles.profile === PowerProfile.Performance)
return "rocket_launch";
return "balance";
}
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return "battery_full";
const perc = UPower.displayDevice.percentage;
const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state);
if (perc >= 0.99)
return "battery_full";
let level = Math.floor(perc * 7);
if (charging && (level === 4 || level === 1))
level--;
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
}
font.pointSize: Appearance.font.size.large
color: batteryTank.accentColor
}
StyledText {
Layout.fillWidth: true
text: qsTr("Battery")
font.pointSize: Appearance.font.size.normal
color: Colours.palette.m3onSurface
}
}
Item {
Layout.fillHeight: true
}
// Bottom Info Section
ColumnLayout {
Layout.fillWidth: true
spacing: -4
StyledText {
Layout.alignment: Qt.AlignRight
text: `${Math.round(batteryTank.percentage * 100)}%`
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
color: batteryTank.accentColor
}
StyledText {
Layout.alignment: Qt.AlignRight
text: {
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return qsTr("Full");
if (batteryTank.isCharging)
return qsTr("Charging");
const s = UPower.displayDevice.timeToEmpty;
if (s === 0)
return qsTr("...");
const hr = Math.floor(s / 3600);
const min = Math.floor((s % 3600) / 60);
if (hr > 0)
return `${hr}h ${min}m`;
return `${min}m`;
}
font.pointSize: Appearance.font.size.smaller
color: Colours.palette.m3onSurfaceVariant
}
}
}
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component CardHeader: RowLayout {
property string icon
property string title
property color accentColor: Colours.palette.m3primary
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
text: parent.icon
fill: 1
color: parent.accentColor
font.pointSize: Appearance.spacing.large
}
StyledText {
Layout.fillWidth: true
text: parent.title
font.pointSize: Appearance.font.size.normal
elide: Text.ElideRight
}
}
component ProgressBar: StyledRect {
id: progressBar
property real value: 0
property color fgColor: Colours.palette.m3primary
property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2)
property real animatedValue: 0
color: bgColor
radius: Appearance.rounding.full
Component.onCompleted: animatedValue = value
onValueChanged: animatedValue = value
StyledRect {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * progressBar.animatedValue
color: progressBar.fgColor
radius: Appearance.rounding.full
}
Behavior on animatedValue {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component HeroCard: StyledClippingRect {
id: heroCard
property string icon
property string title
property string mainValue
property string mainLabel
property string secondaryValue
property string secondaryLabel
property real usage: 0
property real temperature: 0
property color accentColor: Colours.palette.m3primary
readonly property real maxTemp: 100
readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp))
property real animatedUsage: 0
property real animatedTemp: 0
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
Component.onCompleted: {
animatedUsage = usage;
animatedTemp = tempProgress;
}
onUsageChanged: animatedUsage = usage
onTempProgressChanged: animatedTemp = tempProgress
StyledRect {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * heroCard.animatedUsage
color: Qt.alpha(heroCard.accentColor, 0.15)
}
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large
anchors.topMargin: Appearance.padding.normal
anchors.bottomMargin: Appearance.padding.normal
spacing: Appearance.spacing.small
CardHeader {
icon: heroCard.icon
title: heroCard.title
accentColor: heroCard.accentColor
}
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: Appearance.spacing.normal
Column {
Layout.alignment: Qt.AlignBottom
Layout.fillWidth: true
spacing: Appearance.spacing.small
Row {
spacing: Appearance.spacing.small
StyledText {
text: heroCard.secondaryValue
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
}
StyledText {
text: heroCard.secondaryLabel
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
anchors.baseline: parent.children[0].baseline
}
}
ProgressBar {
width: parent.width * 0.5
height: 6
value: heroCard.tempProgress
fgColor: heroCard.accentColor
bgColor: Qt.alpha(heroCard.accentColor, 0.2)
}
}
Item {
Layout.fillWidth: true
}
}
}
Column {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.large
anchors.rightMargin: 32
spacing: 0
StyledText {
anchors.right: parent.right
text: heroCard.mainLabel
font.pointSize: Appearance.font.size.normal
color: Colours.palette.m3onSurfaceVariant
}
StyledText {
anchors.right: parent.right
text: heroCard.mainValue
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
color: heroCard.accentColor
}
}
Behavior on animatedUsage {
Anim {
duration: Appearance.anim.durations.large
}
}
Behavior on animatedTemp {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component GaugeCard: StyledRect {
id: gaugeCard
property string icon
property string title
property real percentage: 0
property string subtitle
property color accentColor: Colours.palette.m3primary
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
property real animatedPercentage: 0
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
clip: true
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
icon: gaugeCard.icon
title: gaugeCard.title
accentColor: gaugeCard.accentColor
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Canvas {
id: gaugeCanvas
anchors.centerIn: parent
width: Math.min(parent.width, parent.height)
height: width
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 10;
ctx.beginPath();
ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (gaugeCard.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep * gaugeCard.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = gaugeCard.accentColor;
ctx.stroke();
}
}
Component.onCompleted: requestPaint()
Connections {
function onAnimatedPercentageChanged() {
gaugeCanvas.requestPaint();
}
target: gaugeCard
}
Connections {
function onPaletteChanged() {
gaugeCanvas.requestPaint();
}
target: Colours
}
}
StyledText {
anchors.centerIn: parent
text: `${Math.round(gaugeCard.percentage * 100)}%`
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
color: gaugeCard.accentColor
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: gaugeCard.subtitle
font.pointSize: Appearance.font.size.smaller
color: Colours.palette.m3onSurfaceVariant
}
}
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component StorageGaugeCard: StyledRect {
id: storageGaugeCard
property int currentDiskIndex: 0
readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null
property int diskCount: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
property real animatedPercentage: 0
property color accentColor: Colours.palette.m3secondary
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
clip: true
Component.onCompleted: {
diskCount = SystemUsage.disks.length;
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
onCurrentDiskChanged: {
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
// Update diskCount and animatedPercentage when disks data changes
Connections {
function onDisksChanged() {
if (SystemUsage.disks.length !== storageGaugeCard.diskCount)
storageGaugeCard.diskCount = SystemUsage.disks.length;
// Update animated percentage when disk data refreshes
if (storageGaugeCard.currentDisk)
storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc;
}
target: SystemUsage
}
MouseArea {
anchors.fill: parent
onWheel: wheel => {
if (wheel.angleDelta.y > 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount;
else if (wheel.angleDelta.y < 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount;
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
icon: "hard_disk"
title: {
const base = qsTr("Storage");
if (!storageGaugeCard.currentDisk)
return base;
return `${base} - ${storageGaugeCard.currentDisk.mount}`;
}
accentColor: storageGaugeCard.accentColor
// Scroll hint icon
MaterialIcon {
text: "unfold_more"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
visible: storageGaugeCard.diskCount > 1
opacity: 0.7
ToolTip.visible: hintHover.hovered
ToolTip.text: qsTr("Scroll to switch disks")
ToolTip.delay: 500
HoverHandler {
id: hintHover
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Canvas {
id: storageGaugeCanvas
anchors.centerIn: parent
width: Math.min(parent.width, parent.height)
height: width
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = height / 2;
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 10;
ctx.beginPath();
ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (storageGaugeCard.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep * storageGaugeCard.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = storageGaugeCard.accentColor;
ctx.stroke();
}
}
Component.onCompleted: requestPaint()
Connections {
function onAnimatedPercentageChanged() {
storageGaugeCanvas.requestPaint();
}
target: storageGaugeCard
}
Connections {
function onPaletteChanged() {
storageGaugeCanvas.requestPaint();
}
target: Colours
}
}
StyledText {
anchors.centerIn: parent
text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—"
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
color: storageGaugeCard.accentColor
}
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: {
if (!storageGaugeCard.currentDisk)
return "—";
const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used);
const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
font.pointSize: Appearance.font.size.smaller
color: Colours.palette.m3onSurfaceVariant
}
}
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
}
component NetworkCard: StyledRect {
id: networkCard
property color accentColor: Colours.palette.m3primary
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.large
clip: true
Ref {
service: NetworkUsage
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
CardHeader {
icon: "swap_vert"
title: qsTr("Network")
accentColor: networkCard.accentColor
}
// Sparkline graph
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Canvas {
id: sparklineCanvas
property var downHistory: NetworkUsage.downloadHistory
property var upHistory: NetworkUsage.uploadHistory
property real targetMax: 1024
property real smoothMax: targetMax
property real slideProgress: 0
property int _tickCount: 0
property int _lastTickCount: -1
function checkAndAnimate(): void {
const currentLength = (downHistory || []).length;
if (currentLength > 0 && _tickCount !== _lastTickCount) {
_lastTickCount = _tickCount;
updateMax();
}
}
function updateMax(): void {
const downHist = downHistory || [];
const upHist = upHistory || [];
const allValues = downHist.concat(upHist);
targetMax = Math.max(...allValues, 1024);
requestPaint();
}
anchors.fill: parent
onDownHistoryChanged: checkAndAnimate()
onUpHistoryChanged: checkAndAnimate()
onSmoothMaxChanged: requestPaint()
onSlideProgressChanged: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const w = width;
const h = height;
const downHist = downHistory || [];
const upHist = upHistory || [];
if (downHist.length < 2 && upHist.length < 2)
return;
const maxVal = smoothMax;
const drawLine = (history, color, fillAlpha) => {
if (history.length < 2)
return;
const len = history.length;
const stepX = w / (NetworkUsage.historyLength - 1);
const startX = w - (len - 1) * stepX - stepX * slideProgress + stepX;
ctx.beginPath();
ctx.moveTo(startX, h - (history[0] / maxVal) * h);
for (let i = 1; i < len; i++) {
const x = startX + i * stepX;
const y = h - (history[i] / maxVal) * h;
ctx.lineTo(x, y);
}
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
ctx.lineTo(startX + (len - 1) * stepX, h);
ctx.lineTo(startX, h);
ctx.closePath();
ctx.fillStyle = Qt.rgba(Qt.color(color).r, Qt.color(color).g, Qt.color(color).b, fillAlpha);
ctx.fill();
};
drawLine(upHist, Colours.palette.m3secondary.toString(), 0.15);
drawLine(downHist, Colours.palette.m3tertiary.toString(), 0.2);
}
Component.onCompleted: updateMax()
Connections {
function onPaletteChanged() {
sparklineCanvas.requestPaint();
}
target: Colours
}
Timer {
interval: Config.dashboard.resourceUpdateInterval
running: true
repeat: true
onTriggered: sparklineCanvas._tickCount++
}
NumberAnimation on slideProgress {
from: 0
to: 1
duration: Config.dashboard.resourceUpdateInterval
loops: Animation.Infinite
running: true
}
Behavior on smoothMax {
Anim {
duration: Appearance.anim.durations.large
}
}
}
// "No data" placeholder
StyledText {
anchors.centerIn: parent
text: qsTr("Collecting data...")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
visible: NetworkUsage.downloadHistory.length < 2
opacity: 0.6
}
}
// Download row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
text: "download"
color: Colours.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
}
StyledText {
text: qsTr("Download")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
Item {
Layout.fillWidth: true
}
StyledText {
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
color: Colours.palette.m3tertiary
}
}
// Upload row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
text: "upload"
color: Colours.palette.m3secondary
font.pointSize: Appearance.font.size.normal
}
StyledText {
text: qsTr("Upload")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
Item {
Layout.fillWidth: true
}
StyledText {
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
color: Colours.palette.m3secondary
}
}
// Session totals
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
text: "history"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
}
StyledText {
text: qsTr("Total")
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
Item {
Layout.fillWidth: true
}
StyledText {
text: {
const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0);
const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0);
return (down && up) ? `${down.value.toFixed(1)}${down.unit} ${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B";
}
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3onSurfaceVariant
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More