mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-26 14:32:52 -05:00
Bluetooth improvements, battery widget, notification fixes
This commit is contained in:
252
Services/BatteryService.qml
Normal file
252
Services/BatteryService.qml
Normal file
@@ -0,0 +1,252 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool batteryAvailable: false
|
||||
property int batteryLevel: 0
|
||||
property string batteryStatus: "Unknown" // "Charging", "Discharging", "Full", "Not charging", "Unknown"
|
||||
property int timeRemaining: 0 // minutes
|
||||
property bool isCharging: false
|
||||
property bool isLowBattery: false
|
||||
property int batteryHealth: 100 // percentage
|
||||
property string batteryTechnology: "Unknown"
|
||||
property int cycleCount: 0
|
||||
property int batteryCapacity: 0 // mAh
|
||||
property var powerProfiles: []
|
||||
property string activePowerProfile: "balanced"
|
||||
|
||||
// Check if battery is available
|
||||
Process {
|
||||
id: batteryAvailabilityChecker
|
||||
command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"]
|
||||
running: true
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
root.batteryAvailable = true
|
||||
console.log("Battery found:", data.trim())
|
||||
batteryStatusChecker.running = true
|
||||
} else {
|
||||
root.batteryAvailable = false
|
||||
console.log("No battery found - this appears to be a desktop system")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Battery status checker
|
||||
Process {
|
||||
id: batteryStatusChecker
|
||||
command: ["bash", "-c", "if [ -d /sys/class/power_supply/BAT0 ] || [ -d /sys/class/power_supply/BAT1 ]; then upower -i $(upower -e | grep 'BAT') | grep -E 'state|percentage|time to|energy|technology|cycle-count' || acpi -b 2>/dev/null || echo 'fallback'; else echo 'no-battery'; fi"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim() === "no-battery") {
|
||||
root.batteryAvailable = false
|
||||
return
|
||||
}
|
||||
|
||||
if (text.trim() && text.trim() !== "fallback") {
|
||||
parseBatteryInfo(text.trim())
|
||||
} else {
|
||||
// Fallback to simple methods
|
||||
fallbackBatteryChecker.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Battery status check failed, trying fallback methods")
|
||||
fallbackBatteryChecker.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback battery checker using /sys files
|
||||
Process {
|
||||
id: fallbackBatteryChecker
|
||||
command: ["bash", "-c", "if [ -f /sys/class/power_supply/BAT0/capacity ]; then BAT=BAT0; elif [ -f /sys/class/power_supply/BAT1/capacity ]; then BAT=BAT1; else echo 'no-battery'; exit 1; fi; echo \"percentage: $(cat /sys/class/power_supply/$BAT/capacity)%\"; echo \"state: $(cat /sys/class/power_supply/$BAT/status 2>/dev/null || echo Unknown)\"; if [ -f /sys/class/power_supply/$BAT/technology ]; then echo \"technology: $(cat /sys/class/power_supply/$BAT/technology)\"; fi; if [ -f /sys/class/power_supply/$BAT/cycle_count ]; then echo \"cycle-count: $(cat /sys/class/power_supply/$BAT/cycle_count)\"; fi"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim() !== "no-battery") {
|
||||
parseBatteryInfo(text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power profiles checker (for systems with power-profiles-daemon)
|
||||
Process {
|
||||
id: powerProfilesChecker
|
||||
command: ["bash", "-c", "if command -v powerprofilesctl > /dev/null; then powerprofilesctl list 2>/dev/null; else echo 'not-available'; fi"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim() !== "not-available") {
|
||||
parsePowerProfiles(text.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseBatteryInfo(batteryText) {
|
||||
let lines = batteryText.split('\n')
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim().toLowerCase()
|
||||
|
||||
if (line.includes('percentage:') || line.includes('capacity:')) {
|
||||
let match = line.match(/(\d+)%?/)
|
||||
if (match) {
|
||||
root.batteryLevel = parseInt(match[1])
|
||||
root.isLowBattery = root.batteryLevel <= 20
|
||||
}
|
||||
} else if (line.includes('state:') || line.includes('status:')) {
|
||||
if (line.includes('charging')) {
|
||||
root.batteryStatus = "Charging"
|
||||
root.isCharging = true
|
||||
} else if (line.includes('discharging')) {
|
||||
root.batteryStatus = "Discharging"
|
||||
root.isCharging = false
|
||||
} else if (line.includes('full')) {
|
||||
root.batteryStatus = "Full"
|
||||
root.isCharging = false
|
||||
} else if (line.includes('not charging')) {
|
||||
root.batteryStatus = "Not charging"
|
||||
root.isCharging = false
|
||||
} else {
|
||||
root.batteryStatus = "Unknown"
|
||||
root.isCharging = false
|
||||
}
|
||||
} else if (line.includes('time to')) {
|
||||
let match = line.match(/(\d+):(\d+)/)
|
||||
if (match) {
|
||||
root.timeRemaining = parseInt(match[1]) * 60 + parseInt(match[2])
|
||||
}
|
||||
} else if (line.includes('technology:')) {
|
||||
let tech = line.split(':')[1]?.trim() || "Unknown"
|
||||
root.batteryTechnology = tech.charAt(0).toUpperCase() + tech.slice(1)
|
||||
} else if (line.includes('cycle-count:')) {
|
||||
let match = line.match(/(\d+)/)
|
||||
if (match) {
|
||||
root.cycleCount = parseInt(match[1])
|
||||
}
|
||||
} else if (line.includes('energy-full:') || line.includes('capacity:')) {
|
||||
let match = line.match(/([\d.]+)\s*wh/i)
|
||||
if (match) {
|
||||
root.batteryCapacity = Math.round(parseFloat(match[1]) * 1000) // Convert to mWh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Battery status updated:", root.batteryLevel + "%", root.batteryStatus)
|
||||
}
|
||||
|
||||
function parsePowerProfiles(profileText) {
|
||||
let lines = profileText.split('\n')
|
||||
let profiles = []
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
if (line.includes('*')) {
|
||||
// Active profile
|
||||
let profileName = line.replace('*', '').trim()
|
||||
if (profileName.includes(':')) {
|
||||
profileName = profileName.split(':')[0].trim()
|
||||
}
|
||||
root.activePowerProfile = profileName
|
||||
profiles.push(profileName)
|
||||
} else if (line && !line.includes(':') && line.length > 0) {
|
||||
profiles.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
root.powerProfiles = profiles
|
||||
console.log("Power profiles available:", profiles, "Active:", root.activePowerProfile)
|
||||
}
|
||||
|
||||
function setBatteryProfile(profileName) {
|
||||
if (!root.powerProfiles.includes(profileName)) {
|
||||
console.warn("Invalid power profile:", profileName)
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Setting power profile to:", profileName)
|
||||
let profileProcess = Qt.createQmlObject(`
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["powerprofilesctl", "set", "${profileName}"]
|
||||
running: true
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
console.log("Power profile changed to:", "${profileName}")
|
||||
root.activePowerProfile = "${profileName}"
|
||||
} else {
|
||||
console.warn("Failed to change power profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
`, root)
|
||||
}
|
||||
|
||||
function getBatteryIcon() {
|
||||
if (!root.batteryAvailable) return "power"
|
||||
|
||||
let level = root.batteryLevel
|
||||
let charging = root.isCharging
|
||||
|
||||
if (charging) {
|
||||
if (level >= 90) return "battery_charging_full"
|
||||
if (level >= 60) return "battery_charging_90"
|
||||
if (level >= 30) return "battery_charging_60"
|
||||
if (level >= 20) return "battery_charging_30"
|
||||
return "battery_charging_20"
|
||||
} else {
|
||||
if (level >= 90) return "battery_full"
|
||||
if (level >= 60) return "battery_6_bar"
|
||||
if (level >= 50) return "battery_5_bar"
|
||||
if (level >= 40) return "battery_4_bar"
|
||||
if (level >= 30) return "battery_3_bar"
|
||||
if (level >= 20) return "battery_2_bar"
|
||||
if (level >= 10) return "battery_1_bar"
|
||||
return "battery_alert"
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimeRemaining() {
|
||||
if (root.timeRemaining <= 0) return "Unknown"
|
||||
|
||||
let hours = Math.floor(root.timeRemaining / 60)
|
||||
let minutes = root.timeRemaining % 60
|
||||
|
||||
if (hours > 0) {
|
||||
return hours + "h " + minutes + "m"
|
||||
} else {
|
||||
return minutes + "m"
|
||||
}
|
||||
}
|
||||
|
||||
// Update battery status every 30 seconds
|
||||
Timer {
|
||||
interval: 30000
|
||||
running: root.batteryAvailable
|
||||
repeat: true
|
||||
triggeredOnStart: false
|
||||
onTriggered: {
|
||||
batteryStatusChecker.running = true
|
||||
powerProfilesChecker.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ Singleton {
|
||||
property bool bluetoothEnabled: false
|
||||
property bool bluetoothAvailable: false
|
||||
property var bluetoothDevices: []
|
||||
property var availableDevices: []
|
||||
property bool scanning: false
|
||||
property bool discoverable: false
|
||||
|
||||
// Real Bluetooth Management
|
||||
Process {
|
||||
@@ -91,6 +94,91 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function startDiscovery() {
|
||||
console.log("Starting Bluetooth discovery...")
|
||||
let discoveryProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bluetoothctl", "scan", "on"]
|
||||
running: true
|
||||
onExited: {
|
||||
root.scanning = true
|
||||
// Scan for 10 seconds then get discovered devices
|
||||
discoveryScanTimer.start()
|
||||
}
|
||||
}
|
||||
', root)
|
||||
}
|
||||
|
||||
function stopDiscovery() {
|
||||
console.log("Stopping Bluetooth discovery...")
|
||||
let stopDiscoveryProcess = Qt.createQmlObject('
|
||||
import Quickshell.Io
|
||||
Process {
|
||||
command: ["bluetoothctl", "scan", "off"]
|
||||
running: true
|
||||
onExited: {
|
||||
root.scanning = false
|
||||
}
|
||||
}
|
||||
', root)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
function toggleBluetoothDevice(mac) {
|
||||
console.log("Toggling Bluetooth device:", mac)
|
||||
let device = root.bluetoothDevices.find(d => d.mac === mac)
|
||||
@@ -118,4 +206,82 @@ Singleton {
|
||||
}
|
||||
', root)
|
||||
}
|
||||
|
||||
// Timer for discovery scanning
|
||||
Timer {
|
||||
id: discoveryScanTimer
|
||||
interval: 8000 // 8 seconds
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
availableDeviceScanner.running = true
|
||||
}
|
||||
}
|
||||
|
||||
// Scan for available/discoverable devices
|
||||
Process {
|
||||
id: availableDeviceScanner
|
||||
command: ["bash", "-c", "timeout 5 bluetoothctl devices | grep -v 'Device.*/' | while read -r line; do if [[ $line =~ Device\ ([0-9A-F:]+)\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ! $name =~ ^/org/bluez ]] && [[ ! $name =~ hci0 ]]; then info=$(timeout 3 bluetoothctl info $mac 2>/dev/null); paired=$(echo \"$info\" | grep 'Paired:' | grep -q 'yes' && echo 'true' || echo 'false'); connected=$(echo \"$info\" | grep 'Connected:' | grep -q 'yes' && echo 'true' || echo 'false'); rssi=$(echo \"$info\" | grep 'RSSI:' | awk '{print $2}' | head -n1); echo \"$mac|$name|$paired|$connected|${rssi:-}\"; 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 >= 4) {
|
||||
let mac = parts[0].trim()
|
||||
let name = parts[1].trim()
|
||||
let paired = parts[2].trim() === 'true'
|
||||
let connected = parts[3].trim() === 'true'
|
||||
let rssi = parts[4] ? parseInt(parts[4]) : 0
|
||||
|
||||
// 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") || 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"
|
||||
|
||||
// Signal strength assessment
|
||||
let signalStrength = "unknown"
|
||||
if (rssi !== 0) {
|
||||
if (rssi >= -50) signalStrength = "excellent"
|
||||
else if (rssi >= -60) signalStrength = "good"
|
||||
else if (rssi >= -70) signalStrength = "fair"
|
||||
else signalStrength = "weak"
|
||||
}
|
||||
|
||||
devices.push({
|
||||
mac: mac,
|
||||
name: name,
|
||||
type: type,
|
||||
paired: paired,
|
||||
connected: connected,
|
||||
rssi: rssi,
|
||||
signalStrength: signalStrength,
|
||||
canPair: !paired
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.availableDevices = devices
|
||||
console.log("Found", devices.length, "available Bluetooth devices")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,62 @@ Singleton {
|
||||
pressure: 0
|
||||
})
|
||||
|
||||
// Weather icon mapping (based on wttr.in weather codes)
|
||||
property var weatherIcons: ({
|
||||
"113": "clear_day",
|
||||
"116": "partly_cloudy_day",
|
||||
"119": "cloud",
|
||||
"122": "cloud",
|
||||
"143": "foggy",
|
||||
"176": "rainy",
|
||||
"179": "rainy",
|
||||
"182": "rainy",
|
||||
"185": "rainy",
|
||||
"200": "thunderstorm",
|
||||
"227": "cloudy_snowing",
|
||||
"230": "snowing_heavy",
|
||||
"248": "foggy",
|
||||
"260": "foggy",
|
||||
"263": "rainy",
|
||||
"266": "rainy",
|
||||
"281": "rainy",
|
||||
"284": "rainy",
|
||||
"293": "rainy",
|
||||
"296": "rainy",
|
||||
"299": "rainy",
|
||||
"302": "weather_hail",
|
||||
"305": "rainy",
|
||||
"308": "weather_hail",
|
||||
"311": "rainy",
|
||||
"314": "rainy",
|
||||
"317": "rainy",
|
||||
"320": "cloudy_snowing",
|
||||
"323": "cloudy_snowing",
|
||||
"326": "cloudy_snowing",
|
||||
"329": "snowing_heavy",
|
||||
"332": "snowing_heavy",
|
||||
"335": "snowing_heavy",
|
||||
"338": "snowing_heavy",
|
||||
"350": "rainy",
|
||||
"353": "rainy",
|
||||
"356": "weather_hail",
|
||||
"359": "weather_hail",
|
||||
"362": "rainy",
|
||||
"365": "weather_hail",
|
||||
"368": "cloudy_snowing",
|
||||
"371": "snowing_heavy",
|
||||
"374": "weather_hail",
|
||||
"377": "weather_hail",
|
||||
"386": "thunderstorm",
|
||||
"389": "thunderstorm",
|
||||
"392": "snowing_heavy",
|
||||
"395": "snowing_heavy"
|
||||
})
|
||||
|
||||
function getWeatherIcon(code) {
|
||||
return weatherIcons[code] || "cloud"
|
||||
}
|
||||
|
||||
Process {
|
||||
id: weatherFetcher
|
||||
command: ["bash", "-c", "curl -s 'wttr.in/?format=j1' | jq '{current: .current_condition[0], location: .nearest_area[0], astronomy: .weather[0].astronomy[0]}'"]
|
||||
|
||||
@@ -6,4 +6,5 @@ singleton NetworkService 1.0 NetworkService.qml
|
||||
singleton WifiService 1.0 WifiService.qml
|
||||
singleton AudioService 1.0 AudioService.qml
|
||||
singleton BluetoothService 1.0 BluetoothService.qml
|
||||
singleton BrightnessService 1.0 BrightnessService.qml
|
||||
singleton BrightnessService 1.0 BrightnessService.qml
|
||||
singleton BatteryService 1.0 BatteryService.qml
|
||||
Reference in New Issue
Block a user