Merge branch 'noctalia-dev:main' into main
This commit is contained in:
commit
85bd0ed2f8
26 changed files with 929 additions and 650 deletions
|
|
@ -276,6 +276,9 @@ Singleton {
|
|||
property string startTime: "20:00"
|
||||
property string stopTime: "07:00"
|
||||
property bool autoSchedule: false
|
||||
// wlsunset temperatures (Kelvin)
|
||||
property int lowTemp: 3500
|
||||
property int highTemp: 6500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
262
Modules/BluetoothPanel/BluetoothDevicesList.qml
Normal file
262
Modules/BluetoothPanel/BluetoothDevicesList.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -301,6 +301,7 @@ NPanel {
|
|||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
padding: Style.marginL * scaling
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
active: true
|
||||
|
|
|
|||
|
|
@ -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 +"
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ ColumnLayout {
|
|||
onEditingFinished: {
|
||||
Settings.data.screenRecorder.directory = text
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
|
||||
Layout.maximumWidth: 420 * scaling
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -27,11 +27,11 @@ Features a modern modular architecture with a status bar, notification system, c
|
|||
|
||||
## Preview
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
|
@ -70,7 +70,6 @@ Features a modern modular architecture with a status bar, notification system, c
|
|||
- `gpu-screen-recorder` - Screen recording functionality
|
||||
- `brightnessctl` - For internal/laptop monitor brightness
|
||||
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
|
||||
- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal)
|
||||
|
||||
|
||||
### Optional
|
||||
|
|
@ -79,6 +78,7 @@ Features a modern modular architecture with a status bar, notification system, c
|
|||
- `swww` - Wallpaper animations and effects
|
||||
- `matugen` - Material You color scheme generation
|
||||
- `cava` - Audio visualizer component
|
||||
- `wlsunset` - To be able to use NightLight
|
||||
|
||||
> There are 2 more optional dependencies.
|
||||
> Any `polkit agent` to be able to use the ArchUpdater widget.
|
||||
|
|
@ -152,14 +152,12 @@ Alternatively, you can add it to your NixOS configuration or flake:
|
|||
quickshell = {
|
||||
url = "github:outfoxxed/quickshell";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.quickshell.follows = "quickshell"
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, noctalia, quickshell, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
in {
|
||||
{
|
||||
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
|
||||
modules = [
|
||||
./configuration.nix
|
||||
|
|
@ -173,8 +171,8 @@ Alternatively, you can add it to your NixOS configuration or flake:
|
|||
```nix
|
||||
{
|
||||
environment.systemPackages = with pkgs; [
|
||||
noctalia.packages.${system}.default
|
||||
quickshell.packages.${system}.default
|
||||
inputs.noctalia.packages.${system}.default
|
||||
inputs.quickshell.packages.${system}.default
|
||||
];
|
||||
}
|
||||
```
|
||||
|
|
@ -196,6 +194,10 @@ The following commands apply to the Nix flake and also the AUR package installat
|
|||
| Open Calculator | `noctalia-shell ipc call launcher calculator` |
|
||||
| Increase Brightness | `noctalia-shell ipc call brightness increase` |
|
||||
| Decrease Brightness | `noctalia-shell ipc call brightness decrease` |
|
||||
| Increase Output Volume | `noctalia-shell ipc call volume increase` |
|
||||
| Decrease Output Volume | `noctalia-shell ipc call volume decrease` |
|
||||
| Toggle Mute Audio Output | `noctalia-shell ipc call volume muteOutput` |
|
||||
| Toggle Mute Audio Input | `noctalia-shell ipc call volume muteInput` |
|
||||
| Toggle Power Panel | `noctalia-shell ipc call powerPanel toggle` |
|
||||
| Toggle Idle Inhibitor | `noctalia-shell ipc call idleInhibitor toggle` |
|
||||
| Toggle Settings Window | `noctalia-shell ipc call settings toggle` |
|
||||
|
|
|
|||
|
|
@ -142,28 +142,40 @@ Singleton {
|
|||
return
|
||||
|
||||
updateInProgress = true
|
||||
|
||||
// Split selected packages by source
|
||||
const repoPkgs = selectedPackages.filter(pkg => {
|
||||
const repoPkg = repoPackages.find(p => p.name === pkg)
|
||||
return repoPkg && repoPkg.source === "repo"
|
||||
})
|
||||
const aurPkgs = selectedPackages.filter(pkg => {
|
||||
const aurPkg = aurPackages.find(p => p.name === pkg)
|
||||
return aurPkg && aurPkg.source === "aur"
|
||||
})
|
||||
const repoPkgs = []
|
||||
const aurPkgs = []
|
||||
|
||||
for (const pkgName of selectedPackages) {
|
||||
const repoPkg = repoPackages.find(p => p.name === pkgName)
|
||||
if (repoPkg && repoPkg.source === "repo") {
|
||||
repoPkgs.push(pkgName)
|
||||
}
|
||||
|
||||
const aurPkg = aurPackages.find(p => p.name === pkgName)
|
||||
if (aurPkg && aurPkg.source === "aur") {
|
||||
aurPkgs.push(pkgName)
|
||||
}
|
||||
}
|
||||
|
||||
// Update repo packages
|
||||
if (repoPkgs.length > 0) {
|
||||
const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs)
|
||||
Logger.log("ArchUpdater", "Running repo command:", repoCommand.join(" "))
|
||||
Quickshell.execDetached(repoCommand)
|
||||
}
|
||||
|
||||
// Update AUR packages
|
||||
if (aurPkgs.length > 0) {
|
||||
const aurCommand = ["sh", "-c", `command -v yay >/dev/null 2>&1 && yay -S ${aurPkgs.join(
|
||||
' ')} --noconfirm || command -v paru >/dev/null 2>&1 && paru -S ${aurPkgs.join(
|
||||
' ')} --noconfirm || true`]
|
||||
Quickshell.execDetached(aurCommand)
|
||||
const aurHelper = getAurHelper()
|
||||
if (aurHelper) {
|
||||
const aurCommand = [aurHelper, "-S", "--noconfirm"].concat(aurPkgs)
|
||||
Logger.log("ArchUpdater", "Running AUR command:", aurCommand.join(" "))
|
||||
Quickshell.execDetached(aurCommand)
|
||||
} else {
|
||||
Logger.warn("ArchUpdater", "No AUR helper found for packages:", aurPkgs.join(", "))
|
||||
}
|
||||
}
|
||||
|
||||
// Clear selection and refresh
|
||||
|
|
@ -172,6 +184,22 @@ Singleton {
|
|||
refreshAfterUpdate()
|
||||
}
|
||||
|
||||
// Helper function to detect AUR helper
|
||||
function getAurHelper() {
|
||||
// Check for yay first, then paru
|
||||
const yayCheck = Quickshell.exec("command -v yay", true)
|
||||
if (yayCheck.exitCode === 0 && yayCheck.stdout.trim()) {
|
||||
return "yay"
|
||||
}
|
||||
|
||||
const paruCheck = Quickshell.exec("command -v paru", true)
|
||||
if (paruCheck.exitCode === 0 && paruCheck.stdout.trim()) {
|
||||
return "paru"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Package selection functions
|
||||
function togglePackageSelection(packageName) {
|
||||
const index = selectedPackages.indexOf(packageName)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,13 @@ Singleton {
|
|||
readonly property alias muted: root._muted
|
||||
property bool _muted: !!sink?.audio?.muted
|
||||
|
||||
// Input volume [0..1] is readonly from outside
|
||||
readonly property alias inputVolume: root._inputVolume
|
||||
property real _inputVolume: source?.audio?.volume ?? 0
|
||||
|
||||
readonly property alias inputMuted: root._inputMuted
|
||||
property bool _inputMuted: !!source?.audio?.muted
|
||||
|
||||
readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0
|
||||
|
||||
PwObjectTracker {
|
||||
|
|
@ -58,6 +65,23 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: source?.audio ? source?.audio : null
|
||||
|
||||
function onVolumeChanged() {
|
||||
var vol = (source?.audio.volume ?? 0)
|
||||
if (isNaN(vol)) {
|
||||
vol = 0
|
||||
}
|
||||
root._inputVolume = vol
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
root._inputMuted = (source?.audio.muted ?? true)
|
||||
Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted)
|
||||
}
|
||||
}
|
||||
|
||||
function increaseVolume() {
|
||||
setVolume(volume + stepVolume)
|
||||
}
|
||||
|
|
@ -85,6 +109,24 @@ Singleton {
|
|||
}
|
||||
}
|
||||
|
||||
function setInputVolume(newVolume: real) {
|
||||
if (source?.ready && source?.audio) {
|
||||
// Clamp it accordingly
|
||||
source.audio.muted = false
|
||||
source.audio.volume = Math.max(0, Math.min(1, newVolume))
|
||||
} else {
|
||||
Logger.warn("AudioService", "No source available")
|
||||
}
|
||||
}
|
||||
|
||||
function setInputMuted(muted: bool) {
|
||||
if (source?.ready && source?.audio) {
|
||||
source.audio.muted = muted
|
||||
} else {
|
||||
Logger.warn("AudioService", "No source available")
|
||||
}
|
||||
}
|
||||
|
||||
function setAudioSink(newSink: PwNode): void {
|
||||
Pipewire.preferredDefaultAudioSink = newSink
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ Singleton {
|
|||
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 []
|
||||
|
||||
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 []
|
||||
|
||||
if (!adapter || !adapter.devices) {
|
||||
return []
|
||||
}
|
||||
return adapter.devices.values.filter(dev => {
|
||||
return dev && dev.batteryAvailable && dev.battery > 0
|
||||
})
|
||||
|
|
@ -49,34 +49,36 @@ Singleton {
|
|||
}
|
||||
|
||||
function getDeviceIcon(device) {
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return "bluetooth"
|
||||
}
|
||||
|
||||
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"))
|
||||
|| name.includes("headset") || name.includes("arctis")) {
|
||||
return "headset"
|
||||
}
|
||||
|
||||
if (icon.includes("mouse") || name.includes("mouse"))
|
||||
if (icon.includes("mouse") || name.includes("mouse")) {
|
||||
return "mouse"
|
||||
|
||||
if (icon.includes("keyboard") || name.includes("keyboard"))
|
||||
}
|
||||
if (icon.includes("keyboard") || name.includes("keyboard")) {
|
||||
return "keyboard"
|
||||
|
||||
}
|
||||
if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android")
|
||||
|| name.includes("samsung"))
|
||||
|| name.includes("samsung")) {
|
||||
return "smartphone"
|
||||
|
||||
if (icon.includes("watch") || name.includes("watch"))
|
||||
}
|
||||
if (icon.includes("watch") || name.includes("watch")) {
|
||||
return "watch"
|
||||
|
||||
if (icon.includes("speaker") || name.includes("speaker"))
|
||||
}
|
||||
if (icon.includes("speaker") || name.includes("speaker")) {
|
||||
return "speaker"
|
||||
|
||||
if (icon.includes("display") || name.includes("tv"))
|
||||
}
|
||||
if (icon.includes("display") || name.includes("tv")) {
|
||||
return "tv"
|
||||
|
||||
}
|
||||
return "bluetooth"
|
||||
}
|
||||
|
||||
|
|
@ -88,60 +90,91 @@ Singleton {
|
|||
}
|
||||
|
||||
function getSignalStrength(device) {
|
||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
|
||||
return "Unknown"
|
||||
|
||||
if (device.pairing) {
|
||||
return "Pairing..."
|
||||
}
|
||||
if (device.blocked) {
|
||||
return "Blocked"
|
||||
}
|
||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
|
||||
return "Signal: Unknown"
|
||||
}
|
||||
var signal = device.signalStrength
|
||||
if (signal >= 80)
|
||||
return "Excellent"
|
||||
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"
|
||||
}
|
||||
|
||||
if (signal >= 60)
|
||||
return "Good"
|
||||
|
||||
if (signal >= 40)
|
||||
return "Fair"
|
||||
|
||||
if (signal >= 20)
|
||||
return "Poor"
|
||||
|
||||
return "Very Poor"
|
||||
function getBattery(device) {
|
||||
return `Battery: ${Math.round(device.battery * 100)}`
|
||||
}
|
||||
|
||||
function getSignalIcon(device) {
|
||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
|
||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
|
||||
return "signal_cellular_null"
|
||||
|
||||
}
|
||||
var signal = device.signalStrength
|
||||
if (signal >= 80)
|
||||
if (signal >= 80) {
|
||||
return "signal_cellular_4_bar"
|
||||
|
||||
if (signal >= 60)
|
||||
}
|
||||
if (signal >= 60) {
|
||||
return "signal_cellular_3_bar"
|
||||
|
||||
if (signal >= 40)
|
||||
}
|
||||
if (signal >= 40) {
|
||||
return "signal_cellular_2_bar"
|
||||
|
||||
if (signal >= 20)
|
||||
}
|
||||
if (signal >= 20) {
|
||||
return "signal_cellular_1_bar"
|
||||
|
||||
}
|
||||
return "signal_cellular_0_bar"
|
||||
}
|
||||
|
||||
function isDeviceBusy(device) {
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return false
|
||||
}
|
||||
|
||||
return device.pairing || device.state === BluetoothDeviceState.Disconnecting
|
||||
|| device.state === BluetoothDeviceState.Connecting
|
||||
}
|
||||
|
||||
function connectDeviceWithTrust(device) {
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return
|
||||
}
|
||||
|
||||
device.trusted = true
|
||||
device.connect()
|
||||
}
|
||||
|
||||
function disconnectDevice(device) {
|
||||
if (!device) {
|
||||
return
|
||||
}
|
||||
|
||||
device.trusted = false
|
||||
device.disconnect()
|
||||
}
|
||||
|
||||
function forgetDevice(device) {
|
||||
if (!device) {
|
||||
return
|
||||
}
|
||||
|
||||
device.trusted = false
|
||||
device.forget()
|
||||
}
|
||||
|
||||
function setBluetoothEnabled(enabled) {
|
||||
if (!adapter) {
|
||||
Logger.warn("Bluetooth", "No adapter available")
|
||||
|
|
|
|||
|
|
@ -4,63 +4,129 @@ import QtQuick
|
|||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Night Light properties - directly bound to settings
|
||||
readonly property var params: Settings.data.nightLight
|
||||
// Deprecated overlay flag removed; service only manages wlsunset now
|
||||
property bool isActive: false
|
||||
property bool isRunning: false
|
||||
property string lastCommand: ""
|
||||
property var nextCommand: []
|
||||
|
||||
// Computed properties
|
||||
readonly property color overlayColor: params.enabled ? calculateOverlayColor() : "transparent"
|
||||
property bool isActive: params.enabled && (params.autoSchedule ? isWithinSchedule() : true)
|
||||
Component.onCompleted: apply()
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("NightLight", "Service started")
|
||||
}
|
||||
|
||||
function calculateOverlayColor() {
|
||||
if (!isActive) {
|
||||
return "transparent"
|
||||
}
|
||||
|
||||
// More vibrant color formula - stronger effect at high warmth
|
||||
var red = 1.0
|
||||
var green = 1.0 - (0.43 * params.intensity)
|
||||
var blue = 1.0 - (0.84 * params.intensity)
|
||||
var alpha = (params.intensity * 0.25) // Higher alpha for more noticeable effect
|
||||
|
||||
return Qt.rgba(red, green, blue, alpha)
|
||||
}
|
||||
|
||||
function isWithinSchedule() {
|
||||
if (!params.autoSchedule) {
|
||||
return true
|
||||
}
|
||||
|
||||
var now = new Date()
|
||||
var currentTime = now.getHours() * 60 + now.getMinutes()
|
||||
|
||||
var startParts = params.startTime.split(":")
|
||||
var stopParts = params.stopTime.split(":")
|
||||
var startMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1])
|
||||
var stopMinutes = parseInt(stopParts[0]) * 60 + parseInt(stopParts[1])
|
||||
|
||||
// Handle overnight schedule (e.g., 20:00 to 07:00)
|
||||
if (stopMinutes < startMinutes) {
|
||||
return currentTime >= startMinutes || currentTime <= stopMinutes
|
||||
function buildCommand() {
|
||||
var cmd = ["wlsunset"]
|
||||
// Use user-configured temps; if intensity is used, bias lowTemp towards user low
|
||||
var i = Math.max(0, Math.min(1, params.intensity))
|
||||
var loCfg = params.lowTemp || 3500
|
||||
var hiCfg = params.highTemp || 6500
|
||||
var lowTemp = Math.round(hiCfg - (hiCfg - loCfg) * Math.pow(i, 0.6))
|
||||
cmd.push("-t", lowTemp.toString())
|
||||
cmd.push("-T", hiCfg.toString())
|
||||
if (params.autoSchedule && LocationService.data.coordinatesReady && LocationService.data.stableLatitude !== ""
|
||||
&& LocationService.data.stableLongitude !== "") {
|
||||
cmd.push("-l", LocationService.data.stableLatitude)
|
||||
cmd.push("-L", LocationService.data.stableLongitude)
|
||||
} else {
|
||||
return currentTime >= startMinutes && currentTime <= stopMinutes
|
||||
// Manual schedule
|
||||
if (params.startTime && params.stopTime) {
|
||||
cmd.push("-S", params.startTime)
|
||||
cmd.push("-s", params.stopTime)
|
||||
}
|
||||
// Optional: do not pass duration, use wlsunset defaults
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
function stopIfRunning() {
|
||||
// Best-effort stop; wlsunset runs as foreground, so pkill is simplest
|
||||
Quickshell.execDetached(["pkill", "-x", "wlsunset"])
|
||||
isRunning = false
|
||||
}
|
||||
|
||||
function apply() {
|
||||
if (!params.enabled) {
|
||||
// Disable immediately
|
||||
debounceStart.stop()
|
||||
nextCommand = []
|
||||
stopIfRunning()
|
||||
return
|
||||
}
|
||||
// Debounce rapid changes (slider)
|
||||
nextCommand = buildCommand()
|
||||
lastCommand = nextCommand.join(" ")
|
||||
stopIfRunning()
|
||||
debounceStart.restart()
|
||||
}
|
||||
|
||||
// Observe setting changes and location readiness
|
||||
Connections {
|
||||
target: Settings.data.nightLight
|
||||
function onEnabledChanged() {
|
||||
apply()
|
||||
}
|
||||
function onIntensityChanged() {
|
||||
apply()
|
||||
}
|
||||
function onAutoScheduleChanged() {
|
||||
apply()
|
||||
}
|
||||
function onStartTimeChanged() {
|
||||
apply()
|
||||
}
|
||||
function onStopTimeChanged() {
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to check schedule changes
|
||||
Connections {
|
||||
target: LocationService.data
|
||||
function onCoordinatesReadyChanged() {
|
||||
if (params.enabled && params.autoSchedule)
|
||||
apply()
|
||||
}
|
||||
function onStableLatitudeChanged() {
|
||||
if (params.enabled && params.autoSchedule)
|
||||
apply()
|
||||
}
|
||||
function onStableLongitudeChanged() {
|
||||
if (params.enabled && params.autoSchedule)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
// Foreground process runner
|
||||
Process {
|
||||
id: runner
|
||||
running: false
|
||||
onStarted: {
|
||||
isRunning = true
|
||||
Logger.log("NightLight", "Started wlsunset:", root.lastCommand)
|
||||
}
|
||||
onExited: function (code, status) {
|
||||
isRunning = false
|
||||
Logger.log("NightLight", "wlsunset exited:", code, status)
|
||||
// Do not auto-restart here; debounceStart handles starts
|
||||
}
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Debounce timer to avoid flicker when moving sliders
|
||||
Timer {
|
||||
interval: 60000 // Check every minute
|
||||
running: params.enabled && params.autoSchedule
|
||||
repeat: true
|
||||
id: debounceStart
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
isActive = isWithinSchedule()
|
||||
if (params.enabled && nextCommand.length > 0) {
|
||||
runner.command = nextCommand
|
||||
runner.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ RowLayout {
|
|||
property string description: ""
|
||||
property bool checked: false
|
||||
property bool hovering: false
|
||||
// Smaller default footprint than NToggle
|
||||
property int baseSize: Math.max(Style.baseWidgetSize * 0.8, Math.round(14 / scaling))
|
||||
property color activeColor: Color.mPrimary
|
||||
property color activeOnColor: Color.mOnPrimary
|
||||
property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14)
|
||||
|
||||
signal toggled(bool checked)
|
||||
signal entered
|
||||
|
|
@ -23,6 +24,7 @@ RowLayout {
|
|||
NLabel {
|
||||
label: root.label
|
||||
description: root.description
|
||||
visible: root.label !== "" || root.description !== ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
|
@ -30,16 +32,16 @@ RowLayout {
|
|||
|
||||
implicitWidth: root.baseSize * scaling
|
||||
implicitHeight: root.baseSize * scaling
|
||||
radius: Math.max(2 * scaling, Style.radiusXS * scaling)
|
||||
color: root.checked ? Color.mPrimary : Color.mSurface
|
||||
border.color: root.checked ? Color.mPrimary : Color.mOutline
|
||||
radius: Style.radiusXS * scaling
|
||||
color: root.checked ? root.activeColor : Color.mSurface
|
||||
border.color: root.checked ? root.activeColor : Color.mOutline
|
||||
border.width: Math.max(1, Style.borderM * scaling)
|
||||
|
||||
NIcon {
|
||||
visible: root.checked
|
||||
anchors.centerIn: parent
|
||||
text: "check"
|
||||
color: Color.mOnPrimary
|
||||
color: root.activeOnColor
|
||||
font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,74 +4,68 @@ import QtQuick.Layouts
|
|||
import qs.Commons
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string description: ""
|
||||
property bool readOnly: false
|
||||
property bool enabled: true
|
||||
property int inputMaxWidth: 420 * scaling
|
||||
|
||||
property alias text: input.text
|
||||
property alias placeholderText: input.placeholderText
|
||||
property alias inputMethodHints: input.inputMethodHints
|
||||
|
||||
signal editingFinished
|
||||
|
||||
// Sizing
|
||||
implicitWidth: Style.sliderWidth * 1.6 * scaling
|
||||
implicitHeight: Style.baseWidgetSize * 2.75 * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
implicitHeight: frame.height
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Style.marginXXS * scaling
|
||||
Layout.fillWidth: true
|
||||
NLabel {
|
||||
label: root.label
|
||||
description: root.description
|
||||
visible: root.label !== "" || root.description !== ""
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: root.label
|
||||
description: root.description
|
||||
// Container
|
||||
Rectangle {
|
||||
id: frame
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: Style.baseWidgetSize * 1.1 * scaling
|
||||
Layout.minimumWidth: 80 * scaling
|
||||
Layout.maximumWidth: root.inputMaxWidth
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
|
||||
// Focus ring
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: frame.radius
|
||||
color: Color.transparent
|
||||
border.color: input.activeFocus ? Color.mSecondary : Color.transparent
|
||||
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0
|
||||
}
|
||||
|
||||
// Container
|
||||
Rectangle {
|
||||
id: frame
|
||||
Layout.topMargin: Style.marginXS * scaling
|
||||
implicitWidth: root.width
|
||||
implicitHeight: Style.baseWidgetSize * 1.35 * scaling
|
||||
radius: Style.radiusM * scaling
|
||||
color: Color.mSurface
|
||||
border.color: Color.mOutline
|
||||
border.width: Math.max(1, Style.borderS * scaling)
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM * scaling
|
||||
anchors.rightMargin: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Focus ring
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: frame.radius
|
||||
color: Color.transparent
|
||||
border.color: input.activeFocus ? Color.mSecondary : Color.transparent
|
||||
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Style.marginM * scaling
|
||||
anchors.rightMargin: Style.marginM * scaling
|
||||
spacing: Style.marginS * scaling
|
||||
|
||||
// Optional leading icon slot in the future
|
||||
// Item { Layout.preferredWidth: 0 }
|
||||
TextField {
|
||||
id: input
|
||||
Layout.fillWidth: true
|
||||
echoMode: TextInput.Normal
|
||||
readOnly: root.readOnly
|
||||
enabled: root.enabled
|
||||
color: Color.mOnSurface
|
||||
placeholderTextColor: Color.mOnSurface
|
||||
background: null
|
||||
font.pointSize: Style.fontSizeXS * scaling
|
||||
onEditingFinished: root.editingFinished()
|
||||
// Text changes are observable via the aliased 'text' property (root.text) and its 'textChanged' signal.
|
||||
// No additional callback is invoked here to avoid conflicts with QML's onTextChanged handler semantics.
|
||||
}
|
||||
TextField {
|
||||
id: input
|
||||
Layout.fillWidth: true
|
||||
echoMode: TextInput.Normal
|
||||
readOnly: root.readOnly
|
||||
enabled: root.enabled
|
||||
color: Color.mOnSurface
|
||||
placeholderTextColor: Color.mOnSurfaceVariant
|
||||
background: null
|
||||
font.pointSize: Style.fontSizeS * scaling
|
||||
onEditingFinished: root.editingFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import qs.Modules.Calendar
|
|||
import qs.Modules.Dock
|
||||
import qs.Modules.IPC
|
||||
import qs.Modules.LockScreen
|
||||
import qs.Modules.NightLight
|
||||
import qs.Modules.Notification
|
||||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.PowerPanel
|
||||
|
|
@ -51,12 +50,10 @@ ShellRoot {
|
|||
|
||||
ToastOverlay {}
|
||||
|
||||
NightLightOverlay {}
|
||||
|
||||
IPCManager {}
|
||||
|
||||
// ------------------------------
|
||||
// All the panels
|
||||
// All the NPanels
|
||||
Launcher {
|
||||
id: launcherPanel
|
||||
objectName: "launcherPanel"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue