diff --git a/Services/AppSearchService.qml b/Services/AppSearchService.qml index a59d68fb..870343e1 100644 --- a/Services/AppSearchService.qml +++ b/Services/AppSearchService.qml @@ -14,7 +14,6 @@ Singleton { property var applicationsByExec: ({}) property bool ready: false - // Pre-prepared fuzzy search data property var preppedApps: [] @@ -33,7 +32,6 @@ Singleton { function loadApplications() { - // Trigger rescan on next frame to avoid blocking Qt.callLater(function() { var allApps = Array.from(DesktopEntries.applications.values) @@ -41,7 +39,6 @@ Singleton { .filter(app => !app.noDisplay) .sort((a, b) => a.name.localeCompare(b.name)) - // Build lookup maps var byName = {} var byExec = {} @@ -49,7 +46,6 @@ Singleton { var app = applications[i] byName[app.name.toLowerCase()] = app - // Clean exec string for lookup var execProp = app.execString || "" var cleanExec = execProp ? execProp.replace(/%[fFuU]/g, "").trim() : "" if (cleanExec) { @@ -60,7 +56,6 @@ Singleton { applicationsByName = byName applicationsByExec = byExec - // Prepare fuzzy search data preppedApps = applications.map(app => ({ name: Fuzzy.prepare(app.name || ""), comment: Fuzzy.prepare(app.comment || ""), @@ -84,12 +79,10 @@ Singleton { return [] } - // Use fuzzy search with both name and comment fields var results = Fuzzy.go(query, preppedApps, { all: false, keys: ["name", "comment"], scoreFn: r => { - // Prioritize name matches over comment matches var nameScore = r[0] ? r[0].score : 0 var commentScore = r[1] ? r[1].score : 0 return nameScore > 0 ? nameScore * 0.9 + commentScore * 0.1 : commentScore * 0.5 @@ -97,7 +90,6 @@ Singleton { limit: 50 }) - // Extract the desktop entries from results return results.map(r => r.obj.entry) } @@ -180,7 +172,6 @@ Singleton { return false } - // DesktopEntry objects have an execute() method if (typeof app.execute === "function") { app.execute() return true diff --git a/Services/AudioService.qml b/Services/AudioService.qml index ddd48826..53a7edf5 100644 --- a/Services/AudioService.qml +++ b/Services/AudioService.qml @@ -11,16 +11,12 @@ Singleton { property var audioSinks: [] property string currentAudioSink: "" - // Microphone properties property int micLevel: 50 property var audioSources: [] property string currentAudioSource: "" - // Device scanning control property bool deviceScanningEnabled: false property bool initialScanComplete: false - - // Real Audio Control Process { id: volumeChecker command: ["bash", "-c", "pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"] @@ -36,7 +32,6 @@ Singleton { } } - // Microphone level checker Process { id: micLevelChecker 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) { line = line.trim() - // New sink starts if (line.startsWith('Sink #')) { if (currentSink && currentSink.name && currentSink.id) { sinks.push(currentSink) @@ -84,39 +78,31 @@ Singleton { active: false } } - // Get the Name field else if (line.startsWith('Name: ') && currentSink) { currentSink.name = line.replace('Name: ', '').trim() } - // Get the Description field (main display name) else if (line.startsWith('Description: ') && currentSink) { currentSink.description = line.replace('Description: ', '').trim() } - // Get device.description as fallback else if (line.includes('device.description = ') && currentSink && !currentSink.description) { currentSink.description = line.replace('device.description = ', '').replace(/"/g, '').trim() } - // Get node.nick as another fallback option else if (line.includes('node.nick = ') && currentSink && !currentSink.description) { currentSink.nick = line.replace('node.nick = ', '').replace(/"/g, '').trim() } } - // Add the last sink if (currentSink && currentSink.name && currentSink.id) { sinks.push(currentSink) } - // Process display names for (let sink of sinks) { let displayName = sink.description - // If no good description, try nick if (!displayName || displayName === sink.name) { displayName = sink.nick } - // Still no good name? Fall back to smart defaults if (!displayName || displayName === sink.name) { if (sink.name.includes("analog-stereo")) displayName = "Built-in Speakers" else if (sink.name.includes("bluez")) displayName = "Bluetooth Audio" @@ -136,7 +122,6 @@ Singleton { } } - // Audio source (microphone) lister Process { id: audioSourceLister command: ["pactl", "list", "sources"] @@ -153,7 +138,6 @@ Singleton { for (let line of lines) { line = line.trim() - // New source starts if (line.startsWith('Source #')) { if (currentSource && currentSource.name && currentSource.id) { sources.push(currentSource) @@ -165,23 +149,19 @@ Singleton { active: false } } - // Source name else if (line.startsWith('Name: ') && currentSource) { currentSource.name = line.replace('Name: ', '') } - // Description (display name) else if (line.startsWith('Description: ') && currentSource) { let desc = line.replace('Description: ', '') currentSource.displayName = desc } } - // Add the last source if (currentSource && currentSource.name && currentSource.id) { sources.push(currentSource) } - // Filter out monitor sources (we want actual input devices) sources = sources.filter(source => !source.name.includes('.monitor')) root.audioSources = sources @@ -202,7 +182,6 @@ Singleton { if (data.trim()) { root.currentAudioSink = data.trim() - // Update active status in audioSinks let updatedSinks = [] for (let sink of root.audioSinks) { updatedSinks.push({ @@ -218,7 +197,6 @@ Singleton { } } - // Default source (microphone) checker Process { id: defaultSourceChecker command: ["pactl", "get-default-source"] @@ -230,7 +208,6 @@ Singleton { if (data.trim()) { root.currentAudioSource = data.trim() - // Update active status in audioSources let updatedSources = [] for (let source of root.audioSources) { updatedSources.push({ @@ -271,12 +248,10 @@ Singleton { function setAudioSink(sinkName) { console.log("Setting audio sink to:", sinkName) - // Use a more reliable approach instead of Qt.createQmlObject sinkSetProcess.command = ["pactl", "set-default-sink", sinkName] sinkSetProcess.running = true } - // Dedicated process for setting audio sink Process { id: sinkSetProcess running: false @@ -285,7 +260,6 @@ Singleton { console.log("Audio sink change exit code:", exitCode) if (exitCode === 0) { console.log("Audio sink changed successfully") - // Refresh current sink and list defaultSinkChecker.running = true if (root.deviceScanningEnabled) { audioSinkLister.running = true @@ -303,7 +277,6 @@ Singleton { sourceSetProcess.running = true } - // Dedicated process for setting audio source Process { id: sourceSetProcess running: false @@ -312,7 +285,6 @@ Singleton { console.log("Audio source change exit code:", exitCode) if (exitCode === 0) { console.log("Audio source changed successfully") - // Refresh current source and list defaultSourceChecker.running = true if (root.deviceScanningEnabled) { audioSourceLister.running = true @@ -337,25 +309,21 @@ Singleton { Component.onCompleted: { console.log("AudioService: Starting initialization...") - // Do initial device scan audioSinkLister.running = true audioSourceLister.running = true initialScanComplete = true console.log("AudioService: Initialization complete") } - // Control functions for managing device scanning function enableDeviceScanning(enabled) { console.log("AudioService: Device scanning", enabled ? "enabled" : "disabled") root.deviceScanningEnabled = enabled if (enabled && root.initialScanComplete) { - // Immediately scan when enabled audioSinkLister.running = true audioSourceLister.running = true } } - // Manual refresh function for when user opens audio settings function refreshDevices() { console.log("AudioService: Manual device refresh triggered") audioSinkLister.running = true diff --git a/Services/BatteryService.qml b/Services/BatteryService.qml index 210c73d2..25a84c76 100644 --- a/Services/BatteryService.qml +++ b/Services/BatteryService.qml @@ -7,7 +7,6 @@ pragma ComponentBehavior: Bound Singleton { id: root - // Battery properties property bool batteryAvailable: false property int batteryLevel: 0 property string batteryStatus: "Unknown" @@ -20,8 +19,6 @@ Singleton { property int batteryCapacity: 0 property var powerProfiles: [] property string activePowerProfile: "" - - // Check if battery is available Process { id: batteryAvailabilityChecker command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"] @@ -58,7 +55,6 @@ Singleton { if (text.trim() && text.trim() !== "fallback") { parseBatteryInfo(text.trim()) } else { - // Fallback to simple methods fallbackBatteryChecker.running = true } } @@ -72,7 +68,6 @@ Singleton { } } - // Fallback battery checker using /sys files Process { id: fallbackBatteryChecker command: ["bash", "-c", "if [ -f /sys/class/power_supply/BAT0/capacity ]; then BAT=BAT0; elif [ -f /sys/class/power_supply/BAT1/capacity ]; then BAT=BAT1; else echo 'no-battery'; exit 1; fi; echo \"percentage: $(cat /sys/class/power_supply/$BAT/capacity)%\"; echo \"state: $(cat /sys/class/power_supply/$BAT/status 2>/dev/null || echo Unknown)\"; if [ -f /sys/class/power_supply/$BAT/technology ]; then echo \"technology: $(cat /sys/class/power_supply/$BAT/technology)\"; fi; if [ -f /sys/class/power_supply/$BAT/cycle_count ]; then echo \"cycle-count: $(cat /sys/class/power_supply/$BAT/cycle_count)\"; fi"] @@ -87,7 +82,6 @@ Singleton { } } - // Power profiles checker (for systems with power-profiles-daemon) Process { id: powerProfilesChecker command: ["bash", "-c", "if command -v powerprofilesctl > /dev/null; then powerprofilesctl list 2>/dev/null; else echo 'not-available'; fi"] @@ -170,7 +164,6 @@ Singleton { for (let line of lines) { line = line.trim() if (line.includes('*')) { - // Active profile let profileName = line.replace('*', '').trim() if (profileName.includes(':')) { profileName = profileName.split(':')[0].trim() @@ -248,7 +241,6 @@ Singleton { } - // Update timer Timer { interval: 30000 running: root.batteryAvailable diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 0bc94ee3..2359df84 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -1,6 +1,6 @@ import QtQuick import Quickshell -import Quickshell.Io +import Quickshell.Bluetooth pragma Singleton pragma ComponentBehavior: Bound @@ -9,410 +9,466 @@ Singleton { property bool bluetoothEnabled: false property bool bluetoothAvailable: false - property var bluetoothDevices: [] - property var availableDevices: [] + readonly property list bluetoothDevices: [] + readonly property list availableDevices: [] property bool scanning: false property bool discoverable: false - // Real Bluetooth Management - Process { - id: bluetoothStatusChecker - command: ["bluetoothctl", "show"] // Use default controller - running: true + property var connectingDevices: ({}) + + Component.onCompleted: { + refreshBluetoothState() + updateDevices() + } + + Connections { + target: Bluetooth - stdout: StdioCollector { - onStreamFinished: { - root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller") - root.bluetoothEnabled = text.includes("Powered: yes") - - if (root.bluetoothEnabled && root.bluetoothAvailable) { - bluetoothDeviceScanner.running = true + function onDefaultAdapterChanged() { + console.log("BluetoothService: Default adapter changed") + refreshBluetoothState() + updateDevices() + } + } + + Connections { + target: Bluetooth.defaultAdapter + + function onEnabledChanged() { + refreshBluetoothState() + updateDevices() + } + + function onDiscoveringChanged() { + refreshBluetoothState() + updateDevices() + } + } + + Connections { + target: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : null + + function onModelReset() { + updateDevices() + } + + function onItemAdded() { + updateDevices() + } + + function onItemRemoved() { + updateDevices() + } + } + + Connections { + target: Bluetooth.devices + + function onModelReset() { + updateDevices() + } + + function onItemAdded() { + updateDevices() + } + + function onItemRemoved() { + updateDevices() + } + } + + function refreshBluetoothState() { + root.bluetoothAvailable = Bluetooth.defaultAdapter !== null + root.bluetoothEnabled = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.enabled : false + root.scanning = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discovering : false + root.discoverable = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discoverable : false + } + + function updateDevices() { + if (!Bluetooth.defaultAdapter) { + clearDeviceList(root.bluetoothDevices) + clearDeviceList(root.availableDevices) + root.bluetoothDevices = [] + root.availableDevices = [] + return + } + + let newPairedDevices = [] + let newAvailableDevices = [] + let allNativeDevices = [] + + let adapterDevices = Bluetooth.defaultAdapter.devices + if (adapterDevices.values) { + allNativeDevices = allNativeDevices.concat(adapterDevices.values) + } + + if (Bluetooth.devices.values) { + for (let device of Bluetooth.devices.values) { + if (!allNativeDevices.some(d => d.address === device.address)) { + allNativeDevices.push(device) + } + } + } + + for (let device of allNativeDevices) { + if (!device) continue + + let deviceType = getDeviceType(device.name || device.deviceName, device.icon) + let displayName = device.name || device.deviceName + + if (!displayName || displayName.startsWith('/org/bluez') || displayName.includes('hci0') || displayName.length < 2) { + continue + } + + if (device.paired) { + let existingDevice = findDeviceInList(root.bluetoothDevices, device.address) + if (existingDevice) { + updateDeviceData(existingDevice, device, deviceType, displayName) + newPairedDevices.push(existingDevice) } else { - root.bluetoothDevices = [] + let newDevice = createBluetoothDevice(device, deviceType, displayName) + newPairedDevices.push(newDevice) } - } - } - } - - Process { - id: bluetoothDeviceScanner - command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([0-9A-F:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ! $name =~ ^/org/bluez ]]; then info=$(bluetoothctl info $mac); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); battery=$(echo \"$info\" | grep -m1 'Battery Percentage:' | grep -o '[0-9]\\+'); echo \"$mac|$name|$connected|${battery:-}\"; fi; fi; done"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - if (text.trim()) { - let devices = [] - let lines = text.trim().split('\n') - - for (let line of lines) { - if (line.trim()) { - let parts = line.split('|') - if (parts.length >= 3) { - let mac = parts[0].trim() - let name = parts[1].trim() - let connected = parts[2].trim() === 'yes' - let battery = parts[3] ? parseInt(parts[3]) : -1 - - // Skip if name is still a technical path - if (name.startsWith('/org/bluez') || name.includes('hci0')) { - continue - } - - // Determine device type from name - let type = "bluetooth" - let nameLower = name.toLowerCase() - if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis")) type = "headset" - else if (nameLower.includes("mouse")) type = "mouse" - else if (nameLower.includes("keyboard")) type = "keyboard" - else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung")) type = "phone" - else if (nameLower.includes("watch")) type = "watch" - else if (nameLower.includes("speaker")) type = "speaker" - - devices.push({ - mac: mac, - name: name, - type: type, - connected: connected, - battery: battery - }) - } - } + } else { + if (Bluetooth.defaultAdapter.discovering && isDeviceDiscoverable(device)) { + let existingDevice = findDeviceInList(root.availableDevices, device.address) + if (existingDevice) { + updateDeviceData(existingDevice, device, deviceType, displayName) + newAvailableDevices.push(existingDevice) + } else { + let newDevice = createBluetoothDevice(device, deviceType, displayName) + newAvailableDevices.push(newDevice) } - - root.bluetoothDevices = devices } } } + + cleanupOldDevices(root.bluetoothDevices, newPairedDevices) + cleanupOldDevices(root.availableDevices, newAvailableDevices) + + console.log("BluetoothService: Found", newPairedDevices.length, "paired devices and", newAvailableDevices.length, "available devices") + + root.bluetoothDevices = newPairedDevices + root.availableDevices = newAvailableDevices + } + + function createBluetoothDevice(nativeDevice, deviceType, displayName) { + return deviceComponent.createObject(root, { + mac: nativeDevice.address, + name: displayName, + type: deviceType, + paired: nativeDevice.paired, + connected: nativeDevice.connected, + battery: nativeDevice.batteryAvailable ? Math.round(nativeDevice.battery * 100) : -1, + signalStrength: nativeDevice.connected ? "excellent" : "unknown", + canPair: !nativeDevice.paired, + nativeDevice: nativeDevice, + connecting: false, + connectionFailed: false + }) + } + + function updateDeviceData(deviceObj, nativeDevice, deviceType, displayName) { + deviceObj.name = displayName + deviceObj.type = deviceType + deviceObj.paired = nativeDevice.paired + + // If device connected state changed, clear connecting/failed states + 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() { - if (root.bluetoothEnabled && root.bluetoothAvailable) { - bluetoothDeviceScanner.running = true + function clearDeviceList(deviceList) { + for (let device of deviceList) { + device.destroy() } } + function isDeviceDiscoverable(device) { + let displayName = device.name || device.deviceName + if (!displayName || displayName.length < 2) return false + + if (displayName.startsWith('/org/bluez') || displayName.includes('hci0')) return false + + let nameLower = displayName.toLowerCase() + + if (nameLower.match(/^[0-9a-f]{2}[:-][0-9a-f]{2}[:-][0-9a-f]{2}/)) { + return false + } + + if (displayName.length < 3) return false + + if (nameLower.includes('iphone') || nameLower.includes('ipad') || + nameLower.includes('airpods') || nameLower.includes('samsung') || + nameLower.includes('galaxy') || nameLower.includes('pixel') || + nameLower.includes('headphone') || nameLower.includes('speaker') || + nameLower.includes('mouse') || nameLower.includes('keyboard') || + nameLower.includes('watch') || nameLower.includes('buds') || + nameLower.includes('android')) { + return true + } + + return displayName.length >= 4 && !displayName.match(/^[A-Z0-9_-]+$/) + } + + function getDeviceType(name, icon) { + if (!name && !icon) return "bluetooth" + + let nameLower = (name || "").toLowerCase() + let iconLower = (icon || "").toLowerCase() + + if (iconLower.includes("audio") || iconLower.includes("headset") || iconLower.includes("headphone") || + nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || + nameLower.includes("arctis") || nameLower.includes("audio")) return "headset" + else if (iconLower.includes("input-mouse") || nameLower.includes("mouse")) return "mouse" + else if (iconLower.includes("input-keyboard") || nameLower.includes("keyboard")) return "keyboard" + else if (iconLower.includes("phone") || nameLower.includes("phone") || nameLower.includes("iphone") || + nameLower.includes("samsung") || nameLower.includes("android")) return "phone" + else if (iconLower.includes("watch") || nameLower.includes("watch")) return "watch" + else if (iconLower.includes("audio-speakers") || nameLower.includes("speaker")) return "speaker" + else if (iconLower.includes("video-display") || nameLower.includes("tv") || nameLower.includes("display")) return "tv" + + return "bluetooth" + } + function startDiscovery() { - root.scanning = true - // Run comprehensive scan that gets all devices - discoveryScanner.running = true + if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled) { + Bluetooth.defaultAdapter.discovering = true + updateDevices() + } } function stopDiscovery() { - let stopDiscoveryProcess = Qt.createQmlObject(' - import Quickshell.Io - Process { - command: ["bluetoothctl", "scan", "off"] - running: true - onExited: { - root.scanning = false - } - } - ', root) + if (Bluetooth.defaultAdapter) { + Bluetooth.defaultAdapter.discovering = false + updateDevices() + } } function pairDevice(mac) { console.log("Pairing device:", mac) - let pairProcess = Qt.createQmlObject(' - import Quickshell.Io - Process { - command: ["bluetoothctl", "pair", "' + mac + '"] - running: true - onExited: (exitCode) => { - if (exitCode === 0) { - console.log("Pairing successful") - connectDevice("' + mac + '") - } else { - console.warn("Pairing failed with exit code:", exitCode) - } - availableDeviceScanner.running = true - bluetoothDeviceScanner.running = true - } - } - ', root) + let device = findDeviceByMac(mac) + if (device) { + device.pair() + } } function connectDevice(mac) { console.log("Connecting to device:", mac) - let connectProcess = Qt.createQmlObject(' - import Quickshell.Io - Process { - command: ["bluetoothctl", "connect", "' + mac + '"] - running: true - onExited: (exitCode) => { - if (exitCode === 0) { - console.log("Connection successful") - } else { - console.warn("Connection failed with exit code:", exitCode) - } - bluetoothDeviceScanner.running = true - } - } - ', root) + let device = findDeviceByMac(mac) + if (device) { + device.connect() + } } function removeDevice(mac) { console.log("Removing device:", mac) - let removeProcess = Qt.createQmlObject(' - import Quickshell.Io - Process { - command: ["bluetoothctl", "remove", "' + mac + '"] - running: true - onExited: { - bluetoothDeviceScanner.running = true - availableDeviceScanner.running = true - } - } - ', root) + let device = findDeviceByMac(mac) + if (device) { + device.forget() + } } function toggleBluetoothDevice(mac) { - let device = root.bluetoothDevices.find(d => d.mac === mac) - if (device) { - let action = device.connected ? "disconnect" : "connect" - let toggleProcess = Qt.createQmlObject(' - import Quickshell.Io - Process { - command: ["bluetoothctl", "' + action + '", "' + mac + '"] - running: true - onExited: bluetoothDeviceScanner.running = true - } - ', root) + let typedDevice = findDeviceInList(root.bluetoothDevices, mac) + if (!typedDevice) { + typedDevice = findDeviceInList(root.availableDevices, mac) + } + + if (typedDevice && typedDevice.nativeDevice) { + if (typedDevice.connected) { + console.log("Disconnecting device:", mac) + typedDevice.connecting = false + typedDevice.connectionFailed = false + typedDevice.nativeDevice.connected = false + } else { + console.log("Connecting to device:", mac) + typedDevice.connecting = true + typedDevice.connectionFailed = false + + // Set a timeout to handle connection failure + Qt.callLater(() => { + connectionTimeout.deviceMac = mac + connectionTimeout.start() + }) + + typedDevice.nativeDevice.connected = true + } } } function toggleBluetooth() { - let action = root.bluetoothEnabled ? "off" : "on" - let toggleProcess = Qt.createQmlObject(' - import Quickshell.Io - Process { - command: ["bluetoothctl", "power", "' + action + '"] - running: true - onExited: bluetoothStatusChecker.running = true - } - ', root) + if (Bluetooth.defaultAdapter) { + Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled + } } + function findDeviceByMac(mac) { + let typedDevice = findDeviceInList(root.bluetoothDevices, mac) + if (typedDevice && typedDevice.nativeDevice) { + return typedDevice.nativeDevice + } + + typedDevice = findDeviceInList(root.availableDevices, mac) + if (typedDevice && typedDevice.nativeDevice) { + return typedDevice.nativeDevice + } + + if (Bluetooth.defaultAdapter) { + let adapterDevices = Bluetooth.defaultAdapter.devices + if (adapterDevices.values) { + for (let device of adapterDevices.values) { + if (device && device.address === mac) { + return device + } + } + } + } + + if (Bluetooth.devices.values) { + for (let device of Bluetooth.devices.values) { + if (device && device.address === mac) { + return device + } + } + } + return null + } + + Timer { id: bluetoothMonitorTimer - interval: 5000 - running: false; repeat: true + interval: 2000 + running: false + repeat: true onTriggered: { - bluetoothStatusChecker.running = true - if (root.bluetoothEnabled) { - bluetoothDeviceScanner.running = true - // Also refresh paired devices to get current connection status - pairedDeviceChecker.discoveredToMerge = [] - pairedDeviceChecker.running = true - } + updateDevices() } } function enableMonitoring(enabled) { bluetoothMonitorTimer.running = enabled if (enabled) { - // Immediately update when enabled - bluetoothStatusChecker.running = true + refreshBluetoothState() + updateDevices() } } - property var discoveredDevices: [] - - // Handle discovered devices - function _handleDiscovered(found) { - - let discoveredDevices = [] - for (let device of found) { - let type = "bluetooth" - let nameLower = device.name.toLowerCase() - if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset" - else if (nameLower.includes("mouse")) type = "mouse" - else if (nameLower.includes("keyboard")) type = "keyboard" - else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone" - else if (nameLower.includes("watch")) type = "watch" - else if (nameLower.includes("speaker")) type = "speaker" - else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv" - - discoveredDevices.push({ - mac: device.mac, - name: device.name, - type: type, - paired: false, - connected: false, - rssi: -70, - signalStrength: "fair", - canPair: true - }) - - console.log(" -", device.name, "(", device.mac, ")") + Timer { + id: bluetoothStateRefreshTimer + interval: 5000 + running: true + repeat: true + onTriggered: { + refreshBluetoothState() } - - // Get paired devices first, then merge with discovered - pairedDeviceChecker.discoveredToMerge = discoveredDevices - pairedDeviceChecker.running = true } - // Get only currently connected/paired devices that matter - Process { - id: availableDeviceScanner - command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); if [[ \"$paired\" == \"yes\" ]] || [[ \"$connected\" == \"yes\" ]]; then echo \"$mac|$name|$paired|$connected\"; fi; fi; done"] + Timer { + id: connectionTimeout + interval: 10000 // 10 second timeout running: false + repeat: false - stdout: StdioCollector { - onStreamFinished: { - - let devices = [] - if (text.trim()) { - let lines = text.trim().split('\n') - - for (let line of lines) { - if (line.trim()) { - let parts = line.split('|') - if (parts.length >= 4) { - let mac = parts[0].trim() - let name = parts[1].trim() - let paired = parts[2].trim() === 'yes' - let connected = parts[3].trim() === 'yes' - - // Skip technical names - if (name.startsWith('/org/bluez') || name.includes('hci0') || name.length < 3) { - continue - } - - // Determine device type - let type = "bluetooth" - let nameLower = name.toLowerCase() - if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset" - else if (nameLower.includes("mouse")) type = "mouse" - else if (nameLower.includes("keyboard")) type = "keyboard" - else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone" - else if (nameLower.includes("watch")) type = "watch" - else if (nameLower.includes("speaker")) type = "speaker" - else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv" - - devices.push({ - mac: mac, - name: name, - type: type, - paired: paired, - connected: connected, - rssi: 0, - signalStrength: "unknown", - canPair: false // Already paired - }) - } - } - } + property string deviceMac: "" + + onTriggered: { + if (deviceMac) { + let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac) + if (!typedDevice) { + typedDevice = findDeviceInList(root.availableDevices, deviceMac) } - root.availableDevices = devices - } - } - } - - // Discovery scanner using bluetoothctl --timeout - Process { - id: discoveryScanner - // Discover for 8 s in non-interactive mode, then auto-exit - command: ["bluetoothctl", - "--timeout", "8", - "--monitor", // keeps stdout unbuffered - "scan", "on"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - /* - * bluetoothctl prints lines like: - * [NEW] Device 12:34:56:78:9A:BC My-Headphones - */ - const rx = /^\[NEW\] Device ([0-9A-F:]+)\s+(.+)$/i; - const found = text.split('\n') - .filter(l => rx.test(l)) - .map(l => { - const [,mac,name] = l.match(rx); - return { mac, name }; - }); - root._handleDiscovered(found); - } - } - - onExited: { - root.scanning = false - } - } - - // Get paired devices and merge with discovered ones - Process { - id: pairedDeviceChecker - command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ${#name} -gt 3 ]] && [[ ! $name =~ ^/org/bluez ]] && [[ ! $name =~ hci0 ]]; then info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); echo \"$mac|$name|$paired|$connected\"; fi; fi; done"] - running: false - property var discoveredToMerge: [] - - stdout: StdioCollector { - onStreamFinished: { - // Start with discovered devices (unpaired, available to pair) - let allDevices = [...pairedDeviceChecker.discoveredToMerge] - let seenMacs = new Set(allDevices.map(d => d.mac)) - - // Add only actually paired devices from bluetoothctl - if (text.trim()) { - let lines = text.trim().split('\n') + if (typedDevice && typedDevice.connecting && !typedDevice.connected) { + console.log("Connection timeout for device:", deviceMac) + typedDevice.connecting = false + typedDevice.connectionFailed = true - for (let line of lines) { - if (line.trim()) { - let parts = line.split('|') - if (parts.length >= 4) { - let mac = parts[0].trim() - let name = parts[1].trim() - let paired = parts[2].trim() === 'yes' - let connected = parts[3].trim() === 'yes' - - // Only include if actually paired - if (!paired) continue - - // Check if already in discovered list - if (seenMacs.has(mac)) { - // Update existing device to show it's paired - let existing = allDevices.find(d => d.mac === mac) - if (existing) { - existing.paired = true - existing.connected = connected - existing.canPair = false - } - continue - } - - // Add paired device not found during scan - let type = "bluetooth" - let nameLower = name.toLowerCase() - if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset" - else if (nameLower.includes("mouse")) type = "mouse" - else if (nameLower.includes("keyboard")) type = "keyboard" - else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone" - else if (nameLower.includes("watch")) type = "watch" - else if (nameLower.includes("speaker")) type = "speaker" - else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv" - - allDevices.push({ - mac: mac, - name: name, - type: type, - paired: true, - connected: connected, - rssi: -100, - signalStrength: "unknown", - canPair: false - }) - } - } - } + // Clear failure state after 3 seconds + Qt.callLater(() => { + clearFailureTimer.deviceMac = deviceMac + clearFailureTimer.start() + }) + } + deviceMac = "" + } + } + } + + Timer { + id: clearFailureTimer + interval: 3000 + running: false + repeat: false + + property string deviceMac: "" + + onTriggered: { + if (deviceMac) { + let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac) + if (!typedDevice) { + typedDevice = findDeviceInList(root.availableDevices, deviceMac) } - root.availableDevices = allDevices - root.scanning = false - + if (typedDevice) { + typedDevice.connectionFailed = false + } + deviceMac = "" } } } + + component BluetoothDevice: QtObject { + required property string mac + required property string name + required property string type + required property bool paired + required property bool connected + required property int battery + required property string signalStrength + required property bool canPair + required property var nativeDevice // Reference to native Quickshell device + + property bool connecting: false + property bool connectionFailed: false + + readonly property string displayName: name + readonly property bool batteryAvailable: battery >= 0 + readonly property string connectionStatus: { + if (connecting) return "Connecting..." + if (connectionFailed) return "Connection Failed" + if (connected) return "Connected" + return "Disconnected" + } + } + + Component { + id: deviceComponent + BluetoothDevice {} + } } \ No newline at end of file diff --git a/Services/ProcessMonitorService.qml b/Services/ProcessMonitorService.qml index bff751d2..71010cfe 100644 --- a/Services/ProcessMonitorService.qml +++ b/Services/ProcessMonitorService.qml @@ -7,15 +7,12 @@ pragma ComponentBehavior: Bound Singleton { id: root - // Process list properties property var processes: [] property bool isUpdating: false property int processUpdateInterval: 3000 - // Performance control - only run when process monitor is actually visible property bool monitoringEnabled: false - // System information properties property int totalMemoryKB: 0 property int usedMemoryKB: 0 property int totalSwapKB: 0 @@ -24,28 +21,23 @@ Singleton { property real totalCpuUsage: 0.0 property bool systemInfoAvailable: false - // Performance history for charts property var cpuHistory: [] property var memoryHistory: [] property var networkHistory: ({rx: [], tx: []}) property var diskHistory: ({read: [], write: []}) - property int historySize: 60 // Keep 60 data points + property int historySize: 60 - // Per-core CPU usage property var perCoreCpuUsage: [] - // Network stats - property real networkRxRate: 0 // bytes/sec - property real networkTxRate: 0 // bytes/sec + property real networkRxRate: 0 + property real networkTxRate: 0 property var lastNetworkStats: null - // Disk I/O stats - property real diskReadRate: 0 // bytes/sec - property real diskWriteRate: 0 // bytes/sec + property real diskReadRate: 0 + property real diskWriteRate: 0 property var lastDiskStats: null - // Sorting options - property string sortBy: "cpu" // "cpu", "memory", "name", "pid" + property string sortBy: "cpu" property bool sortDescending: true property int maxProcesses: 20 @@ -53,9 +45,6 @@ Singleton { console.log("ProcessMonitorService: Starting initialization...") updateProcessList() console.log("ProcessMonitorService: Initialization complete") - - // Test monitoring disabled - only monitor when explicitly enabled - // testTimer.start() } Timer { @@ -66,7 +55,6 @@ Singleton { onTriggered: { console.log("ProcessMonitorService: Starting test monitoring...") enableMonitoring(true) - // Stop after 8 seconds stopTestTimer.start() } } @@ -82,7 +70,6 @@ Singleton { } } - // System information monitoring Process { id: systemInfoProcess 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 { id: networkStatsProcess 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 { 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}'"] @@ -134,7 +119,6 @@ Singleton { } } - // Process monitoring with ps command Process { id: processListProcess 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 newProcesses = [] - // Skip header line for (let i = 1; i < lines.length; i++) { const line = lines[i].trim() if (!line) continue - // Parse ps output: PID PPID %CPU %MEM RSS COMMAND CMD const parts = line.split(/\s+/) if (parts.length >= 7) { const pid = parseInt(parts[0]) @@ -189,11 +171,10 @@ Singleton { } } - // System and process monitoring timer - now conditional Timer { id: processTimer interval: root.processUpdateInterval - running: root.monitoringEnabled // Only run when monitoring is enabled + running: root.monitoringEnabled repeat: true onTriggered: { @@ -206,29 +187,24 @@ Singleton { } } - // Public functions function updateSystemInfo() { if (!systemInfoProcess.running && root.monitoringEnabled) { systemInfoProcess.running = true } } - // Control functions for enabling/disabling monitoring function enableMonitoring(enabled) { console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled") root.monitoringEnabled = enabled if (enabled) { - // Clear history when starting root.cpuHistory = [] root.memoryHistory = [] root.networkHistory = ({rx: [], tx: []}) root.diskHistory = ({read: [], write: []}) - // Immediately update when enabled updateSystemInfo() updateProcessList() updateNetworkStats() updateDiskStats() - // console.log("ProcessMonitorService: Initial data collection started") } } @@ -248,7 +224,6 @@ Singleton { if (!root.isUpdating && root.monitoringEnabled) { root.isUpdating = true - // Update sort command based on current sort option let sortOption = "" switch (root.sortBy) { case "cpu": @@ -307,7 +282,6 @@ Singleton { } function getProcessIcon(command) { - // Return appropriate Material Design icon for common processes const cmd = command.toLowerCase() if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser")) return "web" if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) return "code" @@ -315,7 +289,7 @@ Singleton { if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify")) return "music_note" if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv")) return "play_circle" if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread")) return "settings" - return "memory" // Default process icon + return "memory" } function formatCpuUsage(cpu) { @@ -444,7 +418,6 @@ Singleton { root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / 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.tx, root.networkTxRate / 1024) } @@ -463,7 +436,7 @@ Singleton { const readSectors = parseInt(parts[1]) const writeSectors = parseInt(parts[2]) if (!isNaN(readSectors) && !isNaN(writeSectors)) { - totalRead += readSectors * 512 // Convert sectors to bytes + totalRead += readSectors * 512 totalWrite += writeSectors * 512 } } @@ -474,7 +447,6 @@ Singleton { root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / 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.write, root.diskWriteRate / (1024 * 1024)) } diff --git a/Services/SystemMonitorService.qml b/Services/SystemMonitorService.qml index fa9846ba..af7d194a 100644 --- a/Services/SystemMonitorService.qml +++ b/Services/SystemMonitorService.qml @@ -7,16 +7,13 @@ pragma ComponentBehavior: Bound Singleton { id: root - // CPU properties property real cpuUsage: 0.0 property int cpuCores: 1 property string cpuModel: "" property real cpuFrequency: 0.0 - // Previous CPU stats for accurate calculation property var prevCpuStats: [0, 0, 0, 0, 0, 0, 0, 0] - // Memory properties property real memoryUsage: 0.0 property real totalMemory: 0.0 property real usedMemory: 0.0 @@ -25,14 +22,12 @@ Singleton { property real bufferMemory: 0.0 property real cacheMemory: 0.0 - // Temperature properties property real cpuTemperature: 0.0 property int cpuUpdateInterval: 3000 property int memoryUpdateInterval: 5000 property int temperatureUpdateInterval: 10000 - // Performance control property bool enabledForTopBar: true property bool enabledForDetailedView: false @@ -43,7 +38,6 @@ Singleton { console.log("SystemMonitorService: Initialization complete") } - // Get CPU information (static) Process { id: cpuInfoProcess command: ["bash", "-c", "lscpu | grep -E 'Model name|CPU\\(s\\):' | head -2"] @@ -69,7 +63,6 @@ Singleton { } } - // CPU usage monitoring with accurate calculation Process { id: cpuUsageProcess command: ["bash", "-c", "head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}'"] @@ -80,17 +73,14 @@ Singleton { if (text.trim()) { const stats = text.trim().split(" ").map(x => parseInt(x)) if (root.prevCpuStats[0] > 0) { - // Calculate differences let diffs = [] for (let i = 0; i < 8; i++) { diffs[i] = stats[i] - root.prevCpuStats[i] } - // Calculate total and idle time const totalTime = diffs.reduce((a, b) => a + b, 0) - const idleTime = diffs[3] + diffs[4] // idle + iowait + const idleTime = diffs[3] + diffs[4] - // CPU usage percentage if (totalTime > 0) { root.cpuUsage = Math.max(0, Math.min(100, ((totalTime - idleTime) / totalTime) * 100)) } @@ -107,7 +97,6 @@ Singleton { } } - // Memory usage monitoring Process { id: memoryUsageProcess command: ["bash", "-c", "free -m | awk 'NR==2{printf \"%.1f %.1f %.1f %.1f\", $3*100/$2, $2, $3, $7}'"] @@ -133,7 +122,6 @@ Singleton { } } - // CPU frequency monitoring Process { id: cpuFrequencyProcess command: ["bash", "-c", "cat /proc/cpuinfo | grep 'cpu MHz' | head -1 | awk '{print $4}'"] @@ -154,7 +142,6 @@ Singleton { } } - // CPU temperature monitoring Process { id: temperatureProcess command: ["bash", "-c", "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then cat /sys/class/thermal/thermal_zone0/temp | awk '{print $1/1000}'; else sensors 2>/dev/null | grep 'Core 0' | awk '{print $3}' | sed 's/+//g;s/°C//g' | head -1; fi"] @@ -175,7 +162,6 @@ Singleton { } } - // CPU monitoring timer Timer { id: cpuTimer interval: root.cpuUpdateInterval @@ -190,7 +176,6 @@ Singleton { } } - // Memory monitoring timer Timer { id: memoryTimer interval: root.memoryUpdateInterval @@ -204,7 +189,6 @@ Singleton { } } - // Temperature monitoring timer Timer { id: temperatureTimer interval: root.temperatureUpdateInterval @@ -218,7 +202,6 @@ Singleton { } } - // Public functions function getCpuInfo() { cpuInfoProcess.running = true } @@ -243,15 +226,15 @@ Singleton { } function getCpuUsageColor() { - if (cpuUsage > 80) return "#e74c3c" // Red - if (cpuUsage > 60) return "#f39c12" // Orange - return "#27ae60" // Green + if (cpuUsage > 80) return "#e74c3c" + if (cpuUsage > 60) return "#f39c12" + return "#27ae60" } function getMemoryUsageColor() { - if (memoryUsage > 90) return "#e74c3c" // Red - if (memoryUsage > 75) return "#f39c12" // Orange - return "#3498db" // Blue + if (memoryUsage > 90) return "#e74c3c" + if (memoryUsage > 75) return "#f39c12" + return "#3498db" } function formatMemory(mb) { @@ -262,8 +245,8 @@ Singleton { } function getTemperatureColor() { - if (cpuTemperature > 80) return "#e74c3c" // Red - if (cpuTemperature > 65) return "#f39c12" // Orange - return "#27ae60" // Green + if (cpuTemperature > 80) return "#e74c3c" + if (cpuTemperature > 65) return "#f39c12" + return "#27ae60" } } \ No newline at end of file diff --git a/Widgets/ControlCenter/BluetoothTab.qml b/Widgets/ControlCenter/BluetoothTab.qml index 85d337ec..a7e023a3 100644 --- a/Widgets/ControlCenter/BluetoothTab.qml +++ b/Widgets/ControlCenter/BluetoothTab.qml @@ -6,206 +6,445 @@ import Quickshell.Io import "../../Common" import "../../Services" -ScrollView { +Item { id: bluetoothTab - clip: true - // These should be bound from parent property bool bluetoothEnabled: false property var bluetoothDevices: [] - Column { - width: parent.width - spacing: Theme.spacingL + ScrollView { + anchors.fill: parent + clip: true - // Bluetooth toggle - Rectangle { - width: parent.width - height: 60 - radius: Theme.cornerRadius - color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : - (bluetoothTab.bluetoothEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12)) - border.color: bluetoothTab.bluetoothEnabled ? Theme.primary : "transparent" - border.width: 2 - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingL - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM - - Text { - text: "bluetooth" - font.family: Theme.iconFont - font.pixelSize: Theme.iconSizeLarge - color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Column { - spacing: 2 - anchors.verticalCenter: parent.verticalCenter - - Text { - text: "Bluetooth" - font.pixelSize: Theme.fontSizeLarge - color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText - font.weight: Font.Medium - } - - Text { - text: bluetoothTab.bluetoothEnabled ? "Enabled" : "Disabled" - font.pixelSize: Theme.fontSizeSmall - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) - } - } - } - - MouseArea { - id: bluetoothToggle - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - BluetoothService.toggleBluetooth() - } - } - } + ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - // Bluetooth devices (when enabled) Column { width: parent.width - spacing: Theme.spacingM - visible: bluetoothTab.bluetoothEnabled + spacing: Theme.spacingL - Text { - text: "Paired Devices" - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - } - - // Real Bluetooth devices - Repeater { - model: bluetoothTab.bluetoothDevices + Rectangle { + width: parent.width + height: 60 + radius: Theme.cornerRadius + color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : + (bluetoothTab.bluetoothEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12)) + border.color: bluetoothTab.bluetoothEnabled ? Theme.primary : "transparent" + border.width: 2 - Rectangle { - width: parent.width - height: 60 - radius: Theme.cornerRadius - color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : - (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) - border.color: modelData.connected ? Theme.primary : "transparent" - border.width: 1 + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingL + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM + Text { + text: "bluetooth" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSizeLarge + color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM Text { - text: { - switch (modelData.type) { - case "headset": return "headset" - case "mouse": return "mouse" - case "keyboard": return "keyboard" - case "phone": return "smartphone" - default: return "bluetooth" - } - } - font.family: Theme.iconFont - font.pixelSize: Theme.iconSize - color: modelData.connected ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter + text: "Bluetooth" + font.pixelSize: Theme.fontSizeLarge + color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText + font.weight: Font.Medium } - Column { - spacing: 2 - anchors.verticalCenter: parent.verticalCenter - - Text { - text: modelData.name - font.pixelSize: Theme.fontSizeMedium - color: modelData.connected ? Theme.primary : Theme.surfaceText - font.weight: modelData.connected ? Font.Medium : Font.Normal - } - - Row { - spacing: Theme.spacingXS - - Text { - text: modelData.connected ? "Connected" : "Disconnected" - font.pixelSize: Theme.fontSizeSmall - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) - } - - Text { - text: modelData.battery >= 0 ? "• " + modelData.battery + "%" : "" - font.pixelSize: Theme.fontSizeSmall - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) - visible: modelData.battery >= 0 - } - } - } - } - - MouseArea { - id: btDeviceArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - BluetoothService.toggleBluetoothDevice(modelData.mac) + Text { + text: bluetoothTab.bluetoothEnabled ? "Enabled" : "Disabled" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) } } } + + MouseArea { + id: bluetoothToggle + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + BluetoothService.toggleBluetooth() + } + } } - } - - // Available devices for pairing (when enabled) - Column { - width: parent.width - spacing: Theme.spacingM - visible: bluetoothTab.bluetoothEnabled - Row { + Column { width: parent.width spacing: Theme.spacingM + visible: bluetoothTab.bluetoothEnabled Text { - text: "Available Devices" + text: "Paired Devices" font.pixelSize: Theme.fontSizeLarge color: Theme.surfaceText font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter } - Item { width: 1; height: 1 } + Repeater { + model: bluetoothTab.bluetoothDevices + + Rectangle { + width: parent.width + height: 60 + radius: Theme.cornerRadius + color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : + (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) + border.color: modelData.connected ? Theme.primary : "transparent" + border.width: 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Text { + text: { + switch (modelData.type) { + case "headset": return "headset" + case "mouse": return "mouse" + case "keyboard": return "keyboard" + case "phone": return "smartphone" + default: return "bluetooth" + } + } + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize + color: { + if (modelData.connecting) return Theme.primary + if (modelData.connected) return Theme.primary + return Theme.surfaceText + } + anchors.verticalCenter: parent.verticalCenter + opacity: modelData.connecting ? 0.6 : 1.0 + + Behavior on opacity { + SequentialAnimation { + running: modelData.connecting + loops: Animation.Infinite + NumberAnimation { from: 1.0; to: 0.3; duration: 800 } + NumberAnimation { from: 0.3; to: 1.0; duration: 800 } + } + } + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + + Text { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: modelData.connected ? Theme.primary : Theme.surfaceText + font.weight: modelData.connected ? Font.Medium : Font.Normal + } + + Row { + spacing: Theme.spacingXS + + Text { + text: modelData.connectionStatus + font.pixelSize: Theme.fontSizeSmall + color: { + if (modelData.connecting) return Theme.primary + if (modelData.connectionFailed) return Theme.error + return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + } + } + + Text { + text: modelData.battery >= 0 ? "• " + modelData.battery + "%" : "" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + visible: modelData.battery >= 0 + } + } + } + } + + Rectangle { + id: btMenuButton + width: 32 + height: 32 + radius: Theme.cornerRadius + color: btMenuButtonArea.containsMouse ? + Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : + "transparent" + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + + Text { + text: "more_vert" + font.family: Theme.iconFont + font.weight: Theme.iconFontWeight + font.pixelSize: Theme.iconSize + color: Theme.surfaceText + opacity: 0.6 + anchors.centerIn: parent + } + + MouseArea { + id: btMenuButtonArea + anchors.fill: parent + hoverEnabled: !modelData.connecting + enabled: !modelData.connecting + cursorShape: modelData.connecting ? Qt.ArrowCursor : Qt.PointingHandCursor + + onClicked: { + if (!modelData.connecting) { + bluetoothContextMenuWindow.deviceData = modelData + let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height) + bluetoothContextMenuWindow.show(localPos.x, localPos.y) + } + } + } + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + } + + MouseArea { + id: btDeviceArea + anchors.fill: parent + anchors.rightMargin: 40 // Don't overlap with menu button + hoverEnabled: !modelData.connecting + enabled: !modelData.connecting + cursorShape: modelData.connecting ? Qt.ArrowCursor : Qt.PointingHandCursor + + onClicked: { + if (!modelData.connecting) { + BluetoothService.toggleBluetoothDevice(modelData.mac) + } + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: bluetoothTab.bluetoothEnabled - Rectangle { - width: Math.max(100, scanText.contentWidth + Theme.spacingM * 2) - height: 32 - radius: Theme.cornerRadius - color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) - border.color: Theme.primary - border.width: 1 + Row { + width: parent.width + spacing: Theme.spacingM + + Text { + text: "Available Devices" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + + Item { width: 1; height: 1 } + + Rectangle { + width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2) + height: 36 + radius: Theme.cornerRadius + color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) + border.color: Theme.primary + border.width: 1 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + Text { + text: BluetoothService.scanning ? "stop" : "bluetooth_searching" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Text { + id: scanText + text: BluetoothService.scanning ? "Stop Scanning" : "Start Scanning" + font.pixelSize: Theme.fontSizeMedium + color: Theme.primary + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: scanArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (BluetoothService.scanning) { + BluetoothService.stopDiscovery() + } else { + BluetoothService.startDiscovery() + } + } + } + } + } + + Repeater { + model: BluetoothService.availableDevices + + Rectangle { + width: parent.width + height: 70 + radius: Theme.cornerRadius + color: availableDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : + (modelData.paired ? Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) + border.color: modelData.paired ? Theme.secondary : (modelData.canPair ? Theme.primary : "transparent") + border.width: 1 + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + Text { + text: { + switch (modelData.type) { + case "headset": return "headset" + case "mouse": return "mouse" + case "keyboard": return "keyboard" + case "phone": return "smartphone" + case "watch": return "watch" + case "speaker": return "speaker" + case "tv": return "tv" + default: return "bluetooth" + } + } + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize + color: modelData.paired ? Theme.secondary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: 2 + anchors.verticalCenter: parent.verticalCenter + + Text { + text: modelData.name + font.pixelSize: Theme.fontSizeMedium + color: modelData.paired ? Theme.secondary : Theme.surfaceText + font.weight: modelData.paired ? Font.Medium : Font.Normal + } + + Row { + spacing: Theme.spacingXS + + Text { + text: { + if (modelData.paired && modelData.connected) return "Connected" + if (modelData.paired) return "Paired" + return "Signal: " + modelData.signalStrength + } + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + } + + Text { + text: modelData.rssi !== 0 ? "• " + modelData.rssi + " dBm" : "" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) + visible: modelData.rssi !== 0 + } + } + } + } + + Rectangle { + width: 80 + height: 28 + radius: Theme.cornerRadiusSmall + anchors.right: parent.right + anchors.rightMargin: Theme.spacingM + anchors.verticalCenter: parent.verticalCenter + color: actionButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + border.color: Theme.primary + border.width: 1 + visible: modelData.canPair || modelData.paired + + Text { + anchors.centerIn: parent + text: modelData.paired ? (modelData.connected ? "Disconnect" : "Connect") : "Pair" + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + } + + MouseArea { + id: actionButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (modelData.paired) { + if (modelData.connected) { + BluetoothService.toggleBluetoothDevice(modelData.mac) + } else { + BluetoothService.connectDevice(modelData.mac) + } + } else { + BluetoothService.pairDevice(modelData.mac) + } + } + } + } + + MouseArea { + id: availableDeviceArea + anchors.fill: parent + anchors.rightMargin: 90 // Don't overlap with action button + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (modelData.paired) { + BluetoothService.toggleBluetoothDevice(modelData.mac) + } else { + BluetoothService.pairDevice(modelData.mac) + } + } + } + } + } + + Column { + width: parent.width + spacing: Theme.spacingM + visible: BluetoothService.scanning && BluetoothService.availableDevices.length === 0 Row { - anchors.centerIn: parent - spacing: Theme.spacingXS + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingM Text { - text: BluetoothService.scanning ? "search" : "bluetooth_searching" + text: "sync" font.family: Theme.iconFont - font.pixelSize: Theme.iconSize - 4 + font.pixelSize: Theme.iconSizeLarge color: Theme.primary anchors.verticalCenter: parent.verticalCenter RotationAnimation on rotation { - running: BluetoothService.scanning + running: true loops: Animation.Infinite from: 0 to: 360 @@ -214,169 +453,237 @@ ScrollView { } Text { - id: scanText - text: BluetoothService.scanning ? "Scanning..." : "Scan" - font.pixelSize: Theme.fontSizeMedium - color: Theme.primary + text: "Scanning for devices..." + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText font.weight: Font.Medium anchors.verticalCenter: parent.verticalCenter } } - MouseArea { - id: scanArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - enabled: !BluetoothService.scanning - - onClicked: { - BluetoothService.startDiscovery() + Text { + text: "Make sure your device is in pairing mode" + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + anchors.horizontalCenter: parent.horizontalCenter + } + } + + Text { + text: "No devices found. Put your device in pairing mode and click Start Scanning." + font.pixelSize: Theme.fontSizeMedium + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignHCenter + } + } + } + } + + Rectangle { + id: bluetoothContextMenuWindow + property var deviceData: null + property bool menuVisible: false + + visible: false + width: 160 + height: menuColumn.implicitHeight + Theme.spacingS * 2 + radius: Theme.cornerRadiusLarge + color: Theme.popupBackground() + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + z: 1000 + + Rectangle { + anchors.fill: parent + anchors.topMargin: 4 + anchors.leftMargin: 2 + anchors.rightMargin: -2 + anchors.bottomMargin: -4 + radius: parent.radius + color: Qt.rgba(0, 0, 0, 0.15) + z: parent.z - 1 + } + + opacity: menuVisible ? 1.0 : 0.0 + scale: menuVisible ? 1.0 : 0.85 + + Behavior on opacity { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Behavior on scale { + NumberAnimation { + duration: Theme.mediumDuration + easing.type: Theme.emphasizedEasing + } + } + + Column { + id: menuColumn + anchors.fill: parent + anchors.margins: Theme.spacingS + spacing: 1 + + Rectangle { + width: parent.width + height: 32 + radius: Theme.cornerRadiusSmall + color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Text { + text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "link_off" : "link" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize - 2 + color: Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "Disconnect" : "Connect" + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: connectArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (bluetoothContextMenuWindow.deviceData) { + BluetoothService.toggleBluetoothDevice(bluetoothContextMenuWindow.deviceData.mac) } + bluetoothContextMenuWindow.hide() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing } } } - // Available devices list - Repeater { - model: BluetoothService.availableDevices + Rectangle { + width: parent.width - Theme.spacingS * 2 + height: 5 + anchors.horizontalCenter: parent.horizontalCenter + color: "transparent" Rectangle { + anchors.centerIn: parent width: parent.width - height: 70 - radius: Theme.cornerRadius - color: availableDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : - (modelData.paired ? Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) - border.color: modelData.paired ? Theme.secondary : (modelData.canPair ? Theme.primary : "transparent") - border.width: 1 - - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingM - - Text { - text: { - switch (modelData.type) { - case "headset": return "headset" - case "mouse": return "mouse" - case "keyboard": return "keyboard" - case "phone": return "smartphone" - case "watch": return "watch" - case "speaker": return "speaker" - case "tv": return "tv" - default: return "bluetooth" - } - } - font.family: Theme.iconFont - font.pixelSize: Theme.iconSize - color: modelData.paired ? Theme.secondary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - Column { - spacing: 2 - anchors.verticalCenter: parent.verticalCenter - - Text { - text: modelData.name - font.pixelSize: Theme.fontSizeMedium - color: modelData.paired ? Theme.secondary : Theme.surfaceText - font.weight: modelData.paired ? Font.Medium : Font.Normal - } - - Row { - spacing: Theme.spacingXS - - Text { - text: { - if (modelData.paired && modelData.connected) return "Connected" - if (modelData.paired) return "Paired" - return "Signal: " + modelData.signalStrength - } - font.pixelSize: Theme.fontSizeSmall - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) - } - - Text { - text: modelData.rssi !== 0 ? "• " + modelData.rssi + " dBm" : "" - font.pixelSize: Theme.fontSizeSmall - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) - visible: modelData.rssi !== 0 - } - } - } - } - - // Action button on the right - Rectangle { - width: 80 - height: 28 - radius: Theme.cornerRadiusSmall - anchors.right: parent.right - anchors.rightMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - color: actionButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" - border.color: Theme.primary - border.width: 1 - visible: modelData.canPair || modelData.paired - - Text { - anchors.centerIn: parent - text: modelData.paired ? (modelData.connected ? "Disconnect" : "Connect") : "Pair" - font.pixelSize: Theme.fontSizeSmall - color: Theme.primary - font.weight: Font.Medium - } - - MouseArea { - id: actionButtonArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - if (modelData.paired) { - if (modelData.connected) { - BluetoothService.toggleBluetoothDevice(modelData.mac) - } else { - BluetoothService.connectDevice(modelData.mac) - } - } else { - BluetoothService.pairDevice(modelData.mac) - } - } - } - } - - MouseArea { - id: availableDeviceArea - anchors.fill: parent - anchors.rightMargin: 90 // Don't overlap with action button - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onClicked: { - if (modelData.paired) { - BluetoothService.toggleBluetoothDevice(modelData.mac) - } else { - BluetoothService.pairDevice(modelData.mac) - } - } - } + height: 1 + color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) } } - // No devices message - Text { - text: "No devices found. Put your device in pairing mode and click Scan." - font.pixelSize: Theme.fontSizeMedium - color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) - visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning - wrapMode: Text.WordWrap + Rectangle { width: parent.width - horizontalAlignment: Text.AlignHCenter + height: 32 + radius: Theme.cornerRadiusSmall + color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent" + + Row { + anchors.left: parent.left + anchors.leftMargin: Theme.spacingS + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + Text { + text: "delete" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize - 2 + color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText + opacity: 0.7 + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: "Forget Device" + font.pixelSize: Theme.fontSizeSmall + color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText + font.weight: Font.Normal + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: forgetArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (bluetoothContextMenuWindow.deviceData) { + BluetoothService.removeDevice(bluetoothContextMenuWindow.deviceData.mac) + } + bluetoothContextMenuWindow.hide() + } + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + } + } + + function show(x, y) { + const menuWidth = 160 + const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2 + + let finalX = x - menuWidth / 2 + let finalY = y + + finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth)) + finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight)) + + bluetoothContextMenuWindow.x = finalX + bluetoothContextMenuWindow.y = finalY + bluetoothContextMenuWindow.visible = true + bluetoothContextMenuWindow.menuVisible = true + } + + function hide() { + bluetoothContextMenuWindow.menuVisible = false + Qt.callLater(() => { bluetoothContextMenuWindow.visible = false }) + } + } + + MouseArea { + anchors.fill: parent + visible: bluetoothContextMenuWindow.visible + onClicked: { + bluetoothContextMenuWindow.hide() + } + + MouseArea { + x: bluetoothContextMenuWindow.x + y: bluetoothContextMenuWindow.y + width: bluetoothContextMenuWindow.width + height: bluetoothContextMenuWindow.height + onClicked: { } } } diff --git a/Widgets/TopBar/TopBar.qml b/Widgets/TopBar/TopBar.qml index b70b4652..30ec7580 100644 --- a/Widgets/TopBar/TopBar.qml +++ b/Widgets/TopBar/TopBar.qml @@ -334,7 +334,7 @@ PanelWindow { topBar.shellRoot.controlCenterVisible = !topBar.shellRoot.controlCenterVisible if (topBar.shellRoot.controlCenterVisible) { WifiService.scanWifi() - BluetoothService.scanDevices() + // Bluetooth devices are automatically updated via signals } } }