Merge branch 'noctalia-dev:main' into main

This commit is contained in:
Michael Thomas 2025-08-28 16:45:50 -04:00 committed by GitHub
commit 85bd0ed2f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 929 additions and 650 deletions

View file

@ -34,7 +34,7 @@ NPanel {
spacing: Style.marginM * scaling
NIcon {
text: "system_update"
text: "system_update_alt"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
@ -77,11 +77,9 @@ NPanel {
}
// Unified list
Rectangle {
NBox {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusM * scaling
// Combine repo and AUR lists in order: repos first, then AUR
property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || [])
@ -89,33 +87,35 @@ NPanel {
ListView {
id: unifiedList
anchors.fill: parent
anchors.margins: Style.marginS * scaling
anchors.margins: Style.marginM * scaling
cacheBuffer: Math.round(300 * scaling)
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
model: parent.items
spacing: Style.marginXS * scaling
cacheBuffer: 300 * scaling
delegate: Rectangle {
width: unifiedList.width
height: 56 * scaling
height: 44 * scaling
color: Color.transparent
radius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
// Checkbox for selection (pure bindings; no imperative state)
NIconButton {
// Checkbox for selection
NCheckbox {
id: checkbox
icon: ArchUpdaterService.isPackageSelected(modelData.name) ? "check_box" : "check_box_outline_blank"
onClicked: ArchUpdaterService.togglePackageSelection(modelData.name)
colorBg: Color.transparent
colorFg: ArchUpdaterService.isPackageSelected(
modelData.name) ? ((modelData.source === "aur") ? Color.mSecondary : Color.mPrimary) : Color.mOnSurfaceVariant
Layout.preferredWidth: 30 * scaling
Layout.preferredHeight: 30 * scaling
label: ""
description: ""
checked: ArchUpdaterService.isPackageSelected(modelData.name)
baseSize: Math.max(Style.baseWidgetSize * 0.7, 14)
onToggled: function (checked) {
ArchUpdaterService.togglePackageSelection(modelData.name)
// Force refresh of the checked property
checkbox.checked = ArchUpdaterService.isPackageSelected(modelData.name)
}
}
// Package info
@ -123,51 +123,43 @@ NPanel {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
RowLayout {
NText {
text: modelData.name
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
spacing: Style.marginXS * scaling
NText {
text: modelData.name
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
Layout.fillWidth: true
}
// Source badge (custom rectangle)
Rectangle {
visible: !!modelData.source
radius: 9999
color: modelData.source === "aur" ? Color.mSecondary : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
implicitHeight: Math.max(Style.fontSizeXS * 1.7 * scaling, 16 * scaling)
// Width based on label content + horizontal padding
implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling)
NText {
id: badgeText
anchors.centerIn: parent
text: modelData.source === "aur" ? "AUR" : "Repo"
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: modelData.source === "aur" ? Color.mOnSecondary : Color.mOnPrimary
}
}
Layout.alignment: Qt.AlignVCenter
}
NText {
text: modelData.oldVersion + " → " + modelData.newVersion
font.pointSize: Style.fontSizeS * scaling
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
// Source tag (AUR vs PAC)
Rectangle {
visible: !!modelData.source
radius: width * 0.5
color: modelData.source === "aur" ? Color.mTertiary : Color.mSecondary
Layout.alignment: Qt.AlignVCenter
implicitHeight: Style.fontSizeS * 1.8 * scaling
// Width based on label content + horizontal padding
implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling)
NText {
id: badgeText
anchors.centerIn: parent
text: modelData.source === "aur" ? "AUR" : "PAC"
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: modelData.source === "aur" ? Color.mOnTertiary : Color.mOnSecondary
}
}
}
}
}
}
@ -190,7 +182,7 @@ NPanel {
}
NIconButton {
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update"
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update_alt"
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages"
enabled: !ArchUpdaterService.updateInProgress
onClicked: {
@ -203,7 +195,7 @@ NPanel {
}
NIconButton {
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings"
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "check_box"
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
onClicked: {
@ -213,9 +205,9 @@ NPanel {
}
}
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
> 0 ? Color.mSecondary : Color.mSurfaceVariant)
> 0 ? Color.mPrimary : Color.mSurfaceVariant)
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
> 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant)
> 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant)
Layout.fillWidth: true
}
}

View file

@ -14,72 +14,51 @@ NIconButton {
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
// Highlight color based on update source
colorFg: {
if (ArchUpdaterService.totalUpdates === 0)
return Color.mOnSurface
if (ArchUpdaterService.updates > 0 && ArchUpdaterService.aurUpdates > 0)
return Color.mPrimary
if (ArchUpdaterService.updates > 0)
return Color.mPrimary
return Color.mSecondary
}
colorBorder: Color.transparent
colorBorderHover: Color.transparent
colorFg: (ArchUpdaterService.totalUpdates === 0) ? Color.mOnSurface : Color.mPrimary
// Icon states
icon: {
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy)
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
return "sync"
}
if (ArchUpdaterService.totalUpdates > 0) {
const count = ArchUpdaterService.totalUpdates
if (count > 50)
return "system_update_alt"
if (count > 10)
return "system_update"
return "system_update"
return "system_update_alt"
}
return "task_alt"
}
// Tooltip with repo vs AUR breakdown and sample lists
tooltipText: {
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy)
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
return "Checking for updates…"
const repoCount = ArchUpdaterService.updates
const aurCount = ArchUpdaterService.aurUpdates
const total = ArchUpdaterService.totalUpdates
if (total === 0)
return "System is up to date ✓"
let header = total === 1 ? "One package can be upgraded:" : (total + " packages can be upgraded:")
function sampleList(arr, n, colorLabel) {
const limit = Math.min(arr.length, n)
let s = ""
for (var i = 0; i < limit; ++i) {
const p = arr[i]
s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion)
}
if (arr.length > limit)
s += "\n… and " + (arr.length - limit) + " more"
return (colorLabel ? (colorLabel + "\n") : "") + (s || "None")
}
const repoHeader = repoCount > 0 ? ("Repo (" + repoCount + "):") : "Repo: 0"
const aurHeader = aurCount > 0 ? ("AUR (" + aurCount + "):") : "AUR: 0"
const total = ArchUpdaterService.totalUpdates
if (total === 0) {
return "System is up to date ✓"
}
let header = (total === 1) ? "1 package can be updated" : (total + " packages can be updated")
const repoBlock = repoCount > 0 ? (repoHeader + "\n\n" + sampleList(ArchUpdaterService.repoPackages,
5)) : repoHeader
const aurBlock = aurCount > 0 ? (aurHeader + "\n\n" + sampleList(ArchUpdaterService.aurPackages, 5)) : aurHeader
const pacCount = ArchUpdaterService.updates
const aurCount = ArchUpdaterService.aurUpdates
const pacmanTooltip = (pacCount > 0) ? ((pacCount === 1) ? "1 system package" : pacCount + " system packages") : ""
const aurTooltip = (aurCount > 0) ? ((aurCount === 1) ? "1 AUR package" : aurCount + " AUR packages") : ""
return header + "\n\n" + repoBlock + "\n\n" + aurBlock + "\n\nClick to update system"
let tooltip = header
if (pacmanTooltip !== "") {
tooltip += "\n" + pacmanTooltip
}
if (aurTooltip !== "") {
tooltip += "\n" + aurTooltip
}
return tooltip
}
onClicked: {
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
ToastService.showNotice("ArchUpdater", "Still fetching updates...")
return
}

View file

@ -20,16 +20,7 @@ NIconButton {
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: {
// Show different icons based on connection status
if (BluetoothService.pairedDevices.length > 0) {
return "bluetooth_connected"
} else if (BluetoothService.discovering) {
return "bluetooth_searching"
} else {
return "bluetooth"
}
}
icon: "bluetooth"
tooltipText: "Bluetooth Devices"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this)
}

View file

@ -17,21 +17,19 @@ Item {
NPill {
id: pill
icon: NightLightService.isActive ? "bedtime" : "bedtime_off"
iconCircleColor: NightLightService.isActive ? Color.mSecondary : Color.mOnSurfaceVariant
collapsedIconColor: NightLightService.isActive ? Color.mOnSecondary : Color.mOnSurface
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off"
iconCircleColor: Settings.data.nightLight.enabled ? Color.mSecondary : Color.mOnSurfaceVariant
collapsedIconColor: Settings.data.nightLight.enabled ? Color.mOnSecondary : Color.mOnSurface
autoHide: false
text: NightLightService.isActive ? "On" : "Off"
text: Settings.data.nightLight.enabled ? "On" : "Off"
tooltipText: {
if (!Settings.isLoaded || !Settings.data.nightLight.enabled) {
return "Night Light: Disabled\nLeft click to open settings.\nRight click to enable."
}
var status = NightLightService.isActive ? "Active" : "Inactive (outside schedule)"
var intensity = Math.round(Settings.data.nightLight.intensity * 100)
var schedule = Settings.data.nightLight.autoSchedule ? `Schedule: ${Settings.data.nightLight.startTime} - ${Settings.data.nightLight.stopTime}` : "Manual mode"
return `Intensity: ${intensity}%\n${schedule}\nLeft click to open settings.\nRight click to toggle.`
var schedule = Settings.data.nightLight.autoSchedule ? `Auto schedule` : `Manual: ${Settings.data.nightLight.startTime} - ${Settings.data.nightLight.stopTime}`
return `Night Light: Enabled\nIntensity: ${intensity}%\n${schedule}\nLeft click to open settings.\nRight click to toggle.`
}
onClicked: {
@ -42,14 +40,11 @@ Item {
}
onRightClicked: {
// Right click - toggle night light
// Right click - toggle night light (debounced apply handled by service)
Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
NightLightService.apply()
}
onWheel: delta => {
var diff = delta > 0 ? 0.05 : -0.05
Settings.data.nightLight.intensity = Math.max(0, Math.min(1.0,
Settings.data.nightLight.intensity + diff))
}
// Wheel handler removed to avoid frequent rapid restarts/flicker
}
}

View file

@ -14,6 +14,7 @@ Item {
// Used to avoid opening the pill on Quickshell startup
property bool firstVolumeReceived: false
property int wheelAccumulator: 0
implicitWidth: pill.width
implicitHeight: pill.height
@ -59,10 +60,13 @@ Item {
tooltipText: "Volume: " + Math.round(
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
onWheel: function (angle) {
if (angle > 0) {
onWheel: function (delta) {
wheelAccumulator += delta
if (wheelAccumulator >= 120) {
wheelAccumulator = 0
AudioService.increaseVolume()
} else if (angle < 0) {
} else if (wheelAccumulator <= -120) {
wheelAccumulator = 0
AudioService.decreaseVolume()
}
}

View file

@ -0,0 +1,262 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
property string label: ""
property var model: {
}
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: root.label
font.pointSize: Style.fontSizeL * scaling
color: Color.mSecondary
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
visible: root.model.length > 0
}
Repeater {
Layout.fillWidth: true
model: root.model
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
Layout.fillWidth: true
Layout.preferredHeight: 64 * scaling + (10 * scaling * modelData.batteryAvailable)
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Color.mTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
}
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
// Device name
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fontSizeM * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
}
// Signal Strength
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXS * scaling
// Device signal strength - "Unknown" when not connected
NText {
text: BluetoothService.getSignalStrength(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
}
NIcon {
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
}
}
NText {
visible: modelData.batteryAvailable
text: BluetoothService.getBattery(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
}
}
// Spacer to push connect button to the right
Item {
Layout.fillWidth: true
}
// Call to action
Rectangle {
Layout.preferredWidth: 80 * scaling
Layout.preferredHeight: 28 * scaling
radius: Style.radiusM * scaling
visible: (modelData.state !== BluetoothDeviceState.Connecting)
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy ? 1 : 0.5
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing) {
return "Pairing..."
}
if (modelData.blocked) {
return "Blocked"
}
if (modelData.paired || modelData.trusted) {
return "Disconnect"
}
return "Connect"
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (!modelData || modelData.pairing) {
return
}
if (modelData.paired || modelData.trusted) {
BluetoothService.disconnectDevice(modelData)
} else {
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
}
}

View file

@ -68,259 +68,56 @@ NPanel {
}
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
// Available devices
Column {
id: column
ColumnLayout {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
width: parent.width
spacing: Style.marginM * scaling
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
RowLayout {
width: parent.width
spacing: Style.marginM * scaling
NText {
text: "Available Devices"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
}
}
Repeater {
// Connected devices
BluetoothDevicesList {
label: "Connected devices"
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing && !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
return dev && !dev.blocked && (dev.paired || dev.trusted)
})
return BluetoothService.sortDevices(filtered)
}
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 70
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Color.mTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
}
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
Row {
anchors.left: parent.left
anchors.leftMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Style.marginXXS * scaling
anchors.verticalCenter: parent.verticalCenter
// One device name
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fonttSizeMedium * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
}
Row {
spacing: Style.marginXS * scaling
Row {
spacing: Style.marginS * spacing
// One device signal strength - "Unknown" when not connected
NText {
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
}
NIcon {
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
}
}
}
}
Rectangle {
width: 80 * scaling
height: 28 * scaling
radius: Style.radiusM * scaling
anchors.right: parent.right
anchors.rightMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy ? 1 : 0.5
// On device connect button
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return "Connect"
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
Layout.fillWidth: true
}
// Fallback if nothing available
Column {
width: parent.width
// Available devices
BluetoothDevicesList {
label: "Available devices"
model: {
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)
}
Layout.fillWidth: true
}
// Fallback
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
return false
}
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
@ -328,18 +125,17 @@ NPanel {
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0
return (availableCount === 0)
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM * scaling
NIcon {
text: "sync"
font.pointSize: Style.fontSizeXLL * 1.5 * scaling
color: Color.mPrimary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
@ -355,7 +151,6 @@ NPanel {
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
}
}
@ -363,36 +158,15 @@ NPanel {
text: "Make sure your device is in pairing mode"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
anchors.horizontalCenter: parent.horizontalCenter
Layout.alignment: Qt.AlignHCenter
}
}
NText {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
visible: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return true
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 && !BluetoothService.adapter.discovering
}
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
Item {
Layout.fillHeight: true
}
}
}
// This item takes up all the remaining vertical space.
Item {
Layout.fillHeight: true
}
}
}
}

View file

@ -99,6 +99,24 @@ Item {
}
}
IpcHandler {
target: "volume"
function increase() {
AudioService.increaseVolume()
}
function decrease() {
AudioService.decreaseVolume()
}
function muteOutput() {
AudioService.setMuted(!AudioService.muted)
}
function muteInput() {
if (AudioService.source?.ready && AudioService.source?.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
}
IpcHandler {
target: "powerPanel"
function toggle() {

View file

@ -1,46 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
Variants {
model: Quickshell.screens
delegate: Loader {
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(modelData)
active: NightLightService.isActive
sourceComponent: PanelWindow {
screen: modelData
color: Color.transparent
anchors {
top: true
bottom: true
left: true
right: true
}
// Ensure a full click through
mask: Region {}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "noctalia-nightlight"
Rectangle {
anchors.fill: parent
color: NightLightService.overlayColor
Behavior on color {
ColorAnimation {
duration: Style.animationSlow
}
}
}
}
}
}

View file

@ -301,6 +301,7 @@ NPanel {
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling
clip: true
Loader {
active: true

View file

@ -81,6 +81,54 @@ ColumnLayout {
}
}
// Input Volume
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NLabel {
label: "Input Volume"
description: "Microphone input volume level."
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1.0
value: AudioService.inputVolume
stepSize: 0.01
onMoved: {
AudioService.setInputVolume(value)
}
}
NText {
text: Math.floor(AudioService.inputVolume * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
// Input Mute Toggle
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Input"
description: "Mute or unmute the default audio input (microphone)."
checked: AudioService.inputMuted
onToggled: checked => {
AudioService.setInputMuted(checked)
}
}
}
// Volume Step Size
ColumnLayout {
spacing: Style.marginS * scaling
@ -216,8 +264,6 @@ ColumnLayout {
}
// Preferred player (persistent)
NTextInput {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
label: "Preferred Player"
description: "Substring to match MPRIS player (identity/bus/desktop)."
placeholderText: "e.g. spotify, vlc, mpv"
@ -239,8 +285,6 @@ ColumnLayout {
NTextInput {
id: blacklistInput
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
label: "Blacklist player"
description: "Substring, e.g. plex, shim, mpv."
placeholderText: "type substring and press +"

View file

@ -344,25 +344,24 @@ ColumnLayout {
visible: Settings.data.colorSchemes.useWallpaperColors
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Matugen Templates"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "Select which external components Matugen should apply theming to."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
NText {
text: "Matugen Templates"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NText {
text: "Select which external components Matugen should apply theming to."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
NCheckbox {
label: "GTK 4 (libadwaita)"

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
@ -27,6 +28,27 @@ ColumnLayout {
}
}
// Check for wlsunset availability when enabling Night Light
Process {
id: wlsunsetCheck
command: ["which", "wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true
NightLightService.apply()
ToastService.showNotice("Night Light", "Enabled")
} else {
Settings.data.nightLight.enabled = false
ToastService.showWarning("Night Light", "wlsunset not installed")
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
// Helper functions to update arrays immutably
function addMonitor(list, name) {
const arr = (list || []).slice()
@ -52,7 +74,6 @@ ColumnLayout {
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.preferredWidth: parent.width - (Style.marginL * 2 * scaling)
}
ColumnLayout {
@ -231,7 +252,6 @@ ColumnLayout {
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.preferredWidth: parent.width - (Style.marginL * 2 * scaling)
}
}
@ -239,7 +259,16 @@ ColumnLayout {
label: "Enable Night Light"
description: "Apply a warm color filter to reduce blue light emission."
checked: Settings.data.nightLight.enabled
onToggled: checked => Settings.data.nightLight.enabled = checked
onToggled: checked => {
if (checked) {
// Verify wlsunset exists before enabling
wlsunsetCheck.running = true
} else {
Settings.data.nightLight.enabled = false
NightLightService.apply()
ToastService.showNotice("Night Light", "Disabled")
}
}
}
// Intensity settings
@ -247,7 +276,7 @@ ColumnLayout {
visible: Settings.data.nightLight.enabled
NLabel {
label: "Intensity"
description: "Higher values create warmer light."
description: "Higher values create warmer tones."
}
RowLayout {
spacing: Style.marginS * scaling
@ -257,7 +286,10 @@ ColumnLayout {
to: 1
stepSize: 0.01
value: Settings.data.nightLight.intensity
onMoved: Settings.data.nightLight.intensity = value
onMoved: {
Settings.data.nightLight.intensity = value
NightLightService.apply()
}
Layout.fillWidth: true
Layout.minimumWidth: 150 * scaling
}
@ -271,11 +303,74 @@ ColumnLayout {
}
}
// Temperature
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NLabel {
label: "Color temperature"
description: "Select two temperatures in Kelvin"
}
RowLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM * scaling
Layout.fillWidth: false
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
NText {
text: "Low"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.lowTemp.toString()
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var v = parseInt(text)
if (!isNaN(v)) {
Settings.data.nightLight.lowTemp = Math.max(1000, Math.min(6500, v))
NightLightService.apply()
}
}
}
Item {}
NText {
text: "High"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.highTemp.toString()
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var v = parseInt(text)
if (!isNaN(v)) {
Settings.data.nightLight.highTemp = Math.max(1000, Math.min(10000, v))
NightLightService.apply()
}
}
}
}
}
NToggle {
label: "Auto Schedule"
description: "Automatically enable night light based on time schedule."
checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked
onToggled: checked => {
Settings.data.nightLight.autoSchedule = checked
NightLightService.apply()
}
visible: Settings.data.nightLight.enabled
}
@ -303,7 +398,10 @@ ColumnLayout {
model: timeOptions
currentKey: Settings.data.nightLight.startTime
placeholder: "Select start time"
onSelected: key => Settings.data.nightLight.startTime = key
onSelected: key => {
Settings.data.nightLight.startTime = key
NightLightService.apply()
}
preferredWidth: 120 * scaling
}
@ -319,7 +417,10 @@ ColumnLayout {
model: timeOptions
currentKey: Settings.data.nightLight.stopTime
placeholder: "Select stop time"
onSelected: key => Settings.data.nightLight.stopTime = key
onSelected: key => {
Settings.data.nightLight.stopTime = key
NightLightService.apply()
}
preferredWidth: 120 * scaling
}
}

View file

@ -25,10 +25,9 @@ ColumnLayout {
NTextInput {
label: "Profile Picture"
description: "Your profile picture displayed in various places throughout the shell."
description: "Your profile picture that appears throughout the interface."
text: Settings.data.general.avatarImage
placeholderText: "/home/user/.face"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.general.avatarImage = text
}

View file

@ -24,7 +24,8 @@ ColumnLayout {
onEditingFinished: {
Settings.data.screenRecorder.directory = text
}
Layout.fillWidth: true
Layout.maximumWidth: 420 * scaling
}
ColumnLayout {

View file

@ -10,6 +10,7 @@ ColumnLayout {
// Location section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginL * scaling
NTextInput {
@ -25,6 +26,7 @@ ColumnLayout {
LocationService.resetWeather()
}
}
Layout.maximumWidth: 420 * scaling
}
NText {

View file

@ -35,10 +35,10 @@ ColumnLayout {
label: "Wallpaper Directory"
description: "Path to your wallpaper directory."
text: Settings.data.wallpaper.directory
Layout.fillWidth: true
onEditingFinished: {
Settings.data.wallpaper.directory = text
}
Layout.maximumWidth: 420 * scaling
}
NDivider {
@ -79,12 +79,7 @@ ColumnLayout {
NText {
// Show friendly H:MM format from current settings
text: {
const s = Settings.data.wallpaper.randomInterval
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
return (h > 0 ? (h + "h ") : "") + (m > 0 ? (m + "m") : (h === 0 ? "0m" : ""))
}
text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomInterval)
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
}
}
@ -284,14 +279,15 @@ ColumnLayout {
NTextInput {
label: "Custom Interval"
description: "Enter time as HH:MM (e.g., 1:30)."
description: "Enter time as HH:MM (e.g., 01:30)."
inputMaxWidth: 100 * scaling
text: {
const s = Settings.data.wallpaper.randomInterval
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
return h + ":" + (m < 10 ? ("0" + m) : m)
}
Layout.fillWidth: true
onEditingFinished: {
const m = text.trim().match(/^(\d{1,2}):(\d{2})$/)
if (m) {