mirror of
https://github.com/belsabbagh/dotfiles.git
synced 2026-04-11 09:36:46 +00:00
quickshell and hyprland additions
This commit is contained in:
56
.config/quickshell/caelestia/modules/BatteryMonitor.qml
Normal file
56
.config/quickshell/caelestia/modules/BatteryMonitor.qml
Normal 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"])
|
||||
}
|
||||
}
|
||||
51
.config/quickshell/caelestia/modules/IdleMonitors.qml
Normal file
51
.config/quickshell/caelestia/modules/IdleMonitors.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
142
.config/quickshell/caelestia/modules/Shortcuts.qml
Normal file
142
.config/quickshell/caelestia/modules/Shortcuts.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
.config/quickshell/caelestia/modules/areapicker/AreaPicker.qml
Normal file
124
.config/quickshell/caelestia/modules/areapicker/AreaPicker.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
300
.config/quickshell/caelestia/modules/areapicker/Picker.qml
Normal file
300
.config/quickshell/caelestia/modules/areapicker/Picker.qml
Normal 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
|
||||
}
|
||||
}
|
||||
153
.config/quickshell/caelestia/modules/background/Background.qml
Normal file
153
.config/quickshell/caelestia/modules/background/Background.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
169
.config/quickshell/caelestia/modules/background/DesktopClock.qml
Normal file
169
.config/quickshell/caelestia/modules/background/DesktopClock.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
151
.config/quickshell/caelestia/modules/background/Visualiser.qml
Normal file
151
.config/quickshell/caelestia/modules/background/Visualiser.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
145
.config/quickshell/caelestia/modules/background/Wallpaper.qml
Normal file
145
.config/quickshell/caelestia/modules/background/Wallpaper.qml
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
205
.config/quickshell/caelestia/modules/bar/Bar.qml
Normal file
205
.config/quickshell/caelestia/modules/bar/Bar.qml
Normal 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
|
||||
}
|
||||
}
|
||||
87
.config/quickshell/caelestia/modules/bar/BarWrapper.qml
Normal file
87
.config/quickshell/caelestia/modules/bar/BarWrapper.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
121
.config/quickshell/caelestia/modules/bar/components/Tray.qml
Normal file
121
.config/quickshell/caelestia/modules/bar/components/Tray.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
120
.config/quickshell/caelestia/modules/bar/popouts/Audio.qml
Normal file
120
.config/quickshell/caelestia/modules/bar/popouts/Audio.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
230
.config/quickshell/caelestia/modules/bar/popouts/Battery.qml
Normal file
230
.config/quickshell/caelestia/modules/bar/popouts/Battery.qml
Normal 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 {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
197
.config/quickshell/caelestia/modules/bar/popouts/Bluetooth.qml
Normal file
197
.config/quickshell/caelestia/modules/bar/popouts/Bluetooth.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
222
.config/quickshell/caelestia/modules/bar/popouts/Content.qml
Normal file
222
.config/quickshell/caelestia/modules/bar/popouts/Content.qml
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
388
.config/quickshell/caelestia/modules/bar/popouts/Network.qml
Normal file
388
.config/quickshell/caelestia/modules/bar/popouts/Network.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
225
.config/quickshell/caelestia/modules/bar/popouts/TrayMenu.qml
Normal file
225
.config/quickshell/caelestia/modules/bar/popouts/TrayMenu.qml
Normal 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 {}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
215
.config/quickshell/caelestia/modules/bar/popouts/Wrapper.qml
Normal file
215
.config/quickshell/caelestia/modules/bar/popouts/Wrapper.qml
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
readonly property int rounding: floating ? 0 : Appearance.rounding.normal
|
||||
|
||||
property alias floating: session.floating
|
||||
property alias active: session.active
|
||||
property alias navExpanded: session.navExpanded
|
||||
|
||||
readonly property Session session: Session {
|
||||
id: session
|
||||
|
||||
root: root
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
}
|
||||
|
||||
implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio
|
||||
implicitHeight: screen.height * Config.controlCenter.sizes.heightMult
|
||||
|
||||
GridLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
rowSpacing: 0
|
||||
columnSpacing: 0
|
||||
rows: root.floating ? 2 : 1
|
||||
columns: 2
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.columnSpan: 2
|
||||
|
||||
active: root.floating
|
||||
visible: active
|
||||
|
||||
sourceComponent: WindowTitle {
|
||||
screen: root.screen
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillHeight: true
|
||||
|
||||
topLeftRadius: root.rounding
|
||||
bottomLeftRadius: root.rounding
|
||||
implicitWidth: navRail.implicitWidth
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
CustomMouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
function onWheel(event: WheelEvent): void {
|
||||
// Prevent tab switching during initial opening animation to avoid blank pages
|
||||
if (!panes.initialOpeningComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.angleDelta.y < 0)
|
||||
root.session.activeIndex = Math.min(root.session.activeIndex + 1, root.session.panes.length - 1);
|
||||
else if (event.angleDelta.y > 0)
|
||||
root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
NavRail {
|
||||
id: navRail
|
||||
|
||||
screen: root.screen
|
||||
session: root.session
|
||||
initialOpeningComplete: root.initialOpeningComplete
|
||||
}
|
||||
}
|
||||
|
||||
Panes {
|
||||
id: panes
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
topRightRadius: root.rounding
|
||||
bottomRightRadius: root.rounding
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool initialOpeningComplete: panes.initialOpeningComplete
|
||||
}
|
||||
231
.config/quickshell/caelestia/modules/controlcenter/NavRail.qml
Normal file
231
.config/quickshell/caelestia/modules/controlcenter/NavRail.qml
Normal file
@@ -0,0 +1,231 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.controlcenter
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property Session session
|
||||
required property bool initialOpeningComplete
|
||||
|
||||
implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.padding.larger * 2
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.session.navExpanded
|
||||
|
||||
PropertyChanges {
|
||||
layout.spacing: Appearance.spacing.small
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "spacing"
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
active: !root.session.floating
|
||||
visible: active
|
||||
|
||||
sourceComponent: StyledRect {
|
||||
readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2
|
||||
|
||||
implicitWidth: nonAnimWidth
|
||||
implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth
|
||||
|
||||
color: Colours.palette.m3primaryContainer
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
StateLayer {
|
||||
id: normalWinState
|
||||
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.root.close();
|
||||
WindowFactory.create(null, {
|
||||
active: root.session.active,
|
||||
navExpanded: root.session.navExpanded
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: normalWinIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
|
||||
text: "select_window"
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: normalWinLabel
|
||||
|
||||
anchors.left: normalWinIcon.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.spacing.normal
|
||||
|
||||
text: qsTr("Float window")
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
opacity: root.session.navExpanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: PaneRegistry.count
|
||||
|
||||
NavItem {
|
||||
required property int index
|
||||
Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0
|
||||
icon: PaneRegistry.getByIndex(index).icon
|
||||
label: PaneRegistry.getByIndex(index).label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NavItem: Item {
|
||||
id: item
|
||||
|
||||
required property string icon
|
||||
required property string label
|
||||
readonly property bool active: root.session.active === label
|
||||
|
||||
implicitWidth: background.implicitWidth
|
||||
implicitHeight: background.implicitHeight + smallLabel.implicitHeight + smallLabel.anchors.topMargin
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.session.navExpanded
|
||||
|
||||
PropertyChanges {
|
||||
expandedLabel.opacity: 1
|
||||
smallLabel.opacity: 0
|
||||
background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth
|
||||
background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
item.implicitHeight: background.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: background
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0)
|
||||
|
||||
implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small
|
||||
|
||||
StateLayer {
|
||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
// Prevent tab switching during initial opening animation to avoid blank pages
|
||||
if (!root.initialOpeningComplete) {
|
||||
return;
|
||||
}
|
||||
root.session.active = item.label;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
|
||||
text: item.icon
|
||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: item.active ? 1 : 0
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: expandedLabel
|
||||
|
||||
anchors.left: icon.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Appearance.spacing.normal
|
||||
|
||||
opacity: 0
|
||||
text: item.label
|
||||
color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
font.capitalization: Font.Capitalize
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: smallLabel
|
||||
|
||||
anchors.horizontalCenter: icon.horizontalCenter
|
||||
anchors.top: icon.bottom
|
||||
anchors.topMargin: Appearance.spacing.small / 2
|
||||
|
||||
text: item.label
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.capitalization: Font.Capitalize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property list<QtObject> panes: [
|
||||
QtObject {
|
||||
readonly property string id: "network"
|
||||
readonly property string label: "network"
|
||||
readonly property string icon: "router"
|
||||
readonly property string component: "network/NetworkingPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "bluetooth"
|
||||
readonly property string label: "bluetooth"
|
||||
readonly property string icon: "settings_bluetooth"
|
||||
readonly property string component: "bluetooth/BtPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "audio"
|
||||
readonly property string label: "audio"
|
||||
readonly property string icon: "volume_up"
|
||||
readonly property string component: "audio/AudioPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "appearance"
|
||||
readonly property string label: "appearance"
|
||||
readonly property string icon: "palette"
|
||||
readonly property string component: "appearance/AppearancePane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "taskbar"
|
||||
readonly property string label: "taskbar"
|
||||
readonly property string icon: "task_alt"
|
||||
readonly property string component: "taskbar/TaskbarPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "launcher"
|
||||
readonly property string label: "launcher"
|
||||
readonly property string icon: "apps"
|
||||
readonly property string component: "launcher/LauncherPane.qml"
|
||||
},
|
||||
QtObject {
|
||||
readonly property string id: "dashboard"
|
||||
readonly property string label: "dashboard"
|
||||
readonly property string icon: "dashboard"
|
||||
readonly property string component: "dashboard/DashboardPane.qml"
|
||||
}
|
||||
]
|
||||
|
||||
readonly property int count: panes.length
|
||||
|
||||
readonly property var labels: {
|
||||
const result = [];
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
result.push(panes[i].label);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getByIndex(index: int): QtObject {
|
||||
if (index >= 0 && index < panes.length) {
|
||||
return panes[index];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getIndexByLabel(label: string): int {
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
if (panes[i].label === label) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function getByLabel(label: string): QtObject {
|
||||
const index = getIndexByLabel(label);
|
||||
return getByIndex(index);
|
||||
}
|
||||
|
||||
function getById(id: string): QtObject {
|
||||
for (let i = 0; i < panes.length; i++) {
|
||||
if (panes[i].id === id) {
|
||||
return panes[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
175
.config/quickshell/caelestia/modules/controlcenter/Panes.qml
Normal file
175
.config/quickshell/caelestia/modules/controlcenter/Panes.qml
Normal file
@@ -0,0 +1,175 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "bluetooth"
|
||||
import "network"
|
||||
import "audio"
|
||||
import "appearance"
|
||||
import "taskbar"
|
||||
import "launcher"
|
||||
import "dashboard"
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.modules.controlcenter
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ClippingRectangle {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
readonly property bool initialOpeningComplete: layout.initialOpeningComplete
|
||||
|
||||
color: "transparent"
|
||||
clip: true
|
||||
focus: false
|
||||
activeFocusOnTab: false
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onPressed: function (mouse) {
|
||||
root.focus = true;
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session
|
||||
|
||||
function onActiveIndexChanged(): void {
|
||||
root.focus = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
spacing: 0
|
||||
y: -root.session.activeIndex * root.height
|
||||
clip: true
|
||||
|
||||
property bool animationComplete: true
|
||||
property bool initialOpeningComplete: false
|
||||
|
||||
Timer {
|
||||
id: animationDelayTimer
|
||||
interval: Appearance.anim.durations.normal
|
||||
onTriggered: {
|
||||
layout.animationComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: initialOpeningTimer
|
||||
interval: Appearance.anim.durations.large
|
||||
running: true
|
||||
onTriggered: {
|
||||
layout.initialOpeningComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: PaneRegistry.count
|
||||
|
||||
Pane {
|
||||
required property int index
|
||||
paneIndex: index
|
||||
componentPath: PaneRegistry.getByIndex(index).component
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session
|
||||
function onActiveIndexChanged(): void {
|
||||
layout.animationComplete = false;
|
||||
animationDelayTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Pane: Item {
|
||||
id: pane
|
||||
|
||||
required property int paneIndex
|
||||
required property string componentPath
|
||||
|
||||
implicitWidth: root.width
|
||||
implicitHeight: root.height
|
||||
|
||||
property bool hasBeenLoaded: false
|
||||
|
||||
function updateActive(): void {
|
||||
const diff = Math.abs(root.session.activeIndex - pane.paneIndex);
|
||||
const isActivePane = diff === 0;
|
||||
let shouldBeActive = false;
|
||||
|
||||
if (!layout.initialOpeningComplete) {
|
||||
shouldBeActive = isActivePane;
|
||||
} else {
|
||||
if (diff <= 1) {
|
||||
shouldBeActive = true;
|
||||
} else if (pane.hasBeenLoaded) {
|
||||
shouldBeActive = true;
|
||||
} else {
|
||||
shouldBeActive = layout.animationComplete;
|
||||
}
|
||||
}
|
||||
|
||||
loader.active = shouldBeActive;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(pane.updateActive);
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && !pane.hasBeenLoaded) {
|
||||
pane.hasBeenLoaded = true;
|
||||
}
|
||||
|
||||
if (active && !item) {
|
||||
loader.setSource(pane.componentPath, {
|
||||
"session": root.session
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onItemChanged: {
|
||||
if (item) {
|
||||
pane.hasBeenLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session
|
||||
function onActiveIndexChanged(): void {
|
||||
pane.updateActive();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: layout
|
||||
function onInitialOpeningCompleteChanged(): void {
|
||||
pane.updateActive();
|
||||
}
|
||||
function onAnimationCompleteChanged(): void {
|
||||
pane.updateActive();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import "./state"
|
||||
import qs.modules.controlcenter
|
||||
|
||||
QtObject {
|
||||
readonly property list<string> panes: PaneRegistry.labels
|
||||
|
||||
required property var root
|
||||
property bool floating: false
|
||||
property string active: "network"
|
||||
property int activeIndex: 0
|
||||
property bool navExpanded: false
|
||||
|
||||
readonly property BluetoothState bt: BluetoothState {}
|
||||
readonly property NetworkState network: NetworkState {}
|
||||
readonly property EthernetState ethernet: EthernetState {}
|
||||
readonly property LauncherState launcher: LauncherState {}
|
||||
readonly property VpnState vpn: VpnState {}
|
||||
|
||||
onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active))
|
||||
onActiveIndexChanged: if (panes[activeIndex])
|
||||
active = panes[activeIndex]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.components
|
||||
import qs.services
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function create(parent: Item, props: var): void {
|
||||
controlCenter.createObject(parent ?? dummy, props);
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: dummy
|
||||
}
|
||||
|
||||
Component {
|
||||
id: controlCenter
|
||||
|
||||
FloatingWindow {
|
||||
id: win
|
||||
|
||||
property alias active: cc.active
|
||||
property alias navExpanded: cc.navExpanded
|
||||
|
||||
color: Colours.tPalette.m3surface
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible)
|
||||
destroy();
|
||||
}
|
||||
|
||||
implicitWidth: cc.implicitWidth
|
||||
implicitHeight: cc.implicitHeight
|
||||
|
||||
minimumSize.width: implicitWidth
|
||||
minimumSize.height: implicitHeight
|
||||
maximumSize.width: implicitWidth
|
||||
maximumSize.height: implicitHeight
|
||||
|
||||
title: qsTr("Caelestia Settings - %1").arg(cc.active.slice(0, 1).toUpperCase() + cc.active.slice(1))
|
||||
|
||||
ControlCenter {
|
||||
id: cc
|
||||
|
||||
anchors.fill: parent
|
||||
screen: win.screen
|
||||
floating: true
|
||||
|
||||
function close(): void {
|
||||
win.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import qs.components
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property Session session
|
||||
|
||||
implicitHeight: text.implicitHeight + Appearance.padding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
StyledText {
|
||||
id: text
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
text: qsTr("Caelestia Settings - %1").arg(root.session.active)
|
||||
font.capitalization: Font.Capitalize
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: closeIcon.implicitHeight + Appearance.padding.small
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
QsWindow.window.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: closeIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "./sections"
|
||||
import "../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.components.images
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia.Models
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1
|
||||
property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded"
|
||||
property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF"
|
||||
property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik"
|
||||
property real fontSizeScale: Config.appearance.font.size.scale ?? 1
|
||||
property real paddingScale: Config.appearance.padding.scale ?? 1
|
||||
property real roundingScale: Config.appearance.rounding.scale ?? 1
|
||||
property real spacingScale: Config.appearance.spacing.scale ?? 1
|
||||
property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false
|
||||
property real transparencyBase: Config.appearance.transparency.base ?? 0.85
|
||||
property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4
|
||||
property real borderRounding: Config.border.rounding ?? 1
|
||||
property real borderThickness: Config.border.thickness ?? 1
|
||||
|
||||
property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false
|
||||
property real desktopClockScale: Config.background.desktopClock.scale ?? 1
|
||||
property string desktopClockPosition: Config.background.desktopClock.position ?? "bottom-right"
|
||||
property bool desktopClockShadowEnabled: Config.background.desktopClock.shadow.enabled ?? true
|
||||
property real desktopClockShadowOpacity: Config.background.desktopClock.shadow.opacity ?? 0.7
|
||||
property real desktopClockShadowBlur: Config.background.desktopClock.shadow.blur ?? 0.4
|
||||
property bool desktopClockBackgroundEnabled: Config.background.desktopClock.background.enabled ?? false
|
||||
property real desktopClockBackgroundOpacity: Config.background.desktopClock.background.opacity ?? 0.7
|
||||
property bool desktopClockBackgroundBlur: Config.background.desktopClock.background.blur ?? false
|
||||
property bool desktopClockInvertColors: Config.background.desktopClock.invertColors ?? false
|
||||
property bool backgroundEnabled: Config.background.enabled ?? true
|
||||
property bool wallpaperEnabled: Config.background.wallpaperEnabled ?? true
|
||||
property bool visualiserEnabled: Config.background.visualiser.enabled ?? false
|
||||
property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true
|
||||
property real visualiserRounding: Config.background.visualiser.rounding ?? 1
|
||||
property real visualiserSpacing: Config.background.visualiser.spacing ?? 1
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function saveConfig() {
|
||||
Config.appearance.anim.durations.scale = root.animDurationsScale;
|
||||
|
||||
Config.appearance.font.family.material = root.fontFamilyMaterial;
|
||||
Config.appearance.font.family.mono = root.fontFamilyMono;
|
||||
Config.appearance.font.family.sans = root.fontFamilySans;
|
||||
Config.appearance.font.size.scale = root.fontSizeScale;
|
||||
|
||||
Config.appearance.padding.scale = root.paddingScale;
|
||||
Config.appearance.rounding.scale = root.roundingScale;
|
||||
Config.appearance.spacing.scale = root.spacingScale;
|
||||
|
||||
Config.appearance.transparency.enabled = root.transparencyEnabled;
|
||||
Config.appearance.transparency.base = root.transparencyBase;
|
||||
Config.appearance.transparency.layers = root.transparencyLayers;
|
||||
|
||||
Config.background.desktopClock.enabled = root.desktopClockEnabled;
|
||||
Config.background.enabled = root.backgroundEnabled;
|
||||
Config.background.desktopClock.scale = root.desktopClockScale;
|
||||
Config.background.desktopClock.position = root.desktopClockPosition;
|
||||
Config.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled;
|
||||
Config.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity;
|
||||
Config.background.desktopClock.shadow.blur = root.desktopClockShadowBlur;
|
||||
Config.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled;
|
||||
Config.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity;
|
||||
Config.background.desktopClock.background.blur = root.desktopClockBackgroundBlur;
|
||||
Config.background.desktopClock.invertColors = root.desktopClockInvertColors;
|
||||
|
||||
Config.background.wallpaperEnabled = root.wallpaperEnabled;
|
||||
|
||||
Config.background.visualiser.enabled = root.visualiserEnabled;
|
||||
Config.background.visualiser.autoHide = root.visualiserAutoHide;
|
||||
Config.background.visualiser.rounding = root.visualiserRounding;
|
||||
Config.background.visualiser.spacing = root.visualiserSpacing;
|
||||
|
||||
Config.border.rounding = root.borderRounding;
|
||||
Config.border.thickness = root.borderThickness;
|
||||
|
||||
Config.save();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appearanceRightContentComponent
|
||||
|
||||
Item {
|
||||
id: rightAppearanceFlickable
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Appearance.spacing.normal
|
||||
text: qsTr("Wallpaper")
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 600
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: wallpaperLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.bottomMargin: -Appearance.padding.large * 2
|
||||
|
||||
active: {
|
||||
const isActive = root.session.activeIndex === 3;
|
||||
const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1;
|
||||
const splitLayout = root.children[0];
|
||||
const loader = splitLayout && splitLayout.rightLoader ? splitLayout.rightLoader : null;
|
||||
const shouldActivate = loader && loader.item !== null && (isActive || isAdjacent);
|
||||
return shouldActivate;
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Loader.Error) {
|
||||
console.error("[AppearancePane] Wallpaper loader error!");
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: WallpaperGrid {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SplitPaneLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
|
||||
StyledFlickable {
|
||||
id: sidebarFlickable
|
||||
readonly property var rootPane: root
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: sidebarLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: sidebarFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: sidebarLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
readonly property var rootPane: sidebarFlickable.rootPane
|
||||
|
||||
readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Appearance")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: sidebarLayout.allSectionsExpanded ? "unfold_less" : "unfold_more"
|
||||
type: IconButton.Text
|
||||
label.animate: true
|
||||
onClicked: {
|
||||
const shouldExpand = !sidebarLayout.allSectionsExpanded;
|
||||
themeModeSection.expanded = shouldExpand;
|
||||
colorVariantSection.expanded = shouldExpand;
|
||||
colorSchemeSection.expanded = shouldExpand;
|
||||
animationsSection.expanded = shouldExpand;
|
||||
fontsSection.expanded = shouldExpand;
|
||||
scalesSection.expanded = shouldExpand;
|
||||
transparencySection.expanded = shouldExpand;
|
||||
borderSection.expanded = shouldExpand;
|
||||
backgroundSection.expanded = shouldExpand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ThemeModeSection {
|
||||
id: themeModeSection
|
||||
}
|
||||
|
||||
ColorVariantSection {
|
||||
id: colorVariantSection
|
||||
}
|
||||
|
||||
ColorSchemeSection {
|
||||
id: colorSchemeSection
|
||||
}
|
||||
|
||||
AnimationsSection {
|
||||
id: animationsSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
FontsSection {
|
||||
id: fontsSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
ScalesSection {
|
||||
id: scalesSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
TransparencySection {
|
||||
id: transparencySection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
BorderSection {
|
||||
id: borderSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
|
||||
BackgroundSection {
|
||||
id: backgroundSection
|
||||
rootPane: sidebarFlickable.rootPane
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: appearanceRightContentComponent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Animations")
|
||||
showBackground: true
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Animation duration scale")
|
||||
value: rootPane.animDurationsScale
|
||||
from: 0.1
|
||||
to: 5.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 5.0
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.animDurationsScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Background")
|
||||
showBackground: true
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Background enabled")
|
||||
checked: rootPane.backgroundEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.backgroundEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Wallpaper enabled")
|
||||
checked: rootPane.wallpaperEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.wallpaperEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Desktop Clock")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Desktop Clock enabled")
|
||||
checked: rootPane.desktopClockEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
id: posContainer
|
||||
|
||||
contentSpacing: Appearance.spacing.small
|
||||
z: 1
|
||||
|
||||
readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-')
|
||||
readonly property string currentV: pos[0]
|
||||
readonly property string currentH: pos[1]
|
||||
|
||||
function updateClockPos(v, h) {
|
||||
rootPane.desktopClockPosition = v + "-" + h;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Positioning")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SplitButtonRow {
|
||||
label: qsTr("Vertical Position")
|
||||
enabled: rootPane.desktopClockEnabled
|
||||
|
||||
menuItems: [
|
||||
MenuItem {
|
||||
text: qsTr("Top")
|
||||
icon: "vertical_align_top"
|
||||
property string val: "top"
|
||||
},
|
||||
MenuItem {
|
||||
text: qsTr("Middle")
|
||||
icon: "vertical_align_center"
|
||||
property string val: "middle"
|
||||
},
|
||||
MenuItem {
|
||||
text: qsTr("Bottom")
|
||||
icon: "vertical_align_bottom"
|
||||
property string val: "bottom"
|
||||
}
|
||||
]
|
||||
|
||||
Component.onCompleted: {
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
if (menuItems[i].val === posContainer.currentV)
|
||||
active = menuItems[i];
|
||||
}
|
||||
}
|
||||
|
||||
// The signal from SplitButtonRow
|
||||
onSelected: item => posContainer.updateClockPos(item.val, posContainer.currentH)
|
||||
}
|
||||
|
||||
SplitButtonRow {
|
||||
label: qsTr("Horizontal Position")
|
||||
enabled: rootPane.desktopClockEnabled
|
||||
expandedZ: 99
|
||||
|
||||
menuItems: [
|
||||
MenuItem {
|
||||
text: qsTr("Left")
|
||||
icon: "align_horizontal_left"
|
||||
property string val: "left"
|
||||
},
|
||||
MenuItem {
|
||||
text: qsTr("Center")
|
||||
icon: "align_horizontal_center"
|
||||
property string val: "center"
|
||||
},
|
||||
MenuItem {
|
||||
text: qsTr("Right")
|
||||
icon: "align_horizontal_right"
|
||||
property string val: "right"
|
||||
}
|
||||
]
|
||||
|
||||
Component.onCompleted: {
|
||||
for (let i = 0; i < menuItems.length; i++) {
|
||||
if (menuItems[i].val === posContainer.currentH)
|
||||
active = menuItems[i];
|
||||
}
|
||||
}
|
||||
|
||||
onSelected: item => posContainer.updateClockPos(posContainer.currentV, item.val)
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Invert colors")
|
||||
checked: rootPane.desktopClockInvertColors
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockInvertColors = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Shadow")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: rootPane.desktopClockShadowEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockShadowEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Opacity")
|
||||
value: rootPane.desktopClockShadowOpacity * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.desktopClockShadowOpacity = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Blur")
|
||||
value: rootPane.desktopClockShadowBlur * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.desktopClockShadowBlur = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Background")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: rootPane.desktopClockBackgroundEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockBackgroundEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Blur enabled")
|
||||
checked: rootPane.desktopClockBackgroundBlur
|
||||
onToggled: checked => {
|
||||
rootPane.desktopClockBackgroundBlur = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Opacity")
|
||||
value: rootPane.desktopClockBackgroundOpacity * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.desktopClockBackgroundOpacity = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Visualiser")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Visualiser enabled")
|
||||
checked: rootPane.visualiserEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.visualiserEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Visualiser auto hide")
|
||||
checked: rootPane.visualiserAutoHide
|
||||
onToggled: checked => {
|
||||
rootPane.visualiserAutoHide = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Visualiser rounding")
|
||||
value: rootPane.visualiserRounding
|
||||
from: 0
|
||||
to: 10
|
||||
stepSize: 1
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 10
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.visualiserRounding = Math.round(newValue);
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Visualiser spacing")
|
||||
value: rootPane.visualiserSpacing
|
||||
from: 0
|
||||
to: 2
|
||||
validator: DoubleValidator {
|
||||
bottom: 0
|
||||
top: 2
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.visualiserSpacing = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Border")
|
||||
showBackground: true
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Border rounding")
|
||||
value: rootPane.borderRounding
|
||||
from: 0.1
|
||||
to: 100
|
||||
decimals: 1
|
||||
suffix: "px"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 100
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.borderRounding = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Border thickness")
|
||||
value: rootPane.borderThickness
|
||||
from: 0.1
|
||||
to: 100
|
||||
decimals: 1
|
||||
suffix: "px"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 100
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.borderThickness = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
title: qsTr("Color scheme")
|
||||
description: qsTr("Available color schemes")
|
||||
showBackground: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
Repeater {
|
||||
model: Schemes.list
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
readonly property string schemeKey: `${modelData.name} ${modelData.flavour}`
|
||||
readonly property bool isCurrent: schemeKey === Schemes.currentScheme
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
const name = modelData.name;
|
||||
const flavour = modelData.flavour;
|
||||
const schemeKey = `${name} ${flavour}`;
|
||||
|
||||
Schemes.currentScheme = schemeKey;
|
||||
Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]);
|
||||
|
||||
Qt.callLater(() => {
|
||||
reloadTimer.restart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reloadTimer
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
Schemes.reload();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: schemeRow
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
id: preview
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
border.width: 1
|
||||
border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5)
|
||||
|
||||
color: `#${modelData.colours?.surface}`
|
||||
radius: Appearance.rounding.full
|
||||
implicitWidth: iconPlaceholder.implicitWidth
|
||||
implicitHeight: iconPlaceholder.implicitWidth
|
||||
|
||||
MaterialIcon {
|
||||
id: iconPlaceholder
|
||||
visible: false
|
||||
text: "circle"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitWidth: parent.implicitWidth / 2
|
||||
clip: true
|
||||
|
||||
StyledRect {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitWidth: preview.implicitWidth
|
||||
color: `#${modelData.colours?.primary}`
|
||||
radius: Appearance.rounding.full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: modelData.flavour ?? ""
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.name ?? ""
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3outline
|
||||
|
||||
elide: Text.ElideRight
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
title: qsTr("Color variant")
|
||||
description: qsTr("Material theme variant")
|
||||
showBackground: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
Repeater {
|
||||
model: M3Variants.list
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: modelData.variant === Schemes.currentVariant ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
const variant = modelData.variant;
|
||||
|
||||
Schemes.currentVariant = variant;
|
||||
Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]);
|
||||
|
||||
Qt.callLater(() => {
|
||||
reloadTimer.restart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reloadTimer
|
||||
interval: 300
|
||||
onTriggered: {
|
||||
Schemes.reload();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: variantRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: modelData.icon
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.variant === Schemes.currentVariant ? 1 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.name
|
||||
font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
visible: modelData.variant === Schemes.currentVariant
|
||||
text: "check"
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Fonts")
|
||||
showBackground: true
|
||||
|
||||
CollapsibleSection {
|
||||
id: materialFontSection
|
||||
title: qsTr("Material font family")
|
||||
expanded: true
|
||||
showBackground: true
|
||||
nested: true
|
||||
|
||||
Loader {
|
||||
id: materialFontLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
|
||||
active: materialFontSection.expanded
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: materialFontList
|
||||
property alias contentHeight: materialFontList.contentHeight
|
||||
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
model: Qt.fontFamilies()
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: materialFontList
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
rootPane.fontFamilyMaterial = modelData;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fontFamilyMaterialRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: monoFontSection
|
||||
title: qsTr("Monospace font family")
|
||||
expanded: false
|
||||
showBackground: true
|
||||
nested: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
|
||||
active: monoFontSection.expanded
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: monoFontList
|
||||
property alias contentHeight: monoFontList.contentHeight
|
||||
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
model: Qt.fontFamilies()
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: monoFontList
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
readonly property bool isCurrent: modelData === rootPane.fontFamilyMono
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
rootPane.fontFamilyMono = modelData;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fontFamilyMonoRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: sansFontSection
|
||||
title: qsTr("Sans-serif font family")
|
||||
expanded: false
|
||||
showBackground: true
|
||||
nested: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0
|
||||
active: sansFontSection.expanded
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: sansFontList
|
||||
property alias contentHeight: sansFontList.contentHeight
|
||||
|
||||
clip: true
|
||||
spacing: Appearance.spacing.small / 2
|
||||
model: Qt.fontFamilies()
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: sansFontList
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view.width
|
||||
|
||||
readonly property bool isCurrent: modelData === rootPane.fontFamilySans
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
border.width: isCurrent ? 1 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
rootPane.fontFamilySans = modelData;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fontFamilySansRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: modelData
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: isCurrent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Font size scale")
|
||||
value: rootPane.fontSizeScale
|
||||
from: 0.7
|
||||
to: 1.5
|
||||
decimals: 2
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.7
|
||||
top: 1.5
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.fontSizeScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Scales")
|
||||
showBackground: true
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Padding scale")
|
||||
value: rootPane.paddingScale
|
||||
from: 0.5
|
||||
to: 2.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.5
|
||||
top: 2.0
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.paddingScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Rounding scale")
|
||||
value: rootPane.roundingScale
|
||||
from: 0.1
|
||||
to: 5.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 5.0
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.roundingScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Spacing scale")
|
||||
value: rootPane.spacingScale
|
||||
from: 0.1
|
||||
to: 2.0
|
||||
decimals: 1
|
||||
suffix: "×"
|
||||
validator: DoubleValidator {
|
||||
bottom: 0.1
|
||||
top: 2.0
|
||||
}
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.spacingScale = newValue;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
CollapsibleSection {
|
||||
title: qsTr("Theme mode")
|
||||
description: qsTr("Light or dark theme")
|
||||
showBackground: true
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Dark mode")
|
||||
checked: !Colours.currentLight
|
||||
onToggled: checked => {
|
||||
Colours.setMode(checked ? "dark" : "light");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
CollapsibleSection {
|
||||
id: root
|
||||
|
||||
required property var rootPane
|
||||
|
||||
title: qsTr("Transparency")
|
||||
showBackground: true
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Transparency enabled")
|
||||
checked: rootPane.transparencyEnabled
|
||||
onToggled: checked => {
|
||||
rootPane.transparencyEnabled = checked;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Transparency base")
|
||||
value: rootPane.transparencyBase * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.transparencyBase = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Transparency layers")
|
||||
value: rootPane.transparencyLayers * 100
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "%"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
rootPane.transparencyLayers = newValue / 100;
|
||||
rootPane.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
SplitPaneLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
|
||||
StyledFlickable {
|
||||
id: leftAudioFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: leftContent.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: leftAudioFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: leftContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Audio")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: outputDevicesSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Output devices")
|
||||
expanded: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Devices (%1)").arg(Audio.sinks.length)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("All available output devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
Repeater {
|
||||
Layout.fillWidth: true
|
||||
model: Audio.sinks
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
Audio.setAudioSink(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: outputRowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: Audio.sink?.id === modelData.id ? 1 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.description || qsTr("Unknown")
|
||||
font.weight: Audio.sink?.id === modelData.id ? 500 : 400
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: inputDevicesSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Input devices")
|
||||
expanded: true
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Devices (%1)").arg(Audio.sources.length)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("All available input devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
Repeater {
|
||||
Layout.fillWidth: true
|
||||
model: Audio.sources
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
Audio.setAudioSource(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: inputRowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: "mic"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: Audio.source?.id === modelData.id ? 1 : 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.description || qsTr("Unknown")
|
||||
font.weight: Audio.source?.id === modelData.id ? 500 : 400
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: Component {
|
||||
StyledFlickable {
|
||||
id: rightAudioFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: contentLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: rightAudioFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "volume_up"
|
||||
title: qsTr("Audio Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Output volume")
|
||||
description: qsTr("Control the volume of your output device")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Volume")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: outputVolumeInput
|
||||
Layout.preferredWidth: 70
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
enabled: !Audio.muted
|
||||
|
||||
Component.onCompleted: {
|
||||
text = Math.round(Audio.volume * 100).toString();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Audio
|
||||
function onVolumeChanged() {
|
||||
if (!outputVolumeInput.hasFocus) {
|
||||
outputVolumeInput.text = Math.round(Audio.volume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: text => {
|
||||
if (hasFocus) {
|
||||
const val = parseInt(text);
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) {
|
||||
Audio.setVolume(val / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = parseInt(text);
|
||||
if (isNaN(val) || val < 0 || val > 100) {
|
||||
text = Math.round(Audio.volume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "%"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
opacity: Audio.muted ? 0.5 : 1
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (Audio.sink?.audio) {
|
||||
Audio.sink.audio.muted = !Audio.sink.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: muteIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Audio.muted ? "volume_off" : "volume_up"
|
||||
color: Audio.muted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: outputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
value: Audio.volume
|
||||
enabled: !Audio.muted
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onMoved: {
|
||||
Audio.setVolume(value);
|
||||
if (!outputVolumeInput.hasFocus) {
|
||||
outputVolumeInput.text = Math.round(value * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Input volume")
|
||||
description: qsTr("Control the volume of your input device")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Volume")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: inputVolumeInput
|
||||
Layout.preferredWidth: 70
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
enabled: !Audio.sourceMuted
|
||||
|
||||
Component.onCompleted: {
|
||||
text = Math.round(Audio.sourceVolume * 100).toString();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Audio
|
||||
function onSourceVolumeChanged() {
|
||||
if (!inputVolumeInput.hasFocus) {
|
||||
inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: text => {
|
||||
if (hasFocus) {
|
||||
const val = parseInt(text);
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) {
|
||||
Audio.setSourceVolume(val / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = parseInt(text);
|
||||
if (isNaN(val) || val < 0 || val > 100) {
|
||||
text = Math.round(Audio.sourceVolume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "%"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
opacity: Audio.sourceMuted ? 0.5 : 1
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (Audio.source?.audio) {
|
||||
Audio.source.audio.muted = !Audio.source.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: muteInputIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "mic_off"
|
||||
color: Audio.sourceMuted ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: inputVolumeSlider
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
value: Audio.sourceVolume
|
||||
enabled: !Audio.sourceMuted
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onMoved: {
|
||||
Audio.setSourceVolume(value);
|
||||
if (!inputVolumeInput.hasFocus) {
|
||||
inputVolumeInput.text = Math.round(value * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Applications")
|
||||
description: qsTr("Control volume for individual applications")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Repeater {
|
||||
model: Audio.streams
|
||||
Layout.fillWidth: true
|
||||
|
||||
delegate: ColumnLayout {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: "apps"
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
fill: 0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
text: Audio.getStreamName(modelData)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: streamVolumeInput
|
||||
Layout.preferredWidth: 70
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
enabled: !Audio.getStreamMuted(modelData)
|
||||
|
||||
Component.onCompleted: {
|
||||
text = Math.round(Audio.getStreamVolume(modelData) * 100).toString();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: modelData
|
||||
function onAudioChanged() {
|
||||
if (!streamVolumeInput.hasFocus && modelData?.audio) {
|
||||
streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTextEdited: text => {
|
||||
if (hasFocus) {
|
||||
const val = parseInt(text);
|
||||
if (!isNaN(val) && val >= 0 && val <= 100) {
|
||||
Audio.setStreamVolume(modelData, val / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = parseInt(text);
|
||||
if (isNaN(val) || val < 0 || val > 100) {
|
||||
text = Math.round(Audio.getStreamVolume(modelData) * 100).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "%"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData));
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: streamMuteIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Audio.getStreamMuted(modelData) ? "volume_off" : "volume_up"
|
||||
color: Audio.getStreamMuted(modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
value: Audio.getStreamVolume(modelData)
|
||||
enabled: !Audio.getStreamMuted(modelData)
|
||||
opacity: enabled ? 1 : 0.5
|
||||
onMoved: {
|
||||
Audio.setStreamVolume(modelData, value);
|
||||
if (!streamVolumeInput.hasFocus) {
|
||||
streamVolumeInput.text = Math.round(value * 100).toString();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: modelData
|
||||
function onAudioChanged() {
|
||||
if (modelData?.audio) {
|
||||
value = modelData.audio.volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
visible: Audio.streams.length === 0
|
||||
text: qsTr("No applications currently playing audio")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
|
||||
SplitPaneWithDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
activeItem: session.bt.active
|
||||
paneIdGenerator: function (item) {
|
||||
return item ? (item.address || "") : "";
|
||||
}
|
||||
|
||||
leftContent: Component {
|
||||
StyledFlickable {
|
||||
id: leftFlickable
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: deviceList.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: leftFlickable
|
||||
}
|
||||
|
||||
DeviceList {
|
||||
id: deviceList
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightDetailsComponent: Component {
|
||||
Details {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightSettingsComponent: Component {
|
||||
StyledFlickable {
|
||||
id: settingsFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: settingsFlickable
|
||||
}
|
||||
|
||||
Settings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledFlickable {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property BluetoothDevice device: session.bt.active
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: detailsWrapper.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: root
|
||||
}
|
||||
|
||||
Item {
|
||||
id: detailsWrapper
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
implicitHeight: details.implicitHeight
|
||||
|
||||
DeviceDetails {
|
||||
id: details
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
session: root.session
|
||||
device: root.device
|
||||
|
||||
headerComponent: Component {
|
||||
SettingsHeader {
|
||||
icon: Icons.getBluetoothIcon(root.device?.icon ?? "")
|
||||
title: root.device?.name ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Connection status")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Connection settings for this device")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceStatus
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Connected")
|
||||
checked: root.device?.connected ?? false
|
||||
toggle.onToggled: root.device.connected = checked
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Paired")
|
||||
checked: root.device?.paired ?? false
|
||||
toggle.onToggled: {
|
||||
if (root.device.paired)
|
||||
root.device.forget();
|
||||
else
|
||||
root.device.pair();
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Blocked")
|
||||
checked: root.device?.blocked ?? false
|
||||
toggle.onToggled: root.device.blocked = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Device properties")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Additional settings")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceProps
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Item {
|
||||
id: renameDevice
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.spacing.small
|
||||
|
||||
implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "editingDeviceName"
|
||||
when: root.session.bt.editingDeviceName
|
||||
|
||||
AnchorChanges {
|
||||
target: deviceNameEdit
|
||||
anchors.top: renameDevice.top
|
||||
}
|
||||
PropertyChanges {
|
||||
renameDevice.implicitHeight: deviceNameEdit.implicitHeight
|
||||
renameLabel.opacity: 0
|
||||
deviceNameEdit.padding: Appearance.padding.normal
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
Anim {
|
||||
properties: "implicitHeight,opacity,padding"
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: renameLabel
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: qsTr("Device name")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: deviceNameEdit
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: renameLabel.bottom
|
||||
anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal
|
||||
|
||||
text: root.device?.name ?? ""
|
||||
readOnly: !root.session.bt.editingDeviceName
|
||||
onAccepted: {
|
||||
root.session.bt.editingDeviceName = false;
|
||||
root.device.name = text;
|
||||
}
|
||||
|
||||
leftPadding: Appearance.padding.normal
|
||||
rightPadding: Appearance.padding.normal
|
||||
|
||||
background: StyledRect {
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 2
|
||||
border.color: Colours.palette.m3primary
|
||||
opacity: root.session.bt.editingDeviceName ? 1 : 0
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.small
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
opacity: root.session.bt.editingDeviceName ? 1 : 0
|
||||
scale: root.session.bt.editingDeviceName ? 1 : 0.5
|
||||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
disabled: !root.session.bt.editingDeviceName
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingDeviceName = false;
|
||||
deviceNameEdit.text = Qt.binding(() => root.device?.name ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: cancelEditIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: "cancel"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
|
||||
color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName;
|
||||
if (root.session.bt.editingDeviceName)
|
||||
deviceNameEdit.forceActiveFocus();
|
||||
else
|
||||
deviceNameEdit.accepted();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: editIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: root.session.bt.editingDeviceName ? "check_circle" : "edit"
|
||||
color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Trusted")
|
||||
checked: root.device?.trusted ?? false
|
||||
toggle.onToggled: root.device.trusted = checked
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Wake allowed")
|
||||
checked: root.device?.wakeAllowed ?? false
|
||||
toggle.onToggled: root.device.wakeAllowed = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Device information")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Information about this device")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceInfo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: batteryPercent
|
||||
Layout.topMargin: Appearance.spacing.small / 2
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Appearance.padding.smaller
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
|
||||
StyledRect {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: parent.height * 0.25
|
||||
|
||||
implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Dbus path")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.dbusPath ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("MAC address")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.address ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Bonded")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.bonded ? qsTr("Yes") : qsTr("No")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("System name")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.device?.deviceName ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.right: fabRoot.right
|
||||
anchors.bottom: fabRoot.top
|
||||
anchors.bottomMargin: Appearance.padding.normal
|
||||
|
||||
Repeater {
|
||||
id: fabMenu
|
||||
|
||||
model: ListModel {
|
||||
ListElement {
|
||||
name: "trust"
|
||||
icon: "handshake"
|
||||
}
|
||||
ListElement {
|
||||
name: "block"
|
||||
icon: "block"
|
||||
}
|
||||
ListElement {
|
||||
name: "pair"
|
||||
icon: "missing_controller"
|
||||
}
|
||||
ListElement {
|
||||
name: "connect"
|
||||
icon: "bluetooth_connected"
|
||||
}
|
||||
}
|
||||
|
||||
StyledClippingRect {
|
||||
id: fabMenuItem
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
Layout.alignment: Qt.AlignRight
|
||||
|
||||
implicitHeight: fabMenuItemInner.implicitHeight + Appearance.padding.larger * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3primaryContainer
|
||||
|
||||
opacity: 0
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.session.bt.fabMenuOpen
|
||||
|
||||
PropertyChanges {
|
||||
fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Appearance.padding.large * 2
|
||||
fabMenuItem.opacity: 1
|
||||
fabMenuItemInner.opacity: 1
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
to: "visible"
|
||||
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: (fabMenu.count - 1 - fabMenuItem.index) * Appearance.anim.durations.small / 8
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "implicitWidth"
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "visible"
|
||||
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: fabMenuItem.index * Appearance.anim.durations.small / 8
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "implicitWidth"
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "opacity"
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.session.bt.fabMenuOpen = false;
|
||||
|
||||
const name = fabMenuItem.modelData.name;
|
||||
if (fabMenuItem.modelData.name !== "pair")
|
||||
root.device[`${name}ed`] = !root.device[`${name}ed`];
|
||||
else if (root.device.paired)
|
||||
root.device.forget();
|
||||
else
|
||||
root.device.pair();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: fabMenuItemInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
opacity: 0
|
||||
|
||||
MaterialIcon {
|
||||
text: fabMenuItem.modelData.icon
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
fill: 1
|
||||
}
|
||||
|
||||
StyledText {
|
||||
animate: true
|
||||
text: (root.device && root.device[`${fabMenuItem.modelData.name}ed`] ? fabMenuItem.modelData.name === "connect" ? "dis" : "un" : "") + fabMenuItem.modelData.name
|
||||
color: Colours.palette.m3onPrimaryContainer
|
||||
font.capitalization: Font.Capitalize
|
||||
Layout.preferredWidth: implicitWidth
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fabRoot
|
||||
|
||||
x: root.contentX + root.width - width
|
||||
y: root.contentY + root.height - height
|
||||
width: 64
|
||||
height: 64
|
||||
z: 10000
|
||||
|
||||
StyledRect {
|
||||
id: fabBg
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
implicitWidth: 64
|
||||
implicitHeight: 64
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.session.bt.fabMenuOpen
|
||||
|
||||
PropertyChanges {
|
||||
fabBg.implicitWidth: 48
|
||||
fabBg.implicitHeight: 48
|
||||
fabBg.radius: 48 / 2
|
||||
fab.font.pointSize: Appearance.font.size.larger
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
Anim {
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
properties: "radius,font.pointSize"
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
level: fabState.containsMouse && !fabState.pressed ? 4 : 3
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: fabState
|
||||
|
||||
color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: fab
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: root.session.bt.fabMenuOpen ? "close" : "settings"
|
||||
color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Toggle: RowLayout {
|
||||
required property string label
|
||||
property alias checked: toggle.checked
|
||||
property alias toggle: toggle
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: parent.label
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
id: toggle
|
||||
|
||||
cLayer: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceList {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property bool smallDiscoverable: width <= 540
|
||||
readonly property bool smallPairable: width <= 480
|
||||
|
||||
title: qsTr("Devices (%1)").arg(Bluetooth.devices.values.length)
|
||||
description: qsTr("All available bluetooth devices")
|
||||
activeItem: session.bt.active
|
||||
|
||||
model: ScriptModel {
|
||||
id: deviceModel
|
||||
|
||||
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Bluetooth")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.enabled ?? false
|
||||
icon: "power"
|
||||
accent: "Tertiary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Toggle Bluetooth")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.enabled = !adapter.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.discoverable ?? false
|
||||
icon: root.smallDiscoverable ? "group_search" : ""
|
||||
label: root.smallDiscoverable ? "" : qsTr("Discoverable")
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Make discoverable")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discoverable = !adapter.discoverable;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.pairable ?? false
|
||||
icon: "missing_controller"
|
||||
label: root.smallPairable ? "" : qsTr("Pairable")
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Make pairable")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.pairable = !adapter.pairable;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Bluetooth.defaultAdapter?.discovering ?? false
|
||||
icon: "bluetooth_searching"
|
||||
accent: "Secondary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Scan for devices")
|
||||
|
||||
onClicked: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discovering = !adapter.discovering;
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.bt.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Bluetooth settings")
|
||||
|
||||
onClicked: {
|
||||
if (root.session.bt.active)
|
||||
root.session.bt.active = null;
|
||||
else {
|
||||
root.session.bt.active = root.model.values[0] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
id: device
|
||||
|
||||
required property BluetoothDevice modelData
|
||||
readonly property bool loading: modelData && (modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting)
|
||||
readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
function onClicked(): void {
|
||||
if (device.modelData)
|
||||
root.session.bt.active = device.modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: deviceInner
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Qt.alpha(device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "")
|
||||
color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: device.connected ? 1 : 0
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: device.modelData ? device.modelData.name : qsTr("Unknown")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: connectBtn
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0)
|
||||
|
||||
CircularIndicator {
|
||||
anchors.fill: parent
|
||||
running: device.loading
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
disabled: device.loading
|
||||
|
||||
function onClicked(): void {
|
||||
if (device.loading)
|
||||
return;
|
||||
|
||||
if (device.connected) {
|
||||
device.modelData.connected = false;
|
||||
} else {
|
||||
if (device.modelData.bonded) {
|
||||
device.modelData.connected = true;
|
||||
} else {
|
||||
device.modelData.pair();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: device.connected ? "link_off" : "link"
|
||||
color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
opacity: device.loading ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelected: item => session.bt.active = item
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "bluetooth"
|
||||
title: qsTr("Bluetooth Settings")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Adapter status")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("General adapter settings")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: adapterStatus.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: adapterStatus
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Powered")
|
||||
checked: Bluetooth.defaultAdapter?.enabled ?? false
|
||||
toggle.onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.enabled = checked;
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Discoverable")
|
||||
checked: Bluetooth.defaultAdapter?.discoverable ?? false
|
||||
toggle.onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.discoverable = checked;
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
label: qsTr("Pairable")
|
||||
checked: Bluetooth.defaultAdapter?.pairable ?? false
|
||||
toggle.onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter;
|
||||
if (adapter)
|
||||
adapter.pairable = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Adapter properties")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Per-adapter settings")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: adapterSettings.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: adapterSettings
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.larger
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Current adapter")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: adapterPickerButton
|
||||
|
||||
property bool expanded
|
||||
|
||||
implicitWidth: adapterPicker.implicitWidth + Appearance.padding.normal * 2
|
||||
implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
function onClicked(): void {
|
||||
adapterPickerButton.expanded = !adapterPickerButton.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: adapterPicker
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.topMargin: Appearance.padding.smaller
|
||||
anchors.bottomMargin: Appearance.padding.smaller
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.leftMargin: Appearance.padding.small
|
||||
text: Bluetooth.defaultAdapter?.name ?? qsTr("None")
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "expand_more"
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: adapterListBg
|
||||
radius: adapterListBg.radius
|
||||
opacity: adapterPickerButton.expanded ? 1 : 0
|
||||
scale: adapterPickerButton.expanded ? 1 : 0.7
|
||||
level: 2
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledClippingRect {
|
||||
id: adapterListBg
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight
|
||||
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
radius: Appearance.rounding.small
|
||||
opacity: adapterPickerButton.expanded ? 1 : 0
|
||||
scale: adapterPickerButton.expanded ? 1 : 0.7
|
||||
|
||||
ColumnLayout {
|
||||
id: adapterList
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: Bluetooth.adapters
|
||||
|
||||
Item {
|
||||
id: adapter
|
||||
|
||||
required property BluetoothAdapter modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
StateLayer {
|
||||
disabled: !adapterPickerButton.expanded
|
||||
|
||||
function onClicked(): void {
|
||||
adapterPickerButton.expanded = false;
|
||||
root.session.bt.currentAdapter = adapter.modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: adapterInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Appearance.padding.small
|
||||
text: adapter.modelData.name
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
text: "check"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
visible: adapter.modelData === root.session.bt.currentAdapter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Discoverable timeout")
|
||||
}
|
||||
|
||||
CustomSpinBox {
|
||||
min: 0
|
||||
value: root.session.bt.currentAdapter?.discoverableTimeout ?? 0
|
||||
onValueModified: value => {
|
||||
if (root.session.bt.currentAdapter) {
|
||||
root.session.bt.currentAdapter.discoverableTimeout = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Item {
|
||||
id: renameAdapter
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: Appearance.spacing.small
|
||||
|
||||
implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "editingAdapterName"
|
||||
when: root.session.bt.editingAdapterName
|
||||
|
||||
AnchorChanges {
|
||||
target: adapterNameEdit
|
||||
anchors.top: renameAdapter.top
|
||||
}
|
||||
PropertyChanges {
|
||||
renameAdapter.implicitHeight: adapterNameEdit.implicitHeight
|
||||
renameLabel.opacity: 0
|
||||
adapterNameEdit.padding: Appearance.padding.normal
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
Anim {
|
||||
properties: "implicitHeight,opacity,padding"
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: renameLabel
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: qsTr("Rename adapter (currently does not work)")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: adapterNameEdit
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: renameLabel.bottom
|
||||
anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal
|
||||
|
||||
text: root.session.bt.currentAdapter?.name ?? ""
|
||||
readOnly: !root.session.bt.editingAdapterName
|
||||
onAccepted: {
|
||||
root.session.bt.editingAdapterName = false;
|
||||
}
|
||||
|
||||
leftPadding: Appearance.padding.normal
|
||||
rightPadding: Appearance.padding.normal
|
||||
|
||||
background: StyledRect {
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 2
|
||||
border.color: Colours.palette.m3primary
|
||||
opacity: root.session.bt.editingAdapterName ? 1 : 0
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.small
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
opacity: root.session.bt.editingAdapterName ? 1 : 0
|
||||
scale: root.session.bt.editingAdapterName ? 1 : 0.5
|
||||
|
||||
StateLayer {
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
disabled: !root.session.bt.editingAdapterName
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingAdapterName = false;
|
||||
adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: cancelEditIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: "cancel"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: root.session.bt.editingAdapterName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale)
|
||||
color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName;
|
||||
if (root.session.bt.editingAdapterName)
|
||||
adapterNameEdit.forceActiveFocus();
|
||||
else
|
||||
adapterNameEdit.accepted();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: editIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: root.session.bt.editingAdapterName ? "check_circle" : "edit"
|
||||
color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Adapter information")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Information about the default adapter")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: adapterInfo.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: adapterInfo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Adapter state")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Dbus path")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Bluetooth.defaultAdapter?.dbusPath ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Adapter id")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Bluetooth.defaultAdapter?.adapterId ?? ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Toggle: RowLayout {
|
||||
required property string label
|
||||
property alias checked: toggle.checked
|
||||
property alias toggle: toggle
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: parent.label
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
id: toggle
|
||||
|
||||
cLayer: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
property var options: [] // Array of {label: string, propertyName: string, onToggled: function}
|
||||
property var rootItem: null // The root item that contains the properties we want to bind to
|
||||
property string title: "" // Optional title text
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
visible: root.title !== ""
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: buttonRow
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: root.options
|
||||
|
||||
delegate: TextButton {
|
||||
id: button
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
text: modelData.label
|
||||
|
||||
property bool _checked: false
|
||||
|
||||
checked: _checked
|
||||
toggle: false
|
||||
type: TextButton.Tonal
|
||||
|
||||
// Create binding in Component.onCompleted
|
||||
Component.onCompleted: {
|
||||
if (root.rootItem && modelData.propertyName) {
|
||||
const propName = modelData.propertyName;
|
||||
const rootItem = root.rootItem;
|
||||
_checked = Qt.binding(function () {
|
||||
return rootItem[propName] ?? false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Match utilities Toggles radius styling
|
||||
// Each button has full rounding (not connected) since they have spacing
|
||||
radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal
|
||||
|
||||
// Match utilities Toggles inactive color
|
||||
inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2)
|
||||
|
||||
// Adjust width similar to utilities toggles
|
||||
Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0)
|
||||
|
||||
onClicked: {
|
||||
if (modelData.onToggled && root.rootItem && modelData.propertyName) {
|
||||
const currentValue = root.rootItem[modelData.propertyName] ?? false;
|
||||
modelData.onToggled(!currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property Session session
|
||||
property var device: null
|
||||
|
||||
property Component headerComponent: null
|
||||
property list<Component> sections: []
|
||||
|
||||
property Component topContent: null
|
||||
property Component bottomContent: null
|
||||
|
||||
implicitWidth: layout.implicitWidth
|
||||
implicitHeight: layout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Loader {
|
||||
id: headerLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.headerComponent
|
||||
visible: root.headerComponent !== null
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: topContentLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.topContent
|
||||
visible: root.topContent !== null
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.sections
|
||||
|
||||
Loader {
|
||||
required property Component modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: modelData
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: bottomContentLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.bottomContent
|
||||
visible: root.bottomContent !== null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property Session session: null
|
||||
property var model: null
|
||||
property Component delegate: null
|
||||
|
||||
property string title: ""
|
||||
property string description: ""
|
||||
property var activeItem: null
|
||||
property Component headerComponent: null
|
||||
property Component titleSuffix: null
|
||||
property bool showHeader: true
|
||||
|
||||
signal itemSelected(var item)
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Loader {
|
||||
id: headerLoader
|
||||
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: root.headerComponent
|
||||
visible: root.headerComponent !== null && root.showHeader
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: root.headerComponent ? 0 : 0
|
||||
spacing: Appearance.spacing.small
|
||||
visible: root.title !== "" || root.description !== ""
|
||||
|
||||
StyledText {
|
||||
visible: root.title !== ""
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Loader {
|
||||
sourceComponent: root.titleSuffix
|
||||
visible: root.titleSuffix !== null
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
property alias view: view
|
||||
|
||||
StyledText {
|
||||
visible: root.description !== ""
|
||||
Layout.fillWidth: true
|
||||
text: root.description
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledListView {
|
||||
id: view
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentHeight
|
||||
|
||||
model: root.model
|
||||
delegate: root.delegate
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
interactive: false
|
||||
clip: false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import QtQuick
|
||||
|
||||
SequentialAnimation {
|
||||
id: root
|
||||
|
||||
required property Item target
|
||||
property list<PropertyAction> propertyActions
|
||||
|
||||
property real scaleFrom: 1.0
|
||||
property real scaleTo: 0.8
|
||||
property real opacityFrom: 1.0
|
||||
property real opacityTo: 0.0
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "opacity"
|
||||
from: root.opacityFrom
|
||||
to: root.opacityTo
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "scale"
|
||||
from: root.scaleFrom
|
||||
to: root.scaleTo
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
for (let i = 0; i < root.propertyActions.length; i++) {
|
||||
const action = root.propertyActions[i];
|
||||
if (action.target && action.property !== undefined) {
|
||||
action.target[action.property] = action.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "opacity"
|
||||
from: root.opacityTo
|
||||
to: root.opacityFrom
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "scale"
|
||||
from: root.scaleTo
|
||||
to: root.scaleFrom
|
||||
duration: Appearance.anim.durations.normal / 2
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
property real from: 0
|
||||
property real to: 100
|
||||
property string suffix: ""
|
||||
property bool readonly: false
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
visible: root.label !== ""
|
||||
text: root.label
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
visible: root.readonly
|
||||
text: "lock"
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1)
|
||||
opacity: root.readonly ? 0.5 : 1.0
|
||||
|
||||
StyledRect {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width * ((root.value - root.from) / (root.to - root.from))
|
||||
radius: parent.radius
|
||||
color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string icon
|
||||
required property string title
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: column.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: column
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.icon
|
||||
font.pointSize: Appearance.font.size.extraLarge * 3
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.title
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property real value: 0
|
||||
property real from: 0
|
||||
property real to: 100
|
||||
property real stepSize: 0
|
||||
property var validator: null
|
||||
property string suffix: "" // Optional suffix text (e.g., "×", "px")
|
||||
property int decimals: 1 // Number of decimal places to show (default: 1)
|
||||
property var formatValueFunction: null // Optional custom format function
|
||||
property var parseValueFunction: null // Optional custom parse function
|
||||
|
||||
function formatValue(val: real): string {
|
||||
if (formatValueFunction) {
|
||||
return formatValueFunction(val);
|
||||
}
|
||||
// Default format function
|
||||
// Check if it's an IntValidator (IntValidator doesn't have a 'decimals' property)
|
||||
if (validator && validator.bottom !== undefined && validator.decimals === undefined) {
|
||||
return Math.round(val).toString();
|
||||
}
|
||||
// For DoubleValidator or no validator, use the decimals property
|
||||
return val.toFixed(root.decimals);
|
||||
}
|
||||
|
||||
function parseValue(text: string): real {
|
||||
if (parseValueFunction) {
|
||||
return parseValueFunction(text);
|
||||
}
|
||||
// Default parse function
|
||||
if (validator && validator.bottom !== undefined) {
|
||||
// Check if it's an integer validator
|
||||
if (validator.top !== undefined && validator.top === Math.floor(validator.top)) {
|
||||
return parseInt(text);
|
||||
}
|
||||
}
|
||||
return parseFloat(text);
|
||||
}
|
||||
|
||||
signal valueModified(real newValue)
|
||||
|
||||
property bool _initialized: false
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Component.onCompleted: {
|
||||
// Set initialized flag after a brief delay to allow component to fully load
|
||||
Qt.callLater(() => {
|
||||
_initialized = true;
|
||||
});
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
visible: root.label !== ""
|
||||
text: root.label
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
StyledInputField {
|
||||
id: inputField
|
||||
Layout.preferredWidth: 70
|
||||
validator: root.validator
|
||||
|
||||
Component.onCompleted: {
|
||||
// Initialize text without triggering valueModified signal
|
||||
text = root.formatValue(root.value);
|
||||
}
|
||||
|
||||
onTextEdited: text => {
|
||||
if (hasFocus) {
|
||||
const val = root.parseValue(text);
|
||||
if (!isNaN(val)) {
|
||||
// Validate against validator bounds if available
|
||||
let isValid = true;
|
||||
if (root.validator) {
|
||||
if (root.validator.bottom !== undefined && val < root.validator.bottom) {
|
||||
isValid = false;
|
||||
}
|
||||
if (root.validator.top !== undefined && val > root.validator.top) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
root.valueModified(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onEditingFinished: {
|
||||
const val = root.parseValue(text);
|
||||
let isValid = true;
|
||||
if (root.validator) {
|
||||
if (root.validator.bottom !== undefined && val < root.validator.bottom) {
|
||||
isValid = false;
|
||||
}
|
||||
if (root.validator.top !== undefined && val > root.validator.top) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNaN(val) || !isValid) {
|
||||
text = root.formatValue(root.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
visible: root.suffix !== ""
|
||||
text: root.suffix
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: slider
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
from: root.from
|
||||
to: root.to
|
||||
stepSize: root.stepSize
|
||||
|
||||
// Use Binding to allow slider to move freely during dragging
|
||||
Binding {
|
||||
target: slider
|
||||
property: "value"
|
||||
value: root.value
|
||||
when: !slider.pressed
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
// Update input field text in real-time as slider moves during dragging
|
||||
// Always update when slider value changes (during dragging or external updates)
|
||||
if (!inputField.hasFocus) {
|
||||
const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;
|
||||
inputField.text = root.formatValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
onMoved: {
|
||||
const newValue = root.stepSize > 0 ? Math.round(value / root.stepSize) * root.stepSize : value;
|
||||
root.valueModified(newValue);
|
||||
if (!inputField.hasFocus) {
|
||||
inputField.text = root.formatValue(newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update input field when value changes externally (slider is already bound)
|
||||
onValueChanged: {
|
||||
// Only update if component is initialized to avoid issues during creation
|
||||
if (root._initialized && !inputField.hasFocus) {
|
||||
inputField.text = root.formatValue(root.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
spacing: 0
|
||||
|
||||
property Component leftContent: null
|
||||
property Component rightContent: null
|
||||
|
||||
property real leftWidthRatio: 0.4
|
||||
property int leftMinimumWidth: 420
|
||||
property var leftLoaderProperties: ({})
|
||||
property var rightLoaderProperties: ({})
|
||||
|
||||
property alias leftLoader: leftLoader
|
||||
property alias rightLoader: rightLoader
|
||||
|
||||
Item {
|
||||
id: leftPane
|
||||
|
||||
Layout.preferredWidth: Math.floor(parent.width * root.leftWidthRatio)
|
||||
Layout.minimumWidth: root.leftMinimumWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
ClippingRectangle {
|
||||
id: leftClippingRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal / 2
|
||||
|
||||
radius: leftBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: leftLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
|
||||
|
||||
sourceComponent: root.leftContent
|
||||
|
||||
Component.onCompleted: {
|
||||
for (const key in root.leftLoaderProperties) {
|
||||
leftLoader[key] = root.leftLoaderProperties[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: leftBorder
|
||||
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal / 2
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightPane
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ClippingRectangle {
|
||||
id: rightClippingRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal / 2
|
||||
|
||||
radius: rightBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: rightLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large * 2
|
||||
|
||||
sourceComponent: root.rightContent
|
||||
|
||||
Component.onCompleted: {
|
||||
for (const key in root.rightLoaderProperties) {
|
||||
rightLoader[key] = root.rightLoaderProperties[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: rightBorder
|
||||
|
||||
leftThickness: Appearance.padding.normal / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Component leftContent
|
||||
required property Component rightDetailsComponent
|
||||
required property Component rightSettingsComponent
|
||||
|
||||
property var activeItem: null
|
||||
property var paneIdGenerator: function (item) {
|
||||
return item ? String(item) : "";
|
||||
}
|
||||
|
||||
property Component overlayComponent: null
|
||||
|
||||
SplitPaneLayout {
|
||||
id: splitLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: root.leftContent
|
||||
|
||||
rightContent: Component {
|
||||
Item {
|
||||
id: rightPaneItem
|
||||
|
||||
property var pane: root.activeItem
|
||||
property string paneId: root.paneIdGenerator(pane)
|
||||
property Component targetComponent: root.rightSettingsComponent
|
||||
property Component nextComponent: root.rightSettingsComponent
|
||||
|
||||
function getComponentForPane() {
|
||||
return pane ? root.rightDetailsComponent : root.rightSettingsComponent;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
targetComponent = getComponentForPane();
|
||||
nextComponent = targetComponent;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: rightLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
transformOrigin: Item.Center
|
||||
|
||||
clip: false
|
||||
sourceComponent: rightPaneItem.targetComponent
|
||||
}
|
||||
|
||||
Behavior on paneId {
|
||||
PaneTransition {
|
||||
target: rightLoader
|
||||
propertyActions: [
|
||||
PropertyAction {
|
||||
target: rightPaneItem
|
||||
property: "targetComponent"
|
||||
value: rightPaneItem.nextComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onPaneChanged: {
|
||||
nextComponent = getComponentForPane();
|
||||
paneId = root.paneIdGenerator(pane);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: overlayLoader
|
||||
|
||||
anchors.fill: parent
|
||||
z: 1000
|
||||
sourceComponent: root.overlayComponent
|
||||
active: root.overlayComponent !== null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.images
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Caelestia.Models
|
||||
import QtQuick
|
||||
|
||||
GridView {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
readonly property int minCellWidth: 200 + Appearance.spacing.normal
|
||||
readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth))
|
||||
|
||||
cellWidth: width / columnsCount
|
||||
cellHeight: 140 + Appearance.spacing.normal
|
||||
|
||||
model: Wallpapers.list
|
||||
|
||||
clip: true
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: root
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: root.cellWidth
|
||||
height: root.cellHeight
|
||||
|
||||
readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent
|
||||
readonly property real itemMargin: Appearance.spacing.normal / 2
|
||||
readonly property real itemRadius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: itemMargin
|
||||
anchors.rightMargin: itemMargin
|
||||
anchors.topMargin: itemMargin
|
||||
anchors.bottomMargin: itemMargin
|
||||
radius: itemRadius
|
||||
|
||||
function onClicked(): void {
|
||||
Wallpapers.setWallpaper(modelData.path);
|
||||
}
|
||||
}
|
||||
|
||||
StyledClippingRect {
|
||||
id: image
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: itemMargin
|
||||
anchors.rightMargin: itemMargin
|
||||
anchors.topMargin: itemMargin
|
||||
anchors.bottomMargin: itemMargin
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
radius: itemRadius
|
||||
antialiasing: true
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
|
||||
CachingImage {
|
||||
id: cachingImage
|
||||
|
||||
path: modelData.path
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
sourceSize: Qt.size(width, height)
|
||||
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if CachingImage fails to load
|
||||
Image {
|
||||
id: fallbackImage
|
||||
|
||||
anchors.fill: parent
|
||||
source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : ""
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: true
|
||||
visible: opacity > 0
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
sourceSize: Qt.size(width, height)
|
||||
|
||||
opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fallbackTimer
|
||||
|
||||
property bool triggered: false
|
||||
interval: 800
|
||||
running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null
|
||||
onTriggered: triggered = true
|
||||
}
|
||||
|
||||
// Gradient overlay for filename
|
||||
Rectangle {
|
||||
id: filenameOverlay
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5
|
||||
radius: 0
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.3
|
||||
color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.7)
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.6
|
||||
color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.9)
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: Qt.rgba(Colours.palette.m3surface.r, Colours.palette.m3surface.g, Colours.palette.m3surface.b, 0.95)
|
||||
}
|
||||
}
|
||||
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: itemMargin
|
||||
anchors.rightMargin: itemMargin
|
||||
anchors.topMargin: itemMargin
|
||||
anchors.bottomMargin: itemMargin
|
||||
color: "transparent"
|
||||
radius: itemRadius + border.width
|
||||
border.width: isCurrent ? 2 : 0
|
||||
border.color: Colours.palette.m3primary
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Appearance.padding.small
|
||||
|
||||
visible: isCurrent
|
||||
text: "check_circle"
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: filenameText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
|
||||
anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2
|
||||
anchors.bottomMargin: Appearance.padding.normal
|
||||
|
||||
text: modelData.name
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
font.weight: 500
|
||||
color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
||||
elide: Text.ElideMiddle
|
||||
maximumLineCount: 1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
// General Settings
|
||||
property bool enabled: Config.dashboard.enabled ?? true
|
||||
property bool showOnHover: Config.dashboard.showOnHover ?? true
|
||||
property int updateInterval: Config.dashboard.updateInterval ?? 1000
|
||||
property int dragThreshold: Config.dashboard.dragThreshold ?? 50
|
||||
|
||||
// Performance Resources
|
||||
property bool showBattery: Config.dashboard.performance.showBattery ?? false
|
||||
property bool showGpu: Config.dashboard.performance.showGpu ?? true
|
||||
property bool showCpu: Config.dashboard.performance.showCpu ?? true
|
||||
property bool showMemory: Config.dashboard.performance.showMemory ?? true
|
||||
property bool showStorage: Config.dashboard.performance.showStorage ?? true
|
||||
property bool showNetwork: Config.dashboard.performance.showNetwork ?? true
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function saveConfig() {
|
||||
Config.dashboard.enabled = root.enabled;
|
||||
Config.dashboard.showOnHover = root.showOnHover;
|
||||
Config.dashboard.updateInterval = root.updateInterval;
|
||||
Config.dashboard.dragThreshold = root.dragThreshold;
|
||||
Config.dashboard.performance.showBattery = root.showBattery;
|
||||
Config.dashboard.performance.showGpu = root.showGpu;
|
||||
Config.dashboard.performance.showCpu = root.showCpu;
|
||||
Config.dashboard.performance.showMemory = root.showMemory;
|
||||
Config.dashboard.performance.showStorage = root.showStorage;
|
||||
Config.dashboard.performance.showNetwork = root.showNetwork;
|
||||
// Note: sizes properties are readonly and cannot be modified
|
||||
Config.save();
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: dashboardClippingRect
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
radius: dashboardBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: dashboardLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large
|
||||
|
||||
sourceComponent: dashboardContentComponent
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: dashboardBorder
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal
|
||||
}
|
||||
|
||||
Component {
|
||||
id: dashboardContentComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: dashboardFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: dashboardLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: dashboardFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: dashboardLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Dashboard")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
// General Settings Section
|
||||
GeneralSection {
|
||||
rootItem: root
|
||||
}
|
||||
|
||||
// Performance Resources Section
|
||||
PerformanceSection {
|
||||
rootItem: root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
SectionContainer {
|
||||
id: root
|
||||
|
||||
required property var rootItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("General Settings")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: root.rootItem.enabled
|
||||
onToggled: checked => {
|
||||
root.rootItem.enabled = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Show on hover")
|
||||
checked: root.rootItem.showOnHover
|
||||
onToggled: checked => {
|
||||
root.rootItem.showOnHover = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Update interval")
|
||||
value: root.rootItem.updateInterval
|
||||
from: 100
|
||||
to: 10000
|
||||
stepSize: 100
|
||||
suffix: "ms"
|
||||
validator: IntValidator { bottom: 100; top: 10000 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
root.rootItem.updateInterval = Math.round(newValue);
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Drag threshold")
|
||||
value: root.rootItem.dragThreshold
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "px"
|
||||
validator: IntValidator { bottom: 0; top: 100 }
|
||||
formatValueFunction: (val) => Math.round(val).toString()
|
||||
parseValueFunction: (text) => parseInt(text)
|
||||
|
||||
onValueModified: (newValue) => {
|
||||
root.rootItem.dragThreshold = Math.round(newValue);
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import ".."
|
||||
import "../components"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.UPower
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SectionContainer {
|
||||
id: root
|
||||
|
||||
required property var rootItem
|
||||
// GPU toggle is hidden when gpuType is "NONE" (no GPU data available)
|
||||
readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE"
|
||||
// Battery toggle is hidden when no laptop battery is present
|
||||
readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery
|
||||
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Performance Resources")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root.rootItem
|
||||
options: {
|
||||
let opts = [];
|
||||
if (root.batteryAvailable)
|
||||
opts.push({
|
||||
"label": qsTr("Battery"),
|
||||
"propertyName": "showBattery",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showBattery = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
});
|
||||
|
||||
if (root.gpuAvailable)
|
||||
opts.push({
|
||||
"label": qsTr("GPU"),
|
||||
"propertyName": "showGpu",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showGpu = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
});
|
||||
|
||||
opts.push({
|
||||
"label": qsTr("CPU"),
|
||||
"propertyName": "showCpu",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showCpu = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}, {
|
||||
"label": qsTr("Memory"),
|
||||
"propertyName": "showMemory",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showMemory = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}, {
|
||||
"label": qsTr("Storage"),
|
||||
"propertyName": "showStorage",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showStorage = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
}, {
|
||||
"label": qsTr("Network"),
|
||||
"propertyName": "showNetwork",
|
||||
"onToggled": function(checked) {
|
||||
root.rootItem.showNetwork = checked;
|
||||
root.rootItem.saveConfig();
|
||||
}
|
||||
});
|
||||
return opts;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,658 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "../../launcher/services"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../../../utils/scripts/fuzzysort.js" as Fuzzy
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property var selectedApp: root.session.launcher.active
|
||||
property bool hideFromLauncherChecked: false
|
||||
property bool favouriteChecked: false
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onSelectedAppChanged: {
|
||||
root.session.launcher.active = root.selectedApp;
|
||||
updateToggleState();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session.launcher
|
||||
function onActiveChanged() {
|
||||
root.selectedApp = root.session.launcher.active;
|
||||
updateToggleState();
|
||||
}
|
||||
}
|
||||
|
||||
function updateToggleState() {
|
||||
if (!root.selectedApp) {
|
||||
root.hideFromLauncherChecked = false;
|
||||
root.favouriteChecked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const appId = root.selectedApp.id || root.selectedApp.entry?.id;
|
||||
|
||||
root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId);
|
||||
root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId);
|
||||
}
|
||||
|
||||
function saveHiddenApps(isHidden) {
|
||||
if (!root.selectedApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appId = root.selectedApp.id || root.selectedApp.entry?.id;
|
||||
|
||||
const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
|
||||
|
||||
if (isHidden) {
|
||||
if (!hiddenApps.includes(appId)) {
|
||||
hiddenApps.push(appId);
|
||||
}
|
||||
} else {
|
||||
const index = hiddenApps.indexOf(appId);
|
||||
if (index !== -1) {
|
||||
hiddenApps.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Config.launcher.hiddenApps = hiddenApps;
|
||||
Config.save();
|
||||
}
|
||||
|
||||
AppDb {
|
||||
id: allAppsDb
|
||||
|
||||
path: `${Paths.state}/apps.sqlite`
|
||||
favouriteApps: Config.launcher.favouriteApps
|
||||
entries: DesktopEntries.applications.values
|
||||
}
|
||||
|
||||
property string searchText: ""
|
||||
|
||||
function filterApps(search: string): list<var> {
|
||||
if (!search || search.trim() === "") {
|
||||
const apps = [];
|
||||
for (let i = 0; i < allAppsDb.apps.length; i++) {
|
||||
apps.push(allAppsDb.apps[i]);
|
||||
}
|
||||
return apps;
|
||||
}
|
||||
|
||||
if (!allAppsDb.apps || allAppsDb.apps.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const preparedApps = [];
|
||||
for (let i = 0; i < allAppsDb.apps.length; i++) {
|
||||
const app = allAppsDb.apps[i];
|
||||
const name = app.name || app.entry?.name || "";
|
||||
preparedApps.push({
|
||||
_item: app,
|
||||
name: Fuzzy.prepare(name)
|
||||
});
|
||||
}
|
||||
|
||||
const results = Fuzzy.go(search, preparedApps, {
|
||||
all: true,
|
||||
keys: ["name"],
|
||||
scoreFn: r => r[0].score
|
||||
});
|
||||
|
||||
return results.sort((a, b) => b._score - a._score).map(r => r.obj._item);
|
||||
}
|
||||
|
||||
property list<var> filteredApps: []
|
||||
|
||||
function updateFilteredApps() {
|
||||
filteredApps = filterApps(searchText);
|
||||
}
|
||||
|
||||
onSearchTextChanged: {
|
||||
updateFilteredApps();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updateFilteredApps();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: allAppsDb
|
||||
function onAppsChanged() {
|
||||
updateFilteredApps();
|
||||
}
|
||||
}
|
||||
|
||||
SplitPaneLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
|
||||
ColumnLayout {
|
||||
id: leftLauncherLayout
|
||||
anchors.fill: parent
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Launcher")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.launcher.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Launcher settings")
|
||||
|
||||
onClicked: {
|
||||
if (root.session.launcher.active) {
|
||||
root.session.launcher.active = null;
|
||||
} else {
|
||||
if (root.filteredApps.length > 0) {
|
||||
root.session.launcher.active = root.filteredApps[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length)
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("All applications available in the launcher")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.bottomMargin: Appearance.spacing.small
|
||||
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight)
|
||||
|
||||
MaterialIcon {
|
||||
id: searchIcon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
|
||||
text: "search"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: searchField
|
||||
|
||||
anchors.left: searchIcon.right
|
||||
anchors.right: clearIcon.left
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
|
||||
topPadding: Appearance.padding.normal
|
||||
bottomPadding: Appearance.padding.normal
|
||||
|
||||
placeholderText: qsTr("Search applications...")
|
||||
|
||||
onTextChanged: {
|
||||
root.searchText = text;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: clearIcon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
width: searchField.text ? implicitWidth : implicitWidth / 2
|
||||
opacity: {
|
||||
if (!searchField.text)
|
||||
return 0;
|
||||
if (clearMouse.pressed)
|
||||
return 0.7;
|
||||
if (clearMouse.containsMouse)
|
||||
return 0.8;
|
||||
return 1;
|
||||
}
|
||||
|
||||
text: "close"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
|
||||
MouseArea {
|
||||
id: clearMouse
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: searchField.text ? Qt.PointingHandCursor : undefined
|
||||
|
||||
onClicked: searchField.text = ""
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: appsListLoader
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
asynchronous: true
|
||||
active: true
|
||||
|
||||
sourceComponent: StyledListView {
|
||||
id: appsListView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: root.filteredApps
|
||||
spacing: Appearance.spacing.small / 2
|
||||
clip: true
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: parent
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
required property var modelData
|
||||
|
||||
width: parent ? parent.width : 0
|
||||
implicitHeight: 40
|
||||
|
||||
readonly property bool isSelected: root.selectedApp === modelData
|
||||
|
||||
color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
opacity: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 1000
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.session.launcher.active = modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
implicitSize: 32
|
||||
source: {
|
||||
const entry = modelData.entry;
|
||||
return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing";
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.name || modelData.entry?.name || qsTr("Unknown")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false
|
||||
readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false
|
||||
active: isHidden || isFav
|
||||
|
||||
sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: hiddenIcon
|
||||
MaterialIcon {
|
||||
text: "visibility_off"
|
||||
fill: 1
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: favouriteIcon
|
||||
MaterialIcon {
|
||||
text: "favorite"
|
||||
fill: 1
|
||||
color: Colours.palette.m3primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: Component {
|
||||
Item {
|
||||
id: rightLauncherPane
|
||||
|
||||
property var pane: root.session.launcher.active
|
||||
property string paneId: pane ? (pane.id || pane.entry?.id || "") : ""
|
||||
property Component targetComponent: settings
|
||||
property Component nextComponent: settings
|
||||
property var displayedApp: null
|
||||
|
||||
function getComponentForPane() {
|
||||
return pane ? appDetails : settings;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
displayedApp = pane;
|
||||
targetComponent = getComponentForPane();
|
||||
nextComponent = targetComponent;
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: rightLauncherLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
transformOrigin: Item.Center
|
||||
clip: false
|
||||
|
||||
sourceComponent: rightLauncherPane.targetComponent
|
||||
active: true
|
||||
|
||||
property var displayedApp: rightLauncherPane.displayedApp
|
||||
|
||||
onItemChanged: {
|
||||
if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) {
|
||||
rightLauncherPane.displayedApp = rightLauncherPane.pane;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on paneId {
|
||||
PaneTransition {
|
||||
target: rightLauncherLoader
|
||||
propertyActions: [
|
||||
PropertyAction {
|
||||
target: rightLauncherPane
|
||||
property: "displayedApp"
|
||||
value: rightLauncherPane.pane
|
||||
},
|
||||
PropertyAction {
|
||||
target: rightLauncherLoader
|
||||
property: "active"
|
||||
value: false
|
||||
},
|
||||
PropertyAction {
|
||||
target: rightLauncherPane
|
||||
property: "targetComponent"
|
||||
value: rightLauncherPane.nextComponent
|
||||
},
|
||||
PropertyAction {
|
||||
target: rightLauncherLoader
|
||||
property: "active"
|
||||
value: true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onPaneChanged: {
|
||||
nextComponent = getComponentForPane();
|
||||
paneId = pane ? (pane.id || pane.entry?.id || "") : "";
|
||||
}
|
||||
|
||||
onDisplayedAppChanged: {
|
||||
if (displayedApp) {
|
||||
const appId = displayedApp.id || displayedApp.entry?.id;
|
||||
root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId);
|
||||
root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId);
|
||||
} else {
|
||||
root.hideFromLauncherChecked = false;
|
||||
root.favouriteChecked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: settings
|
||||
|
||||
StyledFlickable {
|
||||
id: settingsFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: settingsFlickable
|
||||
}
|
||||
|
||||
Settings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appDetails
|
||||
|
||||
ColumnLayout {
|
||||
id: appDetailsLayout
|
||||
anchors.fill: parent
|
||||
|
||||
readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
Layout.leftMargin: Appearance.padding.large * 2
|
||||
Layout.rightMargin: Appearance.padding.large * 2
|
||||
Layout.topMargin: Appearance.padding.large * 2
|
||||
visible: displayedApp === null
|
||||
icon: "apps"
|
||||
title: qsTr("Launcher Applications")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.leftMargin: Appearance.padding.large * 2
|
||||
Layout.rightMargin: Appearance.padding.large * 2
|
||||
Layout.topMargin: Appearance.padding.large * 2
|
||||
visible: displayedApp !== null
|
||||
implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth)
|
||||
implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
IconImage {
|
||||
id: appIconImage
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitSize: Appearance.font.size.extraLarge * 3 * 2
|
||||
source: {
|
||||
const app = appDetailsLayout.displayedApp;
|
||||
if (!app)
|
||||
return "image-missing";
|
||||
const entry = app.entry;
|
||||
if (entry && entry.icon) {
|
||||
return Quickshell.iconPath(entry.icon, "image-missing");
|
||||
}
|
||||
return "image-missing";
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: appTitleText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : ""
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
Layout.leftMargin: Appearance.padding.large * 2
|
||||
Layout.rightMargin: Appearance.padding.large * 2
|
||||
|
||||
StyledFlickable {
|
||||
id: detailsFlickable
|
||||
anchors.fill: parent
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: debugLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: parent
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: debugLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SwitchRow {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
visible: appDetailsLayout.displayedApp !== null
|
||||
label: qsTr("Mark as favourite")
|
||||
checked: root.favouriteChecked
|
||||
// disabled if:
|
||||
// * app is hidden
|
||||
// * app isn't in favouriteApps array but marked as favourite anyway
|
||||
// ^^^ This means that this app is favourited because of a regex check
|
||||
// this button can not toggle regexed apps
|
||||
enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (Config.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked)
|
||||
opacity: enabled ? 1 : 0.6
|
||||
onToggled: checked => {
|
||||
root.favouriteChecked = checked;
|
||||
const app = appDetailsLayout.displayedApp;
|
||||
if (app) {
|
||||
const appId = app.id || app.entry?.id;
|
||||
const favouriteApps = Config.launcher.favouriteApps ? [...Config.launcher.favouriteApps] : [];
|
||||
if (checked) {
|
||||
if (!favouriteApps.includes(appId)) {
|
||||
favouriteApps.push(appId);
|
||||
}
|
||||
} else {
|
||||
const index = favouriteApps.indexOf(appId);
|
||||
if (index !== -1) {
|
||||
favouriteApps.splice(index, 1);
|
||||
}
|
||||
}
|
||||
Config.launcher.favouriteApps = favouriteApps;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
SwitchRow {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
visible: appDetailsLayout.displayedApp !== null
|
||||
label: qsTr("Hide from launcher")
|
||||
checked: root.hideFromLauncherChecked
|
||||
// disabled if:
|
||||
// * app is favourited
|
||||
// * app isn't in hiddenApps array but marked as hidden anyway
|
||||
// ^^^ This means that this app is hidden because of a regex check
|
||||
// this button can not toggle regexed apps
|
||||
enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (Config.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked)
|
||||
opacity: enabled ? 1 : 0.6
|
||||
onToggled: checked => {
|
||||
root.hideFromLauncherChecked = checked;
|
||||
const app = appDetailsLayout.displayedApp;
|
||||
if (app) {
|
||||
const appId = app.id || app.entry?.id;
|
||||
const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : [];
|
||||
if (checked) {
|
||||
if (!hiddenApps.includes(appId)) {
|
||||
hiddenApps.push(appId);
|
||||
}
|
||||
} else {
|
||||
const index = hiddenApps.indexOf(appId);
|
||||
if (index !== -1) {
|
||||
hiddenApps.splice(index, 1);
|
||||
}
|
||||
}
|
||||
Config.launcher.hiddenApps = hiddenApps;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "apps"
|
||||
title: qsTr("Launcher Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("General")
|
||||
description: qsTr("General launcher settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Enabled")
|
||||
checked: Config.launcher.enabled
|
||||
toggle.onToggled: {
|
||||
Config.launcher.enabled = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Show on hover")
|
||||
checked: Config.launcher.showOnHover
|
||||
toggle.onToggled: {
|
||||
Config.launcher.showOnHover = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Vim keybinds")
|
||||
checked: Config.launcher.vimKeybinds
|
||||
toggle.onToggled: {
|
||||
Config.launcher.vimKeybinds = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Enable dangerous actions")
|
||||
checked: Config.launcher.enableDangerousActions
|
||||
toggle.onToggled: {
|
||||
Config.launcher.enableDangerousActions = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Display")
|
||||
description: qsTr("Display and appearance settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Max shown items")
|
||||
value: qsTr("%1").arg(Config.launcher.maxShown)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Max wallpapers")
|
||||
value: qsTr("%1").arg(Config.launcher.maxWallpapers)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Drag threshold")
|
||||
value: qsTr("%1 px").arg(Config.launcher.dragThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Prefixes")
|
||||
description: qsTr("Command prefix settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Special prefix")
|
||||
value: Config.launcher.specialPrefix || qsTr("None")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Action prefix")
|
||||
value: Config.launcher.actionPrefix || qsTr("None")
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Fuzzy search")
|
||||
description: qsTr("Fuzzy search settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Apps")
|
||||
checked: Config.launcher.useFuzzy.apps
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.apps = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Actions")
|
||||
checked: Config.launcher.useFuzzy.actions
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.actions = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Schemes")
|
||||
checked: Config.launcher.useFuzzy.schemes
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.schemes = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Variants")
|
||||
checked: Config.launcher.useFuzzy.variants
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.variants = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("Wallpapers")
|
||||
checked: Config.launcher.useFuzzy.wallpapers
|
||||
toggle.onToggled: {
|
||||
Config.launcher.useFuzzy.wallpapers = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Sizes")
|
||||
description: qsTr("Size settings for launcher items")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Item width")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Item height")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Wallpaper width")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Wallpaper height")
|
||||
value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Hidden apps")
|
||||
description: qsTr("Applications hidden from launcher")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Total hidden")
|
||||
value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property var ethernetDevice: root.session.ethernet.active
|
||||
|
||||
device: ethernetDevice
|
||||
|
||||
Component.onCompleted: {
|
||||
if (ethernetDevice && ethernetDevice.interface) {
|
||||
Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
|
||||
}
|
||||
}
|
||||
|
||||
onEthernetDeviceChanged: {
|
||||
if (ethernetDevice && ethernetDevice.interface) {
|
||||
Nmcli.getEthernetDeviceDetails(ethernetDevice.interface, () => {});
|
||||
} else {
|
||||
Nmcli.ethernetDeviceDetails = null;
|
||||
}
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
ConnectionHeader {
|
||||
icon: "cable"
|
||||
title: root.ethernetDevice?.interface ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection status")
|
||||
description: qsTr("Connection settings for this device")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Connected")
|
||||
checked: root.ethernetDevice?.connected ?? false
|
||||
toggle.onToggled: {
|
||||
if (checked) {
|
||||
Nmcli.connectEthernet(root.ethernetDevice?.connection || "", root.ethernetDevice?.interface || "", () => {});
|
||||
} else {
|
||||
if (root.ethernetDevice?.connection) {
|
||||
Nmcli.disconnectEthernet(root.ethernetDevice.connection, () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Device properties")
|
||||
description: qsTr("Additional information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Interface")
|
||||
value: root.ethernetDevice?.interface ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Connection")
|
||||
value: root.ethernetDevice?.connection || qsTr("Not connected")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("State")
|
||||
value: root.ethernetDevice?.state ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection information")
|
||||
description: qsTr("Network connection details")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ConnectionInfoSection {
|
||||
deviceDetails: Nmcli.ethernetDeviceDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceList {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
title: qsTr("Devices (%1)").arg(Nmcli.ethernetDevices.length)
|
||||
description: qsTr("All available ethernet devices")
|
||||
activeItem: session.ethernet.active
|
||||
|
||||
model: Nmcli.ethernetDevices
|
||||
|
||||
headerComponent: Component {
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Settings")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.ethernet.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
if (root.session.ethernet.active)
|
||||
root.session.ethernet.active = null;
|
||||
else {
|
||||
root.session.ethernet.active = root.view.model.get(0)?.modelData ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
id: ethernetItem
|
||||
|
||||
required property var modelData
|
||||
readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
function onClicked(): void {
|
||||
root.session.ethernet.active = modelData;
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Qt.alpha(modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "cable"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.connected ? 1 : 0
|
||||
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
Behavior on fill {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.interface || qsTr("Unknown")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected")
|
||||
color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: modelData.connected ? 500 : 400
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: connectBtn
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
if (modelData.connected && modelData.connection) {
|
||||
Nmcli.disconnectEthernet(modelData.connection, () => {});
|
||||
} else {
|
||||
Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: true
|
||||
text: modelData.connected ? "link_off" : "link"
|
||||
color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelected: function (item) {
|
||||
session.ethernet.active = item;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
SplitPaneWithDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
activeItem: session.ethernet.active
|
||||
paneIdGenerator: function (item) {
|
||||
return item ? (item.interface || "") : "";
|
||||
}
|
||||
|
||||
leftContent: Component {
|
||||
EthernetList {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightDetailsComponent: Component {
|
||||
EthernetDetails {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightSettingsComponent: Component {
|
||||
StyledFlickable {
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
clip: true
|
||||
|
||||
EthernetSettings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "cable"
|
||||
title: qsTr("Ethernet settings")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
text: qsTr("Ethernet devices")
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Available ethernet devices")
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surfaceContainer
|
||||
|
||||
ColumnLayout {
|
||||
id: ethernetInfo
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.small / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Total devices")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("%1").arg(Nmcli.ethernetDevices.length)
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("Connected devices")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "router"
|
||||
title: qsTr("Network Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Ethernet")
|
||||
description: qsTr("Ethernet device information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Total devices")
|
||||
value: qsTr("%1").arg(Nmcli.ethernetDevices.length)
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Connected devices")
|
||||
value: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length)
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Wireless")
|
||||
description: qsTr("WiFi network settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("WiFi enabled")
|
||||
checked: Nmcli.wifiEnabled
|
||||
toggle.onToggled: {
|
||||
Nmcli.enableWifi(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("VPN")
|
||||
description: qsTr("VPN provider settings")
|
||||
visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0
|
||||
|
||||
ToggleRow {
|
||||
label: qsTr("VPN enabled")
|
||||
checked: Config.utilities.vpn.enabled
|
||||
toggle.onToggled: {
|
||||
Config.utilities.vpn.enabled = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Providers")
|
||||
value: qsTr("%1").arg(Config.utilities.vpn.provider.length)
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
text: qsTr("⚙ Manage VPN Providers")
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
|
||||
onClicked: {
|
||||
vpnSettingsDialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Current connection")
|
||||
description: qsTr("Active network connection information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Network")
|
||||
value: Nmcli.active ? Nmcli.active.ssid : (Nmcli.activeEthernet ? Nmcli.activeEthernet.interface : qsTr("Not connected"))
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
visible: Nmcli.active !== null
|
||||
label: qsTr("Signal strength")
|
||||
value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
visible: Nmcli.active !== null
|
||||
label: qsTr("Security")
|
||||
value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
visible: Nmcli.active !== null
|
||||
label: qsTr("Frequency")
|
||||
value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: vpnSettingsDialog
|
||||
|
||||
parent: Overlay.overlay
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(600, parent.width - Appearance.padding.large * 2)
|
||||
height: Math.min(700, parent.height - Appearance.padding.large * 2)
|
||||
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: StyledRect {
|
||||
color: Colours.palette.m3surface
|
||||
radius: Appearance.rounding.large
|
||||
}
|
||||
|
||||
StyledFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large * 1.5
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: vpnSettingsContent.height
|
||||
clip: true
|
||||
|
||||
VpnSettings {
|
||||
id: vpnSettingsContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
SplitPaneLayout {
|
||||
id: splitLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
leftContent: Component {
|
||||
StyledFlickable {
|
||||
id: leftFlickable
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: leftContent.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: leftFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: leftContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Network")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.wifiEnabled
|
||||
icon: "wifi"
|
||||
accent: "Tertiary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Toggle WiFi")
|
||||
|
||||
onClicked: {
|
||||
Nmcli.toggleWifi(null);
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.scanning
|
||||
icon: "wifi_find"
|
||||
accent: "Secondary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Scan for networks")
|
||||
|
||||
onClicked: {
|
||||
Nmcli.rescanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.ethernet.active && !root.session.network.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
tooltip: qsTr("Network settings")
|
||||
|
||||
onClicked: {
|
||||
if (root.session.ethernet.active || root.session.network.active) {
|
||||
root.session.ethernet.active = null;
|
||||
root.session.network.active = null;
|
||||
} else {
|
||||
if (Nmcli.ethernetDevices.length > 0) {
|
||||
root.session.ethernet.active = Nmcli.ethernetDevices[0];
|
||||
} else if (Nmcli.networks.length > 0) {
|
||||
root.session.network.active = Nmcli.networks[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: vpnListSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("VPN")
|
||||
expanded: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Component {
|
||||
VpnList {
|
||||
session: root.session
|
||||
showHeader: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: ethernetListSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Ethernet")
|
||||
expanded: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Component {
|
||||
EthernetList {
|
||||
session: root.session
|
||||
showHeader: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CollapsibleSection {
|
||||
id: wirelessListSection
|
||||
|
||||
Layout.fillWidth: true
|
||||
title: qsTr("Wireless")
|
||||
expanded: true
|
||||
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: Component {
|
||||
WirelessList {
|
||||
session: root.session
|
||||
showHeader: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightContent: Component {
|
||||
Item {
|
||||
id: rightPaneItem
|
||||
|
||||
property var vpnPane: root.session && root.session.vpn ? root.session.vpn.active : null
|
||||
property var ethernetPane: root.session && root.session.ethernet ? root.session.ethernet.active : null
|
||||
property var wirelessPane: root.session && root.session.network ? root.session.network.active : null
|
||||
property var pane: vpnPane || ethernetPane || wirelessPane
|
||||
property string paneId: vpnPane ? ("vpn:" + (vpnPane.name || "")) : (ethernetPane ? ("eth:" + (ethernetPane.interface || "")) : (wirelessPane ? ("wifi:" + (wirelessPane.ssid || wirelessPane.bssid || "")) : "settings"))
|
||||
property Component targetComponent: settingsComponent
|
||||
property Component nextComponent: settingsComponent
|
||||
|
||||
function getComponentForPane() {
|
||||
if (vpnPane)
|
||||
return vpnDetailsComponent;
|
||||
if (ethernetPane)
|
||||
return ethernetDetailsComponent;
|
||||
if (wirelessPane)
|
||||
return wirelessDetailsComponent;
|
||||
return settingsComponent;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
targetComponent = getComponentForPane();
|
||||
nextComponent = targetComponent;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session && root.session.vpn ? root.session.vpn : null
|
||||
enabled: target !== null
|
||||
|
||||
function onActiveChanged() {
|
||||
// Clear others when VPN is selected
|
||||
if (root.session && root.session.vpn && root.session.vpn.active) {
|
||||
if (root.session.ethernet && root.session.ethernet.active)
|
||||
root.session.ethernet.active = null;
|
||||
if (root.session.network && root.session.network.active)
|
||||
root.session.network.active = null;
|
||||
}
|
||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session && root.session.ethernet ? root.session.ethernet : null
|
||||
enabled: target !== null
|
||||
|
||||
function onActiveChanged() {
|
||||
// Clear others when ethernet is selected
|
||||
if (root.session && root.session.ethernet && root.session.ethernet.active) {
|
||||
if (root.session.vpn && root.session.vpn.active)
|
||||
root.session.vpn.active = null;
|
||||
if (root.session.network && root.session.network.active)
|
||||
root.session.network.active = null;
|
||||
}
|
||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.session && root.session.network ? root.session.network : null
|
||||
enabled: target !== null
|
||||
|
||||
function onActiveChanged() {
|
||||
// Clear others when wireless is selected
|
||||
if (root.session && root.session.network && root.session.network.active) {
|
||||
if (root.session.vpn && root.session.vpn.active)
|
||||
root.session.vpn.active = null;
|
||||
if (root.session.ethernet && root.session.ethernet.active)
|
||||
root.session.ethernet.active = null;
|
||||
}
|
||||
rightPaneItem.nextComponent = rightPaneItem.getComponentForPane();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: rightLoader
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 1
|
||||
scale: 1
|
||||
transformOrigin: Item.Center
|
||||
clip: false
|
||||
|
||||
asynchronous: true
|
||||
sourceComponent: rightPaneItem.targetComponent
|
||||
}
|
||||
|
||||
Behavior on paneId {
|
||||
PaneTransition {
|
||||
target: rightLoader
|
||||
propertyActions: [
|
||||
PropertyAction {
|
||||
target: rightPaneItem
|
||||
property: "targetComponent"
|
||||
value: rightPaneItem.nextComponent
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: settingsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: settingsFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: settingsFlickable
|
||||
}
|
||||
|
||||
NetworkSettings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: ethernetDetailsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: ethernetFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: ethernetDetailsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: ethernetFlickable
|
||||
}
|
||||
|
||||
EthernetDetails {
|
||||
id: ethernetDetailsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: wirelessDetailsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: wirelessFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: wirelessDetailsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: wirelessFlickable
|
||||
}
|
||||
|
||||
WirelessDetails {
|
||||
id: wirelessDetailsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: vpnDetailsComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: vpnFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: vpnDetailsInner.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: vpnFlickable
|
||||
}
|
||||
|
||||
VpnDetails {
|
||||
id: vpnDetailsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WirelessPasswordDialog {
|
||||
anchors.fill: parent
|
||||
session: root.session
|
||||
z: 1000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property var vpnProvider: root.session.vpn.active
|
||||
readonly property bool providerEnabled: {
|
||||
if (!vpnProvider || vpnProvider.index === undefined)
|
||||
return false;
|
||||
const provider = Config.utilities.vpn.provider[vpnProvider.index];
|
||||
return provider && typeof provider === "object" && provider.enabled === true;
|
||||
}
|
||||
|
||||
device: vpnProvider
|
||||
|
||||
headerComponent: Component {
|
||||
ConnectionHeader {
|
||||
icon: "vpn_key"
|
||||
title: root.vpnProvider?.displayName ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection status")
|
||||
description: qsTr("VPN connection settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Enable this provider")
|
||||
checked: root.providerEnabled
|
||||
toggle.onToggled: {
|
||||
if (!root.vpnProvider)
|
||||
return;
|
||||
const providers = [];
|
||||
const index = root.vpnProvider.index;
|
||||
|
||||
// Copy providers and update enabled state
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
const p = Config.utilities.vpn.provider[i];
|
||||
if (typeof p === "object") {
|
||||
const newProvider = {
|
||||
name: p.name,
|
||||
displayName: p.displayName,
|
||||
interface: p.interface
|
||||
};
|
||||
|
||||
if (checked) {
|
||||
// Enable this one, disable others
|
||||
newProvider.enabled = (i === index);
|
||||
} else {
|
||||
// Just disable this one
|
||||
newProvider.enabled = (i === index) ? false : (p.enabled !== false);
|
||||
}
|
||||
|
||||
providers.push(newProvider);
|
||||
} else {
|
||||
providers.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
visible: root.providerEnabled
|
||||
enabled: !VPN.connecting
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
text: VPN.connected ? qsTr("Disconnect") : qsTr("Connect")
|
||||
|
||||
onClicked: {
|
||||
VPN.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Edit Provider")
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
|
||||
onClicked: {
|
||||
editVpnDialog.editIndex = root.vpnProvider.index;
|
||||
editVpnDialog.providerName = root.vpnProvider.name;
|
||||
editVpnDialog.displayName = root.vpnProvider.displayName;
|
||||
editVpnDialog.interfaceName = root.vpnProvider.interface;
|
||||
editVpnDialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Delete Provider")
|
||||
inactiveColour: Colours.palette.m3errorContainer
|
||||
inactiveOnColour: Colours.palette.m3onErrorContainer
|
||||
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i !== root.vpnProvider.index) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
}
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
root.session.vpn.active = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Provider details")
|
||||
description: qsTr("VPN provider information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Provider")
|
||||
value: root.vpnProvider?.name ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Display name")
|
||||
value: root.vpnProvider?.displayName ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Interface")
|
||||
value: root.vpnProvider?.interface || qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Status")
|
||||
value: {
|
||||
if (!root.providerEnabled)
|
||||
return qsTr("Disabled");
|
||||
if (VPN.connecting)
|
||||
return qsTr("Connecting...");
|
||||
if (VPN.connected)
|
||||
return qsTr("Connected");
|
||||
return qsTr("Enabled (Not connected)");
|
||||
}
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Enabled")
|
||||
value: root.providerEnabled ? qsTr("Yes") : qsTr("No")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// Edit VPN Dialog
|
||||
Popup {
|
||||
id: editVpnDialog
|
||||
|
||||
property int editIndex: -1
|
||||
property string providerName: ""
|
||||
property string displayName: ""
|
||||
property string interfaceName: ""
|
||||
|
||||
parent: Overlay.overlay
|
||||
anchors.centerIn: parent
|
||||
width: Math.min(400, parent.width - Appearance.padding.large * 2)
|
||||
padding: Appearance.padding.large * 1.5
|
||||
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
opacity: 0
|
||||
scale: 0.7
|
||||
|
||||
enter: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
from: 0.7
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.7
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
function closeWithAnimation(): void {
|
||||
close();
|
||||
}
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity)
|
||||
}
|
||||
|
||||
background: StyledRect {
|
||||
color: Colours.palette.m3surfaceContainerHigh
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
level: 3
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Edit VPN Provider")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Display Name")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: displayNameField
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: editVpnDialog.displayName
|
||||
onTextChanged: editVpnDialog.displayName = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Interface (e.g., wg0, torguard)")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: interfaceNameField
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: editVpnDialog.interfaceName
|
||||
onTextChanged: editVpnDialog.interfaceName = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cancel")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: editVpnDialog.closeWithAnimation()
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Save")
|
||||
enabled: editVpnDialog.interfaceName.length > 0
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex];
|
||||
const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true;
|
||||
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i === editVpnDialog.editIndex) {
|
||||
providers.push({
|
||||
name: editVpnDialog.providerName,
|
||||
displayName: editVpnDialog.displayName || editVpnDialog.interfaceName,
|
||||
interface: editVpnDialog.interfaceName,
|
||||
enabled: wasEnabled
|
||||
});
|
||||
} else {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
editVpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
property bool showHeader: true
|
||||
property int pendingSwitchIndex: -1
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Connections {
|
||||
target: VPN
|
||||
function onConnectedChanged() {
|
||||
if (!VPN.connected && root.pendingSwitchIndex >= 0) {
|
||||
const targetIndex = root.pendingSwitchIndex;
|
||||
root.pendingSwitchIndex = -1;
|
||||
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
const p = Config.utilities.vpn.provider[i];
|
||||
if (typeof p === "object") {
|
||||
const newProvider = {
|
||||
name: p.name,
|
||||
displayName: p.displayName,
|
||||
interface: p.interface,
|
||||
enabled: (i === targetIndex)
|
||||
};
|
||||
providers.push(newProvider);
|
||||
} else {
|
||||
providers.push(p);
|
||||
}
|
||||
}
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
|
||||
Qt.callLater(function () {
|
||||
VPN.toggle();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("+ Add VPN Provider")
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
onClicked: {
|
||||
vpnDialog.showProviderSelection();
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
|
||||
interactive: false
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
model: ScriptModel {
|
||||
values: Config.utilities.vpn.provider.map((provider, index) => {
|
||||
const isObject = typeof provider === "object";
|
||||
const name = isObject ? (provider.name || "custom") : String(provider);
|
||||
const displayName = isObject ? (provider.displayName || name) : name;
|
||||
const iface = isObject ? (provider.interface || "") : "";
|
||||
const enabled = isObject ? (provider.enabled === true) : false;
|
||||
|
||||
return {
|
||||
index: index,
|
||||
name: name,
|
||||
displayName: displayName,
|
||||
interface: iface,
|
||||
provider: provider,
|
||||
enabled: enabled
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (root.session && root.session.vpn) {
|
||||
root.session.vpn.active = modelData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.enabled && VPN.connected ? 1 : 0
|
||||
color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.displayName || qsTr("Unknown")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
if (modelData.enabled && VPN.connected)
|
||||
return qsTr("Connected");
|
||||
if (modelData.enabled && VPN.connecting)
|
||||
return qsTr("Connecting...");
|
||||
if (modelData.enabled)
|
||||
return qsTr("Enabled");
|
||||
return qsTr("Disabled");
|
||||
}
|
||||
color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: modelData.enabled && VPN.connected ? 500 : 400
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
enabled: !VPN.connecting
|
||||
function onClicked(): void {
|
||||
const clickedIndex = modelData.index;
|
||||
|
||||
if (modelData.enabled) {
|
||||
VPN.toggle();
|
||||
} else {
|
||||
if (VPN.connected) {
|
||||
root.pendingSwitchIndex = clickedIndex;
|
||||
VPN.toggle();
|
||||
} else {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
const p = Config.utilities.vpn.provider[i];
|
||||
if (typeof p === "object") {
|
||||
const newProvider = {
|
||||
name: p.name,
|
||||
displayName: p.displayName,
|
||||
interface: p.interface,
|
||||
enabled: (i === clickedIndex)
|
||||
};
|
||||
providers.push(newProvider);
|
||||
} else {
|
||||
providers.push(p);
|
||||
}
|
||||
}
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
|
||||
Qt.callLater(function () {
|
||||
VPN.toggle();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: VPN.connected && modelData.enabled ? "link_off" : "link"
|
||||
color: VPN.connected && modelData.enabled ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: "transparent"
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i !== modelData.index) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
}
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: deleteIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "delete"
|
||||
color: Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: vpnDialog
|
||||
|
||||
property string currentState: "selection"
|
||||
property int editIndex: -1
|
||||
property string providerName: ""
|
||||
property string displayName: ""
|
||||
property string interfaceName: ""
|
||||
|
||||
parent: Overlay.overlay
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2)
|
||||
padding: Appearance.padding.large * 1.5
|
||||
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
opacity: 0
|
||||
scale: 0.7
|
||||
|
||||
enter: Transition {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
from: 0.7
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
Anim {
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.7
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showProviderSelection(): void {
|
||||
currentState = "selection";
|
||||
open();
|
||||
}
|
||||
|
||||
function closeWithAnimation(): void {
|
||||
close();
|
||||
}
|
||||
|
||||
function showAddForm(providerType: string, defaultDisplayName: string): void {
|
||||
editIndex = -1;
|
||||
providerName = providerType;
|
||||
displayName = defaultDisplayName;
|
||||
interfaceName = "";
|
||||
|
||||
if (currentState === "selection") {
|
||||
transitionToForm.start();
|
||||
} else {
|
||||
currentState = "form";
|
||||
isClosing = false;
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
function showEditForm(index: int): void {
|
||||
const provider = Config.utilities.vpn.provider[index];
|
||||
const isObject = typeof provider === "object";
|
||||
|
||||
editIndex = index;
|
||||
providerName = isObject ? (provider.name || "custom") : String(provider);
|
||||
displayName = isObject ? (provider.displayName || providerName) : providerName;
|
||||
interfaceName = isObject ? (provider.interface || "") : "";
|
||||
|
||||
currentState = "form";
|
||||
open();
|
||||
}
|
||||
|
||||
Overlay.modal: Rectangle {
|
||||
color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity)
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
currentState = "selection";
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: transitionToForm
|
||||
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: selectionContent
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
vpnDialog.currentState = "form";
|
||||
}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: formContent
|
||||
property: "opacity"
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: StyledRect {
|
||||
color: Colours.palette.m3surfaceContainerHigh
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
level: 3
|
||||
z: -1
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
implicitHeight: vpnDialog.currentState === "selection" ? selectionContent.implicitHeight : formContent.implicitHeight
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: selectionContent
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
visible: vpnDialog.currentState === "selection"
|
||||
opacity: vpnDialog.currentState === "selection" ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Add VPN Provider")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Choose a provider to add")
|
||||
wrapMode: Text.WordWrap
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("NetBird")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
providers.push({
|
||||
name: "netbird",
|
||||
displayName: "NetBird",
|
||||
interface: "wt0"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
vpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Tailscale")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
providers.push({
|
||||
name: "tailscale",
|
||||
displayName: "Tailscale",
|
||||
interface: "tailscale0"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
vpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cloudflare WARP")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
providers.push({
|
||||
name: "warp",
|
||||
displayName: "Cloudflare WARP",
|
||||
interface: "CloudflareWARP"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
vpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("WireGuard (Custom)")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: {
|
||||
vpnDialog.showAddForm("wireguard", "WireGuard");
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cancel")
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
onClicked: vpnDialog.closeWithAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: formContent
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
visible: vpnDialog.currentState === "form"
|
||||
opacity: vpnDialog.currentState === "form" ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName)
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Display Name")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: displayNameField
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: vpnDialog.displayName
|
||||
onTextChanged: vpnDialog.displayName = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller / 2
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Interface (e.g., wg0, torguard)")
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 40
|
||||
color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
radius: Appearance.rounding.small
|
||||
border.width: 1
|
||||
border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: interfaceNameField
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Appearance.padding.normal
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
text: vpnDialog.interfaceName
|
||||
onTextChanged: vpnDialog.interfaceName = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Cancel")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
onClicked: vpnDialog.closeWithAnimation()
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Save")
|
||||
enabled: vpnDialog.interfaceName.length > 0
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
onClicked: {
|
||||
const providers = [];
|
||||
const newProvider = {
|
||||
name: vpnDialog.providerName,
|
||||
displayName: vpnDialog.displayName || vpnDialog.interfaceName,
|
||||
interface: vpnDialog.interfaceName
|
||||
};
|
||||
|
||||
if (vpnDialog.editIndex >= 0) {
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
if (i === vpnDialog.editIndex) {
|
||||
providers.push(newProvider);
|
||||
} else {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < Config.utilities.vpn.provider.length; i++) {
|
||||
providers.push(Config.utilities.vpn.provider[i]);
|
||||
}
|
||||
providers.push(newProvider);
|
||||
}
|
||||
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
vpnDialog.closeWithAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "vpn_key"
|
||||
title: qsTr("VPN Settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("General")
|
||||
description: qsTr("VPN configuration")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("VPN enabled")
|
||||
checked: Config.utilities.vpn.enabled
|
||||
toggle.onToggled: {
|
||||
Config.utilities.vpn.enabled = checked;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Providers")
|
||||
description: qsTr("Manage VPN providers")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
ListView {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
|
||||
interactive: false
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
model: ScriptModel {
|
||||
values: Config.utilities.vpn.provider.map((provider, index) => {
|
||||
const isObject = typeof provider === "object";
|
||||
const name = isObject ? (provider.name || "custom") : String(provider);
|
||||
const displayName = isObject ? (provider.displayName || name) : name;
|
||||
const iface = isObject ? (provider.interface || "") : "";
|
||||
|
||||
return {
|
||||
index: index,
|
||||
name: name,
|
||||
displayName: displayName,
|
||||
interface: iface,
|
||||
provider: provider,
|
||||
isActive: index === 0
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
color: Colours.tPalette.m3surfaceContainerHigh
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
text: modelData.isActive ? "vpn_key" : "vpn_key_off"
|
||||
font.pointSize: Appearance.font.size.large
|
||||
color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
text: modelData.displayName
|
||||
font.weight: modelData.isActive ? 500 : 400
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface"))
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.palette.m3outline
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: modelData.isActive ? "arrow_downward" : "arrow_upward"
|
||||
visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1
|
||||
onClicked: {
|
||||
if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) {
|
||||
// Move down
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
const temp = providers[index];
|
||||
providers[index] = providers[index + 1];
|
||||
providers[index + 1] = temp;
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
} else if (!modelData.isActive) {
|
||||
// Make active (move to top)
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
const provider = providers.splice(index, 1)[0];
|
||||
providers.unshift(provider);
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton {
|
||||
icon: "delete"
|
||||
onClicked: {
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
providers.splice(index, 1);
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: 60
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
text: qsTr("+ Add Provider")
|
||||
inactiveColour: Colours.palette.m3primaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onPrimaryContainer
|
||||
|
||||
onClicked: {
|
||||
addProviderDialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Quick Add")
|
||||
description: qsTr("Add common VPN providers")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.smaller
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("+ Add NetBird")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
providers.push({
|
||||
name: "netbird",
|
||||
displayName: "NetBird",
|
||||
interface: "wt0"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("+ Add Tailscale")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
providers.push({
|
||||
name: "tailscale",
|
||||
displayName: "Tailscale",
|
||||
interface: "tailscale0"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("+ Add Cloudflare WARP")
|
||||
inactiveColour: Colours.tPalette.m3surfaceContainerHigh
|
||||
inactiveOnColour: Colours.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
const providers = [...Config.utilities.vpn.provider];
|
||||
providers.push({
|
||||
name: "warp",
|
||||
displayName: "Cloudflare WARP",
|
||||
interface: "CloudflareWARP"
|
||||
});
|
||||
Config.utilities.vpn.provider = providers;
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
readonly property var network: root.session.network.active
|
||||
|
||||
device: network
|
||||
|
||||
Component.onCompleted: {
|
||||
updateDeviceDetails();
|
||||
checkSavedProfile();
|
||||
}
|
||||
|
||||
onNetworkChanged: {
|
||||
connectionUpdateTimer.stop();
|
||||
if (network && network.ssid) {
|
||||
connectionUpdateTimer.start();
|
||||
}
|
||||
updateDeviceDetails();
|
||||
checkSavedProfile();
|
||||
}
|
||||
|
||||
function checkSavedProfile(): void {
|
||||
if (network && network.ssid) {
|
||||
Nmcli.loadSavedConnections(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Nmcli
|
||||
function onActiveChanged() {
|
||||
updateDeviceDetails();
|
||||
}
|
||||
function onWirelessDeviceDetailsChanged() {
|
||||
if (network && network.ssid) {
|
||||
const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
|
||||
if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) {
|
||||
connectionUpdateTimer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionUpdateTimer
|
||||
interval: 500
|
||||
repeat: true
|
||||
running: network && network.ssid
|
||||
onTriggered: {
|
||||
if (network) {
|
||||
const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
|
||||
if (isActive) {
|
||||
if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) {
|
||||
Nmcli.getWirelessDeviceDetails("", () => {});
|
||||
} else {
|
||||
connectionUpdateTimer.stop();
|
||||
}
|
||||
} else {
|
||||
if (Nmcli.wirelessDeviceDetails !== null) {
|
||||
Nmcli.wirelessDeviceDetails = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateDeviceDetails(): void {
|
||||
if (network && network.ssid) {
|
||||
const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid);
|
||||
if (isActive) {
|
||||
Nmcli.getWirelessDeviceDetails("");
|
||||
} else {
|
||||
Nmcli.wirelessDeviceDetails = null;
|
||||
}
|
||||
} else {
|
||||
Nmcli.wirelessDeviceDetails = null;
|
||||
}
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
ConnectionHeader {
|
||||
icon: root.network?.isSecure ? "lock" : "wifi"
|
||||
title: root.network?.ssid ?? qsTr("Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
sections: [
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection status")
|
||||
description: qsTr("Connection settings for this network")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("Connected")
|
||||
checked: root.network?.active ?? false
|
||||
toggle.onToggled: {
|
||||
if (checked) {
|
||||
NetworkConnection.handleConnect(root.network, root.session, null);
|
||||
} else {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
visible: {
|
||||
if (!root.network || !root.network.ssid) {
|
||||
return false;
|
||||
}
|
||||
return Nmcli.hasSavedProfile(root.network.ssid);
|
||||
}
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
text: qsTr("Forget Network")
|
||||
|
||||
onClicked: {
|
||||
if (root.network && root.network.ssid) {
|
||||
if (root.network.active) {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
}
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Network properties")
|
||||
description: qsTr("Additional information")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("SSID")
|
||||
value: root.network?.ssid ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("BSSID")
|
||||
value: root.network?.bssid ?? qsTr("Unknown")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Signal strength")
|
||||
value: root.network ? qsTr("%1%").arg(root.network.strength) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Frequency")
|
||||
value: root.network ? qsTr("%1 MHz").arg(root.network.frequency) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Security")
|
||||
value: root.network ? (root.network.isSecure ? root.network.security : qsTr("Open")) : qsTr("N/A")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Component {
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionHeader {
|
||||
title: qsTr("Connection information")
|
||||
description: qsTr("Network connection details")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ConnectionInfoSection {
|
||||
deviceDetails: Nmcli.wirelessDeviceDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.containers
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DeviceList {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
title: qsTr("Networks (%1)").arg(Nmcli.networks.length)
|
||||
description: qsTr("All available WiFi networks")
|
||||
activeItem: session.network.active
|
||||
|
||||
titleSuffix: Component {
|
||||
StyledText {
|
||||
visible: Nmcli.scanning
|
||||
text: qsTr("Scanning...")
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Nmcli.networks].sort((a, b) => {
|
||||
if (a.active !== b.active)
|
||||
return b.active - a.active;
|
||||
return b.strength - a.strength;
|
||||
})
|
||||
}
|
||||
|
||||
headerComponent: Component {
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Settings")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.wifiEnabled
|
||||
icon: "wifi"
|
||||
accent: "Tertiary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
Nmcli.toggleWifi(null);
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: Nmcli.scanning
|
||||
icon: "wifi_find"
|
||||
accent: "Secondary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
Nmcli.rescanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
ToggleButton {
|
||||
toggled: !root.session.network.active
|
||||
icon: "settings"
|
||||
accent: "Primary"
|
||||
iconSize: Appearance.font.size.normal
|
||||
horizontalPadding: Appearance.padding.normal
|
||||
verticalPadding: Appearance.padding.smaller
|
||||
|
||||
onClicked: {
|
||||
if (root.session.network.active)
|
||||
root.session.network.active = null;
|
||||
else {
|
||||
root.session.network.active = root.view.model.get(0)?.modelData ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Component {
|
||||
StyledRect {
|
||||
required property var modelData
|
||||
|
||||
width: ListView.view ? ListView.view.width : undefined
|
||||
|
||||
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0)
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
root.session.network.active = modelData;
|
||||
if (modelData && modelData.ssid) {
|
||||
root.checkSavedProfileForNetwork(modelData.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure)
|
||||
font.pointSize: Appearance.font.size.large
|
||||
fill: modelData.active ? 1 : 0
|
||||
color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
|
||||
text: modelData.ssid || qsTr("Unknown")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: {
|
||||
if (modelData.active)
|
||||
return qsTr("Connected");
|
||||
if (modelData.isSecure && modelData.security && modelData.security.length > 0) {
|
||||
return modelData.security;
|
||||
}
|
||||
if (modelData.isSecure)
|
||||
return qsTr("Secured");
|
||||
return qsTr("Open");
|
||||
}
|
||||
color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: modelData.active ? 500 : 400
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0)
|
||||
|
||||
StateLayer {
|
||||
function onClicked(): void {
|
||||
if (modelData.active) {
|
||||
Nmcli.disconnectFromNetwork();
|
||||
} else {
|
||||
NetworkConnection.handleConnect(modelData, root.session, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: connectIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: modelData.active ? "link_off" : "link"
|
||||
color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelected: function (item) {
|
||||
session.network.active = item;
|
||||
if (item && item.ssid) {
|
||||
checkSavedProfileForNetwork(item.ssid);
|
||||
}
|
||||
}
|
||||
|
||||
function checkSavedProfileForNetwork(ssid: string): void {
|
||||
if (ssid && ssid.length > 0) {
|
||||
Nmcli.loadSavedConnections(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.containers
|
||||
import qs.config
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
SplitPaneWithDetails {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
activeItem: session.network.active
|
||||
paneIdGenerator: function (item) {
|
||||
return item ? (item.ssid || item.bssid || "") : "";
|
||||
}
|
||||
|
||||
leftContent: Component {
|
||||
WirelessList {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightDetailsComponent: Component {
|
||||
WirelessDetails {
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
|
||||
rightSettingsComponent: Component {
|
||||
StyledFlickable {
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: settingsInner.height
|
||||
clip: true
|
||||
|
||||
WirelessSettings {
|
||||
id: settingsInner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
overlayComponent: Component {
|
||||
WirelessPasswordDialog {
|
||||
anchors.fill: parent
|
||||
session: root.session
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "."
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
readonly property var network: {
|
||||
if (session.network.pendingNetwork) {
|
||||
return session.network.pendingNetwork;
|
||||
}
|
||||
if (session.network.active) {
|
||||
return session.network.active;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
property bool isClosing: false
|
||||
visible: session.network.showPasswordDialog || isClosing
|
||||
enabled: session.network.showPasswordDialog && !isClosing
|
||||
focus: enabled
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: dialog
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: 400
|
||||
implicitHeight: content.implicitHeight + Appearance.padding.large * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.tPalette.m3surface
|
||||
opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0
|
||||
scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
running: root.isClosing
|
||||
onFinished: {
|
||||
if (root.isClosing) {
|
||||
root.session.network.showPasswordDialog = false;
|
||||
root.isClosing = false;
|
||||
}
|
||||
}
|
||||
|
||||
Anim {
|
||||
target: dialog
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: dialog
|
||||
property: "scale"
|
||||
to: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: closeDialog()
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: "lock"
|
||||
font.pointSize: Appearance.font.size.extraLarge * 2
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: qsTr("Enter password")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : ""
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: statusText
|
||||
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
visible: connectButton.connecting || connectButton.hasError
|
||||
text: {
|
||||
if (connectButton.hasError) {
|
||||
return qsTr("Connection failed. Please check your password and try again.");
|
||||
}
|
||||
if (connectButton.connecting) {
|
||||
return qsTr("Connecting...");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
font.weight: 400
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.maximumWidth: parent.width - Appearance.padding.large * 2
|
||||
}
|
||||
|
||||
Item {
|
||||
id: passwordContainer
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2)
|
||||
|
||||
focus: true
|
||||
Keys.onPressed: event => {
|
||||
if (!activeFocus) {
|
||||
forceActiveFocus();
|
||||
}
|
||||
|
||||
if (connectButton.hasError && event.text && event.text.length > 0) {
|
||||
connectButton.hasError = false;
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) {
|
||||
if (connectButton.enabled) {
|
||||
connectButton.clicked();
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
passwordBuffer = "";
|
||||
} else {
|
||||
passwordBuffer = passwordBuffer.slice(0, -1);
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if (event.text && event.text.length > 0) {
|
||||
passwordBuffer += event.text;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
property string passwordBuffer: ""
|
||||
|
||||
Connections {
|
||||
target: root.session.network
|
||||
function onShowPasswordDialogChanged(): void {
|
||||
if (root.session.network.showPasswordDialog) {
|
||||
Qt.callLater(() => {
|
||||
passwordContainer.forceActiveFocus();
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.hasError = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onVisibleChanged(): void {
|
||||
if (root.visible) {
|
||||
Qt.callLater(() => {
|
||||
passwordContainer.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.normal
|
||||
color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer
|
||||
border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0)
|
||||
border.color: {
|
||||
if (connectButton.hasError) {
|
||||
return Colours.palette.m3error;
|
||||
}
|
||||
if (passwordContainer.activeFocus) {
|
||||
return Colours.palette.m3primary;
|
||||
}
|
||||
return root.visible ? Colours.palette.m3outline : "transparent";
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
CAnim {}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
CAnim {}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
hoverEnabled: false
|
||||
cursorShape: Qt.IBeamCursor
|
||||
|
||||
function onClicked(): void {
|
||||
passwordContainer.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeholder
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Password")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.family: Appearance.font.family.mono
|
||||
opacity: passwordContainer.passwordBuffer ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: charList
|
||||
|
||||
readonly property int fullWidth: count * (implicitHeight + spacing) - spacing
|
||||
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: fullWidth
|
||||
implicitHeight: Appearance.font.size.normal
|
||||
|
||||
orientation: Qt.Horizontal
|
||||
spacing: Appearance.spacing.small / 2
|
||||
interactive: false
|
||||
|
||||
model: ScriptModel {
|
||||
values: passwordContainer.passwordBuffer.split("")
|
||||
}
|
||||
|
||||
delegate: StyledRect {
|
||||
id: ch
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: charList.implicitHeight
|
||||
|
||||
color: Colours.palette.m3onSurface
|
||||
radius: Appearance.rounding.small / 2
|
||||
|
||||
opacity: 0
|
||||
scale: 0
|
||||
Component.onCompleted: {
|
||||
opacity = 1;
|
||||
scale = 1;
|
||||
}
|
||||
ListView.onRemove: removeAnim.start()
|
||||
|
||||
SequentialAnimation {
|
||||
id: removeAnim
|
||||
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: true
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: ch
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
Anim {
|
||||
target: ch
|
||||
property: "scale"
|
||||
to: 0.5
|
||||
}
|
||||
}
|
||||
PropertyAction {
|
||||
target: ch
|
||||
property: "ListView.delayRemove"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.topMargin: Appearance.spacing.normal
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
TextButton {
|
||||
id: cancelButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3secondaryContainer
|
||||
inactiveOnColour: Colours.palette.m3onSecondaryContainer
|
||||
text: qsTr("Cancel")
|
||||
|
||||
onClicked: root.closeDialog()
|
||||
}
|
||||
|
||||
TextButton {
|
||||
id: connectButton
|
||||
|
||||
property bool connecting: false
|
||||
property bool hasError: false
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2
|
||||
inactiveColour: Colours.palette.m3primary
|
||||
inactiveOnColour: Colours.palette.m3onPrimary
|
||||
text: qsTr("Connect")
|
||||
enabled: passwordContainer.passwordBuffer.length > 0 && !connecting
|
||||
|
||||
onClicked: {
|
||||
if (!root.network || connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = passwordContainer.passwordBuffer;
|
||||
if (!password || password.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasError = false;
|
||||
connecting = true;
|
||||
enabled = false;
|
||||
text = qsTr("Connecting...");
|
||||
|
||||
NetworkConnection.connectWithPassword(root.network, password, result => {
|
||||
if (result && result.success) {} else if (result && result.needsPassword) {
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
} else {
|
||||
connectionMonitor.stop();
|
||||
connecting = false;
|
||||
hasError = true;
|
||||
enabled = true;
|
||||
text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connectionMonitor.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkConnectionStatus(): void {
|
||||
if (!root.visible || !connectButton.connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
|
||||
if (isConnected) {
|
||||
connectionSuccessTimer.start();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Nmcli.pendingConnection === null && connectButton.connecting) {
|
||||
if (connectionMonitor.repeatCount > 10) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
if (root.network && root.network.ssid) {
|
||||
Nmcli.forgetNetwork(root.network.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionMonitor
|
||||
interval: 1000
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
property int repeatCount: 0
|
||||
|
||||
onTriggered: {
|
||||
repeatCount++;
|
||||
checkConnectionStatus();
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
repeatCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: connectionSuccessTimer
|
||||
interval: 500
|
||||
onTriggered: {
|
||||
if (root.visible && Nmcli.active && Nmcli.active.ssid) {
|
||||
const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||
if (stillConnected) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Nmcli
|
||||
function onActiveChanged() {
|
||||
if (root.visible) {
|
||||
checkConnectionStatus();
|
||||
}
|
||||
}
|
||||
function onConnectionFailed(ssid: string) {
|
||||
if (root.visible && root.network && root.network.ssid === ssid && connectButton.connecting) {
|
||||
connectionMonitor.stop();
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = true;
|
||||
connectButton.enabled = true;
|
||||
connectButton.text = qsTr("Connect");
|
||||
passwordContainer.passwordBuffer = "";
|
||||
Nmcli.forgetNetwork(ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeDialog(): void {
|
||||
if (isClosing) {
|
||||
return;
|
||||
}
|
||||
|
||||
isClosing = true;
|
||||
passwordContainer.passwordBuffer = "";
|
||||
connectButton.connecting = false;
|
||||
connectButton.hasError = false;
|
||||
connectButton.text = qsTr("Connect");
|
||||
connectionMonitor.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.services
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SettingsHeader {
|
||||
icon: "wifi"
|
||||
title: qsTr("Network settings")
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("WiFi status")
|
||||
description: qsTr("General WiFi settings")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
ToggleRow {
|
||||
label: qsTr("WiFi enabled")
|
||||
checked: Nmcli.wifiEnabled
|
||||
toggle.onToggled: {
|
||||
Nmcli.enableWifi(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
Layout.topMargin: Appearance.spacing.large
|
||||
title: qsTr("Network information")
|
||||
description: qsTr("Current network connection")
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.small / 2
|
||||
|
||||
PropertyRow {
|
||||
label: qsTr("Connected network")
|
||||
value: Nmcli.active ? Nmcli.active.ssid : qsTr("Not connected")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Signal strength")
|
||||
value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Security")
|
||||
value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
|
||||
}
|
||||
|
||||
PropertyRow {
|
||||
showTopMargin: true
|
||||
label: qsTr("Frequency")
|
||||
value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Quickshell.Bluetooth
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property BluetoothDevice active: null
|
||||
property BluetoothAdapter currentAdapter: Bluetooth.defaultAdapter
|
||||
property bool editingAdapterName: false
|
||||
property bool fabMenuOpen: false
|
||||
property bool editingDeviceName: false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var active: null
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var active: null
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property var active: null
|
||||
property bool showPasswordDialog: false
|
||||
property var pendingNetwork: null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
property var active: null
|
||||
}
|
||||
@@ -0,0 +1,648 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import ".."
|
||||
import "../components"
|
||||
import qs.components
|
||||
import qs.components.controls
|
||||
import qs.components.effects
|
||||
import qs.components.containers
|
||||
import qs.services
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Session session
|
||||
|
||||
property bool clockShowIcon: Config.bar.clock.showIcon ?? true
|
||||
property bool persistent: Config.bar.persistent ?? true
|
||||
property bool showOnHover: Config.bar.showOnHover ?? true
|
||||
property int dragThreshold: Config.bar.dragThreshold ?? 20
|
||||
property bool showAudio: Config.bar.status.showAudio ?? true
|
||||
property bool showMicrophone: Config.bar.status.showMicrophone ?? true
|
||||
property bool showKbLayout: Config.bar.status.showKbLayout ?? false
|
||||
property bool showNetwork: Config.bar.status.showNetwork ?? true
|
||||
property bool showWifi: Config.bar.status.showWifi ?? true
|
||||
property bool showBluetooth: Config.bar.status.showBluetooth ?? true
|
||||
property bool showBattery: Config.bar.status.showBattery ?? true
|
||||
property bool showLockStatus: Config.bar.status.showLockStatus ?? true
|
||||
property bool trayBackground: Config.bar.tray.background ?? false
|
||||
property bool trayCompact: Config.bar.tray.compact ?? false
|
||||
property bool trayRecolour: Config.bar.tray.recolour ?? false
|
||||
property int workspacesShown: Config.bar.workspaces.shown ?? 5
|
||||
property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true
|
||||
property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false
|
||||
property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false
|
||||
property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true
|
||||
property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true
|
||||
property bool scrollVolume: Config.bar.scrollActions.volume ?? true
|
||||
property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true
|
||||
property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true
|
||||
property bool popoutTray: Config.bar.popouts.tray ?? true
|
||||
property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Component.onCompleted: {
|
||||
if (Config.bar.entries) {
|
||||
entriesModel.clear();
|
||||
for (let i = 0; i < Config.bar.entries.length; i++) {
|
||||
const entry = Config.bar.entries[i];
|
||||
entriesModel.append({
|
||||
id: entry.id,
|
||||
enabled: entry.enabled !== false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig(entryIndex, entryEnabled) {
|
||||
Config.bar.clock.showIcon = root.clockShowIcon;
|
||||
Config.bar.persistent = root.persistent;
|
||||
Config.bar.showOnHover = root.showOnHover;
|
||||
Config.bar.dragThreshold = root.dragThreshold;
|
||||
Config.bar.status.showAudio = root.showAudio;
|
||||
Config.bar.status.showMicrophone = root.showMicrophone;
|
||||
Config.bar.status.showKbLayout = root.showKbLayout;
|
||||
Config.bar.status.showNetwork = root.showNetwork;
|
||||
Config.bar.status.showWifi = root.showWifi;
|
||||
Config.bar.status.showBluetooth = root.showBluetooth;
|
||||
Config.bar.status.showBattery = root.showBattery;
|
||||
Config.bar.status.showLockStatus = root.showLockStatus;
|
||||
Config.bar.tray.background = root.trayBackground;
|
||||
Config.bar.tray.compact = root.trayCompact;
|
||||
Config.bar.tray.recolour = root.trayRecolour;
|
||||
Config.bar.workspaces.shown = root.workspacesShown;
|
||||
Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator;
|
||||
Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg;
|
||||
Config.bar.workspaces.showWindows = root.workspacesShowWindows;
|
||||
Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor;
|
||||
Config.bar.scrollActions.workspaces = root.scrollWorkspaces;
|
||||
Config.bar.scrollActions.volume = root.scrollVolume;
|
||||
Config.bar.scrollActions.brightness = root.scrollBrightness;
|
||||
Config.bar.popouts.activeWindow = root.popoutActiveWindow;
|
||||
Config.bar.popouts.tray = root.popoutTray;
|
||||
Config.bar.popouts.statusIcons = root.popoutStatusIcons;
|
||||
|
||||
const entries = [];
|
||||
for (let i = 0; i < entriesModel.count; i++) {
|
||||
const entry = entriesModel.get(i);
|
||||
let enabled = entry.enabled;
|
||||
if (entryIndex !== undefined && i === entryIndex) {
|
||||
enabled = entryEnabled;
|
||||
}
|
||||
entries.push({
|
||||
id: entry.id,
|
||||
enabled: enabled
|
||||
});
|
||||
}
|
||||
Config.bar.entries = entries;
|
||||
Config.save();
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: entriesModel
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: taskbarClippingRect
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.normal
|
||||
anchors.leftMargin: 0
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
|
||||
radius: taskbarBorder.innerRadius
|
||||
color: "transparent"
|
||||
|
||||
Loader {
|
||||
id: taskbarLoader
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large + Appearance.padding.normal
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.rightMargin: Appearance.padding.large
|
||||
|
||||
sourceComponent: taskbarContentComponent
|
||||
}
|
||||
}
|
||||
|
||||
InnerBorder {
|
||||
id: taskbarBorder
|
||||
leftThickness: 0
|
||||
rightThickness: Appearance.padding.normal
|
||||
}
|
||||
|
||||
Component {
|
||||
id: taskbarContentComponent
|
||||
|
||||
StyledFlickable {
|
||||
id: sidebarFlickable
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
contentHeight: sidebarLayout.height
|
||||
|
||||
StyledScrollBar.vertical: StyledScrollBar {
|
||||
flickable: sidebarFlickable
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: sidebarLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Taskbar")
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Status Icons")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root
|
||||
|
||||
options: [
|
||||
{
|
||||
label: qsTr("Speakers"),
|
||||
propertyName: "showAudio",
|
||||
onToggled: function (checked) {
|
||||
root.showAudio = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Microphone"),
|
||||
propertyName: "showMicrophone",
|
||||
onToggled: function (checked) {
|
||||
root.showMicrophone = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Keyboard"),
|
||||
propertyName: "showKbLayout",
|
||||
onToggled: function (checked) {
|
||||
root.showKbLayout = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Network"),
|
||||
propertyName: "showNetwork",
|
||||
onToggled: function (checked) {
|
||||
root.showNetwork = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Wifi"),
|
||||
propertyName: "showWifi",
|
||||
onToggled: function (checked) {
|
||||
root.showWifi = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Bluetooth"),
|
||||
propertyName: "showBluetooth",
|
||||
onToggled: function (checked) {
|
||||
root.showBluetooth = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Battery"),
|
||||
propertyName: "showBattery",
|
||||
onToggled: function (checked) {
|
||||
root.showBattery = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Capslock"),
|
||||
propertyName: "showLockStatus",
|
||||
onToggled: function (checked) {
|
||||
root.showLockStatus = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: mainRowLayout
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
ColumnLayout {
|
||||
id: leftColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Workspaces")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesShownRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Shown")
|
||||
}
|
||||
|
||||
CustomSpinBox {
|
||||
min: 1
|
||||
max: 20
|
||||
value: root.workspacesShown
|
||||
onValueModified: value => {
|
||||
root.workspacesShown = value;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesActiveIndicatorRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Active indicator")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesActiveIndicator
|
||||
onToggled: {
|
||||
root.workspacesActiveIndicator = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesOccupiedBgRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Occupied background")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesOccupiedBg
|
||||
onToggled: {
|
||||
root.workspacesOccupiedBg = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesShowWindowsRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Show windows")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesShowWindows
|
||||
onToggled: {
|
||||
root.workspacesShowWindows = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
color: Colours.layer(Colours.palette.m3surfaceContainer, 2)
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: workspacesPerMonitorRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Per monitor workspaces")
|
||||
}
|
||||
|
||||
StyledSwitch {
|
||||
checked: root.workspacesPerMonitor
|
||||
onToggled: {
|
||||
root.workspacesPerMonitor = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Scroll Actions")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root
|
||||
|
||||
options: [
|
||||
{
|
||||
label: qsTr("Workspaces"),
|
||||
propertyName: "scrollWorkspaces",
|
||||
onToggled: function (checked) {
|
||||
root.scrollWorkspaces = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Volume"),
|
||||
propertyName: "scrollVolume",
|
||||
onToggled: function (checked) {
|
||||
root.scrollVolume = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Brightness"),
|
||||
propertyName: "scrollBrightness",
|
||||
onToggled: function (checked) {
|
||||
root.scrollBrightness = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: middleColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Clock")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Show clock icon")
|
||||
checked: root.clockShowIcon
|
||||
onToggled: checked => {
|
||||
root.clockShowIcon = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Bar Behavior")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Persistent")
|
||||
checked: root.persistent
|
||||
onToggled: checked => {
|
||||
root.persistent = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Show on hover")
|
||||
checked: root.showOnHover
|
||||
onToggled: checked => {
|
||||
root.showOnHover = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
contentSpacing: Appearance.spacing.normal
|
||||
|
||||
SliderInput {
|
||||
Layout.fillWidth: true
|
||||
|
||||
label: qsTr("Drag threshold")
|
||||
value: root.dragThreshold
|
||||
from: 0
|
||||
to: 100
|
||||
suffix: "px"
|
||||
validator: IntValidator {
|
||||
bottom: 0
|
||||
top: 100
|
||||
}
|
||||
formatValueFunction: val => Math.round(val).toString()
|
||||
parseValueFunction: text => parseInt(text)
|
||||
|
||||
onValueModified: newValue => {
|
||||
root.dragThreshold = Math.round(newValue);
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: rightColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Popouts")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Active window")
|
||||
checked: root.popoutActiveWindow
|
||||
onToggled: checked => {
|
||||
root.popoutActiveWindow = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Tray")
|
||||
checked: root.popoutTray
|
||||
onToggled: checked => {
|
||||
root.popoutTray = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
SwitchRow {
|
||||
label: qsTr("Status icons")
|
||||
checked: root.popoutStatusIcons
|
||||
onToggled: checked => {
|
||||
root.popoutStatusIcons = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionContainer {
|
||||
Layout.fillWidth: true
|
||||
alignTop: true
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Tray Settings")
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
ConnectedButtonGroup {
|
||||
rootItem: root
|
||||
|
||||
options: [
|
||||
{
|
||||
label: qsTr("Background"),
|
||||
propertyName: "trayBackground",
|
||||
onToggled: function (checked) {
|
||||
root.trayBackground = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Compact"),
|
||||
propertyName: "trayCompact",
|
||||
onToggled: function (checked) {
|
||||
root.trayCompact = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: qsTr("Recolour"),
|
||||
propertyName: "trayRecolour",
|
||||
onToggled: function (checked) {
|
||||
root.trayRecolour = checked;
|
||||
root.saveConfig();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
152
.config/quickshell/caelestia/modules/dashboard/Content.qml
Normal file
152
.config/quickshell/caelestia/modules/dashboard/Content.qml
Normal 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);
|
||||
})
|
||||
}
|
||||
}
|
||||
105
.config/quickshell/caelestia/modules/dashboard/Dash.qml
Normal file
105
.config/quickshell/caelestia/modules/dashboard/Dash.qml
Normal 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
|
||||
}
|
||||
}
|
||||
403
.config/quickshell/caelestia/modules/dashboard/Media.qml
Normal file
403
.config/quickshell/caelestia/modules/dashboard/Media.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
967
.config/quickshell/caelestia/modules/dashboard/Performance.qml
Normal file
967
.config/quickshell/caelestia/modules/dashboard/Performance.qml
Normal 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
Reference in New Issue
Block a user