diff --git a/config/niri/config.kdl b/config/niri/config.kdl index af55edf..8d572a3 100644 --- a/config/niri/config.kdl +++ b/config/niri/config.kdl @@ -130,7 +130,6 @@ spawn-at-startup "wallpaper-daemon" spawn-at-startup "fcitx5" // Core -spawn-at-startup "blueman-applet" spawn-at-startup "nm-applet" spawn-sh-at-startup "gnome-keyring-daemon --start --components=secrets" spawn-at-startup "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1" diff --git a/config/quickshell/Assets/Config/Settings.json b/config/quickshell/Assets/Config/Settings.json index 9d2e591..82d79b6 100644 --- a/config/quickshell/Assets/Config/Settings.json +++ b/config/quickshell/Assets/Config/Settings.json @@ -4,5 +4,6 @@ "doNotDisturb": false }, "primaryColor": "#89b4fa", - "showLyricsBar": false + "showLyricsBar": false, + "wifiEnabled": true } diff --git a/config/quickshell/Constants/Color.qml b/config/quickshell/Constants/Color.qml index f925b86..84aac1a 100644 --- a/config/quickshell/Constants/Color.qml +++ b/config/quickshell/Constants/Color.qml @@ -8,19 +8,19 @@ Singleton { id: root // Compatibility colors for noctalia modules - property color mPrimary: Colors.primary - property color mOnPrimary: Colors.base - property color mSecondary: Colors.primary - property color mOnSecondary: Colors.base - property color mTertiary: Colors.primary - property color mOnTertiary: Colors.base - property color mError: Colors.red - property color mOnError: Colors.base - property color mSurface: Colors.base - property color mOnSurface: Colors.text - property color mSurfaceVariant: Colors.surface - property color mOnSurfaceVariant: Colors.overlay1 - property color mOutline: Colors.primary - property color mShadow: Colors.crust - property color transparent: "transparent" + readonly property color mPrimary: Colors.primary + readonly property color mOnPrimary: Colors.base + readonly property color mSecondary: Colors.primary + readonly property color mOnSecondary: Colors.base + readonly property color mTertiary: Colors.primary + readonly property color mOnTertiary: Colors.base + readonly property color mError: Colors.red + readonly property color mOnError: Colors.base + readonly property color mSurface: Colors.base + readonly property color mOnSurface: Colors.text + readonly property color mSurfaceVariant: Colors.surface + readonly property color mOnSurfaceVariant: Colors.overlay1 + readonly property color mOutline: Colors.primary + readonly property color mShadow: Colors.crust + readonly property color transparent: "transparent" } diff --git a/config/quickshell/Constants/Colors.qml b/config/quickshell/Constants/Colors.qml index 567fdd0..4732210 100644 --- a/config/quickshell/Constants/Colors.qml +++ b/config/quickshell/Constants/Colors.qml @@ -6,6 +6,7 @@ pragma Singleton Singleton { id: root + readonly property color primary: SettingsService.primaryColor readonly property color transparent: "transparent" readonly property color rosewater: "#f5e0dc" readonly property color flamingo: "#f2cdcd" @@ -34,7 +35,6 @@ Singleton { readonly property color base: "#1e1e2e" readonly property color mantle: "#181825" readonly property color crust: "#11111b" - property color primary: SettingsService.primaryColor readonly property color distroColor: "#74c7ec" readonly property var cavaList: ["#b4befe", "#89b4fa", "#74c7ec", "#89dceb", "#94e2d5", "#a6e3a1", "#f9e2af", "#fab387"] } diff --git a/config/quickshell/Constants/Icons.qml b/config/quickshell/Constants/Icons.qml index a648bb9..3f5f5a3 100644 --- a/config/quickshell/Constants/Icons.qml +++ b/config/quickshell/Constants/Icons.qml @@ -37,6 +37,10 @@ Singleton { readonly property string reset: "󰑙" readonly property string lines: "" readonly property string record: "" + readonly property string wifiOn: "󰖩" + readonly property string wifiOff: "󰖪" + readonly property string bluetoothOn: "" + readonly property string bluetoothOff: "󰂲" // Tabler icons // Expose the font family name for easy access readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : "" diff --git a/config/quickshell/Constants/Style.qml b/config/quickshell/Constants/Style.qml index 6493e38..9580669 100644 --- a/config/quickshell/Constants/Style.qml +++ b/config/quickshell/Constants/Style.qml @@ -11,58 +11,58 @@ Singleton { id: root // Font size - property real fontSizeXXS: 8 - property real fontSizeXS: 9 - property real fontSizeS: 10 - property real fontSizeM: 11 - property real fontSizeL: 13 - property real fontSizeXL: 16 - property real fontSizeXXL: 18 - property real fontSizeXXXL: 24 + readonly property real fontSizeXXS: 8 + readonly property real fontSizeXS: 9 + readonly property real fontSizeS: 10 + readonly property real fontSizeM: 11 + readonly property real fontSizeL: 13 + readonly property real fontSizeXL: 16 + readonly property real fontSizeXXL: 18 + readonly property real fontSizeXXXL: 24 // Font weight - property int fontWeightRegular: 400 - property int fontWeightMedium: 500 - property int fontWeightSemiBold: 600 - property int fontWeightBold: 700 + readonly property int fontWeightRegular: 400 + readonly property int fontWeightMedium: 500 + readonly property int fontWeightSemiBold: 600 + readonly property int fontWeightBold: 700 // Radii - property int radiusXXS: 4 - property int radiusXS: 8 - property int radiusS: 12 - property int radiusM: 16 - property int radiusL: 20 + readonly property int radiusXXS: 4 + readonly property int radiusXS: 8 + readonly property int radiusS: 12 + readonly property int radiusM: 16 + readonly property int radiusL: 20 //screen Radii - property int screenRadius: 20 + readonly property int screenRadius: 20 // Border - property int borderS: 2 - property int borderM: 3 - property int borderL: 4 + readonly property int borderS: 2 + readonly property int borderM: 3 + readonly property int borderL: 4 // Margins (for margins and spacing) - property int marginXXS: 2 - property int marginXS: 4 - property int marginS: 8 - property int marginM: 12 - property int marginL: 16 - property int marginXL: 24 + readonly property int marginXXS: 2 + readonly property int marginXS: 4 + readonly property int marginS: 8 + readonly property int marginM: 12 + readonly property int marginL: 16 + readonly property int marginXL: 24 // Opacity - property real opacityNone: 0 - property real opacityLight: 0.25 - property real opacityMedium: 0.5 - property real opacityHeavy: 0.75 - property real opacityAlmost: 0.95 - property real opacityFull: 1 + readonly property real opacityNone: 0 + readonly property real opacityLight: 0.25 + readonly property real opacityMedium: 0.5 + readonly property real opacityHeavy: 0.75 + readonly property real opacityAlmost: 0.95 + readonly property real opacityFull: 1 // Animation duration (ms) - property int animationFast: 150 - property int animationNormal: 300 - property int animationSlow: 450 - property int animationSlowest: 1000 + readonly property int animationFast: 150 + readonly property int animationNormal: 300 + readonly property int animationSlow: 450 + readonly property int animationSlowest: 1000 // Delays - property int tooltipDelay: 300 - property int tooltipDelayLong: 1200 - property int pillDelay: 500 + readonly property int tooltipDelay: 300 + readonly property int tooltipDelayLong: 1200 + readonly property int pillDelay: 500 // Settings widgets base size - property real baseWidgetSize: 33 - property real sliderWidth: 200 + readonly property real baseWidgetSize: 33 + readonly property real sliderWidth: 200 // Bar Dimensions - property real barHeight: 45 - property real capsuleHeight: 35 + readonly property real barHeight: 45 + readonly property real capsuleHeight: 35 } diff --git a/config/quickshell/Modules/Bar/Bar.qml b/config/quickshell/Modules/Bar/Bar.qml index 9113cb4..874e689 100644 --- a/config/quickshell/Modules/Bar/Bar.qml +++ b/config/quickshell/Modules/Bar/Bar.qml @@ -85,7 +85,8 @@ Variants { symbol: Icons.distro buttonColor: Colors.distroColor onClicked: { - PanelService.getPanel("controlCenterPanel")?.toggle(this) + // PanelService.getPanel("controlCenterPanel")?.toggle(this) + PanelService.getPanel("wifiPanel")?.toggle(this) } onRightClicked: { if (action.running) { @@ -96,6 +97,27 @@ Variants { } } + SymbolButton { + symbol: SettingsService.wifiEnabled ? Icons.wifiOn : Icons.wifiOff + buttonColor: Colors.green + onClicked: { + PanelService.getPanel("wifiPanel")?.toggle(this) + } + } + + SymbolButton { + symbol: BluetoothService.enabled ? Icons.bluetoothOn : Icons.bluetoothOff + buttonColor: Colors.peach + onClicked: { + PanelService.getPanel("bluetoothPanel")?.toggle(this) + } + } + + + Item { + width: 5 + } + Separator { } @@ -176,7 +198,6 @@ Variants { } Ip { - showCountryCode: true } CpuTemp { diff --git a/config/quickshell/Modules/Bar/Components/Ip.qml b/config/quickshell/Modules/Bar/Components/Ip.qml index 8110375..e975d4c 100644 --- a/config/quickshell/Modules/Bar/Components/Ip.qml +++ b/config/quickshell/Modules/Bar/Components/Ip.qml @@ -5,17 +5,9 @@ import qs.Constants import qs.Services Item { - // Text { - // id: ipText - // anchors.verticalCenter: parent.verticalCenter - // text: Icons.global + " " + (showCountryCode ? IpService.countryCode : IpService.ip) - // font.pixelSize: Fonts.medium - // color: Colors.peach - // } - id: root - property bool showCountryCode: true + property bool _showCountryCode: true implicitHeight: parent.height implicitWidth: layout.width + 10 @@ -43,8 +35,8 @@ Item { Text { id: ipText - text: showCountryCode ? IpService.countryCode : IpService.ip - font.pointSize: showCountryCode ? Fonts.medium : Fonts.small + text: _showCountryCode ? IpService.countryCode : IpService.ip + font.pointSize: _showCountryCode ? Fonts.medium : Fonts.small font.family: Fonts.primary color: Colors.peach anchors.verticalCenter: parent.verticalCenter @@ -73,10 +65,10 @@ Item { acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onClicked: (mouse) => { if (mouse.button === Qt.LeftButton) { - WriteClipboard.write(showCountryCode ? IpService.countryCode : IpService.ip); - SendNotification.show("Copied to clipboard", showCountryCode ? IpService.countryCode : IpService.ip); + WriteClipboard.write(_showCountryCode ? IpService.countryCode : IpService.ip); + SendNotification.show("Copied to clipboard", _showCountryCode ? IpService.countryCode : IpService.ip); } else if (mouse.button === Qt.RightButton) - showCountryCode = !showCountryCode; + _showCountryCode = !_showCountryCode; else if (mouse.button === Qt.MiddleButton) IpService.refresh(); } diff --git a/config/quickshell/Modules/Bar/Components/MemUsage.qml b/config/quickshell/Modules/Bar/Components/MemUsage.qml index 287a7f1..568544c 100644 --- a/config/quickshell/Modules/Bar/Components/MemUsage.qml +++ b/config/quickshell/Modules/Bar/Components/MemUsage.qml @@ -5,15 +5,15 @@ import qs.Modules.Bar.Misc import qs.Services MonitorItem { - property bool showPercent: false + property bool _showPercent: false symbol: Icons.memory fillColor: Colors.green critical: SystemStatService.memPercent > 90 value: Math.round(SystemStatService.memPercent) maxValue: 100 - textValue: showPercent ? SystemStatService.memPercent : SystemStatService.memGb - textSuffix: showPercent ? "%" : "GB" + textValue: _showPercent ? SystemStatService.memPercent : SystemStatService.memGb + textSuffix: _showPercent ? "%" : "GB" onClicked: { if (action.running) { action.signal(15); @@ -22,7 +22,7 @@ MonitorItem { action.exec(["ghostty", "-e", "btop"]); } onRightClicked: { - showPercent = !showPercent; + _showPercent = !_showPercent; } Process { diff --git a/config/quickshell/Modules/Bar/Misc/SystemTray.qml b/config/quickshell/Modules/Bar/Misc/SystemTray.qml index 47f1255..8174d65 100644 --- a/config/quickshell/Modules/Bar/Misc/SystemTray.qml +++ b/config/quickshell/Modules/Bar/Misc/SystemTray.qml @@ -56,7 +56,6 @@ Rectangle { // Process icon path if (icon.includes("?path=")) { - // Seems qmlfmt does not support the following ES6 syntax: const[name, path] = icon.split const chunks = icon.split("?path=") const name = chunks[0] const path = chunks[1] diff --git a/config/quickshell/Modules/Bar/Misc/TrayMenu.qml b/config/quickshell/Modules/Bar/Misc/TrayMenu.qml index e695e04..adcec85 100644 --- a/config/quickshell/Modules/Bar/Misc/TrayMenu.qml +++ b/config/quickshell/Modules/Bar/Misc/TrayMenu.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import Quickshell.Widgets import qs.Constants import qs.Utils import qs.Noctalia @@ -154,12 +155,11 @@ PopupWindow { family: Fonts.sans } - Image { + IconImage { Layout.preferredWidth: Style.marginL Layout.preferredHeight: Style.marginL source: modelData?.icon ?? "" visible: (modelData?.icon ?? "") !== "" - fillMode: Image.PreserveAspectFit } NIcon { diff --git a/config/quickshell/Modules/Panel/BluetoothPanel.qml b/config/quickshell/Modules/Panel/BluetoothPanel.qml new file mode 100644 index 0000000..466cddc --- /dev/null +++ b/config/quickshell/Modules/Panel/BluetoothPanel.qml @@ -0,0 +1,255 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Wayland +import qs.Constants +import qs.Modules.Panel.Misc +import qs.Noctalia +import qs.Services + +NPanel { + id: root + + preferredWidth: 380 + preferredHeight: 500 + panelKeyboardFocus: true + + panelContent: Rectangle { + color: Color.transparent + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // HEADER + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NIcon { + icon: "bluetooth" + pointSize: Style.fontSizeXXL + color: Color.mPrimary + } + + NText { + text: "Bluetooth" + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NToggle { + id: bluetoothSwitch + + checked: BluetoothService.enabled + onToggled: (checked) => { + return BluetoothService.setBluetoothEnabled(checked); + } + baseSize: Style.baseWidgetSize * 0.65 + } + + NIconButton { + enabled: BluetoothService.enabled + icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh" + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + if (BluetoothService.adapter) + BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering; + + } + colorFg: Colors.green + colorBg: Color.transparent + colorFgHover: Colors.base + colorBgHover: Colors.green + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + root.close(); + } + colorFg: Colors.red + colorBg: Color.transparent + colorFgHover: Colors.base + colorBgHover: Colors.red + } + + } + + NDivider { + Layout.fillWidth: true + } + + Rectangle { + visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled) + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.transparent + + // Center the content within this rectangle + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginM + + NIcon { + icon: "bluetooth-off" + pointSize: 64 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Bluetooth is turned off" + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Enable Bluetooth" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + } + + } + + NScrollView { + visible: BluetoothService.adapter && BluetoothService.adapter.enabled + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + clip: true + contentWidth: availableWidth + + ColumnLayout { + width: parent.width + spacing: Style.marginM + + // Connected devices + BluetoothDevicesList { + property var items: { + if (!BluetoothService.adapter || !Bluetooth.devices) + return []; + + var filtered = Bluetooth.devices.values.filter((dev) => { + return dev && !dev.blocked && dev.connected; + }); + return BluetoothService.sortDevices(filtered); + } + + label: "Connected Devices" + model: items + visible: items.length > 0 + Layout.fillWidth: true + } + + // Known devices + BluetoothDevicesList { + property var items: { + if (!BluetoothService.adapter || !Bluetooth.devices) + return []; + + var filtered = Bluetooth.devices.values.filter((dev) => { + return dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted); + }); + return BluetoothService.sortDevices(filtered); + } + + label: "Known Devices" + tooltipText: "Connect/Disconnect Devices" + model: items + visible: items.length > 0 + Layout.fillWidth: true + } + + // Available devices + BluetoothDevicesList { + property var items: { + if (!BluetoothService.adapter || !Bluetooth.devices) + return []; + + var filtered = Bluetooth.devices.values.filter((dev) => { + return dev && !dev.blocked && !dev.paired && !dev.trusted; + }); + return BluetoothService.sortDevices(filtered); + } + + label: "Available Devices" + model: items + visible: items.length > 0 + Layout.fillWidth: true + } + + // Fallback - No devices, scanning + ColumnLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginM + visible: { + if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) + return false; + + var availableCount = Bluetooth.devices.values.filter((dev) => { + return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0); + }).length; + return (availableCount === 0); + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginXS + + NIcon { + icon: "refresh" + pointSize: Style.fontSizeXXL * 1.5 + color: Color.mPrimary + + RotationAnimation on rotation { + running: true + loops: Animation.Infinite + from: 0 + to: 360 + duration: Style.animationSlow * 4 + } + + } + + NText { + text: "Scanning..." + pointSize: Style.fontSizeL + color: Color.mOnSurface + } + + } + + NText { + text: "Pairing Mode" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + } + + Item { + Layout.fillHeight: true + } + + } + + } + + } + + } + +} diff --git a/config/quickshell/Modules/Panel/Misc/BluetoothDevicesList.qml b/config/quickshell/Modules/Panel/Misc/BluetoothDevicesList.qml new file mode 100644 index 0000000..963bb63 --- /dev/null +++ b/config/quickshell/Modules/Panel/Misc/BluetoothDevicesList.qml @@ -0,0 +1,186 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Wayland +import qs.Constants +import qs.Noctalia +import qs.Services + +ColumnLayout { + id: root + + property string label: "" + property string tooltipText: "" + property var model: { + } + + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: root.label + pointSize: Style.fontSizeL + color: Color.mSecondary + font.weight: Style.fontWeightMedium + Layout.fillWidth: true + visible: root.model.length > 0 + } + + Repeater { + id: deviceList + + Layout.fillWidth: true + model: root.model + visible: BluetoothService.adapter && BluetoothService.adapter.enabled + + NBox { + id: device + + readonly property bool canConnect: BluetoothService.canConnect(modelData) + readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData) + readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData) + + function getContentColor(defaultColor = Color.mOnSurface) { + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) + return Color.mPrimary; + + if (modelData.blocked) + return Color.mError; + + return defaultColor; + } + + Layout.fillWidth: true + Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginM * 2) + + RowLayout { + id: deviceLayout + + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + Layout.alignment: Qt.AlignVCenter + + // One device BT icon + NIcon { + icon: BluetoothService.getDeviceIcon(modelData) + pointSize: Style.fontSizeXXL + color: getContentColor(Color.mOnSurface) + Layout.alignment: Qt.AlignVCenter + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + + // Device name + NText { + text: modelData.name || modelData.deviceName + pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + elide: Text.ElideRight + color: getContentColor(Color.mOnSurface) + Layout.fillWidth: true + } + + // Status + NText { + text: BluetoothService.getStatusString(modelData) + visible: text !== "" + pointSize: Style.fontSizeXS + color: getContentColor(Color.mOnSurfaceVariant) + } + + // Signal Strength + RowLayout { + visible: modelData.signalStrength !== undefined + Layout.fillWidth: true + spacing: Style.marginXS + + // Device signal strength - "Unknown" when not connected + NText { + text: BluetoothService.getSignalStrength(modelData) + pointSize: Style.fontSizeXS + color: getContentColor(Color.mOnSurfaceVariant) + } + + NIcon { + visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked + icon: BluetoothService.getSignalIcon(modelData) + pointSize: Style.fontSizeXS + color: getContentColor(Color.mOnSurface) + } + + NText { + visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked + text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : "" + pointSize: Style.fontSizeXS + color: getContentColor(Color.mOnSurface) + } + + } + + // Battery + NText { + visible: modelData.batteryAvailable + text: BluetoothService.getBattery(modelData) + pointSize: Style.fontSizeXS + color: getContentColor(Color.mOnSurfaceVariant) + } + + } + + // Spacer to push connect button to the right + Item { + Layout.fillWidth: true + } + + // Call to action + NButton { + id: button + + visible: (modelData.state !== BluetoothDeviceState.Connecting) + enabled: (canConnect || canDisconnect) && !isBusy + outlined: !button.hovered + fontSize: Style.fontSizeXS + fontWeight: Style.fontWeightMedium + backgroundColor: { + if (device.canDisconnect && !isBusy) + return Color.mError; + + return Color.mPrimary; + } + tooltipText: root.tooltipText + text: { + if (modelData.pairing) + return "Pairing..."; + + if (modelData.blocked) + return "Blocked"; + + if (modelData.connected) + return "Disconnect"; + + return "Connect"; + } + icon: (isBusy ? "busy" : null) + onClicked: { + if (modelData.connected) + BluetoothService.disconnectDevice(modelData); + else + BluetoothService.connectDeviceWithTrust(modelData); + } + onRightClicked: { + BluetoothService.forgetDevice(modelData); + } + } + + } + + } + + } + +} diff --git a/config/quickshell/Modules/Panel/WiFiPanel.qml b/config/quickshell/Modules/Panel/WiFiPanel.qml new file mode 100644 index 0000000..fa73fb2 --- /dev/null +++ b/config/quickshell/Modules/Panel/WiFiPanel.qml @@ -0,0 +1,634 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Constants +import qs.Noctalia +import qs.Services +import qs.Utils + +NPanel { + id: root + + property string passwordSsid: "" + property string passwordInput: "" + property string expandedSsid: "" + + preferredWidth: 400 + preferredHeight: 500 + panelKeyboardFocus: true + onOpened: NetworkService.scan() + + panelContent: Rectangle { + color: Color.transparent + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // Header + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NIcon { + icon: SettingsService.wifiEnabled ? "wifi" : "wifi-off" + pointSize: Style.fontSizeXXL + color: SettingsService.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant + } + + NText { + text: "WiFi" + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NToggle { + id: wifiSwitch + + checked: SettingsService.wifiEnabled + onToggled: (checked) => { + return NetworkService.setWifiEnabled(checked); + } + baseSize: Style.baseWidgetSize * 0.65 + } + + NIconButton { + icon: "refresh" + baseSize: Style.baseWidgetSize * 0.8 + enabled: SettingsService.wifiEnabled && !NetworkService.scanning + onClicked: NetworkService.scan() + colorFg: Colors.green + colorBg: Color.transparent + colorFgHover: Colors.base + colorBgHover: Colors.green + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.8 + onClicked: root.close() + colorFg: Colors.red + colorBg: Color.transparent + colorFgHover: Colors.base + colorBgHover: Colors.red + } + + } + + NDivider { + Layout.fillWidth: true + } + + // Error message + Rectangle { + visible: NetworkService.lastError.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: errorRow.implicitHeight + (Style.marginM * 2) + color: Qt.rgba(Color.mError.r, Color.mError.g, Color.mError.b, 0.1) + radius: Style.radiusS + border.width: Math.max(1, Style.borderS) + border.color: Color.mError + + RowLayout { + id: errorRow + + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NIcon { + icon: "warning" + pointSize: Style.fontSizeL + color: Color.mError + } + + NText { + text: NetworkService.lastError + color: Color.mError + pointSize: Style.fontSizeS + wrapMode: Text.Wrap + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.6 + onClicked: NetworkService.lastError = "" + } + + } + + } + + // Main content area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.transparent + + // WiFi disabled state + ColumnLayout { + visible: !SettingsService.wifiEnabled + anchors.fill: parent + spacing: Style.marginM + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "wifi-off" + pointSize: 64 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Wi-Fi Disabled" + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Please enable Wi-Fi to connect to a network." + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + + } + + // Scanning state + ColumnLayout { + visible: SettingsService.wifiEnabled && NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 + anchors.fill: parent + spacing: Style.marginL + + Item { + Layout.fillHeight: true + } + + NBusyIndicator { + running: true + color: Color.mPrimary + size: Style.baseWidgetSize + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "Searching for networks..." + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + + } + + // Networks list container + NScrollView { + visible: SettingsService.wifiEnabled && (!NetworkService.scanning || Object.keys(NetworkService.networks).length > 0) + anchors.fill: parent + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + clip: true + + ColumnLayout { + width: parent.width + spacing: Style.marginM + + // Network list + Repeater { + model: { + if (!SettingsService.wifiEnabled) + return []; + + const nets = Object.values(NetworkService.networks); + return nets.sort((a, b) => { + if (a.connected !== b.connected) + return b.connected - a.connected; + + return b.signal - a.signal; + }); + } + + NBox { + Layout.fillWidth: true + implicitHeight: netColumn.implicitHeight + (Style.marginM * 2) + // Add opacity for operations in progress + opacity: (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) ? 0.6 : 1 + + ColumnLayout { + id: netColumn + + width: parent.width - (Style.marginM * 2) + x: Style.marginM + y: Style.marginM + spacing: Style.marginS + + // Main row + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NIcon { + icon: NetworkService.signalIcon(modelData.signal) + pointSize: Style.fontSizeXXL + color: modelData.connected ? Color.mPrimary : Color.mOnSurface + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + NText { + text: modelData.ssid + pointSize: Style.fontSizeM + font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium + color: Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + } + + RowLayout { + spacing: Style.marginXS + + NText { + text: `${modelData.signal}%` + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + } + + NText { + text: "•" + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + } + + NText { + text: NetworkService.isSecured(modelData.security) ? modelData.security : "Open" + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + } + + Item { + Layout.preferredWidth: Style.marginXXS + } + + // Update the status badges area (around line 237) + Rectangle { + visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid + color: Color.mPrimary + radius: height * 0.5 + width: connectedText.implicitWidth + (Style.marginS * 2) + height: connectedText.implicitHeight + (Style.marginXXS * 2) + + NText { + id: connectedText + + anchors.centerIn: parent + text: "Connected" + pointSize: Style.fontSizeXXS + color: Color.mOnPrimary + } + + } + + Rectangle { + visible: NetworkService.disconnectingFrom === modelData.ssid + color: Color.mError + radius: height * 0.5 + width: disconnectingText.implicitWidth + (Style.marginS * 2) + height: disconnectingText.implicitHeight + (Style.marginXXS * 2) + + NText { + id: disconnectingText + + anchors.centerIn: parent + text: "disconnecting" + pointSize: Style.fontSizeXXS + color: Color.mOnPrimary + } + + } + + Rectangle { + visible: NetworkService.forgettingNetwork === modelData.ssid + color: Color.mError + radius: height * 0.5 + width: forgettingText.implicitWidth + (Style.marginS * 2) + height: forgettingText.implicitHeight + (Style.marginXXS * 2) + + NText { + id: forgettingText + + anchors.centerIn: parent + text: "forgetting" + pointSize: Style.fontSizeXXS + color: Color.mOnPrimary + } + + } + + Rectangle { + visible: modelData.cached && !modelData.connected && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid + color: Color.transparent + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS) + radius: height * 0.5 + width: savedText.implicitWidth + (Style.marginS * 2) + height: savedText.implicitHeight + (Style.marginXXS * 2) + + NText { + id: savedText + + anchors.centerIn: parent + text: "saved" + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + } + + } + + } + + } + + // Action area + RowLayout { + spacing: Style.marginS + + NBusyIndicator { + visible: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid + running: visible + color: Color.mPrimary + size: Style.baseWidgetSize * 0.5 + } + + NIconButton { + visible: (modelData.existing || modelData.cached) && !modelData.connected && NetworkService.connectingTo !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid + icon: "trash" + baseSize: Style.baseWidgetSize * 0.8 + onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid + } + + NButton { + visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && passwordSsid !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid + text: { + if (modelData.existing || modelData.cached) + return "Connect"; + + if (!NetworkService.isSecured(modelData.security)) + return "Connect"; + + return "Enter Password"; + } + outlined: !hovered + fontSize: Style.fontSizeXS + enabled: !NetworkService.connecting + onClicked: { + if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) { + NetworkService.connect(modelData.ssid); + } else { + passwordSsid = modelData.ssid; + passwordInput = ""; + expandedSsid = ""; + } + } + } + + NButton { + visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid + text: "Disconnect" + outlined: !hovered + fontSize: Style.fontSizeXS + backgroundColor: Color.mError + onClicked: NetworkService.disconnect(modelData.ssid) + } + + } + + } + + // Password input + Rectangle { + visible: passwordSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid + Layout.fillWidth: true + height: passwordRow.implicitHeight + Style.marginS * 2 + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS) + radius: Style.radiusS + + RowLayout { + id: passwordRow + + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginM + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Style.radiusXS + color: Color.mSurface + border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Math.max(1, Style.borderS) + + TextInput { + id: pwdInput + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Style.marginS + text: passwordInput + font.family: Fonts.sans + font.pointSize: Style.fontSizeS + color: Color.mOnSurface + echoMode: TextInput.Password + selectByMouse: true + focus: visible + passwordCharacter: "●" + onTextChanged: passwordInput = text + onVisibleChanged: { + if (visible) + forceActiveFocus(); + + } + onAccepted: { + if (text && !NetworkService.connecting) { + NetworkService.connect(passwordSsid, text); + passwordSsid = ""; + passwordInput = ""; + } + } + + NText { + visible: parent.text.length === 0 + anchors.verticalCenter: parent.verticalCenter + text: "Enter Password" + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } + + } + + } + + NButton { + text: "Connect" + fontSize: Style.fontSizeXXS + enabled: passwordInput.length > 0 && !NetworkService.connecting + outlined: true + onClicked: { + NetworkService.connect(passwordSsid, passwordInput); + passwordSsid = ""; + passwordInput = ""; + } + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + passwordSsid = ""; + passwordInput = ""; + } + } + + } + + } + + // Forget network + Rectangle { + visible: expandedSsid === modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid + Layout.fillWidth: true + height: forgetRow.implicitHeight + Style.marginS * 2 + color: Color.mSurfaceVariant + radius: Style.radiusS + border.width: Math.max(1, Style.borderS) + border.color: Color.mOutline + + RowLayout { + id: forgetRow + + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginM + + RowLayout { + NIcon { + icon: "trash" + pointSize: Style.fontSizeL + color: Color.mError + } + + NText { + text: "Forget this network?" + pointSize: Style.fontSizeS + color: Color.mError + Layout.fillWidth: true + } + + } + + NButton { + id: forgetButton + + text: "Forget" + fontSize: Style.fontSizeXXS + backgroundColor: Color.mError + outlined: forgetButton.hovered ? false : true + onClicked: { + NetworkService.forget(modelData.ssid); + expandedSsid = ""; + } + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.8 + onClicked: expandedSsid = "" + } + + } + + } + + } + + // Smooth opacity animation + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + } + + } + + } + + } + + } + + } + + // Empty state when no networks + ColumnLayout { + visible: SettingsService.wifiEnabled && !NetworkService.scanning && Object.keys(NetworkService.networks).length === 0 + anchors.fill: parent + spacing: Style.marginL + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "search" + pointSize: 64 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "No networks found" + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NButton { + text: "Scan Again" + icon: "refresh" + Layout.alignment: Qt.AlignHCenter + onClicked: NetworkService.scan() + } + + Item { + Layout.fillHeight: true + } + + } + + } + + } + + } + +} diff --git a/config/quickshell/Noctalia/NButton.qml b/config/quickshell/Noctalia/NButton.qml index a237000..0d8e4ba 100644 --- a/config/quickshell/Noctalia/NButton.qml +++ b/config/quickshell/Noctalia/NButton.qml @@ -15,10 +15,10 @@ Rectangle { property color textColor: Color.mOnPrimary property color hoverColor: Color.mTertiary property bool enabled: true - property real fontSize: Style.fontSizeM * scaling + property real fontSize: Style.fontSizeM property int fontWeight: Style.fontWeightBold property string fontFamily: Fonts.primary - property real iconSize: Style.fontSizeL * scaling + property real iconSize: Style.fontSizeL property bool outlined: false // Internal properties property bool hovered: false @@ -30,10 +30,10 @@ Rectangle { signal middleClicked() // Dimensions - implicitWidth: contentRow.implicitWidth + (Style.marginL * 2 * scaling) - implicitHeight: Math.max(Style.baseWidgetSize * scaling, contentRow.implicitHeight + (Style.marginM * scaling)) + implicitWidth: contentRow.implicitWidth + (Style.marginL * 2) + implicitHeight: Math.max(Style.baseWidgetSize, contentRow.implicitHeight + (Style.marginM)) // Appearance - radius: Style.radiusS * scaling + radius: Style.radiusS color: { if (!enabled) return outlined ? Color.transparent : Qt.lighter(Color.mSurfaceVariant, 1.2); @@ -43,7 +43,7 @@ Rectangle { return outlined ? Color.transparent : backgroundColor; } - border.width: outlined ? Math.max(1, Style.borderS * scaling) : 0 + border.width: outlined ? Math.max(1, Style.borderS) : 0 border.color: { if (!enabled) return Color.mOutline; @@ -60,7 +60,7 @@ Rectangle { id: contentRow anchors.centerIn: parent - spacing: Style.marginXS * scaling + spacing: Style.marginXS // Icon (optional) NIcon { diff --git a/config/quickshell/Noctalia/NLabel.qml b/config/quickshell/Noctalia/NLabel.qml new file mode 100644 index 0000000..721761b --- /dev/null +++ b/config/quickshell/Noctalia/NLabel.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts +import qs.Constants + +ColumnLayout { + id: root + + property string label: "" + property string description: "" + property color labelColor: Color.mOnSurface + property color descriptionColor: Color.mOnSurfaceVariant + + spacing: Style.marginXXS + Layout.fillWidth: true + + NText { + text: label + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: labelColor + visible: label !== "" + Layout.fillWidth: true + } + + NText { + text: description + pointSize: Style.fontSizeS + color: descriptionColor + wrapMode: Text.WordWrap + visible: description !== "" + Layout.fillWidth: true + } + +} diff --git a/config/quickshell/Noctalia/NScrollView.qml b/config/quickshell/Noctalia/NScrollView.qml new file mode 100644 index 0000000..699371c --- /dev/null +++ b/config/quickshell/Noctalia/NScrollView.qml @@ -0,0 +1,152 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Templates as T +import qs.Constants + +T.ScrollView { + id: root + + property color handleColor: Qt.alpha(Color.mTertiary, 0.8) + property color handleHoverColor: handleColor + property color handlePressedColor: handleColor + property color trackColor: Color.transparent + property real handleWidth: 6 + property real handleRadius: Style.radiusM + property int verticalPolicy: ScrollBar.AsNeeded + property int horizontalPolicy: ScrollBar.AsNeeded + property bool preventHorizontalScroll: horizontalPolicy === ScrollBar.AlwaysOff + property int boundsBehavior: Flickable.StopAtBounds + property int flickableDirection: Flickable.VerticalFlick + + // Function to configure the underlying Flickable + function configureFlickable() { + // Find the internal Flickable (it's usually the first child) + for (var i = 0; i < children.length; i++) { + var child = children[i]; + if (child.toString().indexOf("Flickable") !== -1) { + // Configure the flickable to prevent horizontal scrolling + child.boundsBehavior = root.boundsBehavior; + if (root.preventHorizontalScroll) { + child.flickableDirection = Flickable.VerticalFlick; + child.contentWidth = Qt.binding(() => { + return child.width; + }); + } else { + child.flickableDirection = root.flickableDirection; + } + break; + } + } + } + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + // Configure the internal flickable when it becomes available + Component.onCompleted: { + configureFlickable(); + } + // Watch for changes in horizontalPolicy + onHorizontalPolicyChanged: { + preventHorizontalScroll = (horizontalPolicy === ScrollBar.AlwaysOff); + configureFlickable(); + } + + ScrollBar.vertical: ScrollBar { + parent: root + x: root.mirrored ? 0 : root.width - width + y: root.topPadding + height: root.availableHeight + active: root.ScrollBar.horizontal.active + policy: root.verticalPolicy + + contentItem: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + + } + + } + + background: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + + } + + } + + } + + ScrollBar.horizontal: ScrollBar { + parent: root + x: root.leftPadding + y: root.height - height + width: root.availableWidth + active: root.ScrollBar.vertical.active + policy: root.horizontalPolicy + + contentItem: Rectangle { + implicitWidth: 100 + implicitHeight: root.handleWidth + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + + } + + } + + background: Rectangle { + implicitWidth: 100 + implicitHeight: root.handleWidth + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn || parent.active ? 0.3 : 0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + + } + + } + + } + +} diff --git a/config/quickshell/Noctalia/NToggle.qml b/config/quickshell/Noctalia/NToggle.qml new file mode 100644 index 0000000..c1e1c53 --- /dev/null +++ b/config/quickshell/Noctalia/NToggle.qml @@ -0,0 +1,90 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Constants + +RowLayout { + id: root + + property string label: "" + property string description: "" + property bool checked: false + property bool hovering: false + property int baseSize: Math.round(Style.baseWidgetSize * 0.8) + + signal toggled(bool checked) + signal entered() + signal exited() + + Layout.fillWidth: true + + NLabel { + label: root.label + description: root.description + } + + Rectangle { + id: switcher + + implicitWidth: Math.round(root.baseSize * 0.85) * 2 + implicitHeight: Math.round(root.baseSize * 0.5) * 2 + radius: height * 0.5 + color: root.checked ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS) + + Rectangle { + implicitWidth: Math.round(root.baseSize * 0.4) * 2 + implicitHeight: Math.round(root.baseSize * 0.4) * 2 + radius: height * 0.5 + color: root.checked ? Color.mOnPrimary : Color.mPrimary + border.color: root.checked ? Color.mSurface : Color.mSurface + border.width: Math.max(1, Style.borderM) + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 0 + x: root.checked ? switcher.width - width - 3 : 3 + + Behavior on x { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + + } + + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: { + hovering = true; + root.entered(); + } + onExited: { + hovering = false; + root.exited(); + } + onClicked: { + root.toggled(!root.checked); + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + + } + + } + +} diff --git a/config/quickshell/Services/BluetoothService.qml b/config/quickshell/Services/BluetoothService.qml new file mode 100644 index 0000000..66b98ee --- /dev/null +++ b/config/quickshell/Services/BluetoothService.qml @@ -0,0 +1,232 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Bluetooth +import qs.Utils + +Singleton { + id: root + + readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter + readonly property bool available: (adapter !== null) + readonly property bool enabled: adapter?.enabled ?? false + readonly property bool discovering: (adapter && adapter.discovering) ?? false + readonly property var devices: adapter ? adapter.devices : null + readonly property var pairedDevices: { + if (!adapter || !adapter.devices) { + return [] + } + return adapter.devices.values.filter(dev => { + return dev && (dev.paired || dev.trusted) + }) + } + + readonly property var allDevicesWithBattery: { + if (!adapter || !adapter.devices) { + return [] + } + return adapter.devices.values.filter(dev => { + return dev && dev.batteryAvailable && dev.battery > 0 + }) + } + + function init() { + Logger.log("Bluetooth", "Service initialized") + } + + Timer { + id: discoveryTimer + interval: 1000 + repeat: false + onTriggered: adapter.discovering = true + } + + Connections { + target: adapter + function onEnabledChanged() { + if (!adapter) { + Logger.warn("Bluetooth", "onEnabledChanged", "No adapter available") + return + } + + Logger.debug("Bluetooth", "onEnableChanged", adapter.enabled) + if (adapter.enabled) { + discoveryTimer.running = true + } + } + } + + function sortDevices(devices) { + return devices.sort((a, b) => { + var aName = a.name || a.deviceName || "" + var bName = b.name || b.deviceName || "" + + var aHasRealName = aName.includes(" ") && aName.length > 3 + var bHasRealName = bName.includes(" ") && bName.length > 3 + + if (aHasRealName && !bHasRealName) + return -1 + if (!aHasRealName && bHasRealName) + return 1 + + var aSignal = (a.signalStrength !== undefined && a.signalStrength > 0) ? a.signalStrength : 0 + var bSignal = (b.signalStrength !== undefined && b.signalStrength > 0) ? b.signalStrength : 0 + return bSignal - aSignal + }) + } + + function getDeviceIcon(device) { + if (!device) { + return "bt-device-generic" + } + + var name = (device.name || device.deviceName || "").toLowerCase() + var icon = (device.icon || "").toLowerCase() + if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") || name.includes("headset") || name.includes("arctis")) { + return "bt-device-headphones" + } + + if (icon.includes("mouse") || name.includes("mouse")) { + return "bt-device-mouse" + } + if (icon.includes("keyboard") || name.includes("keyboard")) { + return "bt-device-keyboard" + } + if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") || name.includes("samsung")) { + return "bt-device-phone" + } + if (icon.includes("watch") || name.includes("watch")) { + return "bt-device-watch" + } + if (icon.includes("speaker") || name.includes("speaker")) { + return "bt-device-speaker" + } + if (icon.includes("display") || name.includes("tv")) { + return "bt-device-tv" + } + return "bt-device-generic" + } + + function canConnect(device) { + if (!device) + return false + + + /* + Paired + Means you’ve successfully exchanged keys with the device. + The devices remember each other and can authenticate without repeating the pairing process. + Example: once your headphones are paired, you don’t need to type a PIN every time. + Hence, instead of !device.paired, should be device.connected + */ + return !device.connected && !device.pairing && !device.blocked + } + + function canDisconnect(device) { + if (!device) + return false + return device.connected && !device.pairing && !device.blocked + } + + function getStatusString(device) { + if (device.state === BluetoothDeviceState.Connecting) { + return "Connecting..." + } + if (device.pairing) { + return "Pairing..." + } + if (device.blocked) { + return "Blocked" + } + return "" + } + + function getSignalStrength(device) { + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) { + return "Signal: Unknown" + } + var signal = device.signalStrength + if (signal >= 80) { + return "Signal: Excellent" + } + if (signal >= 60) { + return "Signal: Good" + } + if (signal >= 40) { + return "Signal: Fair" + } + if (signal >= 20) { + return "Signal: Poor" + } + return "Signal: Very poor" + } + + function getBattery(device) { + return `Battery: ${Math.round(device.battery * 100)}%` + } + + function getSignalIcon(device) { + if (!device || device.signalStrength === undefined || device.signalStrength <= 0) { + return "antenna-bars-off" + } + var signal = device.signalStrength + if (signal >= 80) { + return "antenna-bars-5" + } + if (signal >= 60) { + return "antenna-bars-4" + } + if (signal >= 40) { + return "antenna-bars-3" + } + if (signal >= 20) { + return "antenna-bars-2" + } + return "antenna-bars-1" + } + + function isDeviceBusy(device) { + if (!device) { + return false + } + + return device.pairing || device.state === BluetoothDeviceState.Disconnecting || device.state === BluetoothDeviceState.Connecting + } + + function connectDeviceWithTrust(device) { + if (!device) { + return + } + + device.trusted = true + device.connect() + } + + function disconnectDevice(device) { + if (!device) { + return + } + + device.disconnect() + } + + function forgetDevice(device) { + if (!device) { + return + } + + device.trusted = false + device.forget() + } + + function setBluetoothEnabled(state) { + if (!adapter) { + Logger.w("Bluetooth", "No adapter available") + return + } + + Logger.i("Bluetooth", "SetBluetoothEnabled", state) + adapter.enabled = state + } +} diff --git a/config/quickshell/Services/CacheService.qml b/config/quickshell/Services/CacheService.qml index eed3d72..5a3d0f0 100644 --- a/config/quickshell/Services/CacheService.qml +++ b/config/quickshell/Services/CacheService.qml @@ -7,14 +7,16 @@ pragma Singleton Singleton { id: root - property string cacheDir: Quickshell.env("HOME") + "/.cache/quickshell/" - property string recordingDir: Quickshell.env("HOME") + "/Videos/recordings/" - property var cacheFiles: ["Location.json", "Ip.json", "Notifications.json", "LyricsOffset.txt"] + readonly property string cacheDir: Quickshell.env("HOME") + "/.cache/quickshell/" + // also create recording directory here + readonly property string recordingDir: Quickshell.env("HOME") + "/Videos/recordings/" + readonly property var cacheFiles: Object.freeze(["Location.json", "Ip.json", "Notifications.json", "LyricsOffset.txt", "Network.json"]) property bool loaded: false - property string locationCacheFile: cacheDir + "Location.json" - property string ipCacheFile: cacheDir + "Ip.json" - property string notificationsCacheFile: cacheDir + "Notifications.json" - property string lyricsOffsetCacheFile: cacheDir + "LyricsOffset.txt" + readonly property string locationCacheFile: cacheDir + "Location.json" + readonly property string ipCacheFile: cacheDir + "Ip.json" + readonly property string notificationsCacheFile: cacheDir + "Notifications.json" + readonly property string lyricsOffsetCacheFile: cacheDir + "LyricsOffset.txt" + readonly property string networkCacheFile: cacheDir + "Network.json" Process { id: process @@ -23,7 +25,7 @@ Singleton { command: ["sh", "-c", `mkdir -p ${cacheDir} && mkdir -p ${recordingDir} && touch ${cacheDir + cacheFiles.join(` && touch ${cacheDir}`)}`] onExited: (code, status) => { if (code === 0) - root.loaded = true; + loaded = true; else Logger.error("CacheService", `Failed to create cache files: ${command.join(" ")}`); } diff --git a/config/quickshell/Services/IpService.qml b/config/quickshell/Services/IpService.qml index 2707241..7cfa8a4 100644 --- a/config/quickshell/Services/IpService.qml +++ b/config/quickshell/Services/IpService.qml @@ -7,12 +7,12 @@ pragma Singleton Singleton { property alias ip: cacheFileAdapter.ip - property string cacheFilePath: CacheService.ipCacheFile + readonly property string cacheFilePath: CacheService.ipCacheFile property string countryCode: "N/A" property real fetchInterval: 120 // in s property real fetchTimeout: 10 // in s - property string ipURL: "https://api.uyanide.com/ip" - property string geoURL: "https://api.ipinfo.io/lite/" + readonly property string ipURL: "https://api.uyanide.com/ip" + readonly property string geoURL: "https://api.ipinfo.io/lite/" property string geoURLToken: "" function fetchIP() { @@ -48,8 +48,8 @@ Singleton { let url = geoURL + ip; if (geoURLToken) url += "?token=" + geoURLToken; - - cacheFileAdapter.geoInfo = null + + cacheFileAdapter.geoInfo = null; curl.fetch(url, function(success, data) { if (success) { try { diff --git a/config/quickshell/Services/LyricsService.qml b/config/quickshell/Services/LyricsService.qml index 104b1b7..91f2b3e 100644 --- a/config/quickshell/Services/LyricsService.qml +++ b/config/quickshell/Services/LyricsService.qml @@ -8,10 +8,10 @@ pragma Singleton Singleton { property int linesCount: 3 property int linesAhead: linesCount / 2 - property int currentIndex: linesCount - linesAhead - 1 - property string offsetFile: CacheService.lyricsOffsetCacheFile + readonly property int currentIndex: linesCount - linesAhead - 1 + readonly property string offsetFile: CacheService.lyricsOffsetCacheFile property int offset: 0 // in ms - property int offsetStep: 500 // in ms + readonly property int offsetStep: 500 // in ms property int referenceCount: 0 // with linesCount=3 and linesAhead=1, lyrics will be like: // line 1 diff --git a/config/quickshell/Services/NetworkService.qml b/config/quickshell/Services/NetworkService.qml new file mode 100644 index 0000000..47f38be --- /dev/null +++ b/config/quickshell/Services/NetworkService.qml @@ -0,0 +1,624 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Utils +import qs.Services + +Singleton { + id: root + + // Core state + property var networks: ({}) + property bool scanning: false + property bool connecting: false + property string connectingTo: "" + property string lastError: "" + property bool ethernetConnected: false + property string disconnectingFrom: "" + property string forgettingNetwork: "" + + property bool ignoreScanResults: false + property bool scanPending: false + + // Persistent cache + property string cacheFile: CacheService.networkCacheFile + readonly property string cachedLastConnected: cacheAdapter.lastConnected + readonly property var cachedNetworks: cacheAdapter.knownNetworks + + // Cache file handling + FileView { + id: cacheFileView + path: root.cacheFile + + JsonAdapter { + id: cacheAdapter + property var knownNetworks: ({}) + property string lastConnected: "" + } + + onLoadFailed: { + cacheAdapter.knownNetworks = ({}) + cacheAdapter.lastConnected = "" + } + } + + Component.onCompleted: { + Logger.log("Network", "Service initialized") + syncWifiState() + scan() + } + + // Save cache with debounce + Timer { + id: saveDebounce + interval: 1000 + onTriggered: cacheFileView.writeAdapter() + } + + function saveCache() { + saveDebounce.restart() + } + + // Delayed scan timer + Timer { + id: delayedScanTimer + interval: 7000 + onTriggered: scan() + } + + // Ethernet check timer + // Always running every 30s + Timer { + id: ethernetCheckTimer + interval: 30000 + running: true + repeat: true + onTriggered: ethernetStateProcess.running = true + } + + // Core functions + function syncWifiState() { + wifiStateProcess.running = true + } + + function setWifiEnabled(enabled) { + SettingsService.wifiEnabled = enabled + wifiStateEnableProcess.running = true + } + + function scan() { + if (!SettingsService.wifiEnabled) + return + + if (scanning) { + // Mark current scan results to be ignored and schedule a new scan + Logger.log("Network", "Scan already in progress, will ignore results and rescan") + ignoreScanResults = true + scanPending = true + return + } + + scanning = true + lastError = "" + ignoreScanResults = false + + // Get existing profiles first, then scan + profileCheckProcess.running = true + Logger.log("Network", "Wi-Fi scan in progress...") + } + + function connect(ssid, password = "") { + if (connecting) + return + + connecting = true + connectingTo = ssid + lastError = "" + + // Check if we have a saved connection + if (networks[ssid]?.existing || cachedNetworks[ssid]) { + connectProcess.mode = "saved" + connectProcess.ssid = ssid + connectProcess.password = "" + } else { + connectProcess.mode = "new" + connectProcess.ssid = ssid + connectProcess.password = password + } + + connectProcess.running = true + } + + function disconnect(ssid) { + disconnectingFrom = ssid + disconnectProcess.ssid = ssid + disconnectProcess.running = true + } + + function forget(ssid) { + forgettingNetwork = ssid + + // Remove from cache + let known = cacheAdapter.knownNetworks + delete known[ssid] + cacheAdapter.knownNetworks = known + + if (cacheAdapter.lastConnected === ssid) { + cacheAdapter.lastConnected = "" + } + + saveCache() + + // Remove from system + forgetProcess.ssid = ssid + forgetProcess.running = true + } + + // Helper function to immediately update network status + function updateNetworkStatus(ssid, connected) { + let nets = networks + + // Update all networks connected status + for (let key in nets) { + if (nets[key].connected && key !== ssid) { + nets[key].connected = false + } + } + + // Update the target network if it exists + if (nets[ssid]) { + nets[ssid].connected = connected + nets[ssid].existing = true + nets[ssid].cached = true + } else if (connected) { + // Create a temporary entry if network doesn't exist yet + nets[ssid] = { + "ssid": ssid, + "security": "--", + "signal": 100, + "connected": true, + "existing": true, + "cached": true + } + } + + // Trigger property change notification + networks = ({}) + networks = nets + } + + // Helper functions + function signalIcon(signal) { + if (signal >= 80) + return "wifi" + if (signal >= 50) + return "wifi-2" + if (signal >= 20) + return "wifi-1" + return "wifi-0" + } + + function isSecured(security) { + return security && security !== "--" && security.trim() !== "" + } + + // Processes + Process { + id: ethernetStateProcess + running: true + command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] + + stdout: StdioCollector { + onStreamFinished: { + const connected = text.split("\n").some(line => { + const parts = line.split(":") + return parts[1] === "ethernet" && parts[2] === "connected" + }) + if (root.ethernetConnected !== connected) { + root.ethernetConnected = connected + Logger.log("Network", "Ethernet connected:", root.ethernetConnected) + } + } + } + } + + // Only check the state of the actual interface + // and update our setting to be in sync. + Process { + id: wifiStateProcess + running: false + command: ["nmcli", "radio", "wifi"] + + stdout: StdioCollector { + onStreamFinished: { + const enabled = text.trim() === "enabled" + Logger.log("Network", "Wi-Fi adapter was detect as enabled:", enabled) + if (SettingsService.wifiEnabled !== enabled) { + SettingsService.wifiEnabled = enabled + } + } + } + } + + // Process to enable/disable the Wi-Fi interface + Process { + id: wifiStateEnableProcess + running: false + command: ["nmcli", "radio", "wifi", SettingsService.wifiEnabled ? "on" : "off"] + + stdout: StdioCollector { + onStreamFinished: { + Logger.log("Network", "Wi-Fi state change command executed.") + // Re-check the state to ensure it's in sync + syncWifiState() + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.warn("Network", "Error changing Wi-Fi state: " + text) + } + } + } + } + + // Helper process to get existing profiles + Process { + id: profileCheckProcess + running: false + command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] + + stdout: StdioCollector { + onStreamFinished: { + if (root.ignoreScanResults) { + Logger.log("Network", "Ignoring profile check results (new scan requested)") + root.scanning = false + + // Check if we need to start a new scan + if (root.scanPending) { + root.scanPending = false + delayedScanTimer.interval = 100 + delayedScanTimer.restart() + } + return + } + + const profiles = {} + const lines = text.split("\n").filter(l => l.trim()) + for (const line of lines) { + profiles[line.trim()] = true + } + scanProcess.existingProfiles = profiles + scanProcess.running = true + } + } + } + + Process { + id: scanProcess + running: false + command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list", "--rescan", "yes"] + + property var existingProfiles: ({}) + + stdout: StdioCollector { + onStreamFinished: { + if (root.ignoreScanResults) { + Logger.log("Network", "Ignoring scan results (new scan requested)") + root.scanning = false + + // Check if we need to start a new scan + if (root.scanPending) { + root.scanPending = false + delayedScanTimer.interval = 100 + delayedScanTimer.restart() + } + return + } + + // Process the scan results as before... + const lines = text.split("\n") + const networksMap = {} + + for (var i = 0; i < lines.length; ++i) { + const line = lines[i].trim() + if (!line) + continue + + // Parse from the end to handle SSIDs with colons + // Format is SSID:SECURITY:SIGNAL:IN-USE + // We know the last 3 fields, so everything else is SSID + const lastColonIdx = line.lastIndexOf(":") + if (lastColonIdx === -1) { + Logger.warn("Network", "Malformed nmcli output line:", line) + continue + } + + const inUse = line.substring(lastColonIdx + 1) + const remainingLine = line.substring(0, lastColonIdx) + + const secondLastColonIdx = remainingLine.lastIndexOf(":") + if (secondLastColonIdx === -1) { + Logger.warn("Network", "Malformed nmcli output line:", line) + continue + } + + const signal = remainingLine.substring(secondLastColonIdx + 1) + const remainingLine2 = remainingLine.substring(0, secondLastColonIdx) + + const thirdLastColonIdx = remainingLine2.lastIndexOf(":") + if (thirdLastColonIdx === -1) { + Logger.warn("Network", "Malformed nmcli output line:", line) + continue + } + + const security = remainingLine2.substring(thirdLastColonIdx + 1) + const ssid = remainingLine2.substring(0, thirdLastColonIdx) + + if (ssid) { + const signalInt = parseInt(signal) || 0 + const connected = inUse === "*" + + // Track connected network in cache + if (connected && cacheAdapter.lastConnected !== ssid) { + cacheAdapter.lastConnected = ssid + saveCache() + } + + if (!networksMap[ssid]) { + networksMap[ssid] = { + "ssid": ssid, + "security": security || "--", + "signal": signalInt, + "connected": connected, + "existing": ssid in scanProcess.existingProfiles, + "cached": ssid in cacheAdapter.knownNetworks + } + } else { + // Keep the best signal for duplicate SSIDs + const existingNet = networksMap[ssid] + if (connected) { + existingNet.connected = true + } + if (signalInt > existingNet.signal) { + existingNet.signal = signalInt + existingNet.security = security || "--" + } + } + } + } + + // Logging + const oldSSIDs = Object.keys(root.networks) + const newSSIDs = Object.keys(networksMap) + const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid)) + const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid)) + + if (newNetworks.length > 0 || lostNetworks.length > 0) { + if (newNetworks.length > 0) { + Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", ")) + } + if (lostNetworks.length > 0) { + Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", ")) + } + Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(networksMap).length) + } + + Logger.log("Network", "Wi-Fi scan completed") + root.networks = networksMap + root.scanning = false + + // Check if we need to start a new scan + if (root.scanPending) { + root.scanPending = false + delayedScanTimer.interval = 100 + delayedScanTimer.restart() + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.scanning = false + if (text.trim()) { + Logger.warn("Network", "Scan error: " + text) + + // If scan fails, retry + delayedScanTimer.interval = 5000 + delayedScanTimer.restart() + } + } + } + } + Process { + id: connectProcess + property string mode: "new" + property string ssid: "" + property string password: "" + running: false + + command: { + if (mode === "saved") { + return ["nmcli", "connection", "up", "id", ssid] + } else { + const cmd = ["nmcli", "device", "wifi", "connect", ssid] + if (password) { + cmd.push("password", password) + } + return cmd + } + } + + stdout: StdioCollector { + onStreamFinished: { + // Check if the output actually indicates success + // nmcli outputs "Device '...' successfully activated" or "Connection successfully activated" + // on success. Empty output or other messages indicate failure. + const output = text.trim() + + if (!output || (!output.includes("successfully activated") && !output.includes("Connection successfully"))) { + // No success message - likely an error occurred + // Don't update anything, let stderr handler deal with it + return + } + + // Success - update cache + let known = cacheAdapter.knownNetworks + known[connectProcess.ssid] = { + "profileName": connectProcess.ssid, + "lastConnected": Date.now() + } + cacheAdapter.knownNetworks = known + cacheAdapter.lastConnected = connectProcess.ssid + saveCache() + + // Immediately update the UI before scanning + root.updateNetworkStatus(connectProcess.ssid, true) + + root.connecting = false + root.connectingTo = "" + Logger.log("Network", `Connected to network: '${connectProcess.ssid}'`) + ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("toast.wifi.connected", { + "ssid": connectProcess.ssid + })) + + // Still do a scan to get accurate signal and security info + delayedScanTimer.interval = 5000 + delayedScanTimer.restart() + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.connecting = false + root.connectingTo = "" + + if (text.trim()) { + // Parse common errors + if (text.includes("Secrets were required") || text.includes("no secrets provided")) { + root.lastError = "Incorrect password" + forget(connectProcess.ssid) + } else if (text.includes("No network with SSID")) { + root.lastError = "Network not found" + } else if (text.includes("Timeout")) { + root.lastError = "Connection timeout" + } else { + root.lastError = text.split("\n")[0].trim() + } + + Logger.warn("Network", "Connect error: " + text) + } + } + } + } + + Process { + id: disconnectProcess + property string ssid: "" + running: false + command: ["nmcli", "connection", "down", "id", ssid] + + stdout: StdioCollector { + onStreamFinished: { + Logger.log("Network", `Disconnected from network: '${disconnectProcess.ssid}'`) + ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("toast.wifi.disconnected", { + "ssid": disconnectProcess.ssid + })) + + // Immediately update UI on successful disconnect + root.updateNetworkStatus(disconnectProcess.ssid, false) + root.disconnectingFrom = "" + + // Do a scan to refresh the list + delayedScanTimer.interval = 1000 + delayedScanTimer.restart() + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.disconnectingFrom = "" + if (text.trim()) { + Logger.warn("Network", "Disconnect error: " + text) + } + // Still trigger a scan even on error + delayedScanTimer.interval = 5000 + delayedScanTimer.restart() + } + } + } + + Process { + id: forgetProcess + property string ssid: "" + running: false + + // Try multiple common profile name patterns + command: ["sh", "-c", ` + ssid="$1" + deleted=false + + # Try exact SSID match first + if nmcli connection delete id "$ssid" 2>/dev/null; then + echo "Deleted profile: $ssid" + deleted=true + fi + + # Try "Auto " pattern + if nmcli connection delete id "Auto $ssid" 2>/dev/null; then + echo "Deleted profile: Auto $ssid" + deleted=true + fi + + # Try " 1", " 2", etc. patterns + for i in 1 2 3; do + if nmcli connection delete id "$ssid $i" 2>/dev/null; then + echo "Deleted profile: $ssid $i" + deleted=true + fi + done + + if [ "$deleted" = "false" ]; then + echo "No profiles found for SSID: $ssid" + fi + `, "--", ssid] + + stdout: StdioCollector { + onStreamFinished: { + Logger.log("Network", `Forget network: "${forgetProcess.ssid}"`) + Logger.log("Network", text.trim().replace(/[\r\n]/g, " ")) + + // Update both cached and existing status immediately + let nets = root.networks + if (nets[forgetProcess.ssid]) { + nets[forgetProcess.ssid].cached = false + nets[forgetProcess.ssid].existing = false + // Trigger property change + root.networks = ({}) + root.networks = nets + } + + root.forgettingNetwork = "" + + // Scan to verify the profile is gone + delayedScanTimer.interval = 5000 + delayedScanTimer.restart() + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.forgettingNetwork = "" + if (text.trim() && !text.includes("No profiles found")) { + Logger.warn("Network", "Forget error: " + text) + } + // Still Trigger a scan even on error + delayedScanTimer.interval = 5000 + delayedScanTimer.restart() + } + } + } +} diff --git a/config/quickshell/Services/RecordService.qml b/config/quickshell/Services/RecordService.qml index cb769b5..8179015 100644 --- a/config/quickshell/Services/RecordService.qml +++ b/config/quickshell/Services/RecordService.qml @@ -6,16 +6,16 @@ import qs.Utils pragma Singleton Singleton { - property string recordingDir: CacheService.recordingDir + readonly property string recordingDir: CacheService.recordingDir property bool isRecording: false property bool isStopping: false - property string codec: "av1_nvenc" - property string container: "mkv" - property string pixelFormat: "p010le" + readonly property string codec: "av1_nvenc" + readonly property string container: "mkv" + readonly property string pixelFormat: "p010le" property string recordingDisplay: "" - property int framerate: 60 - property var codecParams: ["preset=p5", "rc=vbr", "cq=18", "b:v=80M", "maxrate=120M", "bufsize=160M", "color_range=tv"] - property var filterArgs: [] + readonly property int framerate: 60 + readonly property var codecParams: Object.freeze(["preset=p5", "rc=vbr", "cq=18", "b:v=80M", "maxrate=120M", "bufsize=160M", "color_range=tv"]) + readonly property var filterArgs: Object.freeze([]) function getFilename() { var d = new Date(); diff --git a/config/quickshell/Services/SettingsService.qml b/config/quickshell/Services/SettingsService.qml index e02ab78..411333f 100644 --- a/config/quickshell/Services/SettingsService.qml +++ b/config/quickshell/Services/SettingsService.qml @@ -10,6 +10,7 @@ Singleton { property alias showLyricsBar: adapter.showLyricsBar property alias notifications: adapter.notifications property alias location: adapter.location + property alias wifiEnabled: adapter.wifiEnabled property string settingsFilePath: Qt.resolvedUrl("../Assets/Config/Settings.json") FileView { @@ -27,6 +28,7 @@ Singleton { property bool showLyricsBar: false property JsonObject notifications property string location: "New York" + property bool wifiEnabled: true notifications: JsonObject { property bool doNotDisturb: false diff --git a/config/quickshell/Services/WriteClipboard.qml b/config/quickshell/Services/WriteClipboard.qml index 55c7e58..bc67b85 100644 --- a/config/quickshell/Services/WriteClipboard.qml +++ b/config/quickshell/Services/WriteClipboard.qml @@ -1,20 +1,12 @@ import QtQuick import Quickshell -import Quickshell.Io pragma Singleton Singleton { id: root function write(text) { - action.command = ["sh", "-c", `echo ${text} | wl-copy -n`]; - action.startDetached(); - } - - Process { - id: action - - running: false + Quickshell.execDetached(["sh", "-c", `echo ${text} | wl-copy -n`]); } } diff --git a/config/quickshell/shell.qml b/config/quickshell/shell.qml index 1490e65..eb12dbb 100644 --- a/config/quickshell/shell.qml +++ b/config/quickshell/shell.qml @@ -50,6 +50,18 @@ ShellRoot { objectName: "notificationHistoryPanel" } + WiFiPanel { + id: wifiPanel + + objectName: "wifiPanel" + } + + BluetoothPanel { + id: bluetoothPanel + + objectName: "bluetoothPanel" + } + } }