mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Merge branch 'master' of https://github.com/bbedward/dank-material-dark-shell
This commit is contained in:
@@ -68,7 +68,6 @@ Singleton {
|
||||
|
||||
function parseSettings(content) {
|
||||
try {
|
||||
console.log("Settings file content:", content)
|
||||
if (content && content.trim()) {
|
||||
var settings = JSON.parse(content)
|
||||
themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0
|
||||
|
||||
@@ -12,12 +12,13 @@ Specifically created for [Niri](https://github.com/YaLTeR/niri).
|
||||
|
||||
```bash
|
||||
# Arch
|
||||
paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava wl-clipboard
|
||||
paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava wl-clipboard ddcutil
|
||||
|
||||
# Some dependencies are optional
|
||||
# - cava for audio visualizer, without it music will just randomly visualize
|
||||
# - cliphist for clipboard history
|
||||
# - matugen for dynamic themes based on wallpaper
|
||||
# - ddcutil for brightness changing
|
||||
```
|
||||
|
||||
2. Configure SwayBG (Optional)
|
||||
|
||||
@@ -14,7 +14,6 @@ Singleton {
|
||||
property var applicationsByExec: ({})
|
||||
property bool ready: false
|
||||
|
||||
// Pre-prepared fuzzy search data
|
||||
property var preppedApps: []
|
||||
|
||||
|
||||
@@ -33,7 +32,6 @@ Singleton {
|
||||
|
||||
|
||||
function loadApplications() {
|
||||
// Trigger rescan on next frame to avoid blocking
|
||||
Qt.callLater(function() {
|
||||
var allApps = Array.from(DesktopEntries.applications.values)
|
||||
|
||||
@@ -41,7 +39,6 @@ Singleton {
|
||||
.filter(app => !app.noDisplay)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// Build lookup maps
|
||||
var byName = {}
|
||||
var byExec = {}
|
||||
|
||||
@@ -49,7 +46,6 @@ Singleton {
|
||||
var app = applications[i]
|
||||
byName[app.name.toLowerCase()] = app
|
||||
|
||||
// Clean exec string for lookup
|
||||
var execProp = app.execString || ""
|
||||
var cleanExec = execProp ? execProp.replace(/%[fFuU]/g, "").trim() : ""
|
||||
if (cleanExec) {
|
||||
@@ -60,7 +56,6 @@ Singleton {
|
||||
applicationsByName = byName
|
||||
applicationsByExec = byExec
|
||||
|
||||
// Prepare fuzzy search data
|
||||
preppedApps = applications.map(app => ({
|
||||
name: Fuzzy.prepare(app.name || ""),
|
||||
comment: Fuzzy.prepare(app.comment || ""),
|
||||
@@ -84,12 +79,10 @@ Singleton {
|
||||
return []
|
||||
}
|
||||
|
||||
// Use fuzzy search with both name and comment fields
|
||||
var results = Fuzzy.go(query, preppedApps, {
|
||||
all: false,
|
||||
keys: ["name", "comment"],
|
||||
scoreFn: r => {
|
||||
// Prioritize name matches over comment matches
|
||||
var nameScore = r[0] ? r[0].score : 0
|
||||
var commentScore = r[1] ? r[1].score : 0
|
||||
return nameScore > 0 ? nameScore * 0.9 + commentScore * 0.1 : commentScore * 0.5
|
||||
@@ -97,7 +90,6 @@ Singleton {
|
||||
limit: 50
|
||||
})
|
||||
|
||||
// Extract the desktop entries from results
|
||||
return results.map(r => r.obj.entry)
|
||||
}
|
||||
|
||||
@@ -180,7 +172,6 @@ Singleton {
|
||||
return false
|
||||
}
|
||||
|
||||
// DesktopEntry objects have an execute() method
|
||||
if (typeof app.execute === "function") {
|
||||
app.execute()
|
||||
return true
|
||||
|
||||
@@ -1,364 +1,264 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
|
||||
readonly property bool sinkMuted: sink?.audio?.muted ?? false
|
||||
readonly property bool sourceMuted: source?.audio?.muted ?? false
|
||||
readonly property real volumeLevel: (sink?.audio?.volume ?? 0) * 100
|
||||
readonly property real micLevel: (source?.audio?.volume ?? 0) * 100
|
||||
|
||||
signal audioVolumeChanged(real volume)
|
||||
signal audioMicLevelChanged(real level)
|
||||
signal audioMuteChanged(bool muted)
|
||||
signal audioMicMuteChanged(bool muted)
|
||||
signal audioDeviceChanged()
|
||||
|
||||
property int volumeLevel: 50
|
||||
onVolumeLevelChanged: audioVolumeChanged(volumeLevel)
|
||||
onMicLevelChanged: audioMicLevelChanged(micLevel)
|
||||
onSinkMutedChanged: audioMuteChanged(sinkMuted)
|
||||
onSourceMutedChanged: audioMicMuteChanged(sourceMuted)
|
||||
onSinkChanged: {
|
||||
audioDeviceChanged()
|
||||
}
|
||||
onSourceChanged: {
|
||||
audioDeviceChanged()
|
||||
}
|
||||
|
||||
property var audioSinks: []
|
||||
property string currentAudioSink: ""
|
||||
|
||||
// Microphone properties
|
||||
property int micLevel: 50
|
||||
property var audioSources: []
|
||||
property string currentAudioSource: ""
|
||||
|
||||
// Device scanning control
|
||||
property bool deviceScanningEnabled: false
|
||||
property bool initialScanComplete: false
|
||||
|
||||
// Real Audio Control
|
||||
Process {
|
||||
id: volumeChecker
|
||||
command: ["bash", "-c", "pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
|
||||
running: true
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
root.volumeLevel = Math.min(100, parseInt(data.trim()) || 50)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(updateDevices)
|
||||
}
|
||||
|
||||
function updateDevices() {
|
||||
updateAudioSinks()
|
||||
updateAudioSources()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Pipewire
|
||||
function onReadyChanged() {
|
||||
if (Pipewire.ready) {
|
||||
updateAudioSinks()
|
||||
updateAudioSources()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Microphone level checker
|
||||
Process {
|
||||
id: micLevelChecker
|
||||
command: ["bash", "-c", "pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
|
||||
running: true
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
root.micLevel = Math.min(100, parseInt(data.trim()) || 50)
|
||||
}
|
||||
}
|
||||
function onDefaultAudioSinkChanged() {
|
||||
updateAudioSinks()
|
||||
}
|
||||
function onDefaultAudioSourceChanged() {
|
||||
updateAudioSources()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: audioSinkLister
|
||||
command: ["pactl", "list", "sinks"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
let sinks = []
|
||||
let lines = text.trim().split('\n')
|
||||
|
||||
let currentSink = null
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
|
||||
// New sink starts
|
||||
if (line.startsWith('Sink #')) {
|
||||
if (currentSink && currentSink.name && currentSink.id) {
|
||||
sinks.push(currentSink)
|
||||
}
|
||||
|
||||
let sinkId = line.replace('Sink #', '').trim()
|
||||
currentSink = {
|
||||
id: sinkId,
|
||||
name: "",
|
||||
displayName: "",
|
||||
description: "",
|
||||
nick: "",
|
||||
active: false
|
||||
}
|
||||
}
|
||||
// Get the Name field
|
||||
else if (line.startsWith('Name: ') && currentSink) {
|
||||
currentSink.name = line.replace('Name: ', '').trim()
|
||||
}
|
||||
// Get the Description field (main display name)
|
||||
else if (line.startsWith('Description: ') && currentSink) {
|
||||
currentSink.description = line.replace('Description: ', '').trim()
|
||||
}
|
||||
// Get device.description as fallback
|
||||
else if (line.includes('device.description = ') && currentSink && !currentSink.description) {
|
||||
currentSink.description = line.replace('device.description = ', '').replace(/"/g, '').trim()
|
||||
}
|
||||
// Get node.nick as another fallback option
|
||||
else if (line.includes('node.nick = ') && currentSink && !currentSink.description) {
|
||||
currentSink.nick = line.replace('node.nick = ', '').replace(/"/g, '').trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last sink
|
||||
if (currentSink && currentSink.name && currentSink.id) {
|
||||
sinks.push(currentSink)
|
||||
}
|
||||
|
||||
// Process display names
|
||||
for (let sink of sinks) {
|
||||
let displayName = sink.description
|
||||
|
||||
// If no good description, try nick
|
||||
if (!displayName || displayName === sink.name) {
|
||||
displayName = sink.nick
|
||||
}
|
||||
|
||||
// Still no good name? Fall back to smart defaults
|
||||
if (!displayName || displayName === sink.name) {
|
||||
if (sink.name.includes("analog-stereo")) displayName = "Built-in Speakers"
|
||||
else if (sink.name.includes("bluez")) displayName = "Bluetooth Audio"
|
||||
else if (sink.name.includes("usb")) displayName = "USB Audio"
|
||||
else if (sink.name.includes("hdmi")) displayName = "HDMI Audio"
|
||||
else if (sink.name.includes("easyeffects")) displayName = "EasyEffects"
|
||||
else displayName = sink.name
|
||||
}
|
||||
|
||||
sink.displayName = displayName
|
||||
}
|
||||
|
||||
root.audioSinks = sinks
|
||||
defaultSinkChecker.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Audio source (microphone) lister
|
||||
Process {
|
||||
id: audioSourceLister
|
||||
command: ["pactl", "list", "sources"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
let sources = []
|
||||
let lines = text.trim().split('\n')
|
||||
|
||||
let currentSource = null
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
|
||||
// New source starts
|
||||
if (line.startsWith('Source #')) {
|
||||
if (currentSource && currentSource.name && currentSource.id) {
|
||||
sources.push(currentSource)
|
||||
}
|
||||
currentSource = {
|
||||
id: line.replace('Source #', '').replace(':', ''),
|
||||
name: '',
|
||||
displayName: '',
|
||||
active: false
|
||||
}
|
||||
}
|
||||
// Source name
|
||||
else if (line.startsWith('Name: ') && currentSource) {
|
||||
currentSource.name = line.replace('Name: ', '')
|
||||
}
|
||||
// Description (display name)
|
||||
else if (line.startsWith('Description: ') && currentSource) {
|
||||
let desc = line.replace('Description: ', '')
|
||||
currentSource.displayName = desc
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last source
|
||||
if (currentSource && currentSource.name && currentSource.id) {
|
||||
sources.push(currentSource)
|
||||
}
|
||||
|
||||
// Filter out monitor sources (we want actual input devices)
|
||||
sources = sources.filter(source => !source.name.includes('.monitor'))
|
||||
|
||||
root.audioSources = sources
|
||||
defaultSourceChecker.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: defaultSinkChecker
|
||||
command: ["pactl", "get-default-sink"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
root.currentAudioSink = data.trim()
|
||||
|
||||
// Update active status in audioSinks
|
||||
let updatedSinks = []
|
||||
for (let sink of root.audioSinks) {
|
||||
updatedSinks.push({
|
||||
id: sink.id,
|
||||
name: sink.name,
|
||||
displayName: sink.displayName,
|
||||
active: sink.name === root.currentAudioSink
|
||||
})
|
||||
}
|
||||
root.audioSinks = updatedSinks
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default source (microphone) checker
|
||||
Process {
|
||||
id: defaultSourceChecker
|
||||
command: ["pactl", "get-default-source"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
root.currentAudioSource = data.trim()
|
||||
|
||||
// Update active status in audioSources
|
||||
let updatedSources = []
|
||||
for (let source of root.audioSources) {
|
||||
updatedSources.push({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
displayName: source.displayName,
|
||||
active: source.name === root.currentAudioSource
|
||||
})
|
||||
}
|
||||
root.audioSources = updatedSources
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVolume(percentage) {
|
||||
let volumeSetProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "' + percentage + '%"]
|
||||
running: true
|
||||
onExited: volumeChecker.running = true
|
||||
}
|
||||
', root)
|
||||
}
|
||||
|
||||
function setMicLevel(percentage) {
|
||||
let micSetProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["pactl", "set-source-volume", "@DEFAULT_SOURCE@", "' + percentage + '%"]
|
||||
running: true
|
||||
onExited: micLevelChecker.running = true
|
||||
}
|
||||
', root)
|
||||
}
|
||||
|
||||
function setAudioSink(sinkName) {
|
||||
console.log("Setting audio sink to:", sinkName)
|
||||
|
||||
// Use a more reliable approach instead of Qt.createQmlObject
|
||||
sinkSetProcess.command = ["pactl", "set-default-sink", sinkName]
|
||||
sinkSetProcess.running = true
|
||||
}
|
||||
|
||||
// Dedicated process for setting audio sink
|
||||
Process {
|
||||
id: sinkSetProcess
|
||||
running: false
|
||||
|
||||
onExited: (exitCode) => {
|
||||
console.log("Audio sink change exit code:", exitCode)
|
||||
if (exitCode === 0) {
|
||||
console.log("Audio sink changed successfully")
|
||||
// Refresh current sink and list
|
||||
defaultSinkChecker.running = true
|
||||
if (root.deviceScanningEnabled) {
|
||||
audioSinkLister.running = true
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to change audio sink")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setAudioSource(sourceName) {
|
||||
console.log("Setting audio source to:", sourceName)
|
||||
|
||||
sourceSetProcess.command = ["pactl", "set-default-source", sourceName]
|
||||
sourceSetProcess.running = true
|
||||
}
|
||||
|
||||
// Dedicated process for setting audio source
|
||||
Process {
|
||||
id: sourceSetProcess
|
||||
running: false
|
||||
|
||||
onExited: (exitCode) => {
|
||||
console.log("Audio source change exit code:", exitCode)
|
||||
if (exitCode === 0) {
|
||||
console.log("Audio source changed successfully")
|
||||
// Refresh current source and list
|
||||
defaultSourceChecker.running = true
|
||||
if (root.deviceScanningEnabled) {
|
||||
audioSourceLister.running = true
|
||||
}
|
||||
} else {
|
||||
console.error("Failed to change audio source")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Timer to check for node changes since ObjectModel doesn't expose change signals
|
||||
Timer {
|
||||
interval: 5000
|
||||
running: root.deviceScanningEnabled && root.initialScanComplete
|
||||
interval: 2000
|
||||
running: Pipewire.ready
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (root.deviceScanningEnabled) {
|
||||
audioSinkLister.running = true
|
||||
audioSourceLister.running = true
|
||||
if (Pipewire.nodes && Pipewire.nodes.values) {
|
||||
let currentCount = Pipewire.nodes.values.length
|
||||
if (currentCount !== lastNodeCount) {
|
||||
lastNodeCount = currentCount
|
||||
updateAudioSinks()
|
||||
updateAudioSources()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property int lastNodeCount: 0
|
||||
|
||||
function updateAudioSinks() {
|
||||
if (!Pipewire.ready || !Pipewire.nodes) return
|
||||
|
||||
let sinks = []
|
||||
|
||||
if (Pipewire.nodes.values) {
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (!node) continue
|
||||
|
||||
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink && !node.isStream) {
|
||||
let displayName = getDisplayName(node)
|
||||
|
||||
sinks.push({
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
displayName: displayName,
|
||||
subtitle: getDeviceSubtitle(node.name),
|
||||
active: node === root.sink,
|
||||
node: node
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioSinks = sinks
|
||||
}
|
||||
|
||||
function updateAudioSources() {
|
||||
if (!Pipewire.ready || !Pipewire.nodes) return
|
||||
|
||||
let sources = []
|
||||
|
||||
if (Pipewire.nodes.values) {
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (!node) continue
|
||||
|
||||
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.isStream && !node.name.includes('.monitor')) {
|
||||
sources.push({
|
||||
id: node.id.toString(),
|
||||
name: node.name,
|
||||
displayName: getDisplayName(node),
|
||||
subtitle: getDeviceSubtitle(node.name),
|
||||
active: node === root.source,
|
||||
node: node
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
audioSources = sources
|
||||
}
|
||||
|
||||
function getDisplayName(node) {
|
||||
// Check properties first (this is key for Bluetooth devices!)
|
||||
if (node.properties && node.properties["device.description"]) {
|
||||
return node.properties["device.description"]
|
||||
}
|
||||
|
||||
if (node.description && node.description !== node.name) {
|
||||
return node.description
|
||||
}
|
||||
|
||||
if (node.nickname && node.nickname !== node.name) {
|
||||
return node.nickname
|
||||
}
|
||||
|
||||
// Fallback to name processing
|
||||
if (node.name.includes("analog-stereo")) return "Built-in Speakers"
|
||||
else if (node.name.includes("bluez")) return "Bluetooth Audio"
|
||||
else if (node.name.includes("usb")) return "USB Audio"
|
||||
else if (node.name.includes("hdmi")) return "HDMI Audio"
|
||||
|
||||
return node.name
|
||||
}
|
||||
|
||||
function getDeviceSubtitle(nodeName) {
|
||||
if (!nodeName) return ""
|
||||
|
||||
// Simple subtitle based on node name patterns
|
||||
if (nodeName.includes('usb-')) {
|
||||
if (nodeName.includes('SteelSeries')) {
|
||||
return "USB Gaming Headset"
|
||||
} else if (nodeName.includes('Generic')) {
|
||||
return "USB Audio Device"
|
||||
}
|
||||
return "USB Audio"
|
||||
} else if (nodeName.includes('pci-')) {
|
||||
if (nodeName.includes('01_00.1') || nodeName.includes('01:00.1')) {
|
||||
return "NVIDIA GPU Audio"
|
||||
}
|
||||
return "PCI Audio"
|
||||
} else if (nodeName.includes('bluez')) {
|
||||
return "Bluetooth Audio"
|
||||
} else if (nodeName.includes('analog')) {
|
||||
return "Built-in Audio"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
readonly property string currentAudioSink: sink?.name ?? ""
|
||||
readonly property string currentAudioSource: source?.name ?? ""
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("AudioService: Starting initialization...")
|
||||
// Do initial device scan
|
||||
audioSinkLister.running = true
|
||||
audioSourceLister.running = true
|
||||
initialScanComplete = true
|
||||
console.log("AudioService: Initialization complete")
|
||||
readonly property string currentSinkDisplayName: {
|
||||
if (!sink) return ""
|
||||
|
||||
for (let sinkInfo of audioSinks) {
|
||||
if (sinkInfo.node === sink) {
|
||||
return sinkInfo.displayName
|
||||
}
|
||||
}
|
||||
|
||||
return sink.description || sink.name
|
||||
}
|
||||
|
||||
// Control functions for managing device scanning
|
||||
function enableDeviceScanning(enabled) {
|
||||
console.log("AudioService: Device scanning", enabled ? "enabled" : "disabled")
|
||||
root.deviceScanningEnabled = enabled
|
||||
if (enabled && root.initialScanComplete) {
|
||||
// Immediately scan when enabled
|
||||
audioSinkLister.running = true
|
||||
audioSourceLister.running = true
|
||||
readonly property string currentSourceDisplayName: {
|
||||
if (!source) return ""
|
||||
|
||||
for (let sourceInfo of audioSources) {
|
||||
if (sourceInfo.node === source) {
|
||||
return sourceInfo.displayName
|
||||
}
|
||||
}
|
||||
|
||||
return source.description || source.name
|
||||
}
|
||||
|
||||
function setVolume(percentage) {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
sink.audio.muted = false
|
||||
sink.audio.volume = percentage / 100
|
||||
}
|
||||
}
|
||||
|
||||
// Manual refresh function for when user opens audio settings
|
||||
function refreshDevices() {
|
||||
console.log("AudioService: Manual device refresh triggered")
|
||||
audioSinkLister.running = true
|
||||
audioSourceLister.running = true
|
||||
|
||||
function setMicLevel(percentage) {
|
||||
if (source?.ready && source?.audio) {
|
||||
source.audio.muted = false
|
||||
source.audio.volume = percentage / 100
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
sink.audio.muted = !sink.audio.muted
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMicMute() {
|
||||
if (source?.ready && source?.audio) {
|
||||
source.audio.muted = !source.audio.muted
|
||||
}
|
||||
}
|
||||
|
||||
function setAudioSink(sinkName) {
|
||||
if (Pipewire.nodes.values) {
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (node && node.name === sinkName && (node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink && !node.isStream) {
|
||||
Pipewire.preferredDefaultAudioSink = node
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setAudioSource(sourceName) {
|
||||
if (Pipewire.nodes.values) {
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (node && node.name === sourceName && (node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.isStream) {
|
||||
Pipewire.preferredDefaultAudioSource = node
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
id: nodeTracker
|
||||
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
|
||||
}
|
||||
}
|
||||
@@ -1,213 +1,120 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.UPower
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Battery properties
|
||||
property bool batteryAvailable: false
|
||||
property int batteryLevel: 0
|
||||
property string batteryStatus: "Unknown"
|
||||
property int timeRemaining: 0
|
||||
property bool isCharging: false
|
||||
property bool isLowBattery: false
|
||||
property int batteryHealth: 100
|
||||
property string batteryTechnology: "Unknown"
|
||||
property int cycleCount: 0
|
||||
property int batteryCapacity: 0
|
||||
property var powerProfiles: []
|
||||
property string activePowerProfile: ""
|
||||
// Debug mode for testing on desktop systems without batteries
|
||||
property bool debugMode: false // Set to true to enable fake battery for testing
|
||||
|
||||
// Check if battery is available
|
||||
Process {
|
||||
id: batteryAvailabilityChecker
|
||||
command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"]
|
||||
running: true
|
||||
// Debug fake battery data
|
||||
property int debugBatteryLevel: 65
|
||||
property string debugBatteryStatus: "Discharging"
|
||||
property int debugTimeRemaining: 7200 // 2 hours in seconds
|
||||
property bool debugIsCharging: false
|
||||
property int debugBatteryHealth: 88
|
||||
property string debugBatteryTechnology: "Li-ion"
|
||||
property int debugBatteryCapacity: 45000 // 45 Wh in mWh
|
||||
|
||||
property bool batteryAvailable: debugMode || (battery.ready && battery.isLaptopBattery)
|
||||
property int batteryLevel: debugMode ? debugBatteryLevel : Math.round(battery.percentage)
|
||||
property string batteryStatus: debugMode ? debugBatteryStatus : UPowerDeviceState.toString(battery.state)
|
||||
property int timeRemaining: debugMode ? debugTimeRemaining : (battery.timeToEmpty || battery.timeToFull)
|
||||
property bool isCharging: debugMode ? debugIsCharging : (battery.state === UPowerDeviceState.Charging)
|
||||
property bool isLowBattery: debugMode ? (debugBatteryLevel <= 20) : (battery.percentage <= 20)
|
||||
property int batteryHealth: debugMode ? debugBatteryHealth : (battery.healthSupported ? Math.round(battery.healthPercentage) : 100)
|
||||
property string batteryTechnology: {
|
||||
if (debugMode) return debugBatteryTechnology
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
root.batteryAvailable = true
|
||||
console.log("Battery found:", data.trim())
|
||||
batteryStatusChecker.running = true
|
||||
} else {
|
||||
root.batteryAvailable = false
|
||||
console.log("No battery found - this appears to be a desktop system")
|
||||
// Try to get technology from any available laptop battery
|
||||
for (let i = 0; i < UPower.devices.length; i++) {
|
||||
let device = UPower.devices[i]
|
||||
if (device.isLaptopBattery && device.ready) {
|
||||
// UPower doesn't expose technology directly, but we can get it from the model
|
||||
let model = device.model || ""
|
||||
if (model.toLowerCase().includes("li-ion") || model.toLowerCase().includes("lithium")) {
|
||||
return "Li-ion"
|
||||
} else if (model.toLowerCase().includes("li-po") || model.toLowerCase().includes("polymer")) {
|
||||
return "Li-polymer"
|
||||
} else if (model.toLowerCase().includes("nimh")) {
|
||||
return "NiMH"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
property int cycleCount: 0 // UPower doesn't expose cycle count
|
||||
property int batteryCapacity: debugMode ? debugBatteryCapacity : Math.round(battery.energyCapacity * 1000)
|
||||
property var powerProfiles: availableProfiles
|
||||
property string activePowerProfile: PowerProfile.toString(PowerProfiles.profile)
|
||||
|
||||
// Battery status checker
|
||||
Process {
|
||||
id: batteryStatusChecker
|
||||
command: ["bash", "-c", "if [ -d /sys/class/power_supply/BAT0 ] || [ -d /sys/class/power_supply/BAT1 ]; then upower -i $(upower -e | grep 'BAT') | grep -E 'state|percentage|time to|energy|technology|cycle-count' || acpi -b 2>/dev/null || echo 'fallback'; else echo 'no-battery'; fi"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim() === "no-battery") {
|
||||
root.batteryAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
if (text.trim() && text.trim() !== "fallback") {
|
||||
parseBatteryInfo(text.trim())
|
||||
} else {
|
||||
// Fallback to simple methods
|
||||
fallbackBatteryChecker.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Battery status check failed, trying fallback methods")
|
||||
fallbackBatteryChecker.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
property var battery: UPower.displayDevice
|
||||
|
||||
// Fallback battery checker using /sys files
|
||||
Process {
|
||||
id: fallbackBatteryChecker
|
||||
command: ["bash", "-c", "if [ -f /sys/class/power_supply/BAT0/capacity ]; then BAT=BAT0; elif [ -f /sys/class/power_supply/BAT1/capacity ]; then BAT=BAT1; else echo 'no-battery'; exit 1; fi; echo \"percentage: $(cat /sys/class/power_supply/$BAT/capacity)%\"; echo \"state: $(cat /sys/class/power_supply/$BAT/status 2>/dev/null || echo Unknown)\"; if [ -f /sys/class/power_supply/$BAT/technology ]; then echo \"technology: $(cat /sys/class/power_supply/$BAT/technology)\"; fi; if [ -f /sys/class/power_supply/$BAT/cycle_count ]; then echo \"cycle-count: $(cat /sys/class/power_supply/$BAT/cycle_count)\"; fi"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim() !== "no-battery") {
|
||||
parseBatteryInfo(text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power profiles checker (for systems with power-profiles-daemon)
|
||||
Process {
|
||||
id: powerProfilesChecker
|
||||
command: ["bash", "-c", "if command -v powerprofilesctl > /dev/null; then powerprofilesctl list 2>/dev/null; else echo 'not-available'; fi"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim() !== "not-available") {
|
||||
parsePowerProfiles(text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseBatteryInfo(batteryText) {
|
||||
let lines = batteryText.split('\n')
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim().toLowerCase()
|
||||
|
||||
if (line.includes('percentage:') || line.includes('capacity:')) {
|
||||
let match = line.match(/(\d+)%?/)
|
||||
if (match) {
|
||||
root.batteryLevel = parseInt(match[1])
|
||||
root.isLowBattery = root.batteryLevel <= 20
|
||||
}
|
||||
} else if (line.includes('state:') || line.includes('status:')) {
|
||||
let statusPart = line.split(':')[1]?.trim().toLowerCase() || line
|
||||
console.log("Raw battery status line:", line, "extracted status:", statusPart)
|
||||
|
||||
if (statusPart === 'charging') {
|
||||
root.batteryStatus = "Charging"
|
||||
root.isCharging = true
|
||||
console.log("Battery is charging")
|
||||
} else if (statusPart === 'discharging') {
|
||||
root.batteryStatus = "Discharging"
|
||||
root.isCharging = false
|
||||
console.log("Battery is discharging")
|
||||
} else if (statusPart === 'full') {
|
||||
root.batteryStatus = "Full"
|
||||
root.isCharging = false
|
||||
console.log("Battery is full")
|
||||
} else if (statusPart === 'not charging') {
|
||||
root.batteryStatus = "Not charging"
|
||||
root.isCharging = false
|
||||
console.log("Battery is not charging")
|
||||
} else {
|
||||
root.batteryStatus = statusPart.charAt(0).toUpperCase() + statusPart.slice(1) || "Unknown"
|
||||
root.isCharging = false
|
||||
console.log("Battery status unknown:", statusPart)
|
||||
}
|
||||
} else if (line.includes('time to')) {
|
||||
let match = line.match(/(\d+):(\d+)/)
|
||||
if (match) {
|
||||
root.timeRemaining = parseInt(match[1]) * 60 + parseInt(match[2])
|
||||
}
|
||||
} else if (line.includes('technology:')) {
|
||||
let tech = line.split(':')[1]?.trim() || "Unknown"
|
||||
root.batteryTechnology = tech.charAt(0).toUpperCase() + tech.slice(1)
|
||||
} else if (line.includes('cycle-count:')) {
|
||||
let match = line.match(/(\d+)/)
|
||||
if (match) {
|
||||
root.cycleCount = parseInt(match[1])
|
||||
}
|
||||
} else if (line.includes('energy-full:') || line.includes('capacity:')) {
|
||||
let match = line.match(/([\d.]+)\s*wh/i)
|
||||
if (match) {
|
||||
root.batteryCapacity = Math.round(parseFloat(match[1]) * 1000) // Convert to mWh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Battery status updated:", root.batteryLevel + "%", root.batteryStatus)
|
||||
}
|
||||
|
||||
function parsePowerProfiles(profileText) {
|
||||
let lines = profileText.split('\n')
|
||||
property var availableProfiles: {
|
||||
let profiles = []
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
if (line.includes('*')) {
|
||||
// Active profile
|
||||
let profileName = line.replace('*', '').trim()
|
||||
if (profileName.includes(':')) {
|
||||
profileName = profileName.split(':')[0].trim()
|
||||
}
|
||||
root.activePowerProfile = profileName
|
||||
profiles.push(profileName)
|
||||
} else if (line && !line.includes(':') && line.length > 0) {
|
||||
profiles.push(line)
|
||||
if (PowerProfiles.profile !== undefined) {
|
||||
profiles.push("power-saver")
|
||||
profiles.push("balanced")
|
||||
if (PowerProfiles.hasPerformanceProfile) {
|
||||
profiles.push("performance")
|
||||
}
|
||||
}
|
||||
|
||||
root.powerProfiles = profiles
|
||||
console.log("Power profiles available:", profiles, "Active:", root.activePowerProfile)
|
||||
return profiles
|
||||
}
|
||||
|
||||
// Timer to simulate battery changes in debug mode
|
||||
Timer {
|
||||
id: debugTimer
|
||||
interval: 5000 // Update every 5 seconds
|
||||
running: debugMode
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
// Simulate battery discharge/charge
|
||||
if (debugIsCharging) {
|
||||
debugBatteryLevel = Math.min(100, debugBatteryLevel + 1)
|
||||
if (debugBatteryLevel >= 100) {
|
||||
debugBatteryStatus = "Full"
|
||||
debugIsCharging = false
|
||||
}
|
||||
} else {
|
||||
debugBatteryLevel = Math.max(0, debugBatteryLevel - 1)
|
||||
if (debugBatteryLevel <= 15) {
|
||||
debugBatteryStatus = "Charging"
|
||||
debugIsCharging = true
|
||||
}
|
||||
}
|
||||
|
||||
// Update time remaining
|
||||
debugTimeRemaining = debugIsCharging ?
|
||||
Math.max(0, debugTimeRemaining - 300) : // 5 minutes less to full
|
||||
Math.max(0, debugTimeRemaining - 300) // 5 minutes less remaining
|
||||
}
|
||||
}
|
||||
|
||||
function setBatteryProfile(profileName) {
|
||||
if (!root.powerProfiles.includes(profileName)) {
|
||||
let profile = PowerProfile.Balanced
|
||||
|
||||
if (profileName === "power-saver") {
|
||||
profile = PowerProfile.PowerSaver
|
||||
} else if (profileName === "balanced") {
|
||||
profile = PowerProfile.Balanced
|
||||
} else if (profileName === "performance") {
|
||||
if (PowerProfiles.hasPerformanceProfile) {
|
||||
profile = PowerProfile.Performance
|
||||
} else {
|
||||
console.warn("Performance profile not available")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
console.warn("Invalid power profile:", profileName)
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Setting power profile to:", profileName)
|
||||
let profileProcess = Qt.createQmlObject(`
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["powerprofilesctl", "set", "${profileName}"]
|
||||
running: true
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("Power profile changed to:", "${profileName}")
|
||||
root.activePowerProfile = "${profileName}"
|
||||
} else {
|
||||
console.warn("Failed to change power profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
`, root)
|
||||
PowerProfiles.profile = profile
|
||||
}
|
||||
|
||||
function getBatteryIcon() {
|
||||
@@ -237,8 +144,8 @@ Singleton {
|
||||
function formatTimeRemaining() {
|
||||
if (root.timeRemaining <= 0) return "Unknown"
|
||||
|
||||
let hours = Math.floor(root.timeRemaining / 60)
|
||||
let minutes = root.timeRemaining % 60
|
||||
let hours = Math.floor(root.timeRemaining / 3600)
|
||||
let minutes = Math.floor((root.timeRemaining % 3600) / 60)
|
||||
|
||||
if (hours > 0) {
|
||||
return hours + "h " + minutes + "m"
|
||||
@@ -246,17 +153,4 @@ Singleton {
|
||||
return minutes + "m"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update timer
|
||||
Timer {
|
||||
interval: 30000
|
||||
running: root.batteryAvailable
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
batteryStatusChecker.running = true
|
||||
powerProfilesChecker.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Bluetooth
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
@@ -9,410 +9,475 @@ Singleton {
|
||||
|
||||
property bool bluetoothEnabled: false
|
||||
property bool bluetoothAvailable: false
|
||||
property var bluetoothDevices: []
|
||||
property var availableDevices: []
|
||||
readonly property list<BluetoothDevice> bluetoothDevices: []
|
||||
readonly property list<BluetoothDevice> availableDevices: []
|
||||
property bool scanning: false
|
||||
property bool discoverable: false
|
||||
|
||||
// Real Bluetooth Management
|
||||
Process {
|
||||
id: bluetoothStatusChecker
|
||||
command: ["bluetoothctl", "show"] // Use default controller
|
||||
running: true
|
||||
property var connectingDevices: ({})
|
||||
|
||||
Component.onCompleted: {
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Bluetooth
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller")
|
||||
root.bluetoothEnabled = text.includes("Powered: yes")
|
||||
|
||||
if (root.bluetoothEnabled && root.bluetoothAvailable) {
|
||||
bluetoothDeviceScanner.running = true
|
||||
function onDefaultAdapterChanged() {
|
||||
console.log("BluetoothService: Default adapter changed")
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Bluetooth.defaultAdapter
|
||||
|
||||
function onEnabledChanged() {
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onDiscoveringChanged() {
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : null
|
||||
|
||||
function onModelReset() {
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onItemAdded() {
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onItemRemoved() {
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Bluetooth.devices
|
||||
|
||||
function onModelReset() {
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onItemAdded() {
|
||||
updateDevices()
|
||||
}
|
||||
|
||||
function onItemRemoved() {
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
function refreshBluetoothState() {
|
||||
root.bluetoothAvailable = Bluetooth.defaultAdapter !== null
|
||||
root.bluetoothEnabled = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.enabled : false
|
||||
root.scanning = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discovering : false
|
||||
root.discoverable = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discoverable : false
|
||||
}
|
||||
|
||||
function updateDevices() {
|
||||
if (!Bluetooth.defaultAdapter) {
|
||||
clearDeviceList(root.bluetoothDevices)
|
||||
clearDeviceList(root.availableDevices)
|
||||
root.bluetoothDevices = []
|
||||
root.availableDevices = []
|
||||
return
|
||||
}
|
||||
|
||||
let newPairedDevices = []
|
||||
let newAvailableDevices = []
|
||||
let allNativeDevices = []
|
||||
|
||||
let adapterDevices = Bluetooth.defaultAdapter.devices
|
||||
if (adapterDevices.values) {
|
||||
allNativeDevices = allNativeDevices.concat(adapterDevices.values)
|
||||
}
|
||||
|
||||
if (Bluetooth.devices.values) {
|
||||
for (let device of Bluetooth.devices.values) {
|
||||
if (!allNativeDevices.some(d => d.address === device.address)) {
|
||||
allNativeDevices.push(device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let device of allNativeDevices) {
|
||||
if (!device) continue
|
||||
|
||||
let deviceType = getDeviceType(device.name || device.deviceName, device.icon)
|
||||
let displayName = device.name || device.deviceName
|
||||
|
||||
if (!displayName || displayName.startsWith('/org/bluez') || displayName.includes('hci0') || displayName.length < 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (device.paired) {
|
||||
let existingDevice = findDeviceInList(root.bluetoothDevices, device.address)
|
||||
if (existingDevice) {
|
||||
updateDeviceData(existingDevice, device, deviceType, displayName)
|
||||
newPairedDevices.push(existingDevice)
|
||||
} else {
|
||||
root.bluetoothDevices = []
|
||||
let newDevice = createBluetoothDevice(device, deviceType, displayName)
|
||||
newPairedDevices.push(newDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: bluetoothDeviceScanner
|
||||
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([0-9A-F:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ! $name =~ ^/org/bluez ]]; then info=$(bluetoothctl info $mac); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); battery=$(echo \"$info\" | grep -m1 'Battery Percentage:' | grep -o '[0-9]\\+'); echo \"$mac|$name|$connected|${battery:-}\"; fi; fi; done"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
let devices = []
|
||||
let lines = text.trim().split('\n')
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.trim()) {
|
||||
let parts = line.split('|')
|
||||
if (parts.length >= 3) {
|
||||
let mac = parts[0].trim()
|
||||
let name = parts[1].trim()
|
||||
let connected = parts[2].trim() === 'yes'
|
||||
let battery = parts[3] ? parseInt(parts[3]) : -1
|
||||
|
||||
// Skip if name is still a technical path
|
||||
if (name.startsWith('/org/bluez') || name.includes('hci0')) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine device type from name
|
||||
let type = "bluetooth"
|
||||
let nameLower = name.toLowerCase()
|
||||
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis")) type = "headset"
|
||||
else if (nameLower.includes("mouse")) type = "mouse"
|
||||
else if (nameLower.includes("keyboard")) type = "keyboard"
|
||||
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung")) type = "phone"
|
||||
else if (nameLower.includes("watch")) type = "watch"
|
||||
else if (nameLower.includes("speaker")) type = "speaker"
|
||||
|
||||
devices.push({
|
||||
mac: mac,
|
||||
name: name,
|
||||
type: type,
|
||||
connected: connected,
|
||||
battery: battery
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Bluetooth.defaultAdapter.discovering && isDeviceDiscoverable(device)) {
|
||||
let existingDevice = findDeviceInList(root.availableDevices, device.address)
|
||||
if (existingDevice) {
|
||||
updateDeviceData(existingDevice, device, deviceType, displayName)
|
||||
newAvailableDevices.push(existingDevice)
|
||||
} else {
|
||||
let newDevice = createBluetoothDevice(device, deviceType, displayName)
|
||||
newAvailableDevices.push(newDevice)
|
||||
}
|
||||
|
||||
root.bluetoothDevices = devices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanupOldDevices(root.bluetoothDevices, newPairedDevices)
|
||||
cleanupOldDevices(root.availableDevices, newAvailableDevices)
|
||||
|
||||
console.log("BluetoothService: Found", newPairedDevices.length, "paired devices and", newAvailableDevices.length, "available devices")
|
||||
|
||||
root.bluetoothDevices = newPairedDevices
|
||||
root.availableDevices = newAvailableDevices
|
||||
}
|
||||
|
||||
function createBluetoothDevice(nativeDevice, deviceType, displayName) {
|
||||
return deviceComponent.createObject(root, {
|
||||
mac: nativeDevice.address,
|
||||
name: displayName,
|
||||
type: deviceType,
|
||||
paired: nativeDevice.paired,
|
||||
connected: nativeDevice.connected,
|
||||
battery: nativeDevice.batteryAvailable ? Math.round(nativeDevice.battery * 100) : -1,
|
||||
signalStrength: nativeDevice.connected ? "excellent" : "unknown",
|
||||
canPair: !nativeDevice.paired,
|
||||
nativeDevice: nativeDevice,
|
||||
connecting: false,
|
||||
connectionFailed: false
|
||||
})
|
||||
}
|
||||
|
||||
function updateDeviceData(deviceObj, nativeDevice, deviceType, displayName) {
|
||||
deviceObj.name = displayName
|
||||
deviceObj.type = deviceType
|
||||
deviceObj.paired = nativeDevice.paired
|
||||
|
||||
// If device connected state changed, clear connecting/failed states and refresh audio
|
||||
if (deviceObj.connected !== nativeDevice.connected) {
|
||||
deviceObj.connecting = false
|
||||
deviceObj.connectionFailed = false
|
||||
|
||||
// Refresh audio devices when bluetooth audio device connects/disconnects
|
||||
if (deviceType === "headset" || deviceType === "speaker") {
|
||||
Qt.callLater(() => {
|
||||
if (typeof AudioService !== 'undefined') {
|
||||
AudioService.updateDevices()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deviceObj.connected = nativeDevice.connected
|
||||
deviceObj.battery = nativeDevice.batteryAvailable ? Math.round(nativeDevice.battery * 100) : -1
|
||||
deviceObj.signalStrength = nativeDevice.connected ? "excellent" : "unknown"
|
||||
deviceObj.canPair = !nativeDevice.paired
|
||||
deviceObj.nativeDevice = nativeDevice
|
||||
}
|
||||
|
||||
function findDeviceInList(deviceList, address) {
|
||||
for (let device of deviceList) {
|
||||
if (device.mac === address) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function cleanupOldDevices(oldList, newList) {
|
||||
for (let oldDevice of oldList) {
|
||||
if (!newList.includes(oldDevice)) {
|
||||
oldDevice.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanDevices() {
|
||||
if (root.bluetoothEnabled && root.bluetoothAvailable) {
|
||||
bluetoothDeviceScanner.running = true
|
||||
function clearDeviceList(deviceList) {
|
||||
for (let device of deviceList) {
|
||||
device.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
function isDeviceDiscoverable(device) {
|
||||
let displayName = device.name || device.deviceName
|
||||
if (!displayName || displayName.length < 2) return false
|
||||
|
||||
if (displayName.startsWith('/org/bluez') || displayName.includes('hci0')) return false
|
||||
|
||||
let nameLower = displayName.toLowerCase()
|
||||
|
||||
if (nameLower.match(/^[0-9a-f]{2}[:-][0-9a-f]{2}[:-][0-9a-f]{2}/)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (displayName.length < 3) return false
|
||||
|
||||
if (nameLower.includes('iphone') || nameLower.includes('ipad') ||
|
||||
nameLower.includes('airpods') || nameLower.includes('samsung') ||
|
||||
nameLower.includes('galaxy') || nameLower.includes('pixel') ||
|
||||
nameLower.includes('headphone') || nameLower.includes('speaker') ||
|
||||
nameLower.includes('mouse') || nameLower.includes('keyboard') ||
|
||||
nameLower.includes('watch') || nameLower.includes('buds') ||
|
||||
nameLower.includes('android')) {
|
||||
return true
|
||||
}
|
||||
|
||||
return displayName.length >= 4 && !displayName.match(/^[A-Z0-9_-]+$/)
|
||||
}
|
||||
|
||||
function getDeviceType(name, icon) {
|
||||
if (!name && !icon) return "bluetooth"
|
||||
|
||||
let nameLower = (name || "").toLowerCase()
|
||||
let iconLower = (icon || "").toLowerCase()
|
||||
|
||||
if (iconLower.includes("audio") || iconLower.includes("headset") || iconLower.includes("headphone") ||
|
||||
nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") ||
|
||||
nameLower.includes("arctis") || nameLower.includes("audio")) return "headset"
|
||||
else if (iconLower.includes("input-mouse") || nameLower.includes("mouse")) return "mouse"
|
||||
else if (iconLower.includes("input-keyboard") || nameLower.includes("keyboard")) return "keyboard"
|
||||
else if (iconLower.includes("phone") || nameLower.includes("phone") || nameLower.includes("iphone") ||
|
||||
nameLower.includes("samsung") || nameLower.includes("android")) return "phone"
|
||||
else if (iconLower.includes("watch") || nameLower.includes("watch")) return "watch"
|
||||
else if (iconLower.includes("audio-speakers") || nameLower.includes("speaker")) return "speaker"
|
||||
else if (iconLower.includes("video-display") || nameLower.includes("tv") || nameLower.includes("display")) return "tv"
|
||||
|
||||
return "bluetooth"
|
||||
}
|
||||
|
||||
function startDiscovery() {
|
||||
root.scanning = true
|
||||
// Run comprehensive scan that gets all devices
|
||||
discoveryScanner.running = true
|
||||
if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled) {
|
||||
Bluetooth.defaultAdapter.discovering = true
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
function stopDiscovery() {
|
||||
let stopDiscoveryProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bluetoothctl", "scan", "off"]
|
||||
running: true
|
||||
onExited: {
|
||||
root.scanning = false
|
||||
}
|
||||
}
|
||||
', root)
|
||||
if (Bluetooth.defaultAdapter) {
|
||||
Bluetooth.defaultAdapter.discovering = false
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
function pairDevice(mac) {
|
||||
console.log("Pairing device:", mac)
|
||||
let pairProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bluetoothctl", "pair", "' + mac + '"]
|
||||
running: true
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("Pairing successful")
|
||||
connectDevice("' + mac + '")
|
||||
} else {
|
||||
console.warn("Pairing failed with exit code:", exitCode)
|
||||
}
|
||||
availableDeviceScanner.running = true
|
||||
bluetoothDeviceScanner.running = true
|
||||
}
|
||||
}
|
||||
', root)
|
||||
let device = findDeviceByMac(mac)
|
||||
if (device) {
|
||||
device.pair()
|
||||
}
|
||||
}
|
||||
|
||||
function connectDevice(mac) {
|
||||
console.log("Connecting to device:", mac)
|
||||
let connectProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bluetoothctl", "connect", "' + mac + '"]
|
||||
running: true
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("Connection successful")
|
||||
} else {
|
||||
console.warn("Connection failed with exit code:", exitCode)
|
||||
}
|
||||
bluetoothDeviceScanner.running = true
|
||||
}
|
||||
}
|
||||
', root)
|
||||
let device = findDeviceByMac(mac)
|
||||
if (device) {
|
||||
device.connect()
|
||||
}
|
||||
}
|
||||
|
||||
function removeDevice(mac) {
|
||||
console.log("Removing device:", mac)
|
||||
let removeProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bluetoothctl", "remove", "' + mac + '"]
|
||||
running: true
|
||||
onExited: {
|
||||
bluetoothDeviceScanner.running = true
|
||||
availableDeviceScanner.running = true
|
||||
}
|
||||
}
|
||||
', root)
|
||||
let device = findDeviceByMac(mac)
|
||||
if (device) {
|
||||
device.forget()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleBluetoothDevice(mac) {
|
||||
let device = root.bluetoothDevices.find(d => d.mac === mac)
|
||||
if (device) {
|
||||
let action = device.connected ? "disconnect" : "connect"
|
||||
let toggleProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bluetoothctl", "' + action + '", "' + mac + '"]
|
||||
running: true
|
||||
onExited: bluetoothDeviceScanner.running = true
|
||||
}
|
||||
', root)
|
||||
let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
|
||||
if (!typedDevice) {
|
||||
typedDevice = findDeviceInList(root.availableDevices, mac)
|
||||
}
|
||||
|
||||
if (typedDevice && typedDevice.nativeDevice) {
|
||||
if (typedDevice.connected) {
|
||||
console.log("Disconnecting device:", mac)
|
||||
typedDevice.connecting = false
|
||||
typedDevice.connectionFailed = false
|
||||
typedDevice.nativeDevice.connected = false
|
||||
} else {
|
||||
console.log("Connecting to device:", mac)
|
||||
typedDevice.connecting = true
|
||||
typedDevice.connectionFailed = false
|
||||
|
||||
// Set a timeout to handle connection failure
|
||||
Qt.callLater(() => {
|
||||
connectionTimeout.deviceMac = mac
|
||||
connectionTimeout.start()
|
||||
})
|
||||
|
||||
typedDevice.nativeDevice.connected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleBluetooth() {
|
||||
let action = root.bluetoothEnabled ? "off" : "on"
|
||||
let toggleProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bluetoothctl", "power", "' + action + '"]
|
||||
running: true
|
||||
onExited: bluetoothStatusChecker.running = true
|
||||
}
|
||||
', root)
|
||||
if (Bluetooth.defaultAdapter) {
|
||||
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
|
||||
}
|
||||
}
|
||||
|
||||
function findDeviceByMac(mac) {
|
||||
let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
|
||||
if (typedDevice && typedDevice.nativeDevice) {
|
||||
return typedDevice.nativeDevice
|
||||
}
|
||||
|
||||
typedDevice = findDeviceInList(root.availableDevices, mac)
|
||||
if (typedDevice && typedDevice.nativeDevice) {
|
||||
return typedDevice.nativeDevice
|
||||
}
|
||||
|
||||
if (Bluetooth.defaultAdapter) {
|
||||
let adapterDevices = Bluetooth.defaultAdapter.devices
|
||||
if (adapterDevices.values) {
|
||||
for (let device of adapterDevices.values) {
|
||||
if (device && device.address === mac) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Bluetooth.devices.values) {
|
||||
for (let device of Bluetooth.devices.values) {
|
||||
if (device && device.address === mac) {
|
||||
return device
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Timer {
|
||||
id: bluetoothMonitorTimer
|
||||
interval: 5000
|
||||
running: false; repeat: true
|
||||
interval: 2000
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
bluetoothStatusChecker.running = true
|
||||
if (root.bluetoothEnabled) {
|
||||
bluetoothDeviceScanner.running = true
|
||||
// Also refresh paired devices to get current connection status
|
||||
pairedDeviceChecker.discoveredToMerge = []
|
||||
pairedDeviceChecker.running = true
|
||||
}
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
function enableMonitoring(enabled) {
|
||||
bluetoothMonitorTimer.running = enabled
|
||||
if (enabled) {
|
||||
// Immediately update when enabled
|
||||
bluetoothStatusChecker.running = true
|
||||
refreshBluetoothState()
|
||||
updateDevices()
|
||||
}
|
||||
}
|
||||
|
||||
property var discoveredDevices: []
|
||||
|
||||
// Handle discovered devices
|
||||
function _handleDiscovered(found) {
|
||||
|
||||
let discoveredDevices = []
|
||||
for (let device of found) {
|
||||
let type = "bluetooth"
|
||||
let nameLower = device.name.toLowerCase()
|
||||
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
|
||||
else if (nameLower.includes("mouse")) type = "mouse"
|
||||
else if (nameLower.includes("keyboard")) type = "keyboard"
|
||||
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
|
||||
else if (nameLower.includes("watch")) type = "watch"
|
||||
else if (nameLower.includes("speaker")) type = "speaker"
|
||||
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
|
||||
|
||||
discoveredDevices.push({
|
||||
mac: device.mac,
|
||||
name: device.name,
|
||||
type: type,
|
||||
paired: false,
|
||||
connected: false,
|
||||
rssi: -70,
|
||||
signalStrength: "fair",
|
||||
canPair: true
|
||||
})
|
||||
|
||||
console.log(" -", device.name, "(", device.mac, ")")
|
||||
Timer {
|
||||
id: bluetoothStateRefreshTimer
|
||||
interval: 5000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
refreshBluetoothState()
|
||||
}
|
||||
|
||||
// Get paired devices first, then merge with discovered
|
||||
pairedDeviceChecker.discoveredToMerge = discoveredDevices
|
||||
pairedDeviceChecker.running = true
|
||||
}
|
||||
|
||||
// Get only currently connected/paired devices that matter
|
||||
Process {
|
||||
id: availableDeviceScanner
|
||||
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); if [[ \"$paired\" == \"yes\" ]] || [[ \"$connected\" == \"yes\" ]]; then echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
|
||||
Timer {
|
||||
id: connectionTimeout
|
||||
interval: 10000 // 10 second timeout
|
||||
running: false
|
||||
repeat: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
|
||||
let devices = []
|
||||
if (text.trim()) {
|
||||
let lines = text.trim().split('\n')
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.trim()) {
|
||||
let parts = line.split('|')
|
||||
if (parts.length >= 4) {
|
||||
let mac = parts[0].trim()
|
||||
let name = parts[1].trim()
|
||||
let paired = parts[2].trim() === 'yes'
|
||||
let connected = parts[3].trim() === 'yes'
|
||||
|
||||
// Skip technical names
|
||||
if (name.startsWith('/org/bluez') || name.includes('hci0') || name.length < 3) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine device type
|
||||
let type = "bluetooth"
|
||||
let nameLower = name.toLowerCase()
|
||||
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
|
||||
else if (nameLower.includes("mouse")) type = "mouse"
|
||||
else if (nameLower.includes("keyboard")) type = "keyboard"
|
||||
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
|
||||
else if (nameLower.includes("watch")) type = "watch"
|
||||
else if (nameLower.includes("speaker")) type = "speaker"
|
||||
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
|
||||
|
||||
devices.push({
|
||||
mac: mac,
|
||||
name: name,
|
||||
type: type,
|
||||
paired: paired,
|
||||
connected: connected,
|
||||
rssi: 0,
|
||||
signalStrength: "unknown",
|
||||
canPair: false // Already paired
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
property string deviceMac: ""
|
||||
|
||||
onTriggered: {
|
||||
if (deviceMac) {
|
||||
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
|
||||
if (!typedDevice) {
|
||||
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
|
||||
}
|
||||
|
||||
root.availableDevices = devices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery scanner using bluetoothctl --timeout
|
||||
Process {
|
||||
id: discoveryScanner
|
||||
// Discover for 8 s in non-interactive mode, then auto-exit
|
||||
command: ["bluetoothctl",
|
||||
"--timeout", "8",
|
||||
"--monitor", // keeps stdout unbuffered
|
||||
"scan", "on"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
/*
|
||||
* bluetoothctl prints lines like:
|
||||
* [NEW] Device 12:34:56:78:9A:BC My-Headphones
|
||||
*/
|
||||
const rx = /^\[NEW\] Device ([0-9A-F:]+)\s+(.+)$/i;
|
||||
const found = text.split('\n')
|
||||
.filter(l => rx.test(l))
|
||||
.map(l => {
|
||||
const [,mac,name] = l.match(rx);
|
||||
return { mac, name };
|
||||
});
|
||||
root._handleDiscovered(found);
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
root.scanning = false
|
||||
}
|
||||
}
|
||||
|
||||
// Get paired devices and merge with discovered ones
|
||||
Process {
|
||||
id: pairedDeviceChecker
|
||||
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ${#name} -gt 3 ]] && [[ ! $name =~ ^/org/bluez ]] && [[ ! $name =~ hci0 ]]; then info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
|
||||
running: false
|
||||
property var discoveredToMerge: []
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
// Start with discovered devices (unpaired, available to pair)
|
||||
let allDevices = [...pairedDeviceChecker.discoveredToMerge]
|
||||
let seenMacs = new Set(allDevices.map(d => d.mac))
|
||||
|
||||
// Add only actually paired devices from bluetoothctl
|
||||
if (text.trim()) {
|
||||
let lines = text.trim().split('\n')
|
||||
if (typedDevice && typedDevice.connecting && !typedDevice.connected) {
|
||||
console.log("Connection timeout for device:", deviceMac)
|
||||
typedDevice.connecting = false
|
||||
typedDevice.connectionFailed = true
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.trim()) {
|
||||
let parts = line.split('|')
|
||||
if (parts.length >= 4) {
|
||||
let mac = parts[0].trim()
|
||||
let name = parts[1].trim()
|
||||
let paired = parts[2].trim() === 'yes'
|
||||
let connected = parts[3].trim() === 'yes'
|
||||
|
||||
// Only include if actually paired
|
||||
if (!paired) continue
|
||||
|
||||
// Check if already in discovered list
|
||||
if (seenMacs.has(mac)) {
|
||||
// Update existing device to show it's paired
|
||||
let existing = allDevices.find(d => d.mac === mac)
|
||||
if (existing) {
|
||||
existing.paired = true
|
||||
existing.connected = connected
|
||||
existing.canPair = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Add paired device not found during scan
|
||||
let type = "bluetooth"
|
||||
let nameLower = name.toLowerCase()
|
||||
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
|
||||
else if (nameLower.includes("mouse")) type = "mouse"
|
||||
else if (nameLower.includes("keyboard")) type = "keyboard"
|
||||
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
|
||||
else if (nameLower.includes("watch")) type = "watch"
|
||||
else if (nameLower.includes("speaker")) type = "speaker"
|
||||
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
|
||||
|
||||
allDevices.push({
|
||||
mac: mac,
|
||||
name: name,
|
||||
type: type,
|
||||
paired: true,
|
||||
connected: connected,
|
||||
rssi: -100,
|
||||
signalStrength: "unknown",
|
||||
canPair: false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear failure state after 3 seconds
|
||||
Qt.callLater(() => {
|
||||
clearFailureTimer.deviceMac = deviceMac
|
||||
clearFailureTimer.start()
|
||||
})
|
||||
}
|
||||
deviceMac = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: clearFailureTimer
|
||||
interval: 3000
|
||||
running: false
|
||||
repeat: false
|
||||
|
||||
property string deviceMac: ""
|
||||
|
||||
onTriggered: {
|
||||
if (deviceMac) {
|
||||
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
|
||||
if (!typedDevice) {
|
||||
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
|
||||
}
|
||||
|
||||
root.availableDevices = allDevices
|
||||
root.scanning = false
|
||||
|
||||
if (typedDevice) {
|
||||
typedDevice.connectionFailed = false
|
||||
}
|
||||
deviceMac = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component BluetoothDevice: QtObject {
|
||||
required property string mac
|
||||
required property string name
|
||||
required property string type
|
||||
required property bool paired
|
||||
required property bool connected
|
||||
required property int battery
|
||||
required property string signalStrength
|
||||
required property bool canPair
|
||||
required property var nativeDevice // Reference to native Quickshell device
|
||||
|
||||
property bool connecting: false
|
||||
property bool connectionFailed: false
|
||||
|
||||
readonly property string displayName: name
|
||||
readonly property bool batteryAvailable: battery >= 0
|
||||
readonly property string connectionStatus: {
|
||||
if (connecting) return "Connecting..."
|
||||
if (connectionFailed) return "Connection Failed"
|
||||
if (connected) return "Connected"
|
||||
return "Disconnected"
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: deviceComponent
|
||||
BluetoothDevice {}
|
||||
}
|
||||
}
|
||||
@@ -7,101 +7,160 @@ pragma ComponentBehavior: Bound
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int brightnessLevel: 75
|
||||
property list<var> ddcMonitors: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
property bool brightnessAvailable: false
|
||||
property int brightnessLevel: 75
|
||||
|
||||
// Check if brightness control is available
|
||||
Process {
|
||||
id: brightnessAvailabilityChecker
|
||||
command: ["bash", "-c", "if command -v brightnessctl > /dev/null; then echo 'brightnessctl'; elif command -v xbacklight > /dev/null; then echo 'xbacklight'; else echo 'none'; fi"]
|
||||
running: true
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
let method = data.trim()
|
||||
if (method === "brightnessctl" || method === "xbacklight") {
|
||||
root.brightnessAvailable = true
|
||||
brightnessChecker.running = true
|
||||
} else {
|
||||
root.brightnessAvailable = false
|
||||
console.log("Brightness control not available - no brightnessctl or xbacklight found")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function getMonitorForScreen(screen: ShellScreen): var {
|
||||
return monitors.find(function(m) { return m.modelData === screen; });
|
||||
}
|
||||
|
||||
// Brightness Control
|
||||
Process {
|
||||
id: brightnessChecker
|
||||
command: ["bash", "-c", "if command -v brightnessctl > /dev/null; then brightnessctl get; elif command -v xbacklight > /dev/null; then xbacklight -get | cut -d. -f1; else echo 75; fi"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
let brightness = parseInt(data.trim()) || 75
|
||||
// brightnessctl returns absolute value, need to convert to percentage
|
||||
if (brightness > 100) {
|
||||
brightnessMaxChecker.running = true
|
||||
} else {
|
||||
root.brightnessLevel = brightness
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: brightnessMaxChecker
|
||||
command: ["brightnessctl", "max"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
let maxBrightness = parseInt(data.trim()) || 100
|
||||
brightnessCurrentChecker.property("maxBrightness", maxBrightness)
|
||||
brightnessCurrentChecker.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: brightnessCurrentChecker
|
||||
property int maxBrightness: 100
|
||||
command: ["brightnessctl", "get"]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
let currentBrightness = parseInt(data.trim()) || 75
|
||||
root.brightnessLevel = Math.round((currentBrightness / maxBrightness) * 100)
|
||||
}
|
||||
property var debounceTimer: Timer {
|
||||
id: debounceTimer
|
||||
interval: 50
|
||||
repeat: false
|
||||
property int pendingValue: 0
|
||||
onTriggered: {
|
||||
const focusedMonitor = monitors.find(function(m) { return m.modelData === Quickshell.screens[0]; });
|
||||
if (focusedMonitor) {
|
||||
focusedMonitor.setBrightness(pendingValue / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightness(percentage) {
|
||||
if (!root.brightnessAvailable) {
|
||||
console.warn("Brightness control not available")
|
||||
return
|
||||
root.brightnessLevel = percentage;
|
||||
debounceTimer.pendingValue = percentage;
|
||||
debounceTimer.restart();
|
||||
}
|
||||
|
||||
function increaseBrightness(): void {
|
||||
const focusedMonitor = monitors.find(function(m) { return m.modelData === Quickshell.screens[0]; });
|
||||
if (focusedMonitor)
|
||||
focusedMonitor.setBrightness(focusedMonitor.brightness + 0.1);
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
const focusedMonitor = monitors.find(function(m) { return m.modelData === Quickshell.screens[0]; });
|
||||
if (focusedMonitor)
|
||||
focusedMonitor.setBrightness(focusedMonitor.brightness - 0.1);
|
||||
}
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = [];
|
||||
if (ddcAvailable) {
|
||||
ddcProc.running = true;
|
||||
}
|
||||
|
||||
let brightnessSetProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bash", "-c", "if command -v brightnessctl > /dev/null; then brightnessctl set ' + percentage + '%; elif command -v xbacklight > /dev/null; then xbacklight -set ' + percentage + '; fi"]
|
||||
running: true
|
||||
onExited: brightnessChecker.running = true
|
||||
// Update brightness level from first monitor
|
||||
if (monitors.length > 0) {
|
||||
root.brightnessLevel = Math.round(monitors[0].brightness * 100);
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
ddcAvailabilityChecker.running = true;
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: variants
|
||||
model: Quickshell.screens
|
||||
Monitor {}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcAvailabilityChecker
|
||||
command: ["which", "ddcutil"]
|
||||
onExited: function(exitCode) {
|
||||
root.brightnessAvailable = (exitCode === 0);
|
||||
if (root.brightnessAvailable) {
|
||||
ddcProc.running = true;
|
||||
}
|
||||
', root)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcProc
|
||||
command: ["ddcutil", "detect", "--brief"]
|
||||
running: false
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.ddcMonitors = text.trim().split("\n\n").filter(function(d) { return d.startsWith("Display "); }).map(function(d) { return ({
|
||||
model: d.match(/Monitor:.*:(.*):.*/)?.[1] || "Unknown",
|
||||
busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)?.[1] || "0"
|
||||
}); });
|
||||
} else {
|
||||
root.ddcMonitors = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
root.ddcMonitors = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Monitor: QtObject {
|
||||
id: monitor
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property bool isDdc: root.ddcMonitors.some(function(m) { return m.model === modelData.model; })
|
||||
readonly property string busNum: root.ddcMonitors.find(function(m) { return m.model === modelData.model; })?.busNum ?? ""
|
||||
property real brightness: 0.75
|
||||
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
const parts = text.trim().split(" ");
|
||||
if (parts.length >= 5) {
|
||||
const current = parseInt(parts[3]) || 75;
|
||||
const max = parseInt(parts[4]) || 100;
|
||||
monitor.brightness = current / max;
|
||||
root.brightnessLevel = Math.round(monitor.brightness * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
monitor.brightness = 0.75;
|
||||
root.brightnessLevel = 75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
value = Math.max(0, Math.min(1, value));
|
||||
const rounded = Math.round(value * 100);
|
||||
if (Math.round(brightness * 100) === rounded)
|
||||
return;
|
||||
|
||||
brightness = value;
|
||||
root.brightnessLevel = rounded;
|
||||
|
||||
if (isDdc && busNum) {
|
||||
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded.toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
onBusNumChanged: {
|
||||
if (isDdc && busNum) {
|
||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
||||
initProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(function() {
|
||||
if (isDdc && busNum) {
|
||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
||||
initProc.running = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,12 @@ pragma ComponentBehavior: Bound
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Process list properties
|
||||
property var processes: []
|
||||
property bool isUpdating: false
|
||||
property int processUpdateInterval: 1500
|
||||
property int processUpdateInterval: 3000
|
||||
|
||||
// Performance control - only run when process monitor is actually visible
|
||||
property bool monitoringEnabled: false
|
||||
|
||||
// System information properties
|
||||
property int totalMemoryKB: 0
|
||||
property int usedMemoryKB: 0
|
||||
property int totalSwapKB: 0
|
||||
@@ -24,8 +21,23 @@ Singleton {
|
||||
property real totalCpuUsage: 0.0
|
||||
property bool systemInfoAvailable: false
|
||||
|
||||
// Sorting options
|
||||
property string sortBy: "cpu" // "cpu", "memory", "name", "pid"
|
||||
property var cpuHistory: []
|
||||
property var memoryHistory: []
|
||||
property var networkHistory: ({rx: [], tx: []})
|
||||
property var diskHistory: ({read: [], write: []})
|
||||
property int historySize: 60
|
||||
|
||||
property var perCoreCpuUsage: []
|
||||
|
||||
property real networkRxRate: 0
|
||||
property real networkTxRate: 0
|
||||
property var lastNetworkStats: null
|
||||
|
||||
property real diskReadRate: 0
|
||||
property real diskWriteRate: 0
|
||||
property var lastDiskStats: null
|
||||
|
||||
property string sortBy: "cpu"
|
||||
property bool sortDescending: true
|
||||
property int maxProcesses: 20
|
||||
|
||||
@@ -35,10 +47,32 @@ Singleton {
|
||||
console.log("ProcessMonitorService: Initialization complete")
|
||||
}
|
||||
|
||||
// System information monitoring
|
||||
Timer {
|
||||
id: testTimer
|
||||
interval: 3000
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
console.log("ProcessMonitorService: Starting test monitoring...")
|
||||
enableMonitoring(true)
|
||||
stopTestTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: stopTestTimer
|
||||
interval: 8000
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
console.log("ProcessMonitorService: Stopping test monitoring...")
|
||||
enableMonitoring(false)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemInfoProcess
|
||||
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu ' /proc/stat"]
|
||||
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu' /proc/stat | head -" + (root.cpuCount + 1)]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
@@ -57,7 +91,34 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Process monitoring with ps command
|
||||
Process {
|
||||
id: networkStatsProcess
|
||||
command: ["bash", "-c", "cat /proc/net/dev | grep -E '(wlan|eth|enp|wlp|ens|eno)' | awk '{print $1,$2,$10}' | sed 's/:/ /'"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
parseNetworkStats(text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: diskStatsProcess
|
||||
command: ["bash", "-c", "cat /proc/diskstats | grep -E ' (sd[a-z]+|nvme[0-9]+n[0-9]+|vd[a-z]+) ' | grep -v 'p[0-9]' | awk '{print $3,$6,$10}'"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
parseDiskStats(text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: processListProcess
|
||||
command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,rss,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)]
|
||||
@@ -69,12 +130,10 @@ Singleton {
|
||||
const lines = text.trim().split('\n')
|
||||
const newProcesses = []
|
||||
|
||||
// Skip header line
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
if (!line) continue
|
||||
|
||||
// Parse ps output: PID PPID %CPU %MEM RSS COMMAND CMD
|
||||
const parts = line.split(/\s+/)
|
||||
if (parts.length >= 7) {
|
||||
const pid = parseInt(parts[0])
|
||||
@@ -112,36 +171,52 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// System and process monitoring timer - now conditional
|
||||
Timer {
|
||||
id: processTimer
|
||||
interval: root.processUpdateInterval
|
||||
running: root.monitoringEnabled // Only run when monitoring is enabled
|
||||
running: root.monitoringEnabled
|
||||
repeat: true
|
||||
|
||||
onTriggered: {
|
||||
if (root.monitoringEnabled) {
|
||||
updateSystemInfo()
|
||||
updateProcessList()
|
||||
updateNetworkStats()
|
||||
updateDiskStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public functions
|
||||
function updateSystemInfo() {
|
||||
if (!systemInfoProcess.running && root.monitoringEnabled) {
|
||||
systemInfoProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
// Control functions for enabling/disabling monitoring
|
||||
function enableMonitoring(enabled) {
|
||||
console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled")
|
||||
root.monitoringEnabled = enabled
|
||||
if (enabled) {
|
||||
// Immediately update when enabled
|
||||
root.cpuHistory = []
|
||||
root.memoryHistory = []
|
||||
root.networkHistory = ({rx: [], tx: []})
|
||||
root.diskHistory = ({read: [], write: []})
|
||||
updateSystemInfo()
|
||||
updateProcessList()
|
||||
updateNetworkStats()
|
||||
updateDiskStats()
|
||||
}
|
||||
}
|
||||
|
||||
function updateNetworkStats() {
|
||||
if (!networkStatsProcess.running && root.monitoringEnabled) {
|
||||
networkStatsProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
function updateDiskStats() {
|
||||
if (!diskStatsProcess.running && root.monitoringEnabled) {
|
||||
diskStatsProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +224,6 @@ Singleton {
|
||||
if (!root.isUpdating && root.monitoringEnabled) {
|
||||
root.isUpdating = true
|
||||
|
||||
// Update sort command based on current sort option
|
||||
let sortOption = ""
|
||||
switch (root.sortBy) {
|
||||
case "cpu":
|
||||
@@ -208,7 +282,6 @@ Singleton {
|
||||
}
|
||||
|
||||
function getProcessIcon(command) {
|
||||
// Return appropriate Material Design icon for common processes
|
||||
const cmd = command.toLowerCase()
|
||||
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser")) return "web"
|
||||
if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) return "code"
|
||||
@@ -216,7 +289,7 @@ Singleton {
|
||||
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify")) return "music_note"
|
||||
if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv")) return "play_circle"
|
||||
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread")) return "settings"
|
||||
return "memory" // Default process icon
|
||||
return "memory"
|
||||
}
|
||||
|
||||
function formatCpuUsage(cpu) {
|
||||
@@ -244,6 +317,10 @@ Singleton {
|
||||
function parseSystemInfo(text) {
|
||||
const lines = text.split('\n')
|
||||
let section = 'memory'
|
||||
const coreUsages = []
|
||||
let memFree = 0
|
||||
let memBuffers = 0
|
||||
let memCached = 0
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
@@ -259,9 +336,12 @@ Singleton {
|
||||
if (section === 'memory') {
|
||||
if (line.startsWith('MemTotal:')) {
|
||||
root.totalMemoryKB = parseInt(line.split(/\s+/)[1])
|
||||
} else if (line.startsWith('MemAvailable:')) {
|
||||
const availableKB = parseInt(line.split(/\s+/)[1])
|
||||
root.usedMemoryKB = root.totalMemoryKB - availableKB
|
||||
} else if (line.startsWith('MemFree:')) {
|
||||
memFree = parseInt(line.split(/\s+/)[1])
|
||||
} else if (line.startsWith('Buffers:')) {
|
||||
memBuffers = parseInt(line.split(/\s+/)[1])
|
||||
} else if (line.startsWith('Cached:')) {
|
||||
memCached = parseInt(line.split(/\s+/)[1])
|
||||
} else if (line.startsWith('SwapTotal:')) {
|
||||
root.totalSwapKB = parseInt(line.split(/\s+/)[1])
|
||||
} else if (line.startsWith('SwapFree:')) {
|
||||
@@ -289,10 +369,104 @@ Singleton {
|
||||
const used = total - idle - iowait
|
||||
root.totalCpuUsage = total > 0 ? (used / total) * 100 : 0
|
||||
}
|
||||
} else if (line.match(/^cpu\d+/)) {
|
||||
const parts = line.split(/\s+/)
|
||||
if (parts.length >= 8) {
|
||||
const user = parseInt(parts[1])
|
||||
const nice = parseInt(parts[2])
|
||||
const system = parseInt(parts[3])
|
||||
const idle = parseInt(parts[4])
|
||||
const iowait = parseInt(parts[5])
|
||||
const irq = parseInt(parts[6])
|
||||
const softirq = parseInt(parts[7])
|
||||
|
||||
const total = user + nice + system + idle + iowait + irq + softirq
|
||||
const used = total - idle - iowait
|
||||
const usage = total > 0 ? (used / total) * 100 : 0
|
||||
coreUsages.push(usage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate used memory as total minus free minus buffers minus cached
|
||||
root.usedMemoryKB = root.totalMemoryKB - memFree - memBuffers - memCached
|
||||
|
||||
// Update per-core usage
|
||||
root.perCoreCpuUsage = coreUsages
|
||||
|
||||
// Update history
|
||||
addToHistory(root.cpuHistory, root.totalCpuUsage)
|
||||
const memoryPercent = root.totalMemoryKB > 0 ? (root.usedMemoryKB / root.totalMemoryKB) * 100 : 0
|
||||
addToHistory(root.memoryHistory, memoryPercent)
|
||||
|
||||
// console.log("ProcessMonitorService: Updated - CPU:", root.totalCpuUsage.toFixed(1) + "%", "Memory:", memoryPercent.toFixed(1) + "%", "History length:", root.cpuHistory.length)
|
||||
|
||||
root.systemInfoAvailable = true
|
||||
}
|
||||
|
||||
function parseNetworkStats(text) {
|
||||
const lines = text.split('\n')
|
||||
let totalRx = 0
|
||||
let totalTx = 0
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
if (parts.length >= 3) {
|
||||
const rx = parseInt(parts[1])
|
||||
const tx = parseInt(parts[2])
|
||||
if (!isNaN(rx) && !isNaN(tx)) {
|
||||
totalRx += rx
|
||||
totalTx += tx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (root.lastNetworkStats) {
|
||||
const timeDiff = root.processUpdateInterval / 1000
|
||||
root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff)
|
||||
root.networkTxRate = Math.max(0, (totalTx - root.lastNetworkStats.tx) / timeDiff)
|
||||
|
||||
addToHistory(root.networkHistory.rx, root.networkRxRate / 1024)
|
||||
addToHistory(root.networkHistory.tx, root.networkTxRate / 1024)
|
||||
}
|
||||
|
||||
root.lastNetworkStats = { rx: totalRx, tx: totalTx }
|
||||
}
|
||||
|
||||
function parseDiskStats(text) {
|
||||
const lines = text.split('\n')
|
||||
let totalRead = 0
|
||||
let totalWrite = 0
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
if (parts.length >= 3) {
|
||||
const readSectors = parseInt(parts[1])
|
||||
const writeSectors = parseInt(parts[2])
|
||||
if (!isNaN(readSectors) && !isNaN(writeSectors)) {
|
||||
totalRead += readSectors * 512
|
||||
totalWrite += writeSectors * 512
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (root.lastDiskStats) {
|
||||
const timeDiff = root.processUpdateInterval / 1000
|
||||
root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff)
|
||||
root.diskWriteRate = Math.max(0, (totalWrite - root.lastDiskStats.write) / timeDiff)
|
||||
|
||||
addToHistory(root.diskHistory.read, root.diskReadRate / (1024 * 1024))
|
||||
addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024))
|
||||
}
|
||||
|
||||
root.lastDiskStats = { read: totalRead, write: totalWrite }
|
||||
}
|
||||
|
||||
function addToHistory(array, value) {
|
||||
array.push(value)
|
||||
if (array.length > root.historySize) {
|
||||
array.shift()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,13 @@ pragma ComponentBehavior: Bound
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// CPU properties
|
||||
property real cpuUsage: 0.0
|
||||
property int cpuCores: 1
|
||||
property string cpuModel: ""
|
||||
property real cpuFrequency: 0.0
|
||||
|
||||
// Previous CPU stats for accurate calculation
|
||||
property var prevCpuStats: [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
|
||||
// Memory properties
|
||||
property real memoryUsage: 0.0
|
||||
property real totalMemory: 0.0
|
||||
property real usedMemory: 0.0
|
||||
@@ -25,14 +22,28 @@ Singleton {
|
||||
property real bufferMemory: 0.0
|
||||
property real cacheMemory: 0.0
|
||||
|
||||
// Temperature properties
|
||||
property real cpuTemperature: 0.0
|
||||
|
||||
property string kernelVersion: ""
|
||||
property string distribution: ""
|
||||
property string hostname: ""
|
||||
property string uptime: ""
|
||||
property string scheduler: ""
|
||||
property string architecture: ""
|
||||
property string loadAverage: ""
|
||||
property int processCount: 0
|
||||
property int threadCount: 0
|
||||
property string bootTime: ""
|
||||
property string motherboard: ""
|
||||
property string biosVersion: ""
|
||||
property var diskMounts: []
|
||||
property string diskUsage: ""
|
||||
|
||||
property int cpuUpdateInterval: 3000
|
||||
property int memoryUpdateInterval: 5000
|
||||
property int temperatureUpdateInterval: 10000
|
||||
property int systemInfoUpdateInterval: 30000
|
||||
|
||||
// Performance control
|
||||
property bool enabledForTopBar: true
|
||||
property bool enabledForDetailedView: false
|
||||
|
||||
@@ -40,10 +51,10 @@ Singleton {
|
||||
console.log("SystemMonitorService: Starting initialization...")
|
||||
getCpuInfo()
|
||||
updateSystemStats()
|
||||
updateSystemInfo()
|
||||
console.log("SystemMonitorService: Initialization complete")
|
||||
}
|
||||
|
||||
// Get CPU information (static)
|
||||
Process {
|
||||
id: cpuInfoProcess
|
||||
command: ["bash", "-c", "lscpu | grep -E 'Model name|CPU\\(s\\):' | head -2"]
|
||||
@@ -69,7 +80,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// CPU usage monitoring with accurate calculation
|
||||
Process {
|
||||
id: cpuUsageProcess
|
||||
command: ["bash", "-c", "head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}'"]
|
||||
@@ -80,17 +90,14 @@ Singleton {
|
||||
if (text.trim()) {
|
||||
const stats = text.trim().split(" ").map(x => parseInt(x))
|
||||
if (root.prevCpuStats[0] > 0) {
|
||||
// Calculate differences
|
||||
let diffs = []
|
||||
for (let i = 0; i < 8; i++) {
|
||||
diffs[i] = stats[i] - root.prevCpuStats[i]
|
||||
}
|
||||
|
||||
// Calculate total and idle time
|
||||
const totalTime = diffs.reduce((a, b) => a + b, 0)
|
||||
const idleTime = diffs[3] + diffs[4] // idle + iowait
|
||||
const idleTime = diffs[3] + diffs[4]
|
||||
|
||||
// CPU usage percentage
|
||||
if (totalTime > 0) {
|
||||
root.cpuUsage = Math.max(0, Math.min(100, ((totalTime - idleTime) / totalTime) * 100))
|
||||
}
|
||||
@@ -107,7 +114,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Memory usage monitoring
|
||||
Process {
|
||||
id: memoryUsageProcess
|
||||
command: ["bash", "-c", "free -m | awk 'NR==2{printf \"%.1f %.1f %.1f %.1f\", $3*100/$2, $2, $3, $7}'"]
|
||||
@@ -133,7 +139,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// CPU frequency monitoring
|
||||
Process {
|
||||
id: cpuFrequencyProcess
|
||||
command: ["bash", "-c", "cat /proc/cpuinfo | grep 'cpu MHz' | head -1 | awk '{print $4}'"]
|
||||
@@ -154,7 +159,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// CPU temperature monitoring
|
||||
Process {
|
||||
id: temperatureProcess
|
||||
command: ["bash", "-c", "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then cat /sys/class/thermal/thermal_zone0/temp | awk '{print $1/1000}'; else sensors 2>/dev/null | grep 'Core 0' | awk '{print $3}' | sed 's/+//g;s/°C//g' | head -1; fi"]
|
||||
@@ -175,7 +179,282 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// CPU monitoring timer
|
||||
Process {
|
||||
id: kernelInfoProcess
|
||||
command: ["bash", "-c", "uname -r"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.kernelVersion = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Kernel info check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: distributionProcess
|
||||
command: ["bash", "-c", "grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '\"' || echo 'Unknown'"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.distribution = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Distribution check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: hostnameProcess
|
||||
command: ["bash", "-c", "hostname"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.hostname = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Hostname check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: uptimeProcess
|
||||
command: ["bash", "-c", "uptime -p | sed 's/up //'"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.uptime = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Uptime check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: schedulerProcess
|
||||
command: ["bash", "-c", "cat /sys/block/sda/queue/scheduler 2>/dev/null | grep -o '\\[.*\\]' | tr -d '[]' || echo 'Unknown'"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.scheduler = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Scheduler check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: architectureProcess
|
||||
command: ["bash", "-c", "uname -m"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.architecture = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Architecture check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: loadAverageProcess
|
||||
command: ["bash", "-c", "cat /proc/loadavg | cut -d' ' -f1,2,3"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.loadAverage = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Load average check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: processCountProcess
|
||||
command: ["bash", "-c", "ps aux | wc -l"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.processCount = parseInt(text.trim()) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Process count check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: threadCountProcess
|
||||
command: ["bash", "-c", "cat /proc/stat | grep processes | awk '{print $2}'"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.threadCount = parseInt(text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Thread count check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: bootTimeProcess
|
||||
command: ["bash", "-c", "who -b | awk '{print $3, $4}' || stat -c %w /proc/1 2>/dev/null | cut -d' ' -f1,2 || echo 'Unknown'"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.bootTime = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Boot time check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: motherboardProcess
|
||||
command: ["bash", "-c", "if [ -r /sys/devices/virtual/dmi/id/board_vendor ] && [ -r /sys/devices/virtual/dmi/id/board_name ]; then echo \"$(cat /sys/devices/virtual/dmi/id/board_vendor 2>/dev/null) $(cat /sys/devices/virtual/dmi/id/board_name 2>/dev/null)\"; else echo 'Unknown'; fi"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.motherboard = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Motherboard check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: biosProcess
|
||||
command: ["bash", "-c", "if [ -r /sys/devices/virtual/dmi/id/bios_version ] && [ -r /sys/devices/virtual/dmi/id/bios_date ]; then echo \"$(cat /sys/devices/virtual/dmi/id/bios_version 2>/dev/null) $(cat /sys/devices/virtual/dmi/id/bios_date 2>/dev/null)\"; else echo 'Unknown'; fi"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
root.biosVersion = text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("BIOS check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: diskMountsProcess
|
||||
command: ["bash", "-c", "df -h --output=source,target,fstype,size,used,avail,pcent | tail -n +2 | grep -v tmpfs | grep -v devtmpfs | head -10"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
let mounts = []
|
||||
const lines = text.trim().split('\n')
|
||||
for (const line of lines) {
|
||||
const parts = line.split(/\s+/)
|
||||
if (parts.length >= 7) {
|
||||
mounts.push({
|
||||
device: parts[0],
|
||||
mount: parts[1],
|
||||
fstype: parts[2],
|
||||
size: parts[3],
|
||||
used: parts[4],
|
||||
avail: parts[5],
|
||||
percent: parts[6]
|
||||
})
|
||||
}
|
||||
}
|
||||
root.diskMounts = mounts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Disk mounts check failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: cpuTimer
|
||||
interval: root.cpuUpdateInterval
|
||||
@@ -190,7 +469,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Memory monitoring timer
|
||||
Timer {
|
||||
id: memoryTimer
|
||||
interval: root.memoryUpdateInterval
|
||||
@@ -204,7 +482,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature monitoring timer
|
||||
Timer {
|
||||
id: temperatureTimer
|
||||
interval: root.temperatureUpdateInterval
|
||||
@@ -218,7 +495,19 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Public functions
|
||||
Timer {
|
||||
id: systemInfoTimer
|
||||
interval: root.systemInfoUpdateInterval
|
||||
running: root.enabledForDetailedView
|
||||
repeat: true
|
||||
|
||||
onTriggered: {
|
||||
if (root.enabledForDetailedView) {
|
||||
updateSystemInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCpuInfo() {
|
||||
cpuInfoProcess.running = true
|
||||
}
|
||||
@@ -234,6 +523,22 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSystemInfo() {
|
||||
kernelInfoProcess.running = true
|
||||
distributionProcess.running = true
|
||||
hostnameProcess.running = true
|
||||
uptimeProcess.running = true
|
||||
schedulerProcess.running = true
|
||||
architectureProcess.running = true
|
||||
loadAverageProcess.running = true
|
||||
processCountProcess.running = true
|
||||
threadCountProcess.running = true
|
||||
bootTimeProcess.running = true
|
||||
motherboardProcess.running = true
|
||||
biosProcess.running = true
|
||||
diskMountsProcess.running = true
|
||||
}
|
||||
|
||||
function enableTopBarMonitoring(enabled) {
|
||||
root.enabledForTopBar = enabled
|
||||
}
|
||||
@@ -243,15 +548,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function getCpuUsageColor() {
|
||||
if (cpuUsage > 80) return "#e74c3c" // Red
|
||||
if (cpuUsage > 60) return "#f39c12" // Orange
|
||||
return "#27ae60" // Green
|
||||
if (cpuUsage > 80) return "#e74c3c"
|
||||
if (cpuUsage > 60) return "#f39c12"
|
||||
return "#27ae60"
|
||||
}
|
||||
|
||||
function getMemoryUsageColor() {
|
||||
if (memoryUsage > 90) return "#e74c3c" // Red
|
||||
if (memoryUsage > 75) return "#f39c12" // Orange
|
||||
return "#3498db" // Blue
|
||||
if (memoryUsage > 90) return "#e74c3c"
|
||||
if (memoryUsage > 75) return "#f39c12"
|
||||
return "#3498db"
|
||||
}
|
||||
|
||||
function formatMemory(mb) {
|
||||
@@ -262,8 +567,8 @@ Singleton {
|
||||
}
|
||||
|
||||
function getTemperatureColor() {
|
||||
if (cpuTemperature > 80) return "#e74c3c" // Red
|
||||
if (cpuTemperature > 65) return "#f39c12" // Orange
|
||||
return "#27ae60" // Green
|
||||
if (cpuTemperature > 80) return "#e74c3c"
|
||||
if (cpuTemperature > 65) return "#f39c12"
|
||||
return "#27ae60"
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ Rectangle {
|
||||
|
||||
property bool batteryPopupVisible: false
|
||||
|
||||
signal toggleBatteryPopup()
|
||||
|
||||
width: 70 // Increased width to accommodate percentage text
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
@@ -63,7 +65,7 @@ Rectangle {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
batteryPopupVisible = !batteryPopupVisible
|
||||
toggleBatteryPopup()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,14 +78,14 @@ Rectangle {
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - theme.spacingM * 2
|
||||
spacing: theme.spacingM
|
||||
anchors.fill: parent
|
||||
anchors.margins: theme.spacingS
|
||||
spacing: theme.spacingS
|
||||
|
||||
// Show different content based on whether we have active media
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 80
|
||||
height: 60
|
||||
|
||||
// Placeholder when no media
|
||||
Column {
|
||||
@@ -117,8 +117,8 @@ Rectangle {
|
||||
|
||||
// Album Art
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 80
|
||||
width: 60
|
||||
height: 60
|
||||
radius: theme.cornerRadius
|
||||
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
|
||||
|
||||
@@ -152,7 +152,7 @@ Rectangle {
|
||||
|
||||
// Track Info
|
||||
Column {
|
||||
width: parent.width - 80 - theme.spacingM
|
||||
width: parent.width - 60 - theme.spacingM
|
||||
spacing: theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -186,45 +186,52 @@ Rectangle {
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
Rectangle {
|
||||
id: progressBarBackground
|
||||
Item {
|
||||
id: progressBarContainer
|
||||
width: parent.width
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
|
||||
visible: activePlayer !== null
|
||||
height: 24
|
||||
|
||||
Rectangle {
|
||||
id: progressFill
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: theme.primary
|
||||
|
||||
width: parent.width * ratio()
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
}
|
||||
|
||||
// Drag handle
|
||||
Rectangle {
|
||||
id: progressHandle
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: theme.primary
|
||||
border.color: Qt.lighter(theme.primary, 1.3)
|
||||
border.width: 1
|
||||
|
||||
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2))
|
||||
id: progressBarBackground
|
||||
width: parent.width
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
|
||||
visible: activePlayer !== null
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
visible: activePlayer && activePlayer.length > 0
|
||||
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
|
||||
Rectangle {
|
||||
id: progressFill
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: theme.primary
|
||||
|
||||
width: parent.width * ratio()
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
// Drag handle
|
||||
Rectangle {
|
||||
id: progressHandle
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: theme.primary
|
||||
border.color: Qt.lighter(theme.primary, 1.3)
|
||||
border.width: 1
|
||||
|
||||
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
visible: activePlayer && activePlayer.length > 0
|
||||
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,22 +241,14 @@ Rectangle {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
|
||||
preventStealing: true
|
||||
|
||||
property bool isSeeking: false
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (activePlayer && activePlayer.length > 0) {
|
||||
let ratio = mouse.x / width
|
||||
let seekPosition = ratio * activePlayer.length
|
||||
activePlayer.position = seekPosition
|
||||
currentPosition = seekPosition
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: function(mouse) {
|
||||
isSeeking = true
|
||||
if (activePlayer && activePlayer.length > 0) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
|
||||
let seekPosition = ratio * activePlayer.length
|
||||
activePlayer.position = seekPosition
|
||||
currentPosition = seekPosition
|
||||
@@ -261,21 +260,54 @@ Rectangle {
|
||||
}
|
||||
|
||||
onPositionChanged: function(mouse) {
|
||||
if (pressed && activePlayer && activePlayer.length > 0) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
if (pressed && isSeeking && activePlayer && activePlayer.length > 0) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
|
||||
let seekPosition = ratio * activePlayer.length
|
||||
activePlayer.position = seekPosition
|
||||
currentPosition = seekPosition
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (activePlayer && activePlayer.length > 0) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
|
||||
let seekPosition = ratio * activePlayer.length
|
||||
activePlayer.position = seekPosition
|
||||
currentPosition = seekPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global mouse area for drag tracking
|
||||
MouseArea {
|
||||
id: progressGlobalMouseArea
|
||||
anchors.fill: parent.parent.parent // Fill the entire media player widget
|
||||
enabled: progressMouseArea.isSeeking
|
||||
visible: false
|
||||
preventStealing: true
|
||||
|
||||
onPositionChanged: function(mouse) {
|
||||
if (progressMouseArea.isSeeking && activePlayer && activePlayer.length > 0) {
|
||||
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y)
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width))
|
||||
let seekPosition = ratio * activePlayer.length
|
||||
activePlayer.position = seekPosition
|
||||
currentPosition = seekPosition
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
progressMouseArea.isSeeking = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Control buttons - always visible
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: theme.spacingL
|
||||
spacing: theme.spacingM
|
||||
visible: activePlayer !== null
|
||||
height: 32
|
||||
|
||||
// Previous button
|
||||
Rectangle {
|
||||
@@ -313,9 +345,9 @@ Rectangle {
|
||||
|
||||
// Play/Pause button
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: 18
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: theme.primary
|
||||
|
||||
Text {
|
||||
|
||||
@@ -11,13 +11,14 @@ Item {
|
||||
|
||||
property int audioSubTab: 0 // 0: Output, 1: Input
|
||||
|
||||
// These should be bound from parent
|
||||
property real volumeLevel: 50
|
||||
property real micLevel: 50
|
||||
property string currentAudioSink: ""
|
||||
property string currentAudioSource: ""
|
||||
property var audioSinks: []
|
||||
property var audioSources: []
|
||||
readonly property real volumeLevel: AudioService.volumeLevel
|
||||
readonly property real micLevel: AudioService.micLevel
|
||||
readonly property bool volumeMuted: AudioService.sinkMuted
|
||||
readonly property bool micMuted: AudioService.sourceMuted
|
||||
readonly property string currentAudioSink: AudioService.currentAudioSink
|
||||
readonly property string currentAudioSource: AudioService.currentAudioSource
|
||||
readonly property var audioSinks: AudioService.audioSinks
|
||||
readonly property var audioSources: AudioService.audioSources
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
@@ -102,50 +103,64 @@ Item {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "volume_down"
|
||||
text: audioTab.volumeMuted ? "volume_off" : "volume_down"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
color: audioTab.volumeMuted ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: AudioService.toggleMute()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: volumeSliderTrack
|
||||
Item {
|
||||
id: volumeSliderContainer
|
||||
width: parent.width - 80
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: volumeSliderFill
|
||||
width: parent.width * (audioTab.volumeLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
}
|
||||
|
||||
// Draggable handle
|
||||
Rectangle {
|
||||
id: volumeHandle
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
|
||||
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width/2))
|
||||
id: volumeSliderTrack
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1.0
|
||||
Rectangle {
|
||||
id: volumeSliderFill
|
||||
width: parent.width * (audioTab.volumeLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
// Draggable handle
|
||||
Rectangle {
|
||||
id: volumeHandle
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
|
||||
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width/2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,20 +169,56 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
preventStealing: true
|
||||
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
property bool isDragging: false
|
||||
|
||||
onPressed: (mouse) => {
|
||||
isDragging = true
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
|
||||
let newVolume = Math.round(ratio * 100)
|
||||
AudioService.setVolume(newVolume)
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
isDragging = false
|
||||
}
|
||||
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
if (pressed && isDragging) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
|
||||
let newVolume = Math.round(ratio * 100)
|
||||
AudioService.setVolume(newVolume)
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
|
||||
let newVolume = Math.round(ratio * 100)
|
||||
AudioService.setVolume(newVolume)
|
||||
}
|
||||
}
|
||||
|
||||
// Global mouse area for drag tracking
|
||||
MouseArea {
|
||||
id: volumeGlobalMouseArea
|
||||
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
|
||||
enabled: volumeMouseArea.isDragging
|
||||
visible: false
|
||||
preventStealing: true
|
||||
|
||||
onPositionChanged: (mouse) => {
|
||||
if (volumeMouseArea.isDragging) {
|
||||
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y)
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width))
|
||||
let newVolume = Math.round(ratio * 100)
|
||||
AudioService.setVolume(newVolume)
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
volumeMouseArea.isDragging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,13 +230,6 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: audioTab.volumeLevel + "%"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Output Devices
|
||||
@@ -224,14 +268,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Current: " + (function() {
|
||||
for (let sink of audioTab.audioSinks) {
|
||||
if (sink.name === audioTab.currentAudioSink) {
|
||||
return sink.displayName
|
||||
}
|
||||
}
|
||||
return audioTab.currentAudioSink
|
||||
})()
|
||||
text: "Current: " + (AudioService.currentSinkDisplayName || "None")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
@@ -283,10 +320,16 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.active ? "Selected" : ""
|
||||
text: {
|
||||
if (modelData.subtitle && modelData.subtitle !== "") {
|
||||
return modelData.subtitle + (modelData.active ? " • Selected" : "")
|
||||
} else {
|
||||
return modelData.active ? "Selected" : ""
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
|
||||
visible: modelData.active
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,8 +341,6 @@ Item {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
console.log("Clicked audio device:", JSON.stringify(modelData))
|
||||
console.log("Device name to set:", modelData.name)
|
||||
AudioService.setAudioSink(modelData.name)
|
||||
}
|
||||
}
|
||||
@@ -337,50 +378,64 @@ Item {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "mic"
|
||||
text: audioTab.micMuted ? "mic_off" : "mic"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
color: audioTab.micMuted ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: AudioService.toggleMicMute()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: micSliderTrack
|
||||
Item {
|
||||
id: micSliderContainer
|
||||
width: parent.width - 80
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: micSliderFill
|
||||
width: parent.width * (audioTab.micLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
}
|
||||
|
||||
// Draggable handle
|
||||
Rectangle {
|
||||
id: micHandle
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
|
||||
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width/2))
|
||||
id: micSliderTrack
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1.0
|
||||
Rectangle {
|
||||
id: micSliderFill
|
||||
width: parent.width * (audioTab.micLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 100 }
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
// Draggable handle
|
||||
Rectangle {
|
||||
id: micHandle
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
|
||||
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width/2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,20 +444,56 @@ Item {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
preventStealing: true
|
||||
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
property bool isDragging: false
|
||||
|
||||
onPressed: (mouse) => {
|
||||
isDragging = true
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
|
||||
let newMicLevel = Math.round(ratio * 100)
|
||||
AudioService.setMicLevel(newMicLevel)
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
isDragging = false
|
||||
}
|
||||
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
if (pressed && isDragging) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
|
||||
let newMicLevel = Math.round(ratio * 100)
|
||||
AudioService.setMicLevel(newMicLevel)
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
|
||||
let newMicLevel = Math.round(ratio * 100)
|
||||
AudioService.setMicLevel(newMicLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// Global mouse area for drag tracking
|
||||
MouseArea {
|
||||
id: micGlobalMouseArea
|
||||
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
|
||||
enabled: micMouseArea.isDragging
|
||||
visible: false
|
||||
preventStealing: true
|
||||
|
||||
onPositionChanged: (mouse) => {
|
||||
if (micMouseArea.isDragging) {
|
||||
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y)
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width))
|
||||
let newMicLevel = Math.round(ratio * 100)
|
||||
AudioService.setMicLevel(newMicLevel)
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
micMouseArea.isDragging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,12 +506,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: audioTab.micLevel + "%"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Input Devices
|
||||
@@ -459,14 +544,7 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Current: " + (function() {
|
||||
for (let source of audioTab.audioSources) {
|
||||
if (source.name === audioTab.currentAudioSource) {
|
||||
return source.displayName
|
||||
}
|
||||
}
|
||||
return audioTab.currentAudioSource
|
||||
})()
|
||||
text: "Current: " + (AudioService.currentSourceDisplayName || "None")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
@@ -517,10 +595,16 @@ Item {
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.active ? "Selected" : ""
|
||||
text: {
|
||||
if (modelData.subtitle && modelData.subtitle !== "") {
|
||||
return modelData.subtitle + (modelData.active ? " • Selected" : "")
|
||||
} else {
|
||||
return modelData.active ? "Selected" : ""
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
|
||||
visible: modelData.active
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,8 +616,6 @@ Item {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
console.log("Clicked audio source:", JSON.stringify(modelData))
|
||||
console.log("Source name to set:", modelData.name)
|
||||
AudioService.setAudioSource(modelData.name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,206 +6,445 @@ import Quickshell.Io
|
||||
import "../../Common"
|
||||
import "../../Services"
|
||||
|
||||
ScrollView {
|
||||
Item {
|
||||
id: bluetoothTab
|
||||
clip: true
|
||||
|
||||
// These should be bound from parent
|
||||
property bool bluetoothEnabled: false
|
||||
property var bluetoothDevices: []
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
// Bluetooth toggle
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
|
||||
(bluetoothTab.bluetoothEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
|
||||
border.color: bluetoothTab.bluetoothEnabled ? Theme.primary : "transparent"
|
||||
border.width: 2
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "bluetooth"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeLarge
|
||||
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "Bluetooth"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: bluetoothTab.bluetoothEnabled ? "Enabled" : "Disabled"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: bluetoothToggle
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
BluetoothService.toggleBluetooth()
|
||||
}
|
||||
}
|
||||
}
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
// Bluetooth devices (when enabled)
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: bluetoothTab.bluetoothEnabled
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Text {
|
||||
text: "Paired Devices"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
// Real Bluetooth devices
|
||||
Repeater {
|
||||
model: bluetoothTab.bluetoothDevices
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
|
||||
(bluetoothTab.bluetoothEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
|
||||
border.color: bluetoothTab.bluetoothEnabled ? Theme.primary : "transparent"
|
||||
border.width: 2
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
(modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
Text {
|
||||
text: "bluetooth"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeLarge
|
||||
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: {
|
||||
switch (modelData.type) {
|
||||
case "headset": return "headset"
|
||||
case "mouse": return "mouse"
|
||||
case "keyboard": return "keyboard"
|
||||
case "phone": return "smartphone"
|
||||
default: return "bluetooth"
|
||||
}
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Bluetooth"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: modelData.connected ? "Connected" : "Disconnected"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.battery >= 0 ? "• " + modelData.battery + "%" : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: modelData.battery >= 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btDeviceArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
BluetoothService.toggleBluetoothDevice(modelData.mac)
|
||||
Text {
|
||||
text: bluetoothTab.bluetoothEnabled ? "Enabled" : "Disabled"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: bluetoothToggle
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
BluetoothService.toggleBluetooth()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Available devices for pairing (when enabled)
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: bluetoothTab.bluetoothEnabled
|
||||
|
||||
Row {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: bluetoothTab.bluetoothEnabled
|
||||
|
||||
Text {
|
||||
text: "Available Devices"
|
||||
text: "Paired Devices"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: 1; height: 1 }
|
||||
Repeater {
|
||||
model: bluetoothTab.bluetoothDevices
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
(modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: {
|
||||
switch (modelData.type) {
|
||||
case "headset": return "headset"
|
||||
case "mouse": return "mouse"
|
||||
case "keyboard": return "keyboard"
|
||||
case "phone": return "smartphone"
|
||||
default: return "bluetooth"
|
||||
}
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: {
|
||||
if (modelData.connecting) return Theme.primary
|
||||
if (modelData.connected) return Theme.primary
|
||||
return Theme.surfaceText
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: modelData.connecting ? 0.6 : 1.0
|
||||
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
running: modelData.connecting
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { from: 1.0; to: 0.3; duration: 800 }
|
||||
NumberAnimation { from: 0.3; to: 1.0; duration: 800 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: modelData.connectionStatus
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (modelData.connecting) return Theme.primary
|
||||
if (modelData.connectionFailed) return Theme.error
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.battery >= 0 ? "• " + modelData.battery + "%" : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: modelData.battery >= 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: btMenuButton
|
||||
width: 32
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: btMenuButtonArea.containsMouse ?
|
||||
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
|
||||
"transparent"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "more_vert"
|
||||
font.family: Theme.iconFont
|
||||
font.weight: Theme.iconFontWeight
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.6
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btMenuButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: !modelData.connecting
|
||||
enabled: !modelData.connecting
|
||||
cursorShape: modelData.connecting ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (!modelData.connecting) {
|
||||
bluetoothContextMenuWindow.deviceData = modelData
|
||||
let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height)
|
||||
bluetoothContextMenuWindow.show(localPos.x, localPos.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btDeviceArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40 // Don't overlap with menu button
|
||||
hoverEnabled: !modelData.connecting
|
||||
enabled: !modelData.connecting
|
||||
cursorShape: modelData.connecting ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (!modelData.connecting) {
|
||||
BluetoothService.toggleBluetoothDevice(modelData.mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: bluetoothTab.bluetoothEnabled
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(100, scanText.contentWidth + Theme.spacingM * 2)
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Available Devices"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item { width: 1; height: 1 }
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: BluetoothService.scanning ? "stop" : "bluetooth_searching"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: scanText
|
||||
text: BluetoothService.scanning ? "Stop Scanning" : "Start Scanning"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: scanArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (BluetoothService.scanning) {
|
||||
BluetoothService.stopDiscovery()
|
||||
} else {
|
||||
BluetoothService.startDiscovery()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: BluetoothService.availableDevices
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 70
|
||||
radius: Theme.cornerRadius
|
||||
color: availableDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
(modelData.paired ? Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData.paired ? Theme.secondary : (modelData.canPair ? Theme.primary : "transparent")
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: {
|
||||
switch (modelData.type) {
|
||||
case "headset": return "headset"
|
||||
case "mouse": return "mouse"
|
||||
case "keyboard": return "keyboard"
|
||||
case "phone": return "smartphone"
|
||||
case "watch": return "watch"
|
||||
case "speaker": return "speaker"
|
||||
case "tv": return "tv"
|
||||
default: return "bluetooth"
|
||||
}
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: modelData.paired ? Theme.secondary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData.paired ? Theme.secondary : Theme.surfaceText
|
||||
font.weight: modelData.paired ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.paired && modelData.connected) return "Connected"
|
||||
if (modelData.paired) return "Paired"
|
||||
return "Signal: " + modelData.signalStrength
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.rssi !== 0 ? "• " + modelData.rssi + " dBm" : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
visible: modelData.rssi !== 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 28
|
||||
radius: Theme.cornerRadiusSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: actionButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
visible: modelData.canPair || modelData.paired
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.paired ? (modelData.connected ? "Disconnect" : "Connect") : "Pair"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (modelData.paired) {
|
||||
if (modelData.connected) {
|
||||
BluetoothService.toggleBluetoothDevice(modelData.mac)
|
||||
} else {
|
||||
BluetoothService.connectDevice(modelData.mac)
|
||||
}
|
||||
} else {
|
||||
BluetoothService.pairDevice(modelData.mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: availableDeviceArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 90 // Don't overlap with action button
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (modelData.paired) {
|
||||
BluetoothService.toggleBluetoothDevice(modelData.mac)
|
||||
} else {
|
||||
BluetoothService.pairDevice(modelData.mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: BluetoothService.scanning && BluetoothService.availableDevices.length === 0
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: BluetoothService.scanning ? "search" : "bluetooth_searching"
|
||||
text: "sync"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
font.pixelSize: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: BluetoothService.scanning
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
@@ -214,169 +453,237 @@ ScrollView {
|
||||
}
|
||||
|
||||
Text {
|
||||
id: scanText
|
||||
text: BluetoothService.scanning ? "Scanning..." : "Scan"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
text: "Scanning for devices..."
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: scanArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !BluetoothService.scanning
|
||||
|
||||
onClicked: {
|
||||
BluetoothService.startDiscovery()
|
||||
Text {
|
||||
text: "Make sure your device is in pairing mode"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "No devices found. Put your device in pairing mode and click Start Scanning."
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bluetoothContextMenuWindow
|
||||
property var deviceData: null
|
||||
property bool menuVisible: false
|
||||
|
||||
visible: false
|
||||
width: 160
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Theme.popupBackground()
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
z: 1000
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
opacity: menuVisible ? 1.0 : 0.0
|
||||
scale: menuVisible ? 1.0 : 0.85
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "link_off" : "link"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "Disconnect" : "Connect"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (bluetoothContextMenuWindow.deviceData) {
|
||||
BluetoothService.toggleBluetoothDevice(bluetoothContextMenuWindow.deviceData.mac)
|
||||
}
|
||||
bluetoothContextMenuWindow.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Available devices list
|
||||
Repeater {
|
||||
model: BluetoothService.availableDevices
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 70
|
||||
radius: Theme.cornerRadius
|
||||
color: availableDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
(modelData.paired ? Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData.paired ? Theme.secondary : (modelData.canPair ? Theme.primary : "transparent")
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: {
|
||||
switch (modelData.type) {
|
||||
case "headset": return "headset"
|
||||
case "mouse": return "mouse"
|
||||
case "keyboard": return "keyboard"
|
||||
case "phone": return "smartphone"
|
||||
case "watch": return "watch"
|
||||
case "speaker": return "speaker"
|
||||
case "tv": return "tv"
|
||||
default: return "bluetooth"
|
||||
}
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: modelData.paired ? Theme.secondary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData.paired ? Theme.secondary : Theme.surfaceText
|
||||
font.weight: modelData.paired ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.paired && modelData.connected) return "Connected"
|
||||
if (modelData.paired) return "Paired"
|
||||
return "Signal: " + modelData.signalStrength
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.rssi !== 0 ? "• " + modelData.rssi + " dBm" : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
visible: modelData.rssi !== 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action button on the right
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 28
|
||||
radius: Theme.cornerRadiusSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: actionButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
visible: modelData.canPair || modelData.paired
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.paired ? (modelData.connected ? "Disconnect" : "Connect") : "Pair"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionButtonArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (modelData.paired) {
|
||||
if (modelData.connected) {
|
||||
BluetoothService.toggleBluetoothDevice(modelData.mac)
|
||||
} else {
|
||||
BluetoothService.connectDevice(modelData.mac)
|
||||
}
|
||||
} else {
|
||||
BluetoothService.pairDevice(modelData.mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: availableDeviceArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 90 // Don't overlap with action button
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (modelData.paired) {
|
||||
BluetoothService.toggleBluetoothDevice(modelData.mac)
|
||||
} else {
|
||||
BluetoothService.pairDevice(modelData.mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
// No devices message
|
||||
Text {
|
||||
text: "No devices found. Put your device in pairing mode and click Scan."
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning
|
||||
wrapMode: Text.WordWrap
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "delete"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 2
|
||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Forget Device"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forgetArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: {
|
||||
if (bluetoothContextMenuWindow.deviceData) {
|
||||
BluetoothService.removeDevice(bluetoothContextMenuWindow.deviceData.mac)
|
||||
}
|
||||
bluetoothContextMenuWindow.hide()
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function show(x, y) {
|
||||
const menuWidth = 160
|
||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
|
||||
let finalX = x - menuWidth / 2
|
||||
let finalY = y
|
||||
|
||||
finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth))
|
||||
finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight))
|
||||
|
||||
bluetoothContextMenuWindow.x = finalX
|
||||
bluetoothContextMenuWindow.y = finalY
|
||||
bluetoothContextMenuWindow.visible = true
|
||||
bluetoothContextMenuWindow.menuVisible = true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
bluetoothContextMenuWindow.menuVisible = false
|
||||
Qt.callLater(() => { bluetoothContextMenuWindow.visible = false })
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: bluetoothContextMenuWindow.visible
|
||||
onClicked: {
|
||||
bluetoothContextMenuWindow.hide()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
x: bluetoothContextMenuWindow.x
|
||||
y: bluetoothContextMenuWindow.y
|
||||
width: bluetoothContextMenuWindow.width
|
||||
height: bluetoothContextMenuWindow.height
|
||||
onClicked: {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,14 +676,6 @@ PanelWindow {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: controlCenterPopup.currentTab === "audio"
|
||||
|
||||
// Bind properties from root
|
||||
volumeLevel: root.volumeLevel
|
||||
micLevel: root.micLevel
|
||||
currentAudioSink: root.currentAudioSink
|
||||
currentAudioSource: root.currentAudioSource
|
||||
audioSinks: root.audioSinks
|
||||
audioSources: root.audioSources
|
||||
}
|
||||
|
||||
// Bluetooth Tab
|
||||
|
||||
@@ -36,7 +36,7 @@ ScrollView {
|
||||
rightIcon: "brightness_high"
|
||||
enabled: BrightnessService.brightnessAvailable
|
||||
|
||||
onSliderValueChanged: (newValue) => {
|
||||
onSliderValueChanged: function(newValue) {
|
||||
BrightnessService.setBrightness(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,34 +110,79 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sliderMouseArea
|
||||
Item {
|
||||
id: sliderContainer
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: slider.enabled
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (slider.enabled) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
|
||||
slider.value = newValue
|
||||
slider.sliderValueChanged(newValue)
|
||||
MouseArea {
|
||||
id: sliderMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: slider.enabled
|
||||
preventStealing: true
|
||||
|
||||
property bool isDragging: false
|
||||
|
||||
onPressed: (mouse) => {
|
||||
if (slider.enabled) {
|
||||
isDragging = true
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
|
||||
slider.value = newValue
|
||||
slider.sliderValueChanged(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (slider.enabled) {
|
||||
isDragging = false
|
||||
slider.sliderDragFinished(slider.value)
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && isDragging && slider.enabled) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
|
||||
slider.value = newValue
|
||||
slider.sliderValueChanged(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (slider.enabled) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
|
||||
slider.value = newValue
|
||||
slider.sliderValueChanged(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && slider.enabled) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
|
||||
slider.value = newValue
|
||||
slider.sliderValueChanged(newValue)
|
||||
// Global mouse area for drag tracking
|
||||
MouseArea {
|
||||
id: sliderGlobalMouseArea
|
||||
anchors.fill: slider.parent // Fill the entire settings popup
|
||||
enabled: sliderMouseArea.isDragging
|
||||
visible: false
|
||||
preventStealing: true
|
||||
|
||||
onPositionChanged: (mouse) => {
|
||||
if (sliderMouseArea.isDragging && slider.enabled) {
|
||||
let globalPos = mapToItem(sliderTrack, mouse.x, mouse.y)
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / sliderTrack.width))
|
||||
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
|
||||
slider.value = newValue
|
||||
slider.sliderValueChanged(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (slider.enabled) {
|
||||
slider.sliderDragFinished(slider.value)
|
||||
|
||||
onReleased: {
|
||||
if (sliderMouseArea.isDragging && slider.enabled) {
|
||||
sliderMouseArea.isDragging = false
|
||||
slider.sliderDragFinished(slider.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,10 +281,10 @@ PanelWindow {
|
||||
height: 80
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: ProcessMonitorService.totalSwapKB > 0 ?
|
||||
Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.08) :
|
||||
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) :
|
||||
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04)
|
||||
border.color: ProcessMonitorService.totalSwapKB > 0 ?
|
||||
Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.2) :
|
||||
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2) :
|
||||
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
@@ -298,7 +298,7 @@ PanelWindow {
|
||||
text: "Swap"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: ProcessMonitorService.totalSwapKB > 0 ? Theme.tertiary : Theme.surfaceText
|
||||
color: ProcessMonitorService.totalSwapKB > 0 ? Theme.warning : Theme.surfaceText
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
|
||||
@@ -443,10 +443,10 @@ PanelWindow {
|
||||
height: 80
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: ProcessMonitorService.totalSwapKB > 0 ?
|
||||
Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.08) :
|
||||
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) :
|
||||
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04)
|
||||
border.color: ProcessMonitorService.totalSwapKB > 0 ?
|
||||
Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.2) :
|
||||
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2) :
|
||||
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12)
|
||||
border.width: 1
|
||||
|
||||
@@ -460,7 +460,7 @@ PanelWindow {
|
||||
text: "Swap"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: ProcessMonitorService.totalSwapKB > 0 ? Theme.tertiary : Theme.surfaceText
|
||||
color: ProcessMonitorService.totalSwapKB > 0 ? Theme.warning : Theme.surfaceText
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
@@ -749,36 +749,365 @@ PanelWindow {
|
||||
// Define inline components for tabs
|
||||
Component {
|
||||
id: performanceTabComponent
|
||||
Rectangle {
|
||||
color: "transparent"
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
// CPU Section - Compact with per-core bars
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 200
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
text: "analytics"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 48
|
||||
color: Theme.primary
|
||||
opacity: 0.6
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// CPU Header with overall usage
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "CPU"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 24
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: ProcessMonitorService.totalCpuUsage.toFixed(1) + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primary
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: parent.width - 280; height: 1 }
|
||||
|
||||
Text {
|
||||
text: ProcessMonitorService.cpuCount + " cores"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Per-core CPU bars - Scrollable
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 40
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: ProcessMonitorService.perCoreCpuUsage.length
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 20
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Core label
|
||||
Text {
|
||||
text: "C" + index
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Usage bar
|
||||
Rectangle {
|
||||
width: parent.width - 80
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: parent.width * Math.min(1.0, ProcessMonitorService.perCoreCpuUsage[index] / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: {
|
||||
const usage = ProcessMonitorService.perCoreCpuUsage[index]
|
||||
if (usage > 80) return Theme.error
|
||||
if (usage > 60) return Theme.warning
|
||||
return Theme.primary
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Theme.shortDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage percentage
|
||||
Text {
|
||||
text: ProcessMonitorService.perCoreCpuUsage[index] ?
|
||||
ProcessMonitorService.perCoreCpuUsage[index].toFixed(0) + "%" : "0%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
width: 32
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Memory Section - Simplified
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "Memory"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Text {
|
||||
text: ProcessMonitorService.formatSystemMemory(ProcessMonitorService.usedMemoryKB) +
|
||||
" / " + ProcessMonitorService.formatSystemMemory(ProcessMonitorService.totalMemoryKB)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: Theme.spacingL; height: 1 }
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
width: 200
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 16
|
||||
radius: 8
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: ProcessMonitorService.totalMemoryKB > 0 ?
|
||||
parent.width * (ProcessMonitorService.usedMemoryKB / ProcessMonitorService.totalMemoryKB) : 0
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: {
|
||||
const usage = ProcessMonitorService.totalMemoryKB > 0 ?
|
||||
(ProcessMonitorService.usedMemoryKB / ProcessMonitorService.totalMemoryKB) : 0
|
||||
if (usage > 0.9) return Theme.error
|
||||
if (usage > 0.7) return Theme.warning
|
||||
return Theme.secondary
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: Theme.mediumDuration }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: ProcessMonitorService.totalMemoryKB > 0 ?
|
||||
((ProcessMonitorService.usedMemoryKB / ProcessMonitorService.totalMemoryKB) * 100).toFixed(1) + "% used" :
|
||||
"No data"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: parent.width - 300; height: 1 }
|
||||
|
||||
// Swap info - compact
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
visible: ProcessMonitorService.totalSwapKB > 0
|
||||
|
||||
Text {
|
||||
text: "Swap"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.warning
|
||||
}
|
||||
|
||||
Text {
|
||||
text: ProcessMonitorService.formatSystemMemory(ProcessMonitorService.usedSwapKB) +
|
||||
" / " + ProcessMonitorService.formatSystemMemory(ProcessMonitorService.totalSwapKB)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network & Disk I/O - Combined compact view
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 80
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Network I/O
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "Network"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "↓"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.info
|
||||
}
|
||||
|
||||
Text {
|
||||
text: formatNetworkSpeed(ProcessMonitorService.networkRxRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "↑"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
}
|
||||
|
||||
Text {
|
||||
text: formatNetworkSpeed(ProcessMonitorService.networkTxRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Performance Monitoring"
|
||||
font.pixelSize: Theme.fontSizeLarge + 2
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Real-time system performance charts\nwill be displayed here"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
// Disk I/O
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "Disk"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "R"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: formatDiskSpeed(ProcessMonitorService.diskReadRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "W"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.warning
|
||||
}
|
||||
|
||||
Text {
|
||||
text: formatDiskSpeed(ProcessMonitorService.diskWriteRate)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -786,36 +1115,481 @@ PanelWindow {
|
||||
|
||||
Component {
|
||||
id: systemTabComponent
|
||||
Rectangle {
|
||||
color: "transparent"
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "settings"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 48
|
||||
color: Theme.primary
|
||||
opacity: 0.6
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 140
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 140
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingM + 4
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "computer"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "System"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Host: " + SystemMonitorService.hostname
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "OS: " + SystemMonitorService.distribution
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Arch: " + SystemMonitorService.architecture
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Kernel: " + SystemMonitorService.kernelVersion
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 140
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.16)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingM + 4
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "developer_board"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.secondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Hardware"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "CPU: " + SystemMonitorService.cpuModel
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Motherboard: " + SystemMonitorService.motherboard
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "BIOS: " + SystemMonitorService.biosVersion
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Memory: " + SystemMonitorService.formatMemory(SystemMonitorService.totalMemory)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "System Information"
|
||||
font.pixelSize: Theme.fontSizeLarge + 2
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 120
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 120
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.16)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingM + 4
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "timer"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.warning
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Uptime"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SystemMonitorService.uptime
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Boot: " + SystemMonitorService.bootTime
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Load: " + SystemMonitorService.loadAverage
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 120
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.16)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
anchors.bottomMargin: Theme.spacingM + 4
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "list_alt"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.info
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Processes"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SystemMonitorService.processCount + " Running"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: SystemMonitorService.threadCount + " Total Created"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Kernel information, schedulers,\nand system details will be shown here"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: Math.max(200, diskMountRepeater.count * 28 + 60)
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.08)
|
||||
border.color: Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.16)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "storage"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.success
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Storage & Disks"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "I/O Scheduler: " + SystemMonitorService.scheduler
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 60
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 24
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "Device"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.25
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Mount"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Size"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Used"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Available"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Use%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.1
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: diskMountRepeater
|
||||
model: SystemMonitorService.diskMounts
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 24
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: diskMouseArea.containsMouse ?
|
||||
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) :
|
||||
"transparent"
|
||||
|
||||
MouseArea {
|
||||
id: diskMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: modelData.device
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.25
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.mount
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.2
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.size
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.used
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.avail
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width * 0.15
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.percent
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
const percent = parseInt(modelData.percent)
|
||||
if (percent > 90) return Theme.error
|
||||
if (percent > 75) return Theme.warning
|
||||
return Theme.surfaceText
|
||||
}
|
||||
width: parent.width * 0.1
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1155,10 +1929,13 @@ PanelWindow {
|
||||
processListWidget.isVisible = true
|
||||
ProcessMonitorService.updateSystemInfo()
|
||||
ProcessMonitorService.updateProcessList()
|
||||
SystemMonitorService.enableDetailedMonitoring(true)
|
||||
SystemMonitorService.updateSystemInfo()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
processListWidget.isVisible = false
|
||||
SystemMonitorService.enableDetailedMonitoring(false)
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
@@ -1168,4 +1945,27 @@ PanelWindow {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for formatting
|
||||
function formatNetworkSpeed(bytesPerSec) {
|
||||
if (bytesPerSec < 1024) {
|
||||
return bytesPerSec.toFixed(0) + " B/s"
|
||||
} else if (bytesPerSec < 1024 * 1024) {
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s"
|
||||
} else if (bytesPerSec < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
|
||||
} else {
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
|
||||
}
|
||||
}
|
||||
|
||||
function formatDiskSpeed(bytesPerSec) {
|
||||
if (bytesPerSec < 1024 * 1024) {
|
||||
return (bytesPerSec / 1024).toFixed(1) + " KB/s"
|
||||
} else if (bytesPerSec < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
|
||||
} else {
|
||||
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ Item {
|
||||
property bool hasActiveMedia: false
|
||||
property var activePlayer: null
|
||||
property bool cavaAvailable: false
|
||||
property bool configCreated: false
|
||||
|
||||
width: 20
|
||||
height: Theme.iconSize
|
||||
@@ -22,53 +21,20 @@ Item {
|
||||
running: true
|
||||
onExited: (exitCode) => {
|
||||
root.cavaAvailable = exitCode === 0
|
||||
if (root.cavaAvailable && !root.configCreated) {
|
||||
console.log("cava found - creating config and enabling real audio visualization")
|
||||
configWriter.running = true
|
||||
} else if (!root.cavaAvailable) {
|
||||
if (root.cavaAvailable) {
|
||||
console.log("cava found - enabling real audio visualization")
|
||||
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
|
||||
} else {
|
||||
console.log("cava not found - using fallback animation")
|
||||
fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: configWriter
|
||||
running: false
|
||||
command: [
|
||||
"sh", "-c",
|
||||
`cat > /tmp/quickshell_cava_config << 'EOF'
|
||||
[general]
|
||||
mode = normal
|
||||
framerate = 30
|
||||
autosens = 0
|
||||
sensitivity = 50
|
||||
bars = 4
|
||||
|
||||
[output]
|
||||
method = raw
|
||||
raw_target = /dev/stdout
|
||||
data_format = ascii
|
||||
channels = mono
|
||||
mono_option = average
|
||||
|
||||
[smoothing]
|
||||
noise_reduction = 20
|
||||
EOF`
|
||||
]
|
||||
|
||||
onExited: {
|
||||
root.configCreated = true
|
||||
if (root.cavaAvailable) {
|
||||
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProcess
|
||||
running: false
|
||||
command: ["cava", "-p", "/tmp/quickshell_cava_config"]
|
||||
command: ["sh", "-c", `printf '[general]\nmode=normal\nframerate=30\nautosens=0\nsensitivity=50\nbars=4\n[output]\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nchannels=mono\nmono_option=average\n[smoothing]\nnoise_reduction=20' | cava -p /dev/stdin`]
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
|
||||
@@ -8,6 +8,7 @@ Rectangle {
|
||||
property string networkStatus: "disconnected"
|
||||
property string wifiSignalStrength: "good"
|
||||
property int volumeLevel: 50
|
||||
property bool volumeMuted: false
|
||||
property bool bluetoothAvailable: false
|
||||
property bool bluetoothEnabled: false
|
||||
property bool isActive: false
|
||||
@@ -62,7 +63,7 @@ Rectangle {
|
||||
|
||||
// Audio Icon
|
||||
Text {
|
||||
text: root.volumeLevel === 0 ? "volume_off" :
|
||||
text: root.volumeMuted ? "volume_off" :
|
||||
root.volumeLevel < 33 ? "volume_down" : "volume_up"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
|
||||
@@ -41,6 +41,7 @@ PanelWindow {
|
||||
property string networkStatus: "disconnected"
|
||||
property string wifiSignalStrength: "good"
|
||||
property int volumeLevel: 50
|
||||
property bool volumeMuted: false
|
||||
property bool bluetoothAvailable: false
|
||||
property bool bluetoothEnabled: false
|
||||
|
||||
@@ -298,6 +299,10 @@ PanelWindow {
|
||||
// Battery Widget
|
||||
BatteryWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
batteryPopupVisible: topBar.shellRoot.batteryPopupVisible
|
||||
onToggleBatteryPopup: {
|
||||
topBar.shellRoot.batteryPopupVisible = !topBar.shellRoot.batteryPopupVisible
|
||||
}
|
||||
}
|
||||
|
||||
ControlCenterButton {
|
||||
@@ -305,6 +310,7 @@ PanelWindow {
|
||||
networkStatus: topBar.networkStatus
|
||||
wifiSignalStrength: topBar.wifiSignalStrength
|
||||
volumeLevel: topBar.volumeLevel
|
||||
volumeMuted: topBar.volumeMuted
|
||||
bluetoothAvailable: topBar.bluetoothAvailable
|
||||
bluetoothEnabled: topBar.bluetoothEnabled
|
||||
isActive: topBar.shellRoot ? topBar.shellRoot.controlCenterVisible : false
|
||||
@@ -314,7 +320,7 @@ PanelWindow {
|
||||
topBar.shellRoot.controlCenterVisible = !topBar.shellRoot.controlCenterVisible
|
||||
if (topBar.shellRoot.controlCenterVisible) {
|
||||
WifiService.scanWifi()
|
||||
BluetoothService.scanDevices()
|
||||
// Bluetooth devices are automatically updated via signals
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
shell.qml
11
shell.qml
@@ -27,7 +27,7 @@ ShellRoot {
|
||||
// Initialize service monitoring states based on preferences
|
||||
SystemMonitorService.enableTopBarMonitoring(Prefs.showSystemResources)
|
||||
ProcessMonitorService.enableMonitoring(false) // Start disabled, enable when process dropdown is opened
|
||||
AudioService.enableDeviceScanning(false) // Start disabled, enable when control center is opened
|
||||
// Audio service auto-updates devices, no manual scanning needed
|
||||
}
|
||||
|
||||
property bool calendarVisible: false
|
||||
@@ -44,14 +44,13 @@ ShellRoot {
|
||||
property bool hasActiveMedia: activePlayer && (activePlayer.trackTitle || activePlayer.trackArtist)
|
||||
property bool controlCenterVisible: false
|
||||
|
||||
// Monitor control center visibility to enable/disable audio device scanning
|
||||
// Monitor control center visibility to enable/disable bluetooth scanning
|
||||
onControlCenterVisibleChanged: {
|
||||
console.log("Control center", controlCenterVisible ? "opened" : "closed")
|
||||
AudioService.enableDeviceScanning(controlCenterVisible)
|
||||
BluetoothService.enableMonitoring(controlCenterVisible)
|
||||
if (controlCenterVisible) {
|
||||
// Immediately refresh devices when opening control center
|
||||
AudioService.refreshDevices()
|
||||
// Refresh devices when opening control center
|
||||
AudioService.updateDevices()
|
||||
}
|
||||
}
|
||||
property bool batteryPopupVisible: false
|
||||
@@ -80,6 +79,7 @@ ShellRoot {
|
||||
|
||||
// Audio properties from AudioService
|
||||
property int volumeLevel: AudioService.volumeLevel
|
||||
property bool volumeMuted: AudioService.sinkMuted
|
||||
property var audioSinks: AudioService.audioSinks
|
||||
property string currentAudioSink: AudioService.currentAudioSink
|
||||
|
||||
@@ -180,6 +180,7 @@ ShellRoot {
|
||||
networkStatus: root.networkStatus
|
||||
wifiSignalStrength: root.wifiSignalStrength
|
||||
volumeLevel: root.volumeLevel
|
||||
volumeMuted: root.volumeMuted
|
||||
bluetoothAvailable: root.bluetoothAvailable
|
||||
bluetoothEnabled: root.bluetoothEnabled
|
||||
shellRoot: root
|
||||
|
||||
Reference in New Issue
Block a user