mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-07 14:05:38 -05:00
bluetooth: switch to native quickshell bluetooth service
removes dependency on bluetoothctl commands
This commit is contained in:
@@ -14,7 +14,6 @@ Singleton {
|
|||||||
property var applicationsByExec: ({})
|
property var applicationsByExec: ({})
|
||||||
property bool ready: false
|
property bool ready: false
|
||||||
|
|
||||||
// Pre-prepared fuzzy search data
|
|
||||||
property var preppedApps: []
|
property var preppedApps: []
|
||||||
|
|
||||||
|
|
||||||
@@ -33,7 +32,6 @@ Singleton {
|
|||||||
|
|
||||||
|
|
||||||
function loadApplications() {
|
function loadApplications() {
|
||||||
// Trigger rescan on next frame to avoid blocking
|
|
||||||
Qt.callLater(function() {
|
Qt.callLater(function() {
|
||||||
var allApps = Array.from(DesktopEntries.applications.values)
|
var allApps = Array.from(DesktopEntries.applications.values)
|
||||||
|
|
||||||
@@ -41,7 +39,6 @@ Singleton {
|
|||||||
.filter(app => !app.noDisplay)
|
.filter(app => !app.noDisplay)
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
|
||||||
// Build lookup maps
|
|
||||||
var byName = {}
|
var byName = {}
|
||||||
var byExec = {}
|
var byExec = {}
|
||||||
|
|
||||||
@@ -49,7 +46,6 @@ Singleton {
|
|||||||
var app = applications[i]
|
var app = applications[i]
|
||||||
byName[app.name.toLowerCase()] = app
|
byName[app.name.toLowerCase()] = app
|
||||||
|
|
||||||
// Clean exec string for lookup
|
|
||||||
var execProp = app.execString || ""
|
var execProp = app.execString || ""
|
||||||
var cleanExec = execProp ? execProp.replace(/%[fFuU]/g, "").trim() : ""
|
var cleanExec = execProp ? execProp.replace(/%[fFuU]/g, "").trim() : ""
|
||||||
if (cleanExec) {
|
if (cleanExec) {
|
||||||
@@ -60,7 +56,6 @@ Singleton {
|
|||||||
applicationsByName = byName
|
applicationsByName = byName
|
||||||
applicationsByExec = byExec
|
applicationsByExec = byExec
|
||||||
|
|
||||||
// Prepare fuzzy search data
|
|
||||||
preppedApps = applications.map(app => ({
|
preppedApps = applications.map(app => ({
|
||||||
name: Fuzzy.prepare(app.name || ""),
|
name: Fuzzy.prepare(app.name || ""),
|
||||||
comment: Fuzzy.prepare(app.comment || ""),
|
comment: Fuzzy.prepare(app.comment || ""),
|
||||||
@@ -84,12 +79,10 @@ Singleton {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use fuzzy search with both name and comment fields
|
|
||||||
var results = Fuzzy.go(query, preppedApps, {
|
var results = Fuzzy.go(query, preppedApps, {
|
||||||
all: false,
|
all: false,
|
||||||
keys: ["name", "comment"],
|
keys: ["name", "comment"],
|
||||||
scoreFn: r => {
|
scoreFn: r => {
|
||||||
// Prioritize name matches over comment matches
|
|
||||||
var nameScore = r[0] ? r[0].score : 0
|
var nameScore = r[0] ? r[0].score : 0
|
||||||
var commentScore = r[1] ? r[1].score : 0
|
var commentScore = r[1] ? r[1].score : 0
|
||||||
return nameScore > 0 ? nameScore * 0.9 + commentScore * 0.1 : commentScore * 0.5
|
return nameScore > 0 ? nameScore * 0.9 + commentScore * 0.1 : commentScore * 0.5
|
||||||
@@ -97,7 +90,6 @@ Singleton {
|
|||||||
limit: 50
|
limit: 50
|
||||||
})
|
})
|
||||||
|
|
||||||
// Extract the desktop entries from results
|
|
||||||
return results.map(r => r.obj.entry)
|
return results.map(r => r.obj.entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +172,6 @@ Singleton {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// DesktopEntry objects have an execute() method
|
|
||||||
if (typeof app.execute === "function") {
|
if (typeof app.execute === "function") {
|
||||||
app.execute()
|
app.execute()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ Singleton {
|
|||||||
property var audioSinks: []
|
property var audioSinks: []
|
||||||
property string currentAudioSink: ""
|
property string currentAudioSink: ""
|
||||||
|
|
||||||
// Microphone properties
|
|
||||||
property int micLevel: 50
|
property int micLevel: 50
|
||||||
property var audioSources: []
|
property var audioSources: []
|
||||||
property string currentAudioSource: ""
|
property string currentAudioSource: ""
|
||||||
|
|
||||||
// Device scanning control
|
|
||||||
property bool deviceScanningEnabled: false
|
property bool deviceScanningEnabled: false
|
||||||
property bool initialScanComplete: false
|
property bool initialScanComplete: false
|
||||||
|
|
||||||
// Real Audio Control
|
|
||||||
Process {
|
Process {
|
||||||
id: volumeChecker
|
id: volumeChecker
|
||||||
command: ["bash", "-c", "pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
|
command: ["bash", "-c", "pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
|
||||||
@@ -36,7 +32,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Microphone level checker
|
|
||||||
Process {
|
Process {
|
||||||
id: micLevelChecker
|
id: micLevelChecker
|
||||||
command: ["bash", "-c", "pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
|
command: ["bash", "-c", "pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
|
||||||
@@ -68,7 +63,6 @@ Singleton {
|
|||||||
for (let line of lines) {
|
for (let line of lines) {
|
||||||
line = line.trim()
|
line = line.trim()
|
||||||
|
|
||||||
// New sink starts
|
|
||||||
if (line.startsWith('Sink #')) {
|
if (line.startsWith('Sink #')) {
|
||||||
if (currentSink && currentSink.name && currentSink.id) {
|
if (currentSink && currentSink.name && currentSink.id) {
|
||||||
sinks.push(currentSink)
|
sinks.push(currentSink)
|
||||||
@@ -84,39 +78,31 @@ Singleton {
|
|||||||
active: false
|
active: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Get the Name field
|
|
||||||
else if (line.startsWith('Name: ') && currentSink) {
|
else if (line.startsWith('Name: ') && currentSink) {
|
||||||
currentSink.name = line.replace('Name: ', '').trim()
|
currentSink.name = line.replace('Name: ', '').trim()
|
||||||
}
|
}
|
||||||
// Get the Description field (main display name)
|
|
||||||
else if (line.startsWith('Description: ') && currentSink) {
|
else if (line.startsWith('Description: ') && currentSink) {
|
||||||
currentSink.description = line.replace('Description: ', '').trim()
|
currentSink.description = line.replace('Description: ', '').trim()
|
||||||
}
|
}
|
||||||
// Get device.description as fallback
|
|
||||||
else if (line.includes('device.description = ') && currentSink && !currentSink.description) {
|
else if (line.includes('device.description = ') && currentSink && !currentSink.description) {
|
||||||
currentSink.description = line.replace('device.description = ', '').replace(/"/g, '').trim()
|
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) {
|
else if (line.includes('node.nick = ') && currentSink && !currentSink.description) {
|
||||||
currentSink.nick = line.replace('node.nick = ', '').replace(/"/g, '').trim()
|
currentSink.nick = line.replace('node.nick = ', '').replace(/"/g, '').trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the last sink
|
|
||||||
if (currentSink && currentSink.name && currentSink.id) {
|
if (currentSink && currentSink.name && currentSink.id) {
|
||||||
sinks.push(currentSink)
|
sinks.push(currentSink)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process display names
|
|
||||||
for (let sink of sinks) {
|
for (let sink of sinks) {
|
||||||
let displayName = sink.description
|
let displayName = sink.description
|
||||||
|
|
||||||
// If no good description, try nick
|
|
||||||
if (!displayName || displayName === sink.name) {
|
if (!displayName || displayName === sink.name) {
|
||||||
displayName = sink.nick
|
displayName = sink.nick
|
||||||
}
|
}
|
||||||
|
|
||||||
// Still no good name? Fall back to smart defaults
|
|
||||||
if (!displayName || displayName === sink.name) {
|
if (!displayName || displayName === sink.name) {
|
||||||
if (sink.name.includes("analog-stereo")) displayName = "Built-in Speakers"
|
if (sink.name.includes("analog-stereo")) displayName = "Built-in Speakers"
|
||||||
else if (sink.name.includes("bluez")) displayName = "Bluetooth Audio"
|
else if (sink.name.includes("bluez")) displayName = "Bluetooth Audio"
|
||||||
@@ -136,7 +122,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio source (microphone) lister
|
|
||||||
Process {
|
Process {
|
||||||
id: audioSourceLister
|
id: audioSourceLister
|
||||||
command: ["pactl", "list", "sources"]
|
command: ["pactl", "list", "sources"]
|
||||||
@@ -153,7 +138,6 @@ Singleton {
|
|||||||
for (let line of lines) {
|
for (let line of lines) {
|
||||||
line = line.trim()
|
line = line.trim()
|
||||||
|
|
||||||
// New source starts
|
|
||||||
if (line.startsWith('Source #')) {
|
if (line.startsWith('Source #')) {
|
||||||
if (currentSource && currentSource.name && currentSource.id) {
|
if (currentSource && currentSource.name && currentSource.id) {
|
||||||
sources.push(currentSource)
|
sources.push(currentSource)
|
||||||
@@ -165,23 +149,19 @@ Singleton {
|
|||||||
active: false
|
active: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Source name
|
|
||||||
else if (line.startsWith('Name: ') && currentSource) {
|
else if (line.startsWith('Name: ') && currentSource) {
|
||||||
currentSource.name = line.replace('Name: ', '')
|
currentSource.name = line.replace('Name: ', '')
|
||||||
}
|
}
|
||||||
// Description (display name)
|
|
||||||
else if (line.startsWith('Description: ') && currentSource) {
|
else if (line.startsWith('Description: ') && currentSource) {
|
||||||
let desc = line.replace('Description: ', '')
|
let desc = line.replace('Description: ', '')
|
||||||
currentSource.displayName = desc
|
currentSource.displayName = desc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the last source
|
|
||||||
if (currentSource && currentSource.name && currentSource.id) {
|
if (currentSource && currentSource.name && currentSource.id) {
|
||||||
sources.push(currentSource)
|
sources.push(currentSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out monitor sources (we want actual input devices)
|
|
||||||
sources = sources.filter(source => !source.name.includes('.monitor'))
|
sources = sources.filter(source => !source.name.includes('.monitor'))
|
||||||
|
|
||||||
root.audioSources = sources
|
root.audioSources = sources
|
||||||
@@ -202,7 +182,6 @@ Singleton {
|
|||||||
if (data.trim()) {
|
if (data.trim()) {
|
||||||
root.currentAudioSink = data.trim()
|
root.currentAudioSink = data.trim()
|
||||||
|
|
||||||
// Update active status in audioSinks
|
|
||||||
let updatedSinks = []
|
let updatedSinks = []
|
||||||
for (let sink of root.audioSinks) {
|
for (let sink of root.audioSinks) {
|
||||||
updatedSinks.push({
|
updatedSinks.push({
|
||||||
@@ -218,7 +197,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default source (microphone) checker
|
|
||||||
Process {
|
Process {
|
||||||
id: defaultSourceChecker
|
id: defaultSourceChecker
|
||||||
command: ["pactl", "get-default-source"]
|
command: ["pactl", "get-default-source"]
|
||||||
@@ -230,7 +208,6 @@ Singleton {
|
|||||||
if (data.trim()) {
|
if (data.trim()) {
|
||||||
root.currentAudioSource = data.trim()
|
root.currentAudioSource = data.trim()
|
||||||
|
|
||||||
// Update active status in audioSources
|
|
||||||
let updatedSources = []
|
let updatedSources = []
|
||||||
for (let source of root.audioSources) {
|
for (let source of root.audioSources) {
|
||||||
updatedSources.push({
|
updatedSources.push({
|
||||||
@@ -271,12 +248,10 @@ Singleton {
|
|||||||
function setAudioSink(sinkName) {
|
function setAudioSink(sinkName) {
|
||||||
console.log("Setting audio sink to:", 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.command = ["pactl", "set-default-sink", sinkName]
|
||||||
sinkSetProcess.running = true
|
sinkSetProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dedicated process for setting audio sink
|
|
||||||
Process {
|
Process {
|
||||||
id: sinkSetProcess
|
id: sinkSetProcess
|
||||||
running: false
|
running: false
|
||||||
@@ -285,7 +260,6 @@ Singleton {
|
|||||||
console.log("Audio sink change exit code:", exitCode)
|
console.log("Audio sink change exit code:", exitCode)
|
||||||
if (exitCode === 0) {
|
if (exitCode === 0) {
|
||||||
console.log("Audio sink changed successfully")
|
console.log("Audio sink changed successfully")
|
||||||
// Refresh current sink and list
|
|
||||||
defaultSinkChecker.running = true
|
defaultSinkChecker.running = true
|
||||||
if (root.deviceScanningEnabled) {
|
if (root.deviceScanningEnabled) {
|
||||||
audioSinkLister.running = true
|
audioSinkLister.running = true
|
||||||
@@ -303,7 +277,6 @@ Singleton {
|
|||||||
sourceSetProcess.running = true
|
sourceSetProcess.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dedicated process for setting audio source
|
|
||||||
Process {
|
Process {
|
||||||
id: sourceSetProcess
|
id: sourceSetProcess
|
||||||
running: false
|
running: false
|
||||||
@@ -312,7 +285,6 @@ Singleton {
|
|||||||
console.log("Audio source change exit code:", exitCode)
|
console.log("Audio source change exit code:", exitCode)
|
||||||
if (exitCode === 0) {
|
if (exitCode === 0) {
|
||||||
console.log("Audio source changed successfully")
|
console.log("Audio source changed successfully")
|
||||||
// Refresh current source and list
|
|
||||||
defaultSourceChecker.running = true
|
defaultSourceChecker.running = true
|
||||||
if (root.deviceScanningEnabled) {
|
if (root.deviceScanningEnabled) {
|
||||||
audioSourceLister.running = true
|
audioSourceLister.running = true
|
||||||
@@ -337,25 +309,21 @@ Singleton {
|
|||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
console.log("AudioService: Starting initialization...")
|
console.log("AudioService: Starting initialization...")
|
||||||
// Do initial device scan
|
|
||||||
audioSinkLister.running = true
|
audioSinkLister.running = true
|
||||||
audioSourceLister.running = true
|
audioSourceLister.running = true
|
||||||
initialScanComplete = true
|
initialScanComplete = true
|
||||||
console.log("AudioService: Initialization complete")
|
console.log("AudioService: Initialization complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control functions for managing device scanning
|
|
||||||
function enableDeviceScanning(enabled) {
|
function enableDeviceScanning(enabled) {
|
||||||
console.log("AudioService: Device scanning", enabled ? "enabled" : "disabled")
|
console.log("AudioService: Device scanning", enabled ? "enabled" : "disabled")
|
||||||
root.deviceScanningEnabled = enabled
|
root.deviceScanningEnabled = enabled
|
||||||
if (enabled && root.initialScanComplete) {
|
if (enabled && root.initialScanComplete) {
|
||||||
// Immediately scan when enabled
|
|
||||||
audioSinkLister.running = true
|
audioSinkLister.running = true
|
||||||
audioSourceLister.running = true
|
audioSourceLister.running = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manual refresh function for when user opens audio settings
|
|
||||||
function refreshDevices() {
|
function refreshDevices() {
|
||||||
console.log("AudioService: Manual device refresh triggered")
|
console.log("AudioService: Manual device refresh triggered")
|
||||||
audioSinkLister.running = true
|
audioSinkLister.running = true
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ pragma ComponentBehavior: Bound
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Battery properties
|
|
||||||
property bool batteryAvailable: false
|
property bool batteryAvailable: false
|
||||||
property int batteryLevel: 0
|
property int batteryLevel: 0
|
||||||
property string batteryStatus: "Unknown"
|
property string batteryStatus: "Unknown"
|
||||||
@@ -20,8 +19,6 @@ Singleton {
|
|||||||
property int batteryCapacity: 0
|
property int batteryCapacity: 0
|
||||||
property var powerProfiles: []
|
property var powerProfiles: []
|
||||||
property string activePowerProfile: ""
|
property string activePowerProfile: ""
|
||||||
|
|
||||||
// Check if battery is available
|
|
||||||
Process {
|
Process {
|
||||||
id: batteryAvailabilityChecker
|
id: batteryAvailabilityChecker
|
||||||
command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"]
|
command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"]
|
||||||
@@ -58,7 +55,6 @@ Singleton {
|
|||||||
if (text.trim() && text.trim() !== "fallback") {
|
if (text.trim() && text.trim() !== "fallback") {
|
||||||
parseBatteryInfo(text.trim())
|
parseBatteryInfo(text.trim())
|
||||||
} else {
|
} else {
|
||||||
// Fallback to simple methods
|
|
||||||
fallbackBatteryChecker.running = true
|
fallbackBatteryChecker.running = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,7 +68,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback battery checker using /sys files
|
|
||||||
Process {
|
Process {
|
||||||
id: fallbackBatteryChecker
|
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"]
|
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"]
|
||||||
@@ -87,7 +82,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Power profiles checker (for systems with power-profiles-daemon)
|
|
||||||
Process {
|
Process {
|
||||||
id: powerProfilesChecker
|
id: powerProfilesChecker
|
||||||
command: ["bash", "-c", "if command -v powerprofilesctl > /dev/null; then powerprofilesctl list 2>/dev/null; else echo 'not-available'; fi"]
|
command: ["bash", "-c", "if command -v powerprofilesctl > /dev/null; then powerprofilesctl list 2>/dev/null; else echo 'not-available'; fi"]
|
||||||
@@ -170,7 +164,6 @@ Singleton {
|
|||||||
for (let line of lines) {
|
for (let line of lines) {
|
||||||
line = line.trim()
|
line = line.trim()
|
||||||
if (line.includes('*')) {
|
if (line.includes('*')) {
|
||||||
// Active profile
|
|
||||||
let profileName = line.replace('*', '').trim()
|
let profileName = line.replace('*', '').trim()
|
||||||
if (profileName.includes(':')) {
|
if (profileName.includes(':')) {
|
||||||
profileName = profileName.split(':')[0].trim()
|
profileName = profileName.split(':')[0].trim()
|
||||||
@@ -248,7 +241,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Update timer
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 30000
|
interval: 30000
|
||||||
running: root.batteryAvailable
|
running: root.batteryAvailable
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Bluetooth
|
||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
@@ -9,410 +9,466 @@ Singleton {
|
|||||||
|
|
||||||
property bool bluetoothEnabled: false
|
property bool bluetoothEnabled: false
|
||||||
property bool bluetoothAvailable: false
|
property bool bluetoothAvailable: false
|
||||||
property var bluetoothDevices: []
|
readonly property list<BluetoothDevice> bluetoothDevices: []
|
||||||
property var availableDevices: []
|
readonly property list<BluetoothDevice> availableDevices: []
|
||||||
property bool scanning: false
|
property bool scanning: false
|
||||||
property bool discoverable: false
|
property bool discoverable: false
|
||||||
|
|
||||||
// Real Bluetooth Management
|
property var connectingDevices: ({})
|
||||||
Process {
|
|
||||||
id: bluetoothStatusChecker
|
Component.onCompleted: {
|
||||||
command: ["bluetoothctl", "show"] // Use default controller
|
refreshBluetoothState()
|
||||||
running: true
|
updateDevices()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Bluetooth
|
||||||
|
|
||||||
stdout: StdioCollector {
|
function onDefaultAdapterChanged() {
|
||||||
onStreamFinished: {
|
console.log("BluetoothService: Default adapter changed")
|
||||||
root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller")
|
refreshBluetoothState()
|
||||||
root.bluetoothEnabled = text.includes("Powered: yes")
|
updateDevices()
|
||||||
|
}
|
||||||
if (root.bluetoothEnabled && root.bluetoothAvailable) {
|
}
|
||||||
bluetoothDeviceScanner.running = true
|
|
||||||
|
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 {
|
} else {
|
||||||
root.bluetoothDevices = []
|
let newDevice = createBluetoothDevice(device, deviceType, displayName)
|
||||||
|
newPairedDevices.push(newDevice)
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
if (Bluetooth.defaultAdapter.discovering && isDeviceDiscoverable(device)) {
|
||||||
}
|
let existingDevice = findDeviceInList(root.availableDevices, device.address)
|
||||||
|
if (existingDevice) {
|
||||||
Process {
|
updateDeviceData(existingDevice, device, deviceType, displayName)
|
||||||
id: bluetoothDeviceScanner
|
newAvailableDevices.push(existingDevice)
|
||||||
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"]
|
} else {
|
||||||
running: false
|
let newDevice = createBluetoothDevice(device, deviceType, displayName)
|
||||||
|
newAvailableDevices.push(newDevice)
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
if (deviceObj.connected !== nativeDevice.connected) {
|
||||||
|
deviceObj.connecting = false
|
||||||
|
deviceObj.connectionFailed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
function clearDeviceList(deviceList) {
|
||||||
if (root.bluetoothEnabled && root.bluetoothAvailable) {
|
for (let device of deviceList) {
|
||||||
bluetoothDeviceScanner.running = true
|
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() {
|
function startDiscovery() {
|
||||||
root.scanning = true
|
if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled) {
|
||||||
// Run comprehensive scan that gets all devices
|
Bluetooth.defaultAdapter.discovering = true
|
||||||
discoveryScanner.running = true
|
updateDevices()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopDiscovery() {
|
function stopDiscovery() {
|
||||||
let stopDiscoveryProcess = Qt.createQmlObject('
|
if (Bluetooth.defaultAdapter) {
|
||||||
import Quickshell.Io
|
Bluetooth.defaultAdapter.discovering = false
|
||||||
Process {
|
updateDevices()
|
||||||
command: ["bluetoothctl", "scan", "off"]
|
}
|
||||||
running: true
|
|
||||||
onExited: {
|
|
||||||
root.scanning = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
', root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pairDevice(mac) {
|
function pairDevice(mac) {
|
||||||
console.log("Pairing device:", mac)
|
console.log("Pairing device:", mac)
|
||||||
let pairProcess = Qt.createQmlObject('
|
let device = findDeviceByMac(mac)
|
||||||
import Quickshell.Io
|
if (device) {
|
||||||
Process {
|
device.pair()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectDevice(mac) {
|
function connectDevice(mac) {
|
||||||
console.log("Connecting to device:", mac)
|
console.log("Connecting to device:", mac)
|
||||||
let connectProcess = Qt.createQmlObject('
|
let device = findDeviceByMac(mac)
|
||||||
import Quickshell.Io
|
if (device) {
|
||||||
Process {
|
device.connect()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeDevice(mac) {
|
function removeDevice(mac) {
|
||||||
console.log("Removing device:", mac)
|
console.log("Removing device:", mac)
|
||||||
let removeProcess = Qt.createQmlObject('
|
let device = findDeviceByMac(mac)
|
||||||
import Quickshell.Io
|
if (device) {
|
||||||
Process {
|
device.forget()
|
||||||
command: ["bluetoothctl", "remove", "' + mac + '"]
|
}
|
||||||
running: true
|
|
||||||
onExited: {
|
|
||||||
bluetoothDeviceScanner.running = true
|
|
||||||
availableDeviceScanner.running = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
', root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleBluetoothDevice(mac) {
|
function toggleBluetoothDevice(mac) {
|
||||||
let device = root.bluetoothDevices.find(d => d.mac === mac)
|
let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
|
||||||
if (device) {
|
if (!typedDevice) {
|
||||||
let action = device.connected ? "disconnect" : "connect"
|
typedDevice = findDeviceInList(root.availableDevices, mac)
|
||||||
let toggleProcess = Qt.createQmlObject('
|
}
|
||||||
import Quickshell.Io
|
|
||||||
Process {
|
if (typedDevice && typedDevice.nativeDevice) {
|
||||||
command: ["bluetoothctl", "' + action + '", "' + mac + '"]
|
if (typedDevice.connected) {
|
||||||
running: true
|
console.log("Disconnecting device:", mac)
|
||||||
onExited: bluetoothDeviceScanner.running = true
|
typedDevice.connecting = false
|
||||||
}
|
typedDevice.connectionFailed = false
|
||||||
', root)
|
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() {
|
function toggleBluetooth() {
|
||||||
let action = root.bluetoothEnabled ? "off" : "on"
|
if (Bluetooth.defaultAdapter) {
|
||||||
let toggleProcess = Qt.createQmlObject('
|
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
|
||||||
import Quickshell.Io
|
}
|
||||||
Process {
|
|
||||||
command: ["bluetoothctl", "power", "' + action + '"]
|
|
||||||
running: true
|
|
||||||
onExited: bluetoothStatusChecker.running = true
|
|
||||||
}
|
|
||||||
', root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
Timer {
|
||||||
id: bluetoothMonitorTimer
|
id: bluetoothMonitorTimer
|
||||||
interval: 5000
|
interval: 2000
|
||||||
running: false; repeat: true
|
running: false
|
||||||
|
repeat: true
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
bluetoothStatusChecker.running = true
|
updateDevices()
|
||||||
if (root.bluetoothEnabled) {
|
|
||||||
bluetoothDeviceScanner.running = true
|
|
||||||
// Also refresh paired devices to get current connection status
|
|
||||||
pairedDeviceChecker.discoveredToMerge = []
|
|
||||||
pairedDeviceChecker.running = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableMonitoring(enabled) {
|
function enableMonitoring(enabled) {
|
||||||
bluetoothMonitorTimer.running = enabled
|
bluetoothMonitorTimer.running = enabled
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
// Immediately update when enabled
|
refreshBluetoothState()
|
||||||
bluetoothStatusChecker.running = true
|
updateDevices()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property var discoveredDevices: []
|
Timer {
|
||||||
|
id: bluetoothStateRefreshTimer
|
||||||
// Handle discovered devices
|
interval: 5000
|
||||||
function _handleDiscovered(found) {
|
running: true
|
||||||
|
repeat: true
|
||||||
let discoveredDevices = []
|
onTriggered: {
|
||||||
for (let device of found) {
|
refreshBluetoothState()
|
||||||
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, ")")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get paired devices first, then merge with discovered
|
|
||||||
pairedDeviceChecker.discoveredToMerge = discoveredDevices
|
|
||||||
pairedDeviceChecker.running = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get only currently connected/paired devices that matter
|
Timer {
|
||||||
Process {
|
id: connectionTimeout
|
||||||
id: availableDeviceScanner
|
interval: 10000 // 10 second timeout
|
||||||
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"]
|
|
||||||
running: false
|
running: false
|
||||||
|
repeat: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
property string deviceMac: ""
|
||||||
onStreamFinished: {
|
|
||||||
|
onTriggered: {
|
||||||
let devices = []
|
if (deviceMac) {
|
||||||
if (text.trim()) {
|
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
|
||||||
let lines = text.trim().split('\n')
|
if (!typedDevice) {
|
||||||
|
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
root.availableDevices = devices
|
if (typedDevice && typedDevice.connecting && !typedDevice.connected) {
|
||||||
}
|
console.log("Connection timeout for device:", deviceMac)
|
||||||
}
|
typedDevice.connecting = false
|
||||||
}
|
typedDevice.connectionFailed = true
|
||||||
|
|
||||||
// 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')
|
|
||||||
|
|
||||||
for (let line of lines) {
|
// Clear failure state after 3 seconds
|
||||||
if (line.trim()) {
|
Qt.callLater(() => {
|
||||||
let parts = line.split('|')
|
clearFailureTimer.deviceMac = deviceMac
|
||||||
if (parts.length >= 4) {
|
clearFailureTimer.start()
|
||||||
let mac = parts[0].trim()
|
})
|
||||||
let name = parts[1].trim()
|
}
|
||||||
let paired = parts[2].trim() === 'yes'
|
deviceMac = ""
|
||||||
let connected = parts[3].trim() === 'yes'
|
}
|
||||||
|
}
|
||||||
// Only include if actually paired
|
}
|
||||||
if (!paired) continue
|
|
||||||
|
Timer {
|
||||||
// Check if already in discovered list
|
id: clearFailureTimer
|
||||||
if (seenMacs.has(mac)) {
|
interval: 3000
|
||||||
// Update existing device to show it's paired
|
running: false
|
||||||
let existing = allDevices.find(d => d.mac === mac)
|
repeat: false
|
||||||
if (existing) {
|
|
||||||
existing.paired = true
|
property string deviceMac: ""
|
||||||
existing.connected = connected
|
|
||||||
existing.canPair = false
|
onTriggered: {
|
||||||
}
|
if (deviceMac) {
|
||||||
continue
|
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
|
||||||
}
|
if (!typedDevice) {
|
||||||
|
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
|
||||||
// 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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
root.availableDevices = allDevices
|
if (typedDevice) {
|
||||||
root.scanning = false
|
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,15 +7,12 @@ pragma ComponentBehavior: Bound
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Process list properties
|
|
||||||
property var processes: []
|
property var processes: []
|
||||||
property bool isUpdating: false
|
property bool isUpdating: false
|
||||||
property int processUpdateInterval: 3000
|
property int processUpdateInterval: 3000
|
||||||
|
|
||||||
// Performance control - only run when process monitor is actually visible
|
|
||||||
property bool monitoringEnabled: false
|
property bool monitoringEnabled: false
|
||||||
|
|
||||||
// System information properties
|
|
||||||
property int totalMemoryKB: 0
|
property int totalMemoryKB: 0
|
||||||
property int usedMemoryKB: 0
|
property int usedMemoryKB: 0
|
||||||
property int totalSwapKB: 0
|
property int totalSwapKB: 0
|
||||||
@@ -24,28 +21,23 @@ Singleton {
|
|||||||
property real totalCpuUsage: 0.0
|
property real totalCpuUsage: 0.0
|
||||||
property bool systemInfoAvailable: false
|
property bool systemInfoAvailable: false
|
||||||
|
|
||||||
// Performance history for charts
|
|
||||||
property var cpuHistory: []
|
property var cpuHistory: []
|
||||||
property var memoryHistory: []
|
property var memoryHistory: []
|
||||||
property var networkHistory: ({rx: [], tx: []})
|
property var networkHistory: ({rx: [], tx: []})
|
||||||
property var diskHistory: ({read: [], write: []})
|
property var diskHistory: ({read: [], write: []})
|
||||||
property int historySize: 60 // Keep 60 data points
|
property int historySize: 60
|
||||||
|
|
||||||
// Per-core CPU usage
|
|
||||||
property var perCoreCpuUsage: []
|
property var perCoreCpuUsage: []
|
||||||
|
|
||||||
// Network stats
|
property real networkRxRate: 0
|
||||||
property real networkRxRate: 0 // bytes/sec
|
property real networkTxRate: 0
|
||||||
property real networkTxRate: 0 // bytes/sec
|
|
||||||
property var lastNetworkStats: null
|
property var lastNetworkStats: null
|
||||||
|
|
||||||
// Disk I/O stats
|
property real diskReadRate: 0
|
||||||
property real diskReadRate: 0 // bytes/sec
|
property real diskWriteRate: 0
|
||||||
property real diskWriteRate: 0 // bytes/sec
|
|
||||||
property var lastDiskStats: null
|
property var lastDiskStats: null
|
||||||
|
|
||||||
// Sorting options
|
property string sortBy: "cpu"
|
||||||
property string sortBy: "cpu" // "cpu", "memory", "name", "pid"
|
|
||||||
property bool sortDescending: true
|
property bool sortDescending: true
|
||||||
property int maxProcesses: 20
|
property int maxProcesses: 20
|
||||||
|
|
||||||
@@ -53,9 +45,6 @@ Singleton {
|
|||||||
console.log("ProcessMonitorService: Starting initialization...")
|
console.log("ProcessMonitorService: Starting initialization...")
|
||||||
updateProcessList()
|
updateProcessList()
|
||||||
console.log("ProcessMonitorService: Initialization complete")
|
console.log("ProcessMonitorService: Initialization complete")
|
||||||
|
|
||||||
// Test monitoring disabled - only monitor when explicitly enabled
|
|
||||||
// testTimer.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
@@ -66,7 +55,6 @@ Singleton {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
console.log("ProcessMonitorService: Starting test monitoring...")
|
console.log("ProcessMonitorService: Starting test monitoring...")
|
||||||
enableMonitoring(true)
|
enableMonitoring(true)
|
||||||
// Stop after 8 seconds
|
|
||||||
stopTestTimer.start()
|
stopTestTimer.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +70,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// System information monitoring
|
|
||||||
Process {
|
Process {
|
||||||
id: systemInfoProcess
|
id: systemInfoProcess
|
||||||
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu' /proc/stat | head -" + (root.cpuCount + 1)]
|
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu' /proc/stat | head -" + (root.cpuCount + 1)]
|
||||||
@@ -104,7 +91,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network monitoring process
|
|
||||||
Process {
|
Process {
|
||||||
id: networkStatsProcess
|
id: networkStatsProcess
|
||||||
command: ["bash", "-c", "cat /proc/net/dev | grep -E '(wlan|eth|enp|wlp|ens|eno)' | awk '{print $1,$2,$10}' | sed 's/:/ /'"]
|
command: ["bash", "-c", "cat /proc/net/dev | grep -E '(wlan|eth|enp|wlp|ens|eno)' | awk '{print $1,$2,$10}' | sed 's/:/ /'"]
|
||||||
@@ -119,7 +105,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disk I/O monitoring process
|
|
||||||
Process {
|
Process {
|
||||||
id: diskStatsProcess
|
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}'"]
|
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}'"]
|
||||||
@@ -134,7 +119,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process monitoring with ps command
|
|
||||||
Process {
|
Process {
|
||||||
id: processListProcess
|
id: processListProcess
|
||||||
command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,rss,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)]
|
command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,rss,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)]
|
||||||
@@ -146,12 +130,10 @@ Singleton {
|
|||||||
const lines = text.trim().split('\n')
|
const lines = text.trim().split('\n')
|
||||||
const newProcesses = []
|
const newProcesses = []
|
||||||
|
|
||||||
// Skip header line
|
|
||||||
for (let i = 1; i < lines.length; i++) {
|
for (let i = 1; i < lines.length; i++) {
|
||||||
const line = lines[i].trim()
|
const line = lines[i].trim()
|
||||||
if (!line) continue
|
if (!line) continue
|
||||||
|
|
||||||
// Parse ps output: PID PPID %CPU %MEM RSS COMMAND CMD
|
|
||||||
const parts = line.split(/\s+/)
|
const parts = line.split(/\s+/)
|
||||||
if (parts.length >= 7) {
|
if (parts.length >= 7) {
|
||||||
const pid = parseInt(parts[0])
|
const pid = parseInt(parts[0])
|
||||||
@@ -189,11 +171,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// System and process monitoring timer - now conditional
|
|
||||||
Timer {
|
Timer {
|
||||||
id: processTimer
|
id: processTimer
|
||||||
interval: root.processUpdateInterval
|
interval: root.processUpdateInterval
|
||||||
running: root.monitoringEnabled // Only run when monitoring is enabled
|
running: root.monitoringEnabled
|
||||||
repeat: true
|
repeat: true
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
@@ -206,29 +187,24 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public functions
|
|
||||||
function updateSystemInfo() {
|
function updateSystemInfo() {
|
||||||
if (!systemInfoProcess.running && root.monitoringEnabled) {
|
if (!systemInfoProcess.running && root.monitoringEnabled) {
|
||||||
systemInfoProcess.running = true
|
systemInfoProcess.running = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Control functions for enabling/disabling monitoring
|
|
||||||
function enableMonitoring(enabled) {
|
function enableMonitoring(enabled) {
|
||||||
console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled")
|
console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled")
|
||||||
root.monitoringEnabled = enabled
|
root.monitoringEnabled = enabled
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
// Clear history when starting
|
|
||||||
root.cpuHistory = []
|
root.cpuHistory = []
|
||||||
root.memoryHistory = []
|
root.memoryHistory = []
|
||||||
root.networkHistory = ({rx: [], tx: []})
|
root.networkHistory = ({rx: [], tx: []})
|
||||||
root.diskHistory = ({read: [], write: []})
|
root.diskHistory = ({read: [], write: []})
|
||||||
// Immediately update when enabled
|
|
||||||
updateSystemInfo()
|
updateSystemInfo()
|
||||||
updateProcessList()
|
updateProcessList()
|
||||||
updateNetworkStats()
|
updateNetworkStats()
|
||||||
updateDiskStats()
|
updateDiskStats()
|
||||||
// console.log("ProcessMonitorService: Initial data collection started")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +224,6 @@ Singleton {
|
|||||||
if (!root.isUpdating && root.monitoringEnabled) {
|
if (!root.isUpdating && root.monitoringEnabled) {
|
||||||
root.isUpdating = true
|
root.isUpdating = true
|
||||||
|
|
||||||
// Update sort command based on current sort option
|
|
||||||
let sortOption = ""
|
let sortOption = ""
|
||||||
switch (root.sortBy) {
|
switch (root.sortBy) {
|
||||||
case "cpu":
|
case "cpu":
|
||||||
@@ -307,7 +282,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getProcessIcon(command) {
|
function getProcessIcon(command) {
|
||||||
// Return appropriate Material Design icon for common processes
|
|
||||||
const cmd = command.toLowerCase()
|
const cmd = command.toLowerCase()
|
||||||
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser")) return "web"
|
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser")) return "web"
|
||||||
if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) return "code"
|
if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) return "code"
|
||||||
@@ -315,7 +289,7 @@ Singleton {
|
|||||||
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify")) return "music_note"
|
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("video") || cmd.includes("vlc") || cmd.includes("mpv")) return "play_circle"
|
||||||
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread")) return "settings"
|
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread")) return "settings"
|
||||||
return "memory" // Default process icon
|
return "memory"
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCpuUsage(cpu) {
|
function formatCpuUsage(cpu) {
|
||||||
@@ -444,7 +418,6 @@ Singleton {
|
|||||||
root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff)
|
root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff)
|
||||||
root.networkTxRate = Math.max(0, (totalTx - root.lastNetworkStats.tx) / timeDiff)
|
root.networkTxRate = Math.max(0, (totalTx - root.lastNetworkStats.tx) / timeDiff)
|
||||||
|
|
||||||
// Convert to KB/s for history
|
|
||||||
addToHistory(root.networkHistory.rx, root.networkRxRate / 1024)
|
addToHistory(root.networkHistory.rx, root.networkRxRate / 1024)
|
||||||
addToHistory(root.networkHistory.tx, root.networkTxRate / 1024)
|
addToHistory(root.networkHistory.tx, root.networkTxRate / 1024)
|
||||||
}
|
}
|
||||||
@@ -463,7 +436,7 @@ Singleton {
|
|||||||
const readSectors = parseInt(parts[1])
|
const readSectors = parseInt(parts[1])
|
||||||
const writeSectors = parseInt(parts[2])
|
const writeSectors = parseInt(parts[2])
|
||||||
if (!isNaN(readSectors) && !isNaN(writeSectors)) {
|
if (!isNaN(readSectors) && !isNaN(writeSectors)) {
|
||||||
totalRead += readSectors * 512 // Convert sectors to bytes
|
totalRead += readSectors * 512
|
||||||
totalWrite += writeSectors * 512
|
totalWrite += writeSectors * 512
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,7 +447,6 @@ Singleton {
|
|||||||
root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff)
|
root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff)
|
||||||
root.diskWriteRate = Math.max(0, (totalWrite - root.lastDiskStats.write) / timeDiff)
|
root.diskWriteRate = Math.max(0, (totalWrite - root.lastDiskStats.write) / timeDiff)
|
||||||
|
|
||||||
// Convert to MB/s for history
|
|
||||||
addToHistory(root.diskHistory.read, root.diskReadRate / (1024 * 1024))
|
addToHistory(root.diskHistory.read, root.diskReadRate / (1024 * 1024))
|
||||||
addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024))
|
addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,13 @@ pragma ComponentBehavior: Bound
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// CPU properties
|
|
||||||
property real cpuUsage: 0.0
|
property real cpuUsage: 0.0
|
||||||
property int cpuCores: 1
|
property int cpuCores: 1
|
||||||
property string cpuModel: ""
|
property string cpuModel: ""
|
||||||
property real cpuFrequency: 0.0
|
property real cpuFrequency: 0.0
|
||||||
|
|
||||||
// Previous CPU stats for accurate calculation
|
|
||||||
property var prevCpuStats: [0, 0, 0, 0, 0, 0, 0, 0]
|
property var prevCpuStats: [0, 0, 0, 0, 0, 0, 0, 0]
|
||||||
|
|
||||||
// Memory properties
|
|
||||||
property real memoryUsage: 0.0
|
property real memoryUsage: 0.0
|
||||||
property real totalMemory: 0.0
|
property real totalMemory: 0.0
|
||||||
property real usedMemory: 0.0
|
property real usedMemory: 0.0
|
||||||
@@ -25,14 +22,12 @@ Singleton {
|
|||||||
property real bufferMemory: 0.0
|
property real bufferMemory: 0.0
|
||||||
property real cacheMemory: 0.0
|
property real cacheMemory: 0.0
|
||||||
|
|
||||||
// Temperature properties
|
|
||||||
property real cpuTemperature: 0.0
|
property real cpuTemperature: 0.0
|
||||||
|
|
||||||
property int cpuUpdateInterval: 3000
|
property int cpuUpdateInterval: 3000
|
||||||
property int memoryUpdateInterval: 5000
|
property int memoryUpdateInterval: 5000
|
||||||
property int temperatureUpdateInterval: 10000
|
property int temperatureUpdateInterval: 10000
|
||||||
|
|
||||||
// Performance control
|
|
||||||
property bool enabledForTopBar: true
|
property bool enabledForTopBar: true
|
||||||
property bool enabledForDetailedView: false
|
property bool enabledForDetailedView: false
|
||||||
|
|
||||||
@@ -43,7 +38,6 @@ Singleton {
|
|||||||
console.log("SystemMonitorService: Initialization complete")
|
console.log("SystemMonitorService: Initialization complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get CPU information (static)
|
|
||||||
Process {
|
Process {
|
||||||
id: cpuInfoProcess
|
id: cpuInfoProcess
|
||||||
command: ["bash", "-c", "lscpu | grep -E 'Model name|CPU\\(s\\):' | head -2"]
|
command: ["bash", "-c", "lscpu | grep -E 'Model name|CPU\\(s\\):' | head -2"]
|
||||||
@@ -69,7 +63,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU usage monitoring with accurate calculation
|
|
||||||
Process {
|
Process {
|
||||||
id: cpuUsageProcess
|
id: cpuUsageProcess
|
||||||
command: ["bash", "-c", "head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}'"]
|
command: ["bash", "-c", "head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}'"]
|
||||||
@@ -80,17 +73,14 @@ Singleton {
|
|||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
const stats = text.trim().split(" ").map(x => parseInt(x))
|
const stats = text.trim().split(" ").map(x => parseInt(x))
|
||||||
if (root.prevCpuStats[0] > 0) {
|
if (root.prevCpuStats[0] > 0) {
|
||||||
// Calculate differences
|
|
||||||
let diffs = []
|
let diffs = []
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
diffs[i] = stats[i] - root.prevCpuStats[i]
|
diffs[i] = stats[i] - root.prevCpuStats[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate total and idle time
|
|
||||||
const totalTime = diffs.reduce((a, b) => a + b, 0)
|
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) {
|
if (totalTime > 0) {
|
||||||
root.cpuUsage = Math.max(0, Math.min(100, ((totalTime - idleTime) / totalTime) * 100))
|
root.cpuUsage = Math.max(0, Math.min(100, ((totalTime - idleTime) / totalTime) * 100))
|
||||||
}
|
}
|
||||||
@@ -107,7 +97,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory usage monitoring
|
|
||||||
Process {
|
Process {
|
||||||
id: memoryUsageProcess
|
id: memoryUsageProcess
|
||||||
command: ["bash", "-c", "free -m | awk 'NR==2{printf \"%.1f %.1f %.1f %.1f\", $3*100/$2, $2, $3, $7}'"]
|
command: ["bash", "-c", "free -m | awk 'NR==2{printf \"%.1f %.1f %.1f %.1f\", $3*100/$2, $2, $3, $7}'"]
|
||||||
@@ -133,7 +122,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU frequency monitoring
|
|
||||||
Process {
|
Process {
|
||||||
id: cpuFrequencyProcess
|
id: cpuFrequencyProcess
|
||||||
command: ["bash", "-c", "cat /proc/cpuinfo | grep 'cpu MHz' | head -1 | awk '{print $4}'"]
|
command: ["bash", "-c", "cat /proc/cpuinfo | grep 'cpu MHz' | head -1 | awk '{print $4}'"]
|
||||||
@@ -154,7 +142,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU temperature monitoring
|
|
||||||
Process {
|
Process {
|
||||||
id: temperatureProcess
|
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"]
|
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 +162,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CPU monitoring timer
|
|
||||||
Timer {
|
Timer {
|
||||||
id: cpuTimer
|
id: cpuTimer
|
||||||
interval: root.cpuUpdateInterval
|
interval: root.cpuUpdateInterval
|
||||||
@@ -190,7 +176,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memory monitoring timer
|
|
||||||
Timer {
|
Timer {
|
||||||
id: memoryTimer
|
id: memoryTimer
|
||||||
interval: root.memoryUpdateInterval
|
interval: root.memoryUpdateInterval
|
||||||
@@ -204,7 +189,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temperature monitoring timer
|
|
||||||
Timer {
|
Timer {
|
||||||
id: temperatureTimer
|
id: temperatureTimer
|
||||||
interval: root.temperatureUpdateInterval
|
interval: root.temperatureUpdateInterval
|
||||||
@@ -218,7 +202,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public functions
|
|
||||||
function getCpuInfo() {
|
function getCpuInfo() {
|
||||||
cpuInfoProcess.running = true
|
cpuInfoProcess.running = true
|
||||||
}
|
}
|
||||||
@@ -243,15 +226,15 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCpuUsageColor() {
|
function getCpuUsageColor() {
|
||||||
if (cpuUsage > 80) return "#e74c3c" // Red
|
if (cpuUsage > 80) return "#e74c3c"
|
||||||
if (cpuUsage > 60) return "#f39c12" // Orange
|
if (cpuUsage > 60) return "#f39c12"
|
||||||
return "#27ae60" // Green
|
return "#27ae60"
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMemoryUsageColor() {
|
function getMemoryUsageColor() {
|
||||||
if (memoryUsage > 90) return "#e74c3c" // Red
|
if (memoryUsage > 90) return "#e74c3c"
|
||||||
if (memoryUsage > 75) return "#f39c12" // Orange
|
if (memoryUsage > 75) return "#f39c12"
|
||||||
return "#3498db" // Blue
|
return "#3498db"
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMemory(mb) {
|
function formatMemory(mb) {
|
||||||
@@ -262,8 +245,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTemperatureColor() {
|
function getTemperatureColor() {
|
||||||
if (cpuTemperature > 80) return "#e74c3c" // Red
|
if (cpuTemperature > 80) return "#e74c3c"
|
||||||
if (cpuTemperature > 65) return "#f39c12" // Orange
|
if (cpuTemperature > 65) return "#f39c12"
|
||||||
return "#27ae60" // Green
|
return "#27ae60"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,206 +6,445 @@ import Quickshell.Io
|
|||||||
import "../../Common"
|
import "../../Common"
|
||||||
import "../../Services"
|
import "../../Services"
|
||||||
|
|
||||||
ScrollView {
|
Item {
|
||||||
id: bluetoothTab
|
id: bluetoothTab
|
||||||
clip: true
|
|
||||||
|
|
||||||
// These should be bound from parent
|
|
||||||
property bool bluetoothEnabled: false
|
property bool bluetoothEnabled: false
|
||||||
property var bluetoothDevices: []
|
property var bluetoothDevices: []
|
||||||
|
|
||||||
Column {
|
ScrollView {
|
||||||
width: parent.width
|
anchors.fill: parent
|
||||||
spacing: Theme.spacingL
|
clip: true
|
||||||
|
|
||||||
// Bluetooth toggle
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
Rectangle {
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bluetooth devices (when enabled)
|
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingL
|
||||||
visible: bluetoothTab.bluetoothEnabled
|
|
||||||
|
|
||||||
Text {
|
Rectangle {
|
||||||
text: "Paired Devices"
|
width: parent.width
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
height: 60
|
||||||
color: Theme.surfaceText
|
radius: Theme.cornerRadius
|
||||||
font.weight: Font.Medium
|
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"
|
||||||
// Real Bluetooth devices
|
border.width: 2
|
||||||
Repeater {
|
|
||||||
model: bluetoothTab.bluetoothDevices
|
|
||||||
|
|
||||||
Rectangle {
|
Row {
|
||||||
width: parent.width
|
anchors.left: parent.left
|
||||||
height: 60
|
anchors.leftMargin: Theme.spacingL
|
||||||
radius: Theme.cornerRadius
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
spacing: Theme.spacingM
|
||||||
(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 {
|
Text {
|
||||||
anchors.left: parent.left
|
text: "bluetooth"
|
||||||
anchors.leftMargin: Theme.spacingM
|
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
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: {
|
text: "Bluetooth"
|
||||||
switch (modelData.type) {
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
case "headset": return "headset"
|
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
|
||||||
case "mouse": return "mouse"
|
font.weight: Font.Medium
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Text {
|
||||||
spacing: 2
|
text: bluetoothTab.bluetoothEnabled ? "Enabled" : "Disabled"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
visible: bluetoothTab.bluetoothEnabled
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Available Devices"
|
text: "Paired Devices"
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
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 {
|
Row {
|
||||||
width: Math.max(100, scanText.contentWidth + Theme.spacingM * 2)
|
width: parent.width
|
||||||
height: 32
|
spacing: Theme.spacingM
|
||||||
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)
|
Text {
|
||||||
border.color: Theme.primary
|
text: "Available Devices"
|
||||||
border.width: 1
|
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 {
|
Row {
|
||||||
anchors.centerIn: parent
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: Theme.spacingXS
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: BluetoothService.scanning ? "search" : "bluetooth_searching"
|
text: "sync"
|
||||||
font.family: Theme.iconFont
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: Theme.iconSize - 4
|
font.pixelSize: Theme.iconSizeLarge
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
RotationAnimation on rotation {
|
||||||
running: BluetoothService.scanning
|
running: true
|
||||||
loops: Animation.Infinite
|
loops: Animation.Infinite
|
||||||
from: 0
|
from: 0
|
||||||
to: 360
|
to: 360
|
||||||
@@ -214,169 +453,237 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
id: scanText
|
text: "Scanning for devices..."
|
||||||
text: BluetoothService.scanning ? "Scanning..." : "Scan"
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
color: Theme.surfaceText
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
Text {
|
||||||
id: scanArea
|
text: "Make sure your device is in pairing mode"
|
||||||
anchors.fill: parent
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
hoverEnabled: true
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
cursorShape: Qt.PointingHandCursor
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
enabled: !BluetoothService.scanning
|
}
|
||||||
|
}
|
||||||
onClicked: {
|
|
||||||
BluetoothService.startDiscovery()
|
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
|
Rectangle {
|
||||||
Repeater {
|
width: parent.width - Theme.spacingS * 2
|
||||||
model: BluetoothService.availableDevices
|
height: 5
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
anchors.centerIn: parent
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 70
|
height: 1
|
||||||
radius: Theme.cornerRadius
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No devices message
|
Rectangle {
|
||||||
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
|
|
||||||
width: parent.width
|
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: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ PanelWindow {
|
|||||||
topBar.shellRoot.controlCenterVisible = !topBar.shellRoot.controlCenterVisible
|
topBar.shellRoot.controlCenterVisible = !topBar.shellRoot.controlCenterVisible
|
||||||
if (topBar.shellRoot.controlCenterVisible) {
|
if (topBar.shellRoot.controlCenterVisible) {
|
||||||
WifiService.scanWifi()
|
WifiService.scanWifi()
|
||||||
BluetoothService.scanDevices()
|
// Bluetooth devices are automatically updated via signals
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user