mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -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 bluetoothEnabled: false
|
||||||
property bool bluetoothAvailable: false
|
property bool bluetoothAvailable: false
|
||||||
property var bluetoothDevices: []
|
property var bluetoothDevices: []
|
||||||
|
property var availableDevices: []
|
||||||
|
property bool scanning: false
|
||||||
|
property bool discoverable: false
|
||||||
|
|
||||||
// Real Bluetooth Management
|
// Real Bluetooth Management
|
||||||
Process {
|
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) {
|
function toggleBluetoothDevice(mac) {
|
||||||
console.log("Toggling Bluetooth device:", mac)
|
console.log("Toggling Bluetooth device:", mac)
|
||||||
let device = root.bluetoothDevices.find(d => d.mac === mac)
|
let device = root.bluetoothDevices.find(d => d.mac === mac)
|
||||||
@@ -118,4 +206,82 @@ Singleton {
|
|||||||
}
|
}
|
||||||
', root)
|
', 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
|
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 {
|
Process {
|
||||||
id: weatherFetcher
|
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]}'"]
|
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 WifiService 1.0 WifiService.qml
|
||||||
singleton AudioService 1.0 AudioService.qml
|
singleton AudioService 1.0 AudioService.qml
|
||||||
singleton BluetoothService 1.0 BluetoothService.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
|
||||||
367
Widgets/BatteryControlPopup.qml
Normal file
367
Widgets/BatteryControlPopup.qml
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "../Common"
|
||||||
|
import "../Services"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: batteryControlPopup
|
||||||
|
|
||||||
|
visible: root.batteryPopupVisible && BatteryService.batteryAvailable
|
||||||
|
|
||||||
|
implicitWidth: 400
|
||||||
|
implicitHeight: 300
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.min(380, parent.width - Theme.spacingL * 2)
|
||||||
|
height: Math.min(450, parent.height - Theme.barHeight - Theme.spacingS * 2)
|
||||||
|
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
|
||||||
|
y: Theme.barHeight + Theme.spacingS
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
opacity: root.batteryPopupVisible ? 1.0 : 0.0
|
||||||
|
scale: root.batteryPopupVisible ? 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
// Header
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Battery Information"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { width: parent.width - 200; height: 1 }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: closeBatteryArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "close"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize - 4
|
||||||
|
color: closeBatteryArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: closeBatteryArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
root.batteryPopupVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery status card
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 120
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
|
||||||
|
border.color: BatteryService.isCharging ? Theme.primary : (BatteryService.isLowBattery ? Theme.error : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12))
|
||||||
|
border.width: BatteryService.isCharging || BatteryService.isLowBattery ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
// Large battery icon
|
||||||
|
Text {
|
||||||
|
text: BatteryService.getBatteryIcon()
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 48
|
||||||
|
color: {
|
||||||
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
|
||||||
|
if (BatteryService.isCharging) return Theme.primary
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: BatteryService.batteryLevel + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
|
color: {
|
||||||
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
|
||||||
|
if (BatteryService.isCharging) return Theme.primary
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
font.weight: Font.Bold
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: BatteryService.batteryStatus
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: {
|
||||||
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
|
||||||
|
if (BatteryService.isCharging) return Theme.primary
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
let time = BatteryService.formatTimeRemaining()
|
||||||
|
if (time !== "Unknown") {
|
||||||
|
return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery details
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Battery Details"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
width: parent.width
|
||||||
|
columns: 2
|
||||||
|
columnSpacing: Theme.spacingL
|
||||||
|
rowSpacing: Theme.spacingM
|
||||||
|
|
||||||
|
// Technology
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Technology"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: BatteryService.batteryTechnology
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle count
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Cycle Count"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: BatteryService.cycleCount > 0 ? BatteryService.cycleCount.toString() : "Unknown"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Health"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: BatteryService.batteryHealth + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: BatteryService.batteryHealth < 80 ? Theme.error : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capacity
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Capacity"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: BatteryService.batteryCapacity > 0 ? BatteryService.batteryCapacity + " mWh" : "Unknown"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power profiles (if available)
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: BatteryService.powerProfiles.length > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Power Profile"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: BatteryService.powerProfiles
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: profileArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||||
|
(modelData === BatteryService.activePowerProfile ? 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.08))
|
||||||
|
border.color: modelData === BatteryService.activePowerProfile ? Theme.primary : "transparent"
|
||||||
|
border.width: 2
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingL
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
switch (modelData) {
|
||||||
|
case "power-saver": return "battery_saver"
|
||||||
|
case "balanced": return "battery_std"
|
||||||
|
case "performance": return "flash_on"
|
||||||
|
default: return "settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize
|
||||||
|
color: modelData === BatteryService.activePowerProfile ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
switch (modelData) {
|
||||||
|
case "power-saver": return "Power Saver"
|
||||||
|
case "balanced": return "Balanced"
|
||||||
|
case "performance": return "Performance"
|
||||||
|
default: return modelData.charAt(0).toUpperCase() + modelData.slice(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: modelData === BatteryService.activePowerProfile ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: modelData === BatteryService.activePowerProfile ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
switch (modelData) {
|
||||||
|
case "power-saver": return "Extend battery life"
|
||||||
|
case "balanced": return "Balance power and performance"
|
||||||
|
case "performance": return "Prioritize performance"
|
||||||
|
default: return "Custom power profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: profileArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
BatteryService.setBatteryProfile(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
Widgets/BatteryWidget.qml
Normal file
129
Widgets/BatteryWidget.qml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import "../Common"
|
||||||
|
import "../Services"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: batteryWidget
|
||||||
|
|
||||||
|
property bool batteryPopupVisible: false
|
||||||
|
|
||||||
|
width: Theme.barHeight - Theme.spacingS
|
||||||
|
height: Theme.barHeight - Theme.spacingS
|
||||||
|
radius: Theme.cornerRadiusSmall
|
||||||
|
color: batteryArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||||
|
visible: BatteryService.batteryAvailable
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
// Battery icon
|
||||||
|
Text {
|
||||||
|
text: BatteryService.getBatteryIcon()
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize
|
||||||
|
color: {
|
||||||
|
if (!BatteryService.batteryAvailable) return Theme.surfaceText
|
||||||
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
|
||||||
|
if (BatteryService.isCharging) return Theme.primary
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
// Subtle animation for charging
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: BatteryService.isCharging
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 8000
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery percentage
|
||||||
|
Text {
|
||||||
|
text: BatteryService.batteryLevel + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: {
|
||||||
|
if (!BatteryService.batteryAvailable) return Theme.surfaceText
|
||||||
|
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
|
||||||
|
if (BatteryService.isCharging) return Theme.primary
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: BatteryService.batteryAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: batteryArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
batteryPopupVisible = !batteryPopupVisible
|
||||||
|
root.batteryPopupVisible = batteryPopupVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip on hover
|
||||||
|
Rectangle {
|
||||||
|
id: batteryTooltip
|
||||||
|
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
|
||||||
|
height: tooltipText.contentHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
visible: batteryArea.containsMouse && !batteryPopupVisible && BatteryService.batteryAvailable
|
||||||
|
|
||||||
|
anchors.bottom: parent.top
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
opacity: batteryArea.containsMouse ? 1.0 : 0.0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: tooltipText
|
||||||
|
text: {
|
||||||
|
if (!BatteryService.batteryAvailable) return "No battery"
|
||||||
|
|
||||||
|
let status = BatteryService.batteryStatus
|
||||||
|
let level = BatteryService.batteryLevel + "%"
|
||||||
|
let time = BatteryService.formatTimeRemaining()
|
||||||
|
|
||||||
|
if (time !== "Unknown") {
|
||||||
|
return status + " • " + level + " • " + time
|
||||||
|
} else {
|
||||||
|
return status + " • " + level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import Quickshell.Widgets
|
|||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import "../Common"
|
import "../Common"
|
||||||
|
import "../Services"
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: calendarPopup
|
id: calendarPopup
|
||||||
@@ -285,7 +286,7 @@ PanelWindow {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: root.weatherIcons[root.weather.wCode] || "clear_day"
|
text: WeatherService.getWeatherIcon(root.weather.wCode)
|
||||||
font.family: Theme.iconFont
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: Theme.iconSize + 4
|
font.pixelSize: Theme.iconSize + 4
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
|
|||||||
@@ -1217,6 +1217,221 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Available devices for pairing (when enabled)
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
visible: root.bluetoothEnabled
|
||||||
|
|
||||||
|
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(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 {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: BluetoothService.scanning ? "search" : "bluetooth_searching"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: BluetoothService.scanning
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: scanText
|
||||||
|
text: BluetoothService.scanning ? "Scanning..." : "Scan"
|
||||||
|
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
|
||||||
|
enabled: !BluetoothService.scanning
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
BluetoothService.startDiscovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available devices list
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
341
Widgets/InputDialog.qml
Normal file
341
Widgets/InputDialog.qml
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import "../Common"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: inputDialog
|
||||||
|
|
||||||
|
property bool dialogVisible: false
|
||||||
|
property string dialogTitle: "Input Required"
|
||||||
|
property string dialogSubtitle: "Please enter the required information"
|
||||||
|
property string inputPlaceholder: "Enter text"
|
||||||
|
property string inputValue: ""
|
||||||
|
property bool isPassword: false
|
||||||
|
property string confirmButtonText: "Confirm"
|
||||||
|
property string cancelButtonText: "Cancel"
|
||||||
|
|
||||||
|
signal confirmed(string value)
|
||||||
|
signal cancelled()
|
||||||
|
|
||||||
|
visible: dialogVisible
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: dialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
textInput.forceActiveFocus()
|
||||||
|
textInput.text = inputValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDialog(title, subtitle, placeholder, isPass, confirmText, cancelText) {
|
||||||
|
dialogTitle = title || "Input Required"
|
||||||
|
dialogSubtitle = subtitle || "Please enter the required information"
|
||||||
|
inputPlaceholder = placeholder || "Enter text"
|
||||||
|
isPassword = isPass || false
|
||||||
|
confirmButtonText = confirmText || "Confirm"
|
||||||
|
cancelButtonText = cancelText || "Cancel"
|
||||||
|
inputValue = ""
|
||||||
|
dialogVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDialog() {
|
||||||
|
dialogVisible = false
|
||||||
|
inputValue = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.5)
|
||||||
|
opacity: dialogVisible ? 1.0 : 0.0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
inputDialog.cancelled()
|
||||||
|
hideDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.min(400, parent.width - Theme.spacingL * 2)
|
||||||
|
height: Math.min(250, parent.height - Theme.spacingL * 2)
|
||||||
|
anchors.centerIn: parent
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
opacity: dialogVisible ? 1.0 : 0.0
|
||||||
|
scale: dialogVisible ? 1.0 : 0.9
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingL
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
// Header
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width - 40
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: dialogTitle
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: dialogSubtitle
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
maximumLineCount: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: closeDialogArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "close"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize - 4
|
||||||
|
color: closeDialogArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: closeDialogArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
inputDialog.cancelled()
|
||||||
|
hideDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text input
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||||
|
border.color: textInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: textInput.activeFocus ? 2 : 1
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: textInput
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
echoMode: isPassword && !showPasswordCheckbox.checked ? TextInput.Password : TextInput.Normal
|
||||||
|
verticalAlignment: TextInput.AlignVCenter
|
||||||
|
cursorVisible: activeFocus
|
||||||
|
selectByMouse: true
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
text: inputPlaceholder
|
||||||
|
font: parent.font
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
visible: parent.text.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
inputValue = text
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
inputDialog.confirmed(inputValue)
|
||||||
|
hideDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (dialogVisible) {
|
||||||
|
forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.IBeamCursor
|
||||||
|
onClicked: {
|
||||||
|
textInput.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show password checkbox (only visible for password inputs)
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: isPassword
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: showPasswordCheckbox
|
||||||
|
property bool checked: false
|
||||||
|
|
||||||
|
width: 20
|
||||||
|
height: 20
|
||||||
|
radius: 4
|
||||||
|
color: checked ? Theme.primary : "transparent"
|
||||||
|
border.color: checked ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
|
||||||
|
border.width: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "check"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 12
|
||||||
|
color: Theme.background
|
||||||
|
visible: parent.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Show password"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: cancelArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: cancelText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: cancelButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cancelArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
inputDialog.cancelled()
|
||||||
|
hideDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Math.max(80, confirmText.contentWidth + Theme.spacingM * 2)
|
||||||
|
height: 36
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: confirmArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||||
|
enabled: inputValue.length > 0
|
||||||
|
opacity: enabled ? 1.0 : 0.5
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: confirmText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: confirmButtonText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.background
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: confirmArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: parent.enabled
|
||||||
|
onClicked: {
|
||||||
|
inputDialog.confirmed(inputValue)
|
||||||
|
hideDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -159,69 +159,88 @@ PanelWindow {
|
|||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
// Notification icon using reference pattern
|
// Notification icon based on EXAMPLE NotificationAppIcon pattern
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 32
|
width: 48
|
||||||
height: 32
|
height: 48
|
||||||
radius: Theme.cornerRadius
|
radius: width / 2 // Fully rounded like EXAMPLE
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
color: Theme.primaryContainer
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
// Fallback material icon when no app icon
|
// Material icon fallback (when no app icon)
|
||||||
Loader {
|
Loader {
|
||||||
active: !model.appIcon || model.appIcon === ""
|
active: !model.appIcon || model.appIcon === ""
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
sourceComponent: Text {
|
sourceComponent: Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: model.appName ? model.appName.charAt(0).toUpperCase() : "notifications"
|
text: "notifications"
|
||||||
font.family: model.appName ? "Roboto" : Theme.iconFont
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: model.appName ? Theme.fontSizeMedium : 16
|
font.pixelSize: 20
|
||||||
color: Theme.primary
|
color: Theme.primaryText
|
||||||
font.weight: Font.Medium
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// App icon when no notification image
|
// App icon (when no notification image)
|
||||||
Loader {
|
Loader {
|
||||||
active: model.appIcon && model.appIcon !== "" && (!model.image || model.image === "")
|
active: model.appIcon && model.appIcon !== "" && (!model.image || model.image === "")
|
||||||
anchors.fill: parent
|
anchors.centerIn: parent
|
||||||
anchors.margins: 3
|
|
||||||
sourceComponent: IconImage {
|
sourceComponent: IconImage {
|
||||||
anchors.fill: parent
|
width: 32
|
||||||
anchors.margins: 4
|
height: 32
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
source: {
|
source: {
|
||||||
if (!model.appIcon) return ""
|
if (!model.appIcon) return ""
|
||||||
// Skip file:// URLs as they're usually screenshots/images, not icons
|
// Handle file:// URLs directly
|
||||||
if (model.appIcon.startsWith("file://")) return ""
|
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
|
||||||
|
return model.appIcon
|
||||||
|
}
|
||||||
|
// Otherwise treat as icon name
|
||||||
return Quickshell.iconPath(model.appIcon, "image-missing")
|
return Quickshell.iconPath(model.appIcon, "image-missing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification image with rounded corners
|
// Notification image (like Discord user avatar) - PRIORITY
|
||||||
Loader {
|
Loader {
|
||||||
active: model.image && model.image !== ""
|
active: model.image && model.image !== ""
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
sourceComponent: Item {
|
sourceComponent: Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: historyNotifImage
|
id: historyNotifImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
readonly property int size: parent.width
|
||||||
|
|
||||||
source: model.image || ""
|
source: model.image || ""
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
cache: false
|
cache: false
|
||||||
antialiasing: true
|
antialiasing: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
smooth: true
|
||||||
|
|
||||||
|
// Proper sizing like EXAMPLE
|
||||||
|
width: size
|
||||||
|
height: size
|
||||||
|
sourceSize.width: size
|
||||||
|
sourceSize.height: size
|
||||||
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.effect: OpacityMask {
|
layer.effect: OpacityMask {
|
||||||
maskSource: Rectangle {
|
maskSource: Rectangle {
|
||||||
width: historyNotifImage.width
|
width: historyNotifImage.size
|
||||||
height: historyNotifImage.height
|
height: historyNotifImage.size
|
||||||
radius: Theme.cornerRadius
|
radius: historyNotifImage.size / 2 // Fully rounded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Error) {
|
||||||
|
console.warn("Failed to load notification image:", source)
|
||||||
|
} else if (status === Image.Ready) {
|
||||||
|
console.log("Notification image loaded:", source, "size:", sourceSize.width + "x" + sourceSize.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,12 +250,17 @@ PanelWindow {
|
|||||||
active: model.appIcon && model.appIcon !== ""
|
active: model.appIcon && model.appIcon !== ""
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.margins: 2
|
|
||||||
sourceComponent: IconImage {
|
sourceComponent: IconImage {
|
||||||
width: 12
|
width: 16
|
||||||
height: 12
|
height: 16
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
source: model.appIcon ? Quickshell.iconPath(model.appIcon, "image-missing") : ""
|
source: {
|
||||||
|
if (!model.appIcon) return ""
|
||||||
|
if (model.appIcon.startsWith("file://") || model.appIcon.startsWith("/")) {
|
||||||
|
return model.appIcon
|
||||||
|
}
|
||||||
|
return Quickshell.iconPath(model.appIcon, "image-missing")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,6 +310,11 @@ PanelWindow {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
|
// Try to handle notification click if it has actions
|
||||||
|
if (model && root.handleNotificationClick) {
|
||||||
|
root.handleNotificationClick(model)
|
||||||
|
}
|
||||||
|
// Remove from history after handling
|
||||||
notificationHistory.remove(index)
|
notificationHistory.remove(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,10 +329,9 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Empty state - properly centered
|
// Empty state - properly centered
|
||||||
Rectangle {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: notificationHistory.count === 0
|
visible: notificationHistory.count === 0
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Quickshell
|
|||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import "../Common"
|
import "../Common"
|
||||||
|
import "../Common/Utilities.js" as Utils
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: notificationPopup
|
id: notificationPopup
|
||||||
@@ -36,8 +37,7 @@ PanelWindow {
|
|||||||
|
|
||||||
color: Theme.surfaceContainer
|
color: Theme.surfaceContainer
|
||||||
radius: Theme.cornerRadiusLarge
|
radius: Theme.cornerRadiusLarge
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
border.width: 0 // Remove border completely
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
opacity: root.showNotificationPopup ? 1.0 : 0.0
|
opacity: root.showNotificationPopup ? 1.0 : 0.0
|
||||||
|
|
||||||
@@ -47,25 +47,59 @@ PanelWindow {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: Utils.hideNotificationPopup()
|
anchors.rightMargin: 36 // Don't overlap with close button
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
console.log("Popup clicked!")
|
||||||
|
if (root.activeNotification) {
|
||||||
|
root.handleNotificationClick(root.activeNotification)
|
||||||
|
// Remove notification from history entirely
|
||||||
|
for (let i = 0; i < notificationHistory.count; i++) {
|
||||||
|
if (notificationHistory.get(i).id === root.activeNotification.id) {
|
||||||
|
notificationHistory.remove(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Always hide popup after click
|
||||||
|
Utils.hideNotificationPopup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close button with cursor pointer
|
// Close button with hover styling
|
||||||
Text {
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
anchors.margins: 8
|
anchors.margins: 8
|
||||||
text: "×"
|
color: closeButtonArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||||
font.pixelSize: 16
|
|
||||||
color: Theme.surfaceText
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "close"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: closeButtonArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
id: closeButtonArea
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: -4
|
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: Utils.hideNotificationPopup()
|
onClicked: Utils.hideNotificationPopup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content layout
|
// Content layout
|
||||||
@@ -75,81 +109,89 @@ PanelWindow {
|
|||||||
anchors.rightMargin: 32
|
anchors.rightMargin: 32
|
||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
// Notification icon using reference pattern
|
// Notification icon based on EXAMPLE NotificationAppIcon pattern
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: 40
|
width: 48
|
||||||
height: 40
|
height: 48
|
||||||
radius: 8
|
radius: width / 2 // Fully rounded like EXAMPLE
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
color: Theme.primaryContainer
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
// Fallback material icon when no app icon
|
// Material icon fallback (when no app icon)
|
||||||
Loader {
|
Loader {
|
||||||
active: !root.activeNotification || root.activeNotification.appIcon === ""
|
active: !root.activeNotification || !root.activeNotification.appIcon || root.activeNotification.appIcon === ""
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
sourceComponent: Text {
|
sourceComponent: Text {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
text: "notifications"
|
text: "notifications"
|
||||||
font.family: Theme.iconFont
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: 20
|
font.pixelSize: 20
|
||||||
color: Theme.primary
|
color: Theme.primaryText
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// App icon when no notification image
|
// App icon (when no notification image)
|
||||||
Loader {
|
Loader {
|
||||||
active: root.activeNotification && root.activeNotification.appIcon !== "" && (root.activeNotification.image === "" || !root.activeNotification.image)
|
active: root.activeNotification && root.activeNotification.appIcon !== "" && (!root.activeNotification.image || root.activeNotification.image === "")
|
||||||
anchors.fill: parent
|
anchors.centerIn: parent
|
||||||
anchors.margins: 4
|
|
||||||
sourceComponent: IconImage {
|
sourceComponent: IconImage {
|
||||||
anchors.fill: parent
|
width: 32
|
||||||
|
height: 32
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
source: {
|
source: {
|
||||||
if (!root.activeNotification) return ""
|
if (!root.activeNotification || !root.activeNotification.appIcon) return ""
|
||||||
let iconPath = root.activeNotification.appIcon
|
let appIcon = root.activeNotification.appIcon
|
||||||
// Skip file:// URLs as they're usually screenshots/images, not icons
|
// Handle file:// URLs directly
|
||||||
if (iconPath && iconPath.startsWith("file://")) return ""
|
if (appIcon.startsWith("file://") || appIcon.startsWith("/")) {
|
||||||
return iconPath ? Quickshell.iconPath(iconPath, "image-missing") : ""
|
return appIcon
|
||||||
|
}
|
||||||
|
// Otherwise treat as icon name
|
||||||
|
return Quickshell.iconPath(appIcon, "image-missing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification image with rounded corners
|
// Notification image (like Discord user avatar) - PRIORITY
|
||||||
Loader {
|
Loader {
|
||||||
active: root.activeNotification && root.activeNotification.image !== ""
|
active: root.activeNotification && root.activeNotification.image !== ""
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
sourceComponent: Item {
|
sourceComponent: Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
|
||||||
|
|
||||||
Rectangle {
|
Image {
|
||||||
|
id: notifImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
radius: 8
|
readonly property int size: parent.width
|
||||||
color: "transparent"
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Image {
|
source: root.activeNotification ? root.activeNotification.image : ""
|
||||||
id: notifImage
|
fillMode: Image.PreserveAspectCrop
|
||||||
anchors.fill: parent
|
cache: false
|
||||||
source: root.activeNotification ? root.activeNotification.image : ""
|
antialiasing: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
asynchronous: true
|
||||||
cache: false
|
smooth: true
|
||||||
antialiasing: true
|
|
||||||
asynchronous: true
|
// Proper sizing like EXAMPLE
|
||||||
smooth: true
|
width: size
|
||||||
|
height: size
|
||||||
// Ensure minimum size and proper scaling
|
sourceSize.width: size
|
||||||
sourceSize.width: 64
|
sourceSize.height: size
|
||||||
sourceSize.height: 64
|
|
||||||
|
layer.enabled: true
|
||||||
onStatusChanged: {
|
layer.effect: OpacityMask {
|
||||||
if (status === Image.Error) {
|
maskSource: Rectangle {
|
||||||
console.warn("Failed to load notification image:", source)
|
width: notifImage.size
|
||||||
} else if (status === Image.Ready) {
|
height: notifImage.size
|
||||||
console.log("Notification image loaded:", source, "size:", sourceSize)
|
radius: notifImage.size / 2 // Fully rounded
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Image.Error) {
|
||||||
|
console.warn("Failed to load notification image:", source)
|
||||||
|
} else if (status === Image.Ready) {
|
||||||
|
console.log("Notification image loaded:", source, "size:", sourceSize.width + "x" + sourceSize.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,12 +201,18 @@ PanelWindow {
|
|||||||
active: root.activeNotification && root.activeNotification.appIcon !== ""
|
active: root.activeNotification && root.activeNotification.appIcon !== ""
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.margins: 2
|
|
||||||
sourceComponent: IconImage {
|
sourceComponent: IconImage {
|
||||||
width: 16
|
width: 16
|
||||||
height: 16
|
height: 16
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
source: root.activeNotification ? Quickshell.iconPath(root.activeNotification.appIcon, "image-missing") : ""
|
source: {
|
||||||
|
if (!root.activeNotification || !root.activeNotification.appIcon) return ""
|
||||||
|
let appIcon = root.activeNotification.appIcon
|
||||||
|
if (appIcon.startsWith("file://") || appIcon.startsWith("/")) {
|
||||||
|
return appIcon
|
||||||
|
}
|
||||||
|
return Quickshell.iconPath(appIcon, "image-missing")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +221,7 @@ PanelWindow {
|
|||||||
|
|
||||||
// Text content
|
// Text content
|
||||||
Column {
|
Column {
|
||||||
width: parent.width - 52
|
width: parent.width - 68
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: 4
|
spacing: 4
|
||||||
|
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ PanelWindow {
|
|||||||
|
|
||||||
// Weather icon when no media but weather available
|
// Weather icon when no media but weather available
|
||||||
Text {
|
Text {
|
||||||
text: root.weatherIcons[root.weather.wCode] || "clear_day"
|
text: WeatherService.getWeatherIcon(root.weather.wCode)
|
||||||
font.family: Theme.iconFont
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: Theme.iconSize - 2
|
font.pixelSize: Theme.iconSize - 2
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
@@ -702,6 +702,11 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Battery Widget
|
||||||
|
BatteryWidget {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
// Control Center Indicators
|
// Control Center Indicators
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
|
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
|
||||||
|
|||||||
@@ -15,4 +15,7 @@ ControlCenterPopup 1.0 ControlCenterPopup.qml
|
|||||||
WifiPasswordDialog 1.0 WifiPasswordDialog.qml
|
WifiPasswordDialog 1.0 WifiPasswordDialog.qml
|
||||||
AppLauncher 1.0 AppLauncher.qml
|
AppLauncher 1.0 AppLauncher.qml
|
||||||
ClipboardHistory 1.0 ClipboardHistory.qml
|
ClipboardHistory 1.0 ClipboardHistory.qml
|
||||||
CustomSlider 1.0 CustomSlider.qml
|
CustomSlider 1.0 CustomSlider.qml
|
||||||
|
InputDialog 1.0 InputDialog.qml
|
||||||
|
BatteryWidget 1.0 BatteryWidget.qml
|
||||||
|
BatteryControlPopup 1.0 BatteryControlPopup.qml
|
||||||
128
shell.qml
128
shell.qml
@@ -33,6 +33,7 @@ ShellRoot {
|
|||||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
property bool hasActiveMedia: activePlayer && (activePlayer.trackTitle || activePlayer.trackArtist)
|
property bool hasActiveMedia: activePlayer && (activePlayer.trackTitle || activePlayer.trackArtist)
|
||||||
property bool controlCenterVisible: false
|
property bool controlCenterVisible: false
|
||||||
|
property bool batteryPopupVisible: false
|
||||||
|
|
||||||
// Network properties from NetworkService
|
// Network properties from NetworkService
|
||||||
property string networkStatus: NetworkService.networkStatus
|
property string networkStatus: NetworkService.networkStatus
|
||||||
@@ -67,6 +68,65 @@ ShellRoot {
|
|||||||
property string wifiConnectionStatus: ""
|
property string wifiConnectionStatus: ""
|
||||||
property bool wifiAutoRefreshEnabled: false
|
property bool wifiAutoRefreshEnabled: false
|
||||||
|
|
||||||
|
// Notification action handling - ALWAYS invoke action if exists
|
||||||
|
function handleNotificationClick(notifObj) {
|
||||||
|
console.log("Handling notification click for:", notifObj.appName)
|
||||||
|
|
||||||
|
// ALWAYS try to invoke the action first (this is what real notifications do)
|
||||||
|
if (notifObj.notification && notifObj.actions && notifObj.actions.length > 0) {
|
||||||
|
// Look for "default" action first, then fallback to first action
|
||||||
|
let defaultAction = notifObj.actions.find(action => action.identifier === "default") || notifObj.actions[0]
|
||||||
|
if (defaultAction) {
|
||||||
|
console.log("Invoking notification action:", defaultAction.text, "identifier:", defaultAction.identifier)
|
||||||
|
attemptInvokeAction(notifObj.id, defaultAction.identifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no action exists, check for URLs in notification text
|
||||||
|
let notificationText = (notifObj.summary || "") + " " + (notifObj.body || "")
|
||||||
|
let urlRegex = /(https?:\/\/[^\s]+)/g
|
||||||
|
let urls = notificationText.match(urlRegex)
|
||||||
|
|
||||||
|
if (urls && urls.length > 0) {
|
||||||
|
console.log("Opening URL from notification:", urls[0])
|
||||||
|
Qt.openUrlExternally(urls[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("No action or URL found, notification will just dismiss")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to invoke notification actions (based on EXAMPLE)
|
||||||
|
function attemptInvokeAction(notifId, actionIdentifier) {
|
||||||
|
console.log("Attempting to invoke action:", actionIdentifier, "for notification:", notifId)
|
||||||
|
|
||||||
|
// Find the notification in the server's tracked notifications
|
||||||
|
let trackedNotifications = notificationServer.trackedNotifications.values
|
||||||
|
let serverNotification = trackedNotifications.find(notif => notif.id === notifId)
|
||||||
|
|
||||||
|
if (serverNotification) {
|
||||||
|
let action = serverNotification.actions.find(action => action.identifier === actionIdentifier)
|
||||||
|
if (action) {
|
||||||
|
console.log("Invoking action:", action.text)
|
||||||
|
action.invoke()
|
||||||
|
} else {
|
||||||
|
console.warn("Action not found:", actionIdentifier)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Notification not found in server:", notifId, "Available IDs:", trackedNotifications.map(n => n.id))
|
||||||
|
// Try to find by any available action
|
||||||
|
if (trackedNotifications.length > 0) {
|
||||||
|
let latestNotif = trackedNotifications[trackedNotifications.length - 1]
|
||||||
|
let action = latestNotif.actions.find(action => action.identifier === actionIdentifier)
|
||||||
|
if (action) {
|
||||||
|
console.log("Using latest notification for action")
|
||||||
|
action.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Screen size breakpoints for responsive design
|
// Screen size breakpoints for responsive design
|
||||||
property real screenWidth: Screen.width
|
property real screenWidth: Screen.width
|
||||||
property bool isSmallScreen: screenWidth < 1200
|
property bool isSmallScreen: screenWidth < 1200
|
||||||
@@ -79,57 +139,6 @@ ShellRoot {
|
|||||||
// Weather configuration
|
// Weather configuration
|
||||||
property bool useFahrenheit: true // Default to Fahrenheit
|
property bool useFahrenheit: true // Default to Fahrenheit
|
||||||
|
|
||||||
// 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"
|
|
||||||
})
|
|
||||||
|
|
||||||
// WiFi Auto-refresh Timer
|
// WiFi Auto-refresh Timer
|
||||||
Timer {
|
Timer {
|
||||||
@@ -172,7 +181,10 @@ ShellRoot {
|
|||||||
|
|
||||||
console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary")
|
console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary")
|
||||||
|
|
||||||
// Create notification object with correct properties
|
// CRITICAL: Mark notification as tracked so it stays in server list for actions
|
||||||
|
notification.tracked = true
|
||||||
|
|
||||||
|
// Create notification object with correct properties (based on EXAMPLE)
|
||||||
var notifObj = {
|
var notifObj = {
|
||||||
"id": notification.id,
|
"id": notification.id,
|
||||||
"appName": notification.appName || "App",
|
"appName": notification.appName || "App",
|
||||||
@@ -181,7 +193,13 @@ ShellRoot {
|
|||||||
"timestamp": new Date(),
|
"timestamp": new Date(),
|
||||||
"appIcon": notification.appIcon || notification.icon || "",
|
"appIcon": notification.appIcon || notification.icon || "",
|
||||||
"icon": notification.icon || "",
|
"icon": notification.icon || "",
|
||||||
"image": notification.image || ""
|
"image": notification.image || "",
|
||||||
|
"actions": notification.actions ? notification.actions.map(action => ({
|
||||||
|
"identifier": action.identifier,
|
||||||
|
"text": action.text
|
||||||
|
})) : [],
|
||||||
|
"urgency": notification.urgency ? notification.urgency.toString() : "normal",
|
||||||
|
"notification": notification // Keep reference for action handling
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add to history (prepend to show newest first)
|
// Add to history (prepend to show newest first)
|
||||||
@@ -237,6 +255,10 @@ ShellRoot {
|
|||||||
NotificationHistoryPopup {}
|
NotificationHistoryPopup {}
|
||||||
ControlCenterPopup {}
|
ControlCenterPopup {}
|
||||||
WifiPasswordDialog {}
|
WifiPasswordDialog {}
|
||||||
|
InputDialog {
|
||||||
|
id: globalInputDialog
|
||||||
|
}
|
||||||
|
BatteryControlPopup {}
|
||||||
|
|
||||||
// Application and clipboard components
|
// Application and clipboard components
|
||||||
AppLauncher {
|
AppLauncher {
|
||||||
|
|||||||
Reference in New Issue
Block a user