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 startTime: "20:00"
|
||||||
property string stopTime: "07:00"
|
property string stopTime: "07:00"
|
||||||
property bool autoSchedule: false
|
property bool autoSchedule: false
|
||||||
|
// wlsunset temperatures (Kelvin)
|
||||||
|
property int lowTemp: 3500
|
||||||
|
property int highTemp: 6500
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ NPanel {
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
text: "system_update"
|
text: "system_update_alt"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
}
|
}
|
||||||
|
|
@ -77,11 +77,9 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unified list
|
// Unified list
|
||||||
Rectangle {
|
NBox {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
color: Color.mSurfaceVariant
|
|
||||||
radius: Style.radiusM * scaling
|
|
||||||
|
|
||||||
// Combine repo and AUR lists in order: repos first, then AUR
|
// Combine repo and AUR lists in order: repos first, then AUR
|
||||||
property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || [])
|
property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || [])
|
||||||
|
|
@ -89,33 +87,35 @@ NPanel {
|
||||||
ListView {
|
ListView {
|
||||||
id: unifiedList
|
id: unifiedList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Style.marginS * scaling
|
anchors.margins: Style.marginM * scaling
|
||||||
|
cacheBuffer: Math.round(300 * scaling)
|
||||||
clip: true
|
clip: true
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
model: parent.items
|
model: parent.items
|
||||||
spacing: Style.marginXS * scaling
|
|
||||||
cacheBuffer: 300 * scaling
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
width: unifiedList.width
|
width: unifiedList.width
|
||||||
height: 56 * scaling
|
height: 44 * scaling
|
||||||
color: Color.transparent
|
color: Color.transparent
|
||||||
radius: Style.radiusS * scaling
|
radius: Style.radiusS * scaling
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Style.marginS * scaling
|
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
||||||
// Checkbox for selection (pure bindings; no imperative state)
|
// Checkbox for selection
|
||||||
NIconButton {
|
NCheckbox {
|
||||||
id: checkbox
|
id: checkbox
|
||||||
icon: ArchUpdaterService.isPackageSelected(modelData.name) ? "check_box" : "check_box_outline_blank"
|
label: ""
|
||||||
onClicked: ArchUpdaterService.togglePackageSelection(modelData.name)
|
description: ""
|
||||||
colorBg: Color.transparent
|
checked: ArchUpdaterService.isPackageSelected(modelData.name)
|
||||||
colorFg: ArchUpdaterService.isPackageSelected(
|
baseSize: Math.max(Style.baseWidgetSize * 0.7, 14)
|
||||||
modelData.name) ? ((modelData.source === "aur") ? Color.mSecondary : Color.mPrimary) : Color.mOnSurfaceVariant
|
onToggled: function (checked) {
|
||||||
Layout.preferredWidth: 30 * scaling
|
ArchUpdaterService.togglePackageSelection(modelData.name)
|
||||||
Layout.preferredHeight: 30 * scaling
|
// Force refresh of the checked property
|
||||||
|
checkbox.checked = ArchUpdaterService.isPackageSelected(modelData.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Package info
|
// Package info
|
||||||
|
|
@ -123,51 +123,43 @@ NPanel {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Style.marginXXS * scaling
|
spacing: Style.marginXXS * scaling
|
||||||
|
|
||||||
RowLayout {
|
NText {
|
||||||
|
text: modelData.name
|
||||||
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
|
font.weight: Style.fontWeightBold
|
||||||
|
color: Color.mOnSurface
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Style.marginXS * scaling
|
Layout.alignment: Qt.AlignVCenter
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: modelData.oldVersion + " → " + modelData.newVersion
|
text: modelData.oldVersion + " → " + modelData.newVersion
|
||||||
font.pointSize: Style.fontSizeS * scaling
|
font.pointSize: Style.fontSizeXXS * scaling
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
// Source tag (AUR vs PAC)
|
||||||
policy: ScrollBar.AsNeeded
|
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 {
|
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"
|
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages"
|
||||||
enabled: !ArchUpdaterService.updateInProgress
|
enabled: !ArchUpdaterService.updateInProgress
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|
@ -203,7 +195,7 @@ NPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
NIconButton {
|
NIconButton {
|
||||||
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings"
|
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "check_box"
|
||||||
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
|
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
|
||||||
enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
|
enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|
@ -213,9 +205,9 @@ NPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||||
> 0 ? Color.mSecondary : Color.mSurfaceVariant)
|
> 0 ? Color.mPrimary : Color.mSurfaceVariant)
|
||||||
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
|
||||||
> 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant)
|
> 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,72 +14,51 @@ NIconButton {
|
||||||
|
|
||||||
sizeRatio: 0.8
|
sizeRatio: 0.8
|
||||||
colorBg: Color.mSurfaceVariant
|
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
|
colorBorder: Color.transparent
|
||||||
colorBorderHover: Color.transparent
|
colorBorderHover: Color.transparent
|
||||||
|
colorFg: (ArchUpdaterService.totalUpdates === 0) ? Color.mOnSurface : Color.mPrimary
|
||||||
|
|
||||||
// Icon states
|
// Icon states
|
||||||
icon: {
|
icon: {
|
||||||
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy)
|
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
|
||||||
return "sync"
|
return "sync"
|
||||||
|
}
|
||||||
if (ArchUpdaterService.totalUpdates > 0) {
|
if (ArchUpdaterService.totalUpdates > 0) {
|
||||||
const count = ArchUpdaterService.totalUpdates
|
return "system_update_alt"
|
||||||
if (count > 50)
|
|
||||||
return "system_update_alt"
|
|
||||||
if (count > 10)
|
|
||||||
return "system_update"
|
|
||||||
return "system_update"
|
|
||||||
}
|
}
|
||||||
return "task_alt"
|
return "task_alt"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tooltip with repo vs AUR breakdown and sample lists
|
// Tooltip with repo vs AUR breakdown and sample lists
|
||||||
tooltipText: {
|
tooltipText: {
|
||||||
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy)
|
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
|
||||||
return "Checking for updates…"
|
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 total = ArchUpdaterService.totalUpdates
|
||||||
const aurHeader = aurCount > 0 ? ("AUR (" + aurCount + "):") : "AUR: 0"
|
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,
|
const pacCount = ArchUpdaterService.updates
|
||||||
5)) : repoHeader
|
const aurCount = ArchUpdaterService.aurUpdates
|
||||||
const aurBlock = aurCount > 0 ? (aurHeader + "\n\n" + sampleList(ArchUpdaterService.aurPackages, 5)) : aurHeader
|
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: {
|
onClicked: {
|
||||||
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
|
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
|
||||||
|
ToastService.showNotice("ArchUpdater", "Still fetching updates...")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,7 @@ NIconButton {
|
||||||
colorBorder: Color.transparent
|
colorBorder: Color.transparent
|
||||||
colorBorderHover: Color.transparent
|
colorBorderHover: Color.transparent
|
||||||
|
|
||||||
icon: {
|
icon: "bluetooth"
|
||||||
// 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tooltipText: "Bluetooth Devices"
|
tooltipText: "Bluetooth Devices"
|
||||||
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this)
|
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,19 @@ Item {
|
||||||
|
|
||||||
NPill {
|
NPill {
|
||||||
id: pill
|
id: pill
|
||||||
icon: NightLightService.isActive ? "bedtime" : "bedtime_off"
|
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off"
|
||||||
iconCircleColor: NightLightService.isActive ? Color.mSecondary : Color.mOnSurfaceVariant
|
iconCircleColor: Settings.data.nightLight.enabled ? Color.mSecondary : Color.mOnSurfaceVariant
|
||||||
collapsedIconColor: NightLightService.isActive ? Color.mOnSecondary : Color.mOnSurface
|
collapsedIconColor: Settings.data.nightLight.enabled ? Color.mOnSecondary : Color.mOnSurface
|
||||||
autoHide: false
|
autoHide: false
|
||||||
text: NightLightService.isActive ? "On" : "Off"
|
text: Settings.data.nightLight.enabled ? "On" : "Off"
|
||||||
tooltipText: {
|
tooltipText: {
|
||||||
if (!Settings.isLoaded || !Settings.data.nightLight.enabled) {
|
if (!Settings.isLoaded || !Settings.data.nightLight.enabled) {
|
||||||
return "Night Light: Disabled\nLeft click to open settings.\nRight click to enable."
|
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 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"
|
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.`
|
||||||
return `Intensity: ${intensity}%\n${schedule}\nLeft click to open settings.\nRight click to toggle.`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
|
@ -42,14 +40,11 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
onRightClicked: {
|
onRightClicked: {
|
||||||
// Right click - toggle night light
|
// Right click - toggle night light (debounced apply handled by service)
|
||||||
Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
|
Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
|
||||||
|
NightLightService.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
onWheel: delta => {
|
// Wheel handler removed to avoid frequent rapid restarts/flicker
|
||||||
var diff = delta > 0 ? 0.05 : -0.05
|
|
||||||
Settings.data.nightLight.intensity = Math.max(0, Math.min(1.0,
|
|
||||||
Settings.data.nightLight.intensity + diff))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ Item {
|
||||||
|
|
||||||
// Used to avoid opening the pill on Quickshell startup
|
// Used to avoid opening the pill on Quickshell startup
|
||||||
property bool firstVolumeReceived: false
|
property bool firstVolumeReceived: false
|
||||||
|
property int wheelAccumulator: 0
|
||||||
|
|
||||||
implicitWidth: pill.width
|
implicitWidth: pill.width
|
||||||
implicitHeight: pill.height
|
implicitHeight: pill.height
|
||||||
|
|
@ -59,10 +60,13 @@ Item {
|
||||||
tooltipText: "Volume: " + Math.round(
|
tooltipText: "Volume: " + Math.round(
|
||||||
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
|
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
|
||||||
|
|
||||||
onWheel: function (angle) {
|
onWheel: function (delta) {
|
||||||
if (angle > 0) {
|
wheelAccumulator += delta
|
||||||
|
if (wheelAccumulator >= 120) {
|
||||||
|
wheelAccumulator = 0
|
||||||
AudioService.increaseVolume()
|
AudioService.increaseVolume()
|
||||||
} else if (angle < 0) {
|
} else if (wheelAccumulator <= -120) {
|
||||||
|
wheelAccumulator = 0
|
||||||
AudioService.decreaseVolume()
|
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 {
|
ScrollView {
|
||||||
id: scrollView
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
clip: true
|
|
||||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
clip: true
|
||||||
|
contentWidth: availableWidth
|
||||||
|
|
||||||
// Available devices
|
ColumnLayout {
|
||||||
Column {
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
id: column
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
|
||||||
|
|
||||||
RowLayout {
|
// Connected devices
|
||||||
width: parent.width
|
BluetoothDevicesList {
|
||||||
spacing: Style.marginM * scaling
|
label: "Connected devices"
|
||||||
|
|
||||||
NText {
|
|
||||||
text: "Available Devices"
|
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
|
||||||
color: Color.mOnSurface
|
|
||||||
font.weight: Style.fontWeightMedium
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
model: {
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
var filtered = Bluetooth.devices.values.filter(dev => {
|
var filtered = Bluetooth.devices.values.filter(dev => {
|
||||||
return dev && !dev.paired && !dev.pairing && !dev.blocked
|
return dev && !dev.blocked && (dev.paired || dev.trusted)
|
||||||
&& (dev.signalStrength === undefined
|
|
||||||
|| dev.signalStrength > 0)
|
|
||||||
})
|
})
|
||||||
return BluetoothService.sortDevices(filtered)
|
return BluetoothService.sortDevices(filtered)
|
||||||
}
|
}
|
||||||
|
Layout.fillWidth: true
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback if nothing available
|
// Available devices
|
||||||
Column {
|
BluetoothDevicesList {
|
||||||
width: parent.width
|
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
|
spacing: Style.marginM * scaling
|
||||||
visible: {
|
visible: {
|
||||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
var availableCount = Bluetooth.devices.values.filter(dev => {
|
var availableCount = Bluetooth.devices.values.filter(dev => {
|
||||||
return dev && !dev.paired && !dev.pairing
|
return dev && !dev.paired && !dev.pairing
|
||||||
|
|
@ -328,18 +125,17 @@ NPanel {
|
||||||
&& (dev.signalStrength === undefined
|
&& (dev.signalStrength === undefined
|
||||||
|| dev.signalStrength > 0)
|
|| dev.signalStrength > 0)
|
||||||
}).length
|
}).length
|
||||||
return availableCount === 0
|
return (availableCount === 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
RowLayout {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
spacing: Style.marginM * scaling
|
spacing: Style.marginM * scaling
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
text: "sync"
|
text: "sync"
|
||||||
font.pointSize: Style.fontSizeXLL * 1.5 * scaling
|
font.pointSize: Style.fontSizeXLL * 1.5 * scaling
|
||||||
color: Color.mPrimary
|
color: Color.mPrimary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
RotationAnimation on rotation {
|
||||||
running: true
|
running: true
|
||||||
|
|
@ -355,7 +151,6 @@ NPanel {
|
||||||
font.pointSize: Style.fontSizeL * scaling
|
font.pointSize: Style.fontSizeL * scaling
|
||||||
color: Color.mOnSurface
|
color: Color.mOnSurface
|
||||||
font.weight: Style.fontWeightMedium
|
font.weight: Style.fontWeightMedium
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -363,36 +158,15 @@ NPanel {
|
||||||
text: "Make sure your device is in pairing mode"
|
text: "Make sure your device is in pairing mode"
|
||||||
font.pointSize: Style.fontSizeM * scaling
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
Item {
|
||||||
text: "No devices found. Put your device in pairing mode and click Start Scanning."
|
Layout.fillHeight: true
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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 {
|
IpcHandler {
|
||||||
target: "powerPanel"
|
target: "powerPanel"
|
||||||
function toggle() {
|
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.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
padding: Style.marginL * scaling
|
padding: Style.marginL * scaling
|
||||||
|
clip: true
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
active: true
|
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
|
// Volume Step Size
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
@ -216,8 +264,6 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
// Preferred player (persistent)
|
// Preferred player (persistent)
|
||||||
NTextInput {
|
NTextInput {
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
label: "Preferred Player"
|
label: "Preferred Player"
|
||||||
description: "Substring to match MPRIS player (identity/bus/desktop)."
|
description: "Substring to match MPRIS player (identity/bus/desktop)."
|
||||||
placeholderText: "e.g. spotify, vlc, mpv"
|
placeholderText: "e.g. spotify, vlc, mpv"
|
||||||
|
|
@ -239,8 +285,6 @@ ColumnLayout {
|
||||||
|
|
||||||
NTextInput {
|
NTextInput {
|
||||||
id: blacklistInput
|
id: blacklistInput
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.alignment: Qt.AlignTop
|
|
||||||
label: "Blacklist player"
|
label: "Blacklist player"
|
||||||
description: "Substring, e.g. plex, shim, mpv."
|
description: "Substring, e.g. plex, shim, mpv."
|
||||||
placeholderText: "type substring and press +"
|
placeholderText: "type substring and press +"
|
||||||
|
|
|
||||||
|
|
@ -344,25 +344,24 @@ ColumnLayout {
|
||||||
visible: Settings.data.colorSchemes.useWallpaperColors
|
visible: Settings.data.colorSchemes.useWallpaperColors
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
text: "Matugen Templates"
|
text: "Matugen Templates"
|
||||||
font.pointSize: Style.fontSizeXXL * scaling
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
font.weight: Style.fontWeightBold
|
font.weight: Style.fontWeightBold
|
||||||
color: Color.mSecondary
|
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: "Select which external components Matugen should apply theming to."
|
||||||
|
font.pointSize: Style.fontSizeM * scaling
|
||||||
|
color: Color.mOnSurfaceVariant
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NCheckbox {
|
NCheckbox {
|
||||||
label: "GTK 4 (libadwaita)"
|
label: "GTK 4 (libadwaita)"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
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
|
// Helper functions to update arrays immutably
|
||||||
function addMonitor(list, name) {
|
function addMonitor(list, name) {
|
||||||
const arr = (list || []).slice()
|
const arr = (list || []).slice()
|
||||||
|
|
@ -52,7 +74,6 @@ ColumnLayout {
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredWidth: parent.width - (Style.marginL * 2 * scaling)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
@ -231,7 +252,6 @@ ColumnLayout {
|
||||||
color: Color.mOnSurfaceVariant
|
color: Color.mOnSurfaceVariant
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredWidth: parent.width - (Style.marginL * 2 * scaling)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,7 +259,16 @@ ColumnLayout {
|
||||||
label: "Enable Night Light"
|
label: "Enable Night Light"
|
||||||
description: "Apply a warm color filter to reduce blue light emission."
|
description: "Apply a warm color filter to reduce blue light emission."
|
||||||
checked: Settings.data.nightLight.enabled
|
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
|
// Intensity settings
|
||||||
|
|
@ -247,7 +276,7 @@ ColumnLayout {
|
||||||
visible: Settings.data.nightLight.enabled
|
visible: Settings.data.nightLight.enabled
|
||||||
NLabel {
|
NLabel {
|
||||||
label: "Intensity"
|
label: "Intensity"
|
||||||
description: "Higher values create warmer light."
|
description: "Higher values create warmer tones."
|
||||||
}
|
}
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: Style.marginS * scaling
|
spacing: Style.marginS * scaling
|
||||||
|
|
@ -257,7 +286,10 @@ ColumnLayout {
|
||||||
to: 1
|
to: 1
|
||||||
stepSize: 0.01
|
stepSize: 0.01
|
||||||
value: Settings.data.nightLight.intensity
|
value: Settings.data.nightLight.intensity
|
||||||
onMoved: Settings.data.nightLight.intensity = value
|
onMoved: {
|
||||||
|
Settings.data.nightLight.intensity = value
|
||||||
|
NightLightService.apply()
|
||||||
|
}
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.minimumWidth: 150 * scaling
|
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 {
|
NToggle {
|
||||||
label: "Auto Schedule"
|
label: "Auto Schedule"
|
||||||
description: "Automatically enable night light based on time schedule."
|
description: "Automatically enable night light based on time schedule."
|
||||||
checked: Settings.data.nightLight.autoSchedule
|
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
|
visible: Settings.data.nightLight.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,7 +398,10 @@ ColumnLayout {
|
||||||
model: timeOptions
|
model: timeOptions
|
||||||
currentKey: Settings.data.nightLight.startTime
|
currentKey: Settings.data.nightLight.startTime
|
||||||
placeholder: "Select start time"
|
placeholder: "Select start time"
|
||||||
onSelected: key => Settings.data.nightLight.startTime = key
|
onSelected: key => {
|
||||||
|
Settings.data.nightLight.startTime = key
|
||||||
|
NightLightService.apply()
|
||||||
|
}
|
||||||
preferredWidth: 120 * scaling
|
preferredWidth: 120 * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -319,7 +417,10 @@ ColumnLayout {
|
||||||
model: timeOptions
|
model: timeOptions
|
||||||
currentKey: Settings.data.nightLight.stopTime
|
currentKey: Settings.data.nightLight.stopTime
|
||||||
placeholder: "Select stop time"
|
placeholder: "Select stop time"
|
||||||
onSelected: key => Settings.data.nightLight.stopTime = key
|
onSelected: key => {
|
||||||
|
Settings.data.nightLight.stopTime = key
|
||||||
|
NightLightService.apply()
|
||||||
|
}
|
||||||
preferredWidth: 120 * scaling
|
preferredWidth: 120 * scaling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,9 @@ ColumnLayout {
|
||||||
|
|
||||||
NTextInput {
|
NTextInput {
|
||||||
label: "Profile Picture"
|
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
|
text: Settings.data.general.avatarImage
|
||||||
placeholderText: "/home/user/.face"
|
placeholderText: "/home/user/.face"
|
||||||
Layout.fillWidth: true
|
|
||||||
onEditingFinished: {
|
onEditingFinished: {
|
||||||
Settings.data.general.avatarImage = text
|
Settings.data.general.avatarImage = text
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ ColumnLayout {
|
||||||
onEditingFinished: {
|
onEditingFinished: {
|
||||||
Settings.data.screenRecorder.directory = text
|
Settings.data.screenRecorder.directory = text
|
||||||
}
|
}
|
||||||
Layout.fillWidth: true
|
|
||||||
|
Layout.maximumWidth: 420 * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ ColumnLayout {
|
||||||
|
|
||||||
// Location section
|
// Location section
|
||||||
RowLayout {
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
spacing: Style.marginL * scaling
|
spacing: Style.marginL * scaling
|
||||||
|
|
||||||
NTextInput {
|
NTextInput {
|
||||||
|
|
@ -25,6 +26,7 @@ ColumnLayout {
|
||||||
LocationService.resetWeather()
|
LocationService.resetWeather()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Layout.maximumWidth: 420 * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,10 @@ ColumnLayout {
|
||||||
label: "Wallpaper Directory"
|
label: "Wallpaper Directory"
|
||||||
description: "Path to your wallpaper directory."
|
description: "Path to your wallpaper directory."
|
||||||
text: Settings.data.wallpaper.directory
|
text: Settings.data.wallpaper.directory
|
||||||
Layout.fillWidth: true
|
|
||||||
onEditingFinished: {
|
onEditingFinished: {
|
||||||
Settings.data.wallpaper.directory = text
|
Settings.data.wallpaper.directory = text
|
||||||
}
|
}
|
||||||
|
Layout.maximumWidth: 420 * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
NDivider {
|
NDivider {
|
||||||
|
|
@ -79,12 +79,7 @@ ColumnLayout {
|
||||||
|
|
||||||
NText {
|
NText {
|
||||||
// Show friendly H:MM format from current settings
|
// Show friendly H:MM format from current settings
|
||||||
text: {
|
text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomInterval)
|
||||||
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" : ""))
|
|
||||||
}
|
|
||||||
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
|
Layout.alignment: Qt.AlignBottom | Qt.AlignRight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -284,14 +279,15 @@ ColumnLayout {
|
||||||
|
|
||||||
NTextInput {
|
NTextInput {
|
||||||
label: "Custom Interval"
|
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: {
|
text: {
|
||||||
const s = Settings.data.wallpaper.randomInterval
|
const s = Settings.data.wallpaper.randomInterval
|
||||||
const h = Math.floor(s / 3600)
|
const h = Math.floor(s / 3600)
|
||||||
const m = Math.floor((s % 3600) / 60)
|
const m = Math.floor((s % 3600) / 60)
|
||||||
return h + ":" + (m < 10 ? ("0" + m) : m)
|
return h + ":" + (m < 10 ? ("0" + m) : m)
|
||||||
}
|
}
|
||||||
Layout.fillWidth: true
|
|
||||||
onEditingFinished: {
|
onEditingFinished: {
|
||||||
const m = text.trim().match(/^(\d{1,2}):(\d{2})$/)
|
const m = text.trim().match(/^(\d{1,2}):(\d{2})$/)
|
||||||
if (m) {
|
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
|
## Preview
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -70,7 +70,6 @@ Features a modern modular architecture with a status bar, notification system, c
|
||||||
- `gpu-screen-recorder` - Screen recording functionality
|
- `gpu-screen-recorder` - Screen recording functionality
|
||||||
- `brightnessctl` - For internal/laptop monitor brightness
|
- `brightnessctl` - For internal/laptop monitor brightness
|
||||||
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
|
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
|
||||||
- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal)
|
|
||||||
|
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
|
|
@ -79,6 +78,7 @@ Features a modern modular architecture with a status bar, notification system, c
|
||||||
- `swww` - Wallpaper animations and effects
|
- `swww` - Wallpaper animations and effects
|
||||||
- `matugen` - Material You color scheme generation
|
- `matugen` - Material You color scheme generation
|
||||||
- `cava` - Audio visualizer component
|
- `cava` - Audio visualizer component
|
||||||
|
- `wlsunset` - To be able to use NightLight
|
||||||
|
|
||||||
> There are 2 more optional dependencies.
|
> There are 2 more optional dependencies.
|
||||||
> Any `polkit agent` to be able to use the ArchUpdater widget.
|
> 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 = {
|
quickshell = {
|
||||||
url = "github:outfoxxed/quickshell";
|
url = "github:outfoxxed/quickshell";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
inputs.quickshell.follows = "quickshell"
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, noctalia, quickshell, ... }:
|
outputs = { self, nixpkgs, noctalia, quickshell, ... }:
|
||||||
let
|
{
|
||||||
system = "x86_64-linux";
|
|
||||||
pkgs = import nixpkgs { inherit system; };
|
|
||||||
in {
|
|
||||||
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
|
||||||
modules = [
|
modules = [
|
||||||
./configuration.nix
|
./configuration.nix
|
||||||
|
|
@ -173,8 +171,8 @@ Alternatively, you can add it to your NixOS configuration or flake:
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
noctalia.packages.${system}.default
|
inputs.noctalia.packages.${system}.default
|
||||||
quickshell.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` |
|
| Open Calculator | `noctalia-shell ipc call launcher calculator` |
|
||||||
| Increase Brightness | `noctalia-shell ipc call brightness increase` |
|
| Increase Brightness | `noctalia-shell ipc call brightness increase` |
|
||||||
| Decrease Brightness | `noctalia-shell ipc call brightness decrease` |
|
| 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 Power Panel | `noctalia-shell ipc call powerPanel toggle` |
|
||||||
| Toggle Idle Inhibitor | `noctalia-shell ipc call idleInhibitor toggle` |
|
| Toggle Idle Inhibitor | `noctalia-shell ipc call idleInhibitor toggle` |
|
||||||
| Toggle Settings Window | `noctalia-shell ipc call settings toggle` |
|
| Toggle Settings Window | `noctalia-shell ipc call settings toggle` |
|
||||||
|
|
|
||||||
|
|
@ -142,28 +142,40 @@ Singleton {
|
||||||
return
|
return
|
||||||
|
|
||||||
updateInProgress = true
|
updateInProgress = true
|
||||||
|
|
||||||
// Split selected packages by source
|
// Split selected packages by source
|
||||||
const repoPkgs = selectedPackages.filter(pkg => {
|
const repoPkgs = []
|
||||||
const repoPkg = repoPackages.find(p => p.name === pkg)
|
const aurPkgs = []
|
||||||
return repoPkg && repoPkg.source === "repo"
|
|
||||||
})
|
for (const pkgName of selectedPackages) {
|
||||||
const aurPkgs = selectedPackages.filter(pkg => {
|
const repoPkg = repoPackages.find(p => p.name === pkgName)
|
||||||
const aurPkg = aurPackages.find(p => p.name === pkg)
|
if (repoPkg && repoPkg.source === "repo") {
|
||||||
return aurPkg && aurPkg.source === "aur"
|
repoPkgs.push(pkgName)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const aurPkg = aurPackages.find(p => p.name === pkgName)
|
||||||
|
if (aurPkg && aurPkg.source === "aur") {
|
||||||
|
aurPkgs.push(pkgName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update repo packages
|
// Update repo packages
|
||||||
if (repoPkgs.length > 0) {
|
if (repoPkgs.length > 0) {
|
||||||
const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs)
|
const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs)
|
||||||
|
Logger.log("ArchUpdater", "Running repo command:", repoCommand.join(" "))
|
||||||
Quickshell.execDetached(repoCommand)
|
Quickshell.execDetached(repoCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update AUR packages
|
// Update AUR packages
|
||||||
if (aurPkgs.length > 0) {
|
if (aurPkgs.length > 0) {
|
||||||
const aurCommand = ["sh", "-c", `command -v yay >/dev/null 2>&1 && yay -S ${aurPkgs.join(
|
const aurHelper = getAurHelper()
|
||||||
' ')} --noconfirm || command -v paru >/dev/null 2>&1 && paru -S ${aurPkgs.join(
|
if (aurHelper) {
|
||||||
' ')} --noconfirm || true`]
|
const aurCommand = [aurHelper, "-S", "--noconfirm"].concat(aurPkgs)
|
||||||
Quickshell.execDetached(aurCommand)
|
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
|
// Clear selection and refresh
|
||||||
|
|
@ -172,6 +184,22 @@ Singleton {
|
||||||
refreshAfterUpdate()
|
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
|
// Package selection functions
|
||||||
function togglePackageSelection(packageName) {
|
function togglePackageSelection(packageName) {
|
||||||
const index = selectedPackages.indexOf(packageName)
|
const index = selectedPackages.indexOf(packageName)
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ Singleton {
|
||||||
readonly property alias muted: root._muted
|
readonly property alias muted: root._muted
|
||||||
property bool _muted: !!sink?.audio?.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
|
readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0
|
||||||
|
|
||||||
PwObjectTracker {
|
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() {
|
function increaseVolume() {
|
||||||
setVolume(volume + stepVolume)
|
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 {
|
function setAudioSink(newSink: PwNode): void {
|
||||||
Pipewire.preferredDefaultAudioSink = newSink
|
Pipewire.preferredDefaultAudioSink = newSink
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,17 @@ Singleton {
|
||||||
readonly property bool discovering: (adapter && adapter.discovering) ?? false
|
readonly property bool discovering: (adapter && adapter.discovering) ?? false
|
||||||
readonly property var devices: adapter ? adapter.devices : null
|
readonly property var devices: adapter ? adapter.devices : null
|
||||||
readonly property var pairedDevices: {
|
readonly property var pairedDevices: {
|
||||||
if (!adapter || !adapter.devices)
|
if (!adapter || !adapter.devices) {
|
||||||
return []
|
return []
|
||||||
|
}
|
||||||
return adapter.devices.values.filter(dev => {
|
return adapter.devices.values.filter(dev => {
|
||||||
return dev && (dev.paired || dev.trusted)
|
return dev && (dev.paired || dev.trusted)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
readonly property var allDevicesWithBattery: {
|
readonly property var allDevicesWithBattery: {
|
||||||
if (!adapter || !adapter.devices)
|
if (!adapter || !adapter.devices) {
|
||||||
return []
|
return []
|
||||||
|
}
|
||||||
return adapter.devices.values.filter(dev => {
|
return adapter.devices.values.filter(dev => {
|
||||||
return dev && dev.batteryAvailable && dev.battery > 0
|
return dev && dev.batteryAvailable && dev.battery > 0
|
||||||
})
|
})
|
||||||
|
|
@ -49,34 +49,36 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDeviceIcon(device) {
|
function getDeviceIcon(device) {
|
||||||
if (!device)
|
if (!device) {
|
||||||
return "bluetooth"
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
|
||||||
var name = (device.name || device.deviceName || "").toLowerCase()
|
var name = (device.name || device.deviceName || "").toLowerCase()
|
||||||
var icon = (device.icon || "").toLowerCase()
|
var icon = (device.icon || "").toLowerCase()
|
||||||
if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod")
|
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"
|
return "headset"
|
||||||
|
}
|
||||||
|
|
||||||
if (icon.includes("mouse") || name.includes("mouse"))
|
if (icon.includes("mouse") || name.includes("mouse")) {
|
||||||
return "mouse"
|
return "mouse"
|
||||||
|
}
|
||||||
if (icon.includes("keyboard") || name.includes("keyboard"))
|
if (icon.includes("keyboard") || name.includes("keyboard")) {
|
||||||
return "keyboard"
|
return "keyboard"
|
||||||
|
}
|
||||||
if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android")
|
if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android")
|
||||||
|| name.includes("samsung"))
|
|| name.includes("samsung")) {
|
||||||
return "smartphone"
|
return "smartphone"
|
||||||
|
}
|
||||||
if (icon.includes("watch") || name.includes("watch"))
|
if (icon.includes("watch") || name.includes("watch")) {
|
||||||
return "watch"
|
return "watch"
|
||||||
|
}
|
||||||
if (icon.includes("speaker") || name.includes("speaker"))
|
if (icon.includes("speaker") || name.includes("speaker")) {
|
||||||
return "speaker"
|
return "speaker"
|
||||||
|
}
|
||||||
if (icon.includes("display") || name.includes("tv"))
|
if (icon.includes("display") || name.includes("tv")) {
|
||||||
return "tv"
|
return "tv"
|
||||||
|
}
|
||||||
return "bluetooth"
|
return "bluetooth"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,60 +90,91 @@ Singleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSignalStrength(device) {
|
function getSignalStrength(device) {
|
||||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
|
if (device.pairing) {
|
||||||
return "Unknown"
|
return "Pairing..."
|
||||||
|
}
|
||||||
|
if (device.blocked) {
|
||||||
|
return "Blocked"
|
||||||
|
}
|
||||||
|
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
|
||||||
|
return "Signal: Unknown"
|
||||||
|
}
|
||||||
var signal = device.signalStrength
|
var signal = device.signalStrength
|
||||||
if (signal >= 80)
|
if (signal >= 80) {
|
||||||
return "Excellent"
|
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)
|
function getBattery(device) {
|
||||||
return "Good"
|
return `Battery: ${Math.round(device.battery * 100)}`
|
||||||
|
|
||||||
if (signal >= 40)
|
|
||||||
return "Fair"
|
|
||||||
|
|
||||||
if (signal >= 20)
|
|
||||||
return "Poor"
|
|
||||||
|
|
||||||
return "Very Poor"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSignalIcon(device) {
|
function getSignalIcon(device) {
|
||||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
|
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
|
||||||
return "signal_cellular_null"
|
return "signal_cellular_null"
|
||||||
|
}
|
||||||
var signal = device.signalStrength
|
var signal = device.signalStrength
|
||||||
if (signal >= 80)
|
if (signal >= 80) {
|
||||||
return "signal_cellular_4_bar"
|
return "signal_cellular_4_bar"
|
||||||
|
}
|
||||||
if (signal >= 60)
|
if (signal >= 60) {
|
||||||
return "signal_cellular_3_bar"
|
return "signal_cellular_3_bar"
|
||||||
|
}
|
||||||
if (signal >= 40)
|
if (signal >= 40) {
|
||||||
return "signal_cellular_2_bar"
|
return "signal_cellular_2_bar"
|
||||||
|
}
|
||||||
if (signal >= 20)
|
if (signal >= 20) {
|
||||||
return "signal_cellular_1_bar"
|
return "signal_cellular_1_bar"
|
||||||
|
}
|
||||||
return "signal_cellular_0_bar"
|
return "signal_cellular_0_bar"
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDeviceBusy(device) {
|
function isDeviceBusy(device) {
|
||||||
if (!device)
|
if (!device) {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return device.pairing || device.state === BluetoothDeviceState.Disconnecting
|
return device.pairing || device.state === BluetoothDeviceState.Disconnecting
|
||||||
|| device.state === BluetoothDeviceState.Connecting
|
|| device.state === BluetoothDeviceState.Connecting
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectDeviceWithTrust(device) {
|
function connectDeviceWithTrust(device) {
|
||||||
if (!device)
|
if (!device) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
device.trusted = true
|
device.trusted = true
|
||||||
device.connect()
|
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) {
|
function setBluetoothEnabled(enabled) {
|
||||||
if (!adapter) {
|
if (!adapter) {
|
||||||
Logger.warn("Bluetooth", "No adapter available")
|
Logger.warn("Bluetooth", "No adapter available")
|
||||||
|
|
|
||||||
|
|
@ -4,63 +4,129 @@ import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Night Light properties - directly bound to settings
|
// Night Light properties - directly bound to settings
|
||||||
readonly property var params: Settings.data.nightLight
|
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
|
Component.onCompleted: apply()
|
||||||
readonly property color overlayColor: params.enabled ? calculateOverlayColor() : "transparent"
|
|
||||||
property bool isActive: params.enabled && (params.autoSchedule ? isWithinSchedule() : true)
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
function buildCommand() {
|
||||||
Logger.log("NightLight", "Service started")
|
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))
|
||||||
function calculateOverlayColor() {
|
var loCfg = params.lowTemp || 3500
|
||||||
if (!isActive) {
|
var hiCfg = params.highTemp || 6500
|
||||||
return "transparent"
|
var lowTemp = Math.round(hiCfg - (hiCfg - loCfg) * Math.pow(i, 0.6))
|
||||||
}
|
cmd.push("-t", lowTemp.toString())
|
||||||
|
cmd.push("-T", hiCfg.toString())
|
||||||
// More vibrant color formula - stronger effect at high warmth
|
if (params.autoSchedule && LocationService.data.coordinatesReady && LocationService.data.stableLatitude !== ""
|
||||||
var red = 1.0
|
&& LocationService.data.stableLongitude !== "") {
|
||||||
var green = 1.0 - (0.43 * params.intensity)
|
cmd.push("-l", LocationService.data.stableLatitude)
|
||||||
var blue = 1.0 - (0.84 * params.intensity)
|
cmd.push("-L", LocationService.data.stableLongitude)
|
||||||
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
|
|
||||||
} else {
|
} 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 {
|
Timer {
|
||||||
interval: 60000 // Check every minute
|
id: debounceStart
|
||||||
running: params.enabled && params.autoSchedule
|
interval: 300
|
||||||
repeat: true
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
isActive = isWithinSchedule()
|
if (params.enabled && nextCommand.length > 0) {
|
||||||
|
runner.command = nextCommand
|
||||||
|
runner.running = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,9 @@ RowLayout {
|
||||||
property string description: ""
|
property string description: ""
|
||||||
property bool checked: false
|
property bool checked: false
|
||||||
property bool hovering: false
|
property bool hovering: false
|
||||||
// Smaller default footprint than NToggle
|
property color activeColor: Color.mPrimary
|
||||||
property int baseSize: Math.max(Style.baseWidgetSize * 0.8, Math.round(14 / scaling))
|
property color activeOnColor: Color.mOnPrimary
|
||||||
|
property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14)
|
||||||
|
|
||||||
signal toggled(bool checked)
|
signal toggled(bool checked)
|
||||||
signal entered
|
signal entered
|
||||||
|
|
@ -23,6 +24,7 @@ RowLayout {
|
||||||
NLabel {
|
NLabel {
|
||||||
label: root.label
|
label: root.label
|
||||||
description: root.description
|
description: root.description
|
||||||
|
visible: root.label !== "" || root.description !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
@ -30,16 +32,16 @@ RowLayout {
|
||||||
|
|
||||||
implicitWidth: root.baseSize * scaling
|
implicitWidth: root.baseSize * scaling
|
||||||
implicitHeight: root.baseSize * scaling
|
implicitHeight: root.baseSize * scaling
|
||||||
radius: Math.max(2 * scaling, Style.radiusXS * scaling)
|
radius: Style.radiusXS * scaling
|
||||||
color: root.checked ? Color.mPrimary : Color.mSurface
|
color: root.checked ? root.activeColor : Color.mSurface
|
||||||
border.color: root.checked ? Color.mPrimary : Color.mOutline
|
border.color: root.checked ? root.activeColor : Color.mOutline
|
||||||
border.width: Math.max(1, Style.borderM * scaling)
|
border.width: Math.max(1, Style.borderM * scaling)
|
||||||
|
|
||||||
NIcon {
|
NIcon {
|
||||||
visible: root.checked
|
visible: root.checked
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "check"
|
text: "check"
|
||||||
color: Color.mOnPrimary
|
color: root.activeOnColor
|
||||||
font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling
|
font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,74 +4,68 @@ import QtQuick.Layouts
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Item {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string label: ""
|
property string label: ""
|
||||||
property string description: ""
|
property string description: ""
|
||||||
property bool readOnly: false
|
property bool readOnly: false
|
||||||
property bool enabled: true
|
property bool enabled: true
|
||||||
|
property int inputMaxWidth: 420 * scaling
|
||||||
|
|
||||||
property alias text: input.text
|
property alias text: input.text
|
||||||
property alias placeholderText: input.placeholderText
|
property alias placeholderText: input.placeholderText
|
||||||
|
property alias inputMethodHints: input.inputMethodHints
|
||||||
|
|
||||||
signal editingFinished
|
signal editingFinished
|
||||||
|
|
||||||
// Sizing
|
spacing: Style.marginS * scaling
|
||||||
implicitWidth: Style.sliderWidth * 1.6 * scaling
|
implicitHeight: frame.height
|
||||||
implicitHeight: Style.baseWidgetSize * 2.75 * scaling
|
|
||||||
|
|
||||||
ColumnLayout {
|
NLabel {
|
||||||
spacing: Style.marginXXS * scaling
|
label: root.label
|
||||||
Layout.fillWidth: true
|
description: root.description
|
||||||
|
visible: root.label !== "" || root.description !== ""
|
||||||
|
}
|
||||||
|
|
||||||
NLabel {
|
// Container
|
||||||
label: root.label
|
Rectangle {
|
||||||
description: root.description
|
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
|
RowLayout {
|
||||||
Rectangle {
|
anchors.fill: parent
|
||||||
id: frame
|
anchors.leftMargin: Style.marginM * scaling
|
||||||
Layout.topMargin: Style.marginXS * scaling
|
anchors.rightMargin: Style.marginM * scaling
|
||||||
implicitWidth: root.width
|
spacing: Style.marginS * scaling
|
||||||
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)
|
|
||||||
|
|
||||||
// Focus ring
|
TextField {
|
||||||
Rectangle {
|
id: input
|
||||||
anchors.fill: parent
|
Layout.fillWidth: true
|
||||||
radius: frame.radius
|
echoMode: TextInput.Normal
|
||||||
color: Color.transparent
|
readOnly: root.readOnly
|
||||||
border.color: input.activeFocus ? Color.mSecondary : Color.transparent
|
enabled: root.enabled
|
||||||
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0
|
color: Color.mOnSurface
|
||||||
}
|
placeholderTextColor: Color.mOnSurfaceVariant
|
||||||
|
background: null
|
||||||
RowLayout {
|
font.pointSize: Style.fontSizeS * scaling
|
||||||
anchors.fill: parent
|
onEditingFinished: root.editingFinished()
|
||||||
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.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import qs.Modules.Calendar
|
||||||
import qs.Modules.Dock
|
import qs.Modules.Dock
|
||||||
import qs.Modules.IPC
|
import qs.Modules.IPC
|
||||||
import qs.Modules.LockScreen
|
import qs.Modules.LockScreen
|
||||||
import qs.Modules.NightLight
|
|
||||||
import qs.Modules.Notification
|
import qs.Modules.Notification
|
||||||
import qs.Modules.SettingsPanel
|
import qs.Modules.SettingsPanel
|
||||||
import qs.Modules.PowerPanel
|
import qs.Modules.PowerPanel
|
||||||
|
|
@ -51,12 +50,10 @@ ShellRoot {
|
||||||
|
|
||||||
ToastOverlay {}
|
ToastOverlay {}
|
||||||
|
|
||||||
NightLightOverlay {}
|
|
||||||
|
|
||||||
IPCManager {}
|
IPCManager {}
|
||||||
|
|
||||||
// ------------------------------
|
// ------------------------------
|
||||||
// All the panels
|
// All the NPanels
|
||||||
Launcher {
|
Launcher {
|
||||||
id: launcherPanel
|
id: launcherPanel
|
||||||
objectName: "launcherPanel"
|
objectName: "launcherPanel"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue