mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-09 23:15:38 -05:00
replace qmlformat with a better tool
still not perfect, but well - what can ya do
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -8,147 +9,149 @@ import Quickshell.Widgets
|
||||
import "../Common/fuzzysort.js" as Fuzzy
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var applications: DesktopEntries.applications.values
|
||||
|
||||
property var preppedApps: applications.map(app => ({
|
||||
name: Fuzzy.prepare(app.name || ""),
|
||||
comment: Fuzzy.prepare(app.comment || ""),
|
||||
entry: app
|
||||
}))
|
||||
|
||||
function searchApplications(query) {
|
||||
if (!query || query.length === 0) {
|
||||
return applications
|
||||
}
|
||||
|
||||
if (preppedApps.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
var results = Fuzzy.go(query, preppedApps, {
|
||||
all: false,
|
||||
keys: ["name", "comment"],
|
||||
scoreFn: r => {
|
||||
var nameScore = r[0] ? r[0].score : 0
|
||||
var commentScore = r[1] ? r[1].score : 0
|
||||
var appName = r.obj.entry.name || ""
|
||||
var finalScore = 0
|
||||
|
||||
if (nameScore > 0) {
|
||||
var queryLower = query.toLowerCase()
|
||||
var nameLower = appName.toLowerCase()
|
||||
|
||||
if (nameLower === queryLower) {
|
||||
finalScore = nameScore * 100
|
||||
}
|
||||
else if (nameLower.startsWith(queryLower)) {
|
||||
finalScore = nameScore * 50
|
||||
}
|
||||
else if (nameLower.includes(" " + queryLower) || nameLower.includes(queryLower + " ") || nameLower.endsWith(" " + queryLower)) {
|
||||
finalScore = nameScore * 25
|
||||
}
|
||||
else if (nameLower.includes(queryLower)) {
|
||||
finalScore = nameScore * 10
|
||||
}
|
||||
else {
|
||||
finalScore = nameScore * 2 + commentScore * 0.1
|
||||
}
|
||||
} else {
|
||||
finalScore = commentScore * 0.1
|
||||
}
|
||||
|
||||
return finalScore
|
||||
},
|
||||
limit: 50
|
||||
})
|
||||
|
||||
return results.map(r => r.obj.entry)
|
||||
}
|
||||
|
||||
id: root
|
||||
|
||||
|
||||
function getCategoriesForApp(app) {
|
||||
if (!app || !app.categories) return []
|
||||
|
||||
var categoryMap = {
|
||||
"AudioVideo": "Media",
|
||||
"Audio": "Media",
|
||||
"Video": "Media",
|
||||
"Development": "Development",
|
||||
"TextEditor": "Development",
|
||||
"IDE": "Development",
|
||||
"Education": "Education",
|
||||
"Game": "Games",
|
||||
"Graphics": "Graphics",
|
||||
"Photography": "Graphics",
|
||||
"Network": "Internet",
|
||||
"WebBrowser": "Internet",
|
||||
"Email": "Internet",
|
||||
"Office": "Office",
|
||||
"WordProcessor": "Office",
|
||||
"Spreadsheet": "Office",
|
||||
"Presentation": "Office",
|
||||
"Science": "Science",
|
||||
"Settings": "Settings",
|
||||
"System": "System",
|
||||
"Utility": "Utilities",
|
||||
"Accessories": "Utilities",
|
||||
"FileManager": "Utilities",
|
||||
"TerminalEmulator": "Utilities"
|
||||
}
|
||||
|
||||
var mappedCategories = new Set()
|
||||
|
||||
for (var i = 0; i < app.categories.length; i++) {
|
||||
var cat = app.categories[i]
|
||||
if (categoryMap[cat]) {
|
||||
mappedCategories.add(categoryMap[cat])
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(mappedCategories)
|
||||
property var applications: DesktopEntries.applications.values
|
||||
|
||||
property var preppedApps: applications.map(app => ({
|
||||
"name": Fuzzy.prepare(
|
||||
app.name
|
||||
|| ""),
|
||||
"comment": Fuzzy.prepare(
|
||||
app.comment
|
||||
|| ""),
|
||||
"entry": app
|
||||
}))
|
||||
|
||||
function searchApplications(query) {
|
||||
if (!query || query.length === 0) {
|
||||
return applications
|
||||
}
|
||||
|
||||
// Category icon mappings
|
||||
property var categoryIcons: ({
|
||||
"All": "apps",
|
||||
"Media": "music_video",
|
||||
"Development": "code",
|
||||
"Games": "sports_esports",
|
||||
"Graphics": "photo_library",
|
||||
"Internet": "web",
|
||||
"Office": "content_paste",
|
||||
"Settings": "settings",
|
||||
"System": "host",
|
||||
"Utilities": "build"
|
||||
})
|
||||
|
||||
function getCategoryIcon(category) {
|
||||
return categoryIcons[category] || "folder"
|
||||
|
||||
if (preppedApps.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
function getAllCategories() {
|
||||
var categories = new Set(["All"])
|
||||
|
||||
for (var i = 0; i < applications.length; i++) {
|
||||
var appCategories = getCategoriesForApp(applications[i])
|
||||
appCategories.forEach(cat => categories.add(cat))
|
||||
}
|
||||
|
||||
return Array.from(categories).sort()
|
||||
|
||||
var results = Fuzzy.go(query, preppedApps, {
|
||||
"all": false,
|
||||
"keys": ["name", "comment"],
|
||||
"scoreFn": r => {
|
||||
var nameScore = r[0] ? r[0].score : 0
|
||||
var commentScore = r[1] ? r[1].score : 0
|
||||
var appName = r.obj.entry.name || ""
|
||||
var finalScore = 0
|
||||
|
||||
if (nameScore > 0) {
|
||||
var queryLower = query.toLowerCase()
|
||||
var nameLower = appName.toLowerCase()
|
||||
|
||||
if (nameLower === queryLower) {
|
||||
finalScore = nameScore * 100
|
||||
} else if (nameLower.startsWith(queryLower)) {
|
||||
finalScore = nameScore * 50
|
||||
} else if (nameLower.includes(" " + queryLower)
|
||||
|| nameLower.includes(
|
||||
queryLower + " ")
|
||||
|| nameLower.endsWith(
|
||||
" " + queryLower)) {
|
||||
finalScore = nameScore * 25
|
||||
} else if (nameLower.includes(queryLower)) {
|
||||
finalScore = nameScore * 10
|
||||
} else {
|
||||
finalScore = nameScore * 2 + commentScore * 0.1
|
||||
}
|
||||
} else {
|
||||
finalScore = commentScore * 0.1
|
||||
}
|
||||
|
||||
return finalScore
|
||||
},
|
||||
"limit": 50
|
||||
})
|
||||
|
||||
return results.map(r => r.obj.entry)
|
||||
}
|
||||
|
||||
function getCategoriesForApp(app) {
|
||||
if (!app || !app.categories)
|
||||
return []
|
||||
|
||||
var categoryMap = {
|
||||
"AudioVideo": "Media",
|
||||
"Audio": "Media",
|
||||
"Video": "Media",
|
||||
"Development": "Development",
|
||||
"TextEditor": "Development",
|
||||
"IDE": "Development",
|
||||
"Education": "Education",
|
||||
"Game": "Games",
|
||||
"Graphics": "Graphics",
|
||||
"Photography": "Graphics",
|
||||
"Network": "Internet",
|
||||
"WebBrowser": "Internet",
|
||||
"Email": "Internet",
|
||||
"Office": "Office",
|
||||
"WordProcessor": "Office",
|
||||
"Spreadsheet": "Office",
|
||||
"Presentation": "Office",
|
||||
"Science": "Science",
|
||||
"Settings": "Settings",
|
||||
"System": "System",
|
||||
"Utility": "Utilities",
|
||||
"Accessories": "Utilities",
|
||||
"FileManager": "Utilities",
|
||||
"TerminalEmulator": "Utilities"
|
||||
}
|
||||
|
||||
function getAppsInCategory(category) {
|
||||
if (category === "All") {
|
||||
return applications
|
||||
}
|
||||
|
||||
return applications.filter(app => {
|
||||
var appCategories = getCategoriesForApp(app)
|
||||
return appCategories.includes(category)
|
||||
})
|
||||
|
||||
var mappedCategories = new Set()
|
||||
|
||||
for (var i = 0; i < app.categories.length; i++) {
|
||||
var cat = app.categories[i]
|
||||
if (categoryMap[cat]) {
|
||||
mappedCategories.add(categoryMap[cat])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Array.from(mappedCategories)
|
||||
}
|
||||
|
||||
// Category icon mappings
|
||||
property var categoryIcons: ({
|
||||
"All": "apps",
|
||||
"Media": "music_video",
|
||||
"Development": "code",
|
||||
"Games": "sports_esports",
|
||||
"Graphics": "photo_library",
|
||||
"Internet": "web",
|
||||
"Office": "content_paste",
|
||||
"Settings": "settings",
|
||||
"System": "host",
|
||||
"Utilities": "build"
|
||||
})
|
||||
|
||||
function getCategoryIcon(category) {
|
||||
return categoryIcons[category] || "folder"
|
||||
}
|
||||
|
||||
function getAllCategories() {
|
||||
var categories = new Set(["All"])
|
||||
|
||||
for (var i = 0; i < applications.length; i++) {
|
||||
var appCategories = getCategoriesForApp(applications[i])
|
||||
appCategories.forEach(cat => categories.add(cat))
|
||||
}
|
||||
|
||||
return Array.from(categories).sort()
|
||||
}
|
||||
|
||||
function getAppsInCategory(category) {
|
||||
if (category === "All") {
|
||||
return applications
|
||||
}
|
||||
|
||||
return applications.filter(app => {
|
||||
var appCategories = getCategoriesForApp(app)
|
||||
return appCategories.includes(category)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -7,170 +8,174 @@ import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
|
||||
signal volumeChanged()
|
||||
signal micMuteChanged()
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
|
||||
function displayName(node) {
|
||||
if (!node) return ""
|
||||
|
||||
if (node.properties && node.properties["device.description"]) {
|
||||
return node.properties["device.description"]
|
||||
}
|
||||
signal volumeChanged
|
||||
signal micMuteChanged
|
||||
|
||||
if (node.description && node.description !== node.name) {
|
||||
return node.description
|
||||
}
|
||||
function displayName(node) {
|
||||
if (!node)
|
||||
return ""
|
||||
|
||||
if (node.nickname && node.nickname !== node.name) {
|
||||
return node.nickname
|
||||
}
|
||||
|
||||
if (node.name.includes("analog-stereo")) return "Built-in Speakers"
|
||||
else if (node.name.includes("bluez")) return "Bluetooth Audio"
|
||||
else if (node.name.includes("usb")) return "USB Audio"
|
||||
else if (node.name.includes("hdmi")) return "HDMI Audio"
|
||||
|
||||
return node.name
|
||||
if (node.properties && node.properties["device.description"]) {
|
||||
return node.properties["device.description"]
|
||||
}
|
||||
|
||||
function subtitle(name) {
|
||||
if (!name) return ""
|
||||
|
||||
if (name.includes('usb-')) {
|
||||
if (name.includes('SteelSeries')) {
|
||||
return "USB Gaming Headset"
|
||||
} else if (name.includes('Generic')) {
|
||||
return "USB Audio Device"
|
||||
}
|
||||
return "USB Audio"
|
||||
} else if (name.includes('pci-')) {
|
||||
if (name.includes('01_00.1') || name.includes('01:00.1')) {
|
||||
return "NVIDIA GPU Audio"
|
||||
}
|
||||
return "PCI Audio"
|
||||
} else if (name.includes('bluez')) {
|
||||
return "Bluetooth Audio"
|
||||
} else if (name.includes('analog')) {
|
||||
return "Built-in Audio"
|
||||
} else if (name.includes('hdmi')) {
|
||||
return "HDMI Audio"
|
||||
}
|
||||
|
||||
return ""
|
||||
if (node.description && node.description !== node.name) {
|
||||
return node.description
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
|
||||
if (node.nickname && node.nickname !== node.name) {
|
||||
return node.nickname
|
||||
}
|
||||
|
||||
if (node.name.includes("analog-stereo"))
|
||||
return "Built-in Speakers"
|
||||
else if (node.name.includes("bluez"))
|
||||
return "Bluetooth Audio"
|
||||
else if (node.name.includes("usb"))
|
||||
return "USB Audio"
|
||||
else if (node.name.includes("hdmi"))
|
||||
return "HDMI Audio"
|
||||
|
||||
// Volume control functions
|
||||
function setVolume(percentage) {
|
||||
if (root.sink && root.sink.audio) {
|
||||
const clampedVolume = Math.max(0, Math.min(100, percentage));
|
||||
root.sink.audio.volume = clampedVolume / 100;
|
||||
root.volumeChanged();
|
||||
return "Volume set to " + clampedVolume + "%";
|
||||
}
|
||||
return "No audio sink available";
|
||||
return node.name
|
||||
}
|
||||
|
||||
function subtitle(name) {
|
||||
if (!name)
|
||||
return ""
|
||||
|
||||
if (name.includes('usb-')) {
|
||||
if (name.includes('SteelSeries')) {
|
||||
return "USB Gaming Headset"
|
||||
} else if (name.includes('Generic')) {
|
||||
return "USB Audio Device"
|
||||
}
|
||||
return "USB Audio"
|
||||
} else if (name.includes('pci-')) {
|
||||
if (name.includes('01_00.1') || name.includes('01:00.1')) {
|
||||
return "NVIDIA GPU Audio"
|
||||
}
|
||||
return "PCI Audio"
|
||||
} else if (name.includes('bluez')) {
|
||||
return "Bluetooth Audio"
|
||||
} else if (name.includes('analog')) {
|
||||
return "Built-in Audio"
|
||||
} else if (name.includes('hdmi')) {
|
||||
return "HDMI Audio"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (root.sink && root.sink.audio) {
|
||||
root.sink.audio.muted = !root.sink.audio.muted;
|
||||
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted";
|
||||
}
|
||||
return "No audio sink available";
|
||||
// Volume control functions
|
||||
function setVolume(percentage) {
|
||||
if (root.sink && root.sink.audio) {
|
||||
const clampedVolume = Math.max(0, Math.min(100, percentage))
|
||||
root.sink.audio.volume = clampedVolume / 100
|
||||
root.volumeChanged()
|
||||
return "Volume set to " + clampedVolume + "%"
|
||||
}
|
||||
return "No audio sink available"
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (root.sink && root.sink.audio) {
|
||||
root.sink.audio.muted = !root.sink.audio.muted
|
||||
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"
|
||||
}
|
||||
return "No audio sink available"
|
||||
}
|
||||
|
||||
function setMicVolume(percentage) {
|
||||
if (root.source && root.source.audio) {
|
||||
const clampedVolume = Math.max(0, Math.min(100, percentage))
|
||||
root.source.audio.volume = clampedVolume / 100
|
||||
return "Microphone volume set to " + clampedVolume + "%"
|
||||
}
|
||||
return "No audio source available"
|
||||
}
|
||||
|
||||
function toggleMicMute() {
|
||||
if (root.source && root.source.audio) {
|
||||
root.source.audio.muted = !root.source.audio.muted
|
||||
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted"
|
||||
}
|
||||
return "No audio source available"
|
||||
}
|
||||
|
||||
// IPC Handler for external control
|
||||
IpcHandler {
|
||||
target: "audio"
|
||||
|
||||
function setvolume(percentage: string): string {
|
||||
return root.setVolume(parseInt(percentage))
|
||||
}
|
||||
|
||||
function setMicVolume(percentage) {
|
||||
if (root.source && root.source.audio) {
|
||||
const clampedVolume = Math.max(0, Math.min(100, percentage));
|
||||
root.source.audio.volume = clampedVolume / 100;
|
||||
return "Microphone volume set to " + clampedVolume + "%";
|
||||
}
|
||||
return "No audio source available";
|
||||
function increment(step: string): string {
|
||||
if (root.sink && root.sink.audio) {
|
||||
const currentVolume = Math.round(root.sink.audio.volume * 100)
|
||||
const newVolume = Math.max(0, Math.min(100, currentVolume + parseInt(
|
||||
step || "5")))
|
||||
root.sink.audio.volume = newVolume / 100
|
||||
root.volumeChanged()
|
||||
return "Volume increased to " + newVolume + "%"
|
||||
}
|
||||
return "No audio sink available"
|
||||
}
|
||||
|
||||
function toggleMicMute() {
|
||||
if (root.source && root.source.audio) {
|
||||
root.source.audio.muted = !root.source.audio.muted;
|
||||
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted";
|
||||
}
|
||||
return "No audio source available";
|
||||
function decrement(step: string): string {
|
||||
if (root.sink && root.sink.audio) {
|
||||
const currentVolume = Math.round(root.sink.audio.volume * 100)
|
||||
const newVolume = Math.max(0, Math.min(100, currentVolume - parseInt(
|
||||
step || "5")))
|
||||
root.sink.audio.volume = newVolume / 100
|
||||
root.volumeChanged()
|
||||
return "Volume decreased to " + newVolume + "%"
|
||||
}
|
||||
return "No audio sink available"
|
||||
}
|
||||
|
||||
// IPC Handler for external control
|
||||
IpcHandler {
|
||||
target: "audio"
|
||||
|
||||
function setvolume(percentage: string): string {
|
||||
return root.setVolume(parseInt(percentage));
|
||||
}
|
||||
|
||||
function increment(step: string): string {
|
||||
if (root.sink && root.sink.audio) {
|
||||
const currentVolume = Math.round(root.sink.audio.volume * 100);
|
||||
const newVolume = Math.max(0, Math.min(100, currentVolume + parseInt(step || "5")));
|
||||
root.sink.audio.volume = newVolume / 100;
|
||||
root.volumeChanged();
|
||||
return "Volume increased to " + newVolume + "%";
|
||||
}
|
||||
return "No audio sink available";
|
||||
}
|
||||
|
||||
function decrement(step: string): string {
|
||||
if (root.sink && root.sink.audio) {
|
||||
const currentVolume = Math.round(root.sink.audio.volume * 100);
|
||||
const newVolume = Math.max(0, Math.min(100, currentVolume - parseInt(step || "5")));
|
||||
root.sink.audio.volume = newVolume / 100;
|
||||
root.volumeChanged();
|
||||
return "Volume decreased to " + newVolume + "%";
|
||||
}
|
||||
return "No audio sink available";
|
||||
}
|
||||
|
||||
function mute(): string {
|
||||
const result = root.toggleMute();
|
||||
root.volumeChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
function setmic(percentage: string): string {
|
||||
return root.setMicVolume(parseInt(percentage));
|
||||
}
|
||||
|
||||
function micmute(): string {
|
||||
const result = root.toggleMicMute();
|
||||
root.micMuteChanged();
|
||||
return result;
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
let result = "Audio Status:\n";
|
||||
if (root.sink && root.sink.audio) {
|
||||
const volume = Math.round(root.sink.audio.volume * 100);
|
||||
result += "Output: " + volume + "%" + (root.sink.audio.muted ? " (muted)" : "") + "\n";
|
||||
} else {
|
||||
result += "Output: No sink available\n";
|
||||
}
|
||||
|
||||
if (root.source && root.source.audio) {
|
||||
const micVolume = Math.round(root.source.audio.volume * 100);
|
||||
result += "Input: " + micVolume + "%" + (root.source.audio.muted ? " (muted)" : "");
|
||||
} else {
|
||||
result += "Input: No source available";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function mute(): string {
|
||||
const result = root.toggleMute()
|
||||
root.volumeChanged()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
function setmic(percentage: string): string {
|
||||
return root.setMicVolume(parseInt(percentage))
|
||||
}
|
||||
|
||||
function micmute(): string {
|
||||
const result = root.toggleMicMute()
|
||||
root.micMuteChanged()
|
||||
return result
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
let result = "Audio Status:\n"
|
||||
if (root.sink && root.sink.audio) {
|
||||
const volume = Math.round(root.sink.audio.volume * 100)
|
||||
result += "Output: " + volume + "%" + (root.sink.audio.muted ? " (muted)" : "") + "\n"
|
||||
} else {
|
||||
result += "Output: No sink available\n"
|
||||
}
|
||||
|
||||
if (root.source && root.source.audio) {
|
||||
const micVolume = Math.round(root.source.audio.volume * 100)
|
||||
result += "Input: " + micVolume + "%" + (root.source.audio.muted ? " (muted)" : "")
|
||||
} else {
|
||||
result += "Input: No source available"
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,72 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property UPowerDevice device: UPower.displayDevice
|
||||
readonly property bool batteryAvailable: device && device.ready && device.isLaptopBattery
|
||||
readonly property int batteryLevel: batteryAvailable ? device.percentage * 100 : 0
|
||||
readonly property bool isCharging: batteryAvailable && device.state === UPowerDeviceState.Charging
|
||||
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
|
||||
readonly property string batteryHealth: batteryAvailable && device.healthSupported ? Math.round(device.healthPercentage * 100) + "%" : "N/A"
|
||||
readonly property real batteryCapacity: batteryAvailable && device.energyCapacity > 0 ? device.energyCapacity : 0
|
||||
readonly property string batteryStatus: {
|
||||
if (!batteryAvailable)
|
||||
return "No Battery";
|
||||
readonly property UPowerDevice device: UPower.displayDevice
|
||||
readonly property bool batteryAvailable: device && device.ready
|
||||
&& device.isLaptopBattery
|
||||
readonly property int batteryLevel: batteryAvailable ? device.percentage * 100 : 0
|
||||
readonly property bool isCharging: batteryAvailable
|
||||
&& device.state === UPowerDeviceState.Charging
|
||||
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
|
||||
readonly property string batteryHealth: batteryAvailable
|
||||
&& device.healthSupported ? Math.round(
|
||||
device.healthPercentage
|
||||
* 100) + "%" : "N/A"
|
||||
readonly property real batteryCapacity: batteryAvailable
|
||||
&& device.energyCapacity > 0 ? device.energyCapacity : 0
|
||||
readonly property string batteryStatus: {
|
||||
if (!batteryAvailable)
|
||||
return "No Battery"
|
||||
|
||||
return UPowerDeviceState.toString(device.state);
|
||||
return UPowerDeviceState.toString(device.state)
|
||||
}
|
||||
readonly property int timeRemaining: {
|
||||
if (!batteryAvailable)
|
||||
return 0
|
||||
|
||||
return isCharging ? (device.timeToFull || 0) : (device.timeToEmpty || 0)
|
||||
}
|
||||
readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery
|
||||
&& UPower.onBattery
|
||||
&& (typeof PowerProfiles !== "undefined"
|
||||
&& PowerProfiles.profile
|
||||
!== PowerProfile.PowerSaver)
|
||||
readonly property var bluetoothDevices: {
|
||||
var btDevices = []
|
||||
for (var i = 0; i < UPower.devices.count; i++) {
|
||||
var dev = UPower.devices.get(i)
|
||||
if (dev
|
||||
&& dev.ready && (dev.type === UPowerDeviceType.BluetoothGeneric || dev.type
|
||||
=== UPowerDeviceType.Headphones || dev.type
|
||||
=== UPowerDeviceType.Headset || dev.type
|
||||
=== UPowerDeviceType.Keyboard || dev.type
|
||||
=== UPowerDeviceType.Mouse || dev.type === UPowerDeviceType.Speakers))
|
||||
btDevices.push({
|
||||
"name": dev.model || UPowerDeviceType.toString(dev.type),
|
||||
"percentage": Math.round(dev.percentage),
|
||||
"type": dev.type
|
||||
})
|
||||
}
|
||||
readonly property int timeRemaining: {
|
||||
if (!batteryAvailable)
|
||||
return 0;
|
||||
return btDevices
|
||||
}
|
||||
|
||||
return isCharging ? (device.timeToFull || 0) : (device.timeToEmpty || 0);
|
||||
}
|
||||
readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== PowerProfile.PowerSaver)
|
||||
readonly property var bluetoothDevices: {
|
||||
var btDevices = [];
|
||||
for (var i = 0; i < UPower.devices.count; i++) {
|
||||
var dev = UPower.devices.get(i);
|
||||
if (dev && dev.ready && (dev.type === UPowerDeviceType.BluetoothGeneric || dev.type === UPowerDeviceType.Headphones || dev.type === UPowerDeviceType.Headset || dev.type === UPowerDeviceType.Keyboard || dev.type === UPowerDeviceType.Mouse || dev.type === UPowerDeviceType.Speakers))
|
||||
btDevices.push({
|
||||
"name": dev.model || UPowerDeviceType.toString(dev.type),
|
||||
"percentage": Math.round(dev.percentage),
|
||||
"type": dev.type
|
||||
});
|
||||
function formatTimeRemaining() {
|
||||
if (!batteryAvailable || timeRemaining <= 0)
|
||||
return "Unknown"
|
||||
|
||||
}
|
||||
return btDevices;
|
||||
}
|
||||
|
||||
function formatTimeRemaining() {
|
||||
if (!batteryAvailable || timeRemaining <= 0)
|
||||
return "Unknown";
|
||||
|
||||
const hours = Math.floor(timeRemaining / 3600);
|
||||
const minutes = Math.floor((timeRemaining % 3600) / 60);
|
||||
if (hours > 0)
|
||||
return hours + "h " + minutes + "m";
|
||||
else
|
||||
return minutes + "m";
|
||||
}
|
||||
const hours = Math.floor(timeRemaining / 3600)
|
||||
const minutes = Math.floor((timeRemaining % 3600) / 60)
|
||||
if (hours > 0)
|
||||
return hours + "h " + minutes + "m"
|
||||
else
|
||||
return minutes + "m"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,142 +1,155 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
|
||||
readonly property bool available: adapter !== null
|
||||
readonly property bool enabled: (adapter && adapter.enabled) ?? false
|
||||
readonly property bool discovering: (adapter && adapter.discovering) ?? false
|
||||
readonly property var devices: adapter ? adapter.devices : null
|
||||
readonly property var pairedDevices: {
|
||||
if (!adapter || !adapter.devices)
|
||||
return [];
|
||||
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
|
||||
readonly property bool available: adapter !== null
|
||||
readonly property bool enabled: (adapter && adapter.enabled) ?? false
|
||||
readonly property bool discovering: (adapter && adapter.discovering) ?? false
|
||||
readonly property var devices: adapter ? adapter.devices : null
|
||||
readonly property var pairedDevices: {
|
||||
if (!adapter || !adapter.devices)
|
||||
return []
|
||||
|
||||
return adapter.devices.values.filter((dev) => {
|
||||
return dev && (dev.paired || dev.trusted);
|
||||
});
|
||||
}
|
||||
readonly property var allDevicesWithBattery: {
|
||||
if (!adapter || !adapter.devices)
|
||||
return [];
|
||||
return adapter.devices.values.filter(dev => {
|
||||
return dev && (dev.paired
|
||||
|| dev.trusted)
|
||||
})
|
||||
}
|
||||
readonly property var allDevicesWithBattery: {
|
||||
if (!adapter || !adapter.devices)
|
||||
return []
|
||||
|
||||
return adapter.devices.values.filter((dev) => {
|
||||
return dev && dev.batteryAvailable && dev.battery > 0;
|
||||
});
|
||||
}
|
||||
return adapter.devices.values.filter(dev => {
|
||||
return dev && dev.batteryAvailable
|
||||
&& dev.battery > 0
|
||||
})
|
||||
}
|
||||
|
||||
function sortDevices(devices) {
|
||||
return devices.sort((a, b) => {
|
||||
var aName = a.name || a.deviceName || "";
|
||||
var bName = b.name || b.deviceName || "";
|
||||
|
||||
var aHasRealName = aName.includes(" ") && aName.length > 3;
|
||||
var bHasRealName = bName.includes(" ") && bName.length > 3;
|
||||
|
||||
if (aHasRealName && !bHasRealName) return -1;
|
||||
if (!aHasRealName && bHasRealName) return 1;
|
||||
|
||||
var aSignal = (a.signalStrength !== undefined && a.signalStrength > 0) ? a.signalStrength : 0;
|
||||
var bSignal = (b.signalStrength !== undefined && b.signalStrength > 0) ? b.signalStrength : 0;
|
||||
return bSignal - aSignal;
|
||||
});
|
||||
}
|
||||
function sortDevices(devices) {
|
||||
return devices.sort((a, b) => {
|
||||
var aName = a.name || a.deviceName || ""
|
||||
var bName = b.name || b.deviceName || ""
|
||||
|
||||
function getDeviceIcon(device) {
|
||||
if (!device)
|
||||
return "bluetooth";
|
||||
var aHasRealName = aName.includes(" ")
|
||||
&& aName.length > 3
|
||||
var bHasRealName = bName.includes(" ")
|
||||
&& bName.length > 3
|
||||
|
||||
var name = (device.name || device.deviceName || "").toLowerCase();
|
||||
var icon = (device.icon || "").toLowerCase();
|
||||
if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") || name.includes("headset") || name.includes("arctis"))
|
||||
return "headset";
|
||||
if (aHasRealName && !bHasRealName)
|
||||
return -1
|
||||
if (!aHasRealName && bHasRealName)
|
||||
return 1
|
||||
|
||||
if (icon.includes("mouse") || name.includes("mouse"))
|
||||
return "mouse";
|
||||
var aSignal = (a.signalStrength !== undefined
|
||||
&& a.signalStrength > 0) ? a.signalStrength : 0
|
||||
var bSignal = (b.signalStrength !== undefined
|
||||
&& b.signalStrength > 0) ? b.signalStrength : 0
|
||||
return bSignal - aSignal
|
||||
})
|
||||
}
|
||||
|
||||
if (icon.includes("keyboard") || name.includes("keyboard"))
|
||||
return "keyboard";
|
||||
function getDeviceIcon(device) {
|
||||
if (!device)
|
||||
return "bluetooth"
|
||||
|
||||
if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") || name.includes("samsung"))
|
||||
return "smartphone";
|
||||
var name = (device.name || device.deviceName || "").toLowerCase()
|
||||
var icon = (device.icon || "").toLowerCase()
|
||||
if (icon.includes("headset") || icon.includes("audio") || name.includes(
|
||||
"headphone") || name.includes("airpod") || name.includes("headset")
|
||||
|| name.includes("arctis"))
|
||||
return "headset"
|
||||
|
||||
if (icon.includes("watch") || name.includes("watch"))
|
||||
return "watch";
|
||||
if (icon.includes("mouse") || name.includes("mouse"))
|
||||
return "mouse"
|
||||
|
||||
if (icon.includes("speaker") || name.includes("speaker"))
|
||||
return "speaker";
|
||||
if (icon.includes("keyboard") || name.includes("keyboard"))
|
||||
return "keyboard"
|
||||
|
||||
if (icon.includes("display") || name.includes("tv"))
|
||||
return "tv";
|
||||
if (icon.includes("phone") || name.includes("phone") || name.includes(
|
||||
"iphone") || name.includes("android") || name.includes("samsung"))
|
||||
return "smartphone"
|
||||
|
||||
return "bluetooth";
|
||||
}
|
||||
if (icon.includes("watch") || name.includes("watch"))
|
||||
return "watch"
|
||||
|
||||
if (icon.includes("speaker") || name.includes("speaker"))
|
||||
return "speaker"
|
||||
|
||||
if (icon.includes("display") || name.includes("tv"))
|
||||
return "tv"
|
||||
|
||||
function canConnect(device) {
|
||||
if (!device)
|
||||
return false;
|
||||
return "bluetooth"
|
||||
}
|
||||
|
||||
return !device.paired && !device.pairing && !device.blocked;
|
||||
}
|
||||
function canConnect(device) {
|
||||
if (!device)
|
||||
return false
|
||||
|
||||
return !device.paired && !device.pairing && !device.blocked
|
||||
}
|
||||
|
||||
function getSignalStrength(device) {
|
||||
if (!device || device.signalStrength === undefined
|
||||
|| device.signalStrength <= 0)
|
||||
return "Unknown"
|
||||
|
||||
function getSignalStrength(device) {
|
||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
|
||||
return "Unknown";
|
||||
var signal = device.signalStrength
|
||||
if (signal >= 80)
|
||||
return "Excellent"
|
||||
|
||||
var signal = device.signalStrength;
|
||||
if (signal >= 80)
|
||||
return "Excellent";
|
||||
if (signal >= 60)
|
||||
return "Good"
|
||||
|
||||
if (signal >= 60)
|
||||
return "Good";
|
||||
if (signal >= 40)
|
||||
return "Fair"
|
||||
|
||||
if (signal >= 40)
|
||||
return "Fair";
|
||||
if (signal >= 20)
|
||||
return "Poor"
|
||||
|
||||
if (signal >= 20)
|
||||
return "Poor";
|
||||
return "Very Poor"
|
||||
}
|
||||
|
||||
return "Very Poor";
|
||||
}
|
||||
function getSignalIcon(device) {
|
||||
if (!device || device.signalStrength === undefined
|
||||
|| device.signalStrength <= 0)
|
||||
return "signal_cellular_null"
|
||||
|
||||
function getSignalIcon(device) {
|
||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0)
|
||||
return "signal_cellular_null";
|
||||
var signal = device.signalStrength
|
||||
if (signal >= 80)
|
||||
return "signal_cellular_4_bar"
|
||||
|
||||
var signal = device.signalStrength;
|
||||
if (signal >= 80)
|
||||
return "signal_cellular_4_bar";
|
||||
if (signal >= 60)
|
||||
return "signal_cellular_3_bar"
|
||||
|
||||
if (signal >= 60)
|
||||
return "signal_cellular_3_bar";
|
||||
if (signal >= 40)
|
||||
return "signal_cellular_2_bar"
|
||||
|
||||
if (signal >= 40)
|
||||
return "signal_cellular_2_bar";
|
||||
if (signal >= 20)
|
||||
return "signal_cellular_1_bar"
|
||||
|
||||
if (signal >= 20)
|
||||
return "signal_cellular_1_bar";
|
||||
return "signal_cellular_0_bar"
|
||||
}
|
||||
|
||||
return "signal_cellular_0_bar";
|
||||
}
|
||||
|
||||
function isDeviceBusy(device) {
|
||||
if (!device) return false;
|
||||
return device.pairing || device.state === BluetoothDeviceState.Disconnecting || device.state === BluetoothDeviceState.Connecting;
|
||||
}
|
||||
|
||||
function connectDeviceWithTrust(device) {
|
||||
if (!device) return;
|
||||
|
||||
device.trusted = true;
|
||||
device.connect();
|
||||
}
|
||||
function isDeviceBusy(device) {
|
||||
if (!device)
|
||||
return false
|
||||
return device.pairing || device.state === BluetoothDeviceState.Disconnecting
|
||||
|| device.state === BluetoothDeviceState.Connecting
|
||||
}
|
||||
|
||||
function connectDeviceWithTrust(device) {
|
||||
if (!device)
|
||||
return
|
||||
|
||||
device.trusted = true
|
||||
device.connect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,200 +1,211 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool brightnessAvailable: laptopBacklightAvailable || ddcAvailable
|
||||
property bool laptopBacklightAvailable: false
|
||||
property bool ddcAvailable: false
|
||||
property int brightnessLevel: 50
|
||||
property int maxBrightness: 100
|
||||
property int currentRawBrightness: 0
|
||||
property bool brightnessInitialized: false
|
||||
|
||||
signal brightnessChanged()
|
||||
|
||||
function setBrightnessInternal(percentage) {
|
||||
brightnessLevel = Math.max(1, Math.min(100, percentage));
|
||||
|
||||
if (laptopBacklightAvailable) {
|
||||
laptopBrightnessProcess.command = ["brightnessctl", "set", brightnessLevel + "%"];
|
||||
laptopBrightnessProcess.running = true;
|
||||
} else if (ddcAvailable) {
|
||||
|
||||
Quickshell.execDetached(["ddcutil", "setvcp", "10", brightnessLevel.toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightness(percentage) {
|
||||
setBrightnessInternal(percentage);
|
||||
brightnessChanged();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
ddcAvailabilityChecker.running = true;
|
||||
laptopBacklightChecker.running = true;
|
||||
}
|
||||
|
||||
onLaptopBacklightAvailableChanged: {
|
||||
if (laptopBacklightAvailable && !brightnessInitialized) {
|
||||
laptopBrightnessInitProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
onDdcAvailableChanged: {
|
||||
if (ddcAvailable && !laptopBacklightAvailable && !brightnessInitialized) {
|
||||
ddcBrightnessInitProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcAvailabilityChecker
|
||||
command: ["which", "ddcutil"]
|
||||
onExited: function(exitCode) {
|
||||
ddcAvailable = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: laptopBacklightChecker
|
||||
command: ["brightnessctl", "--list"]
|
||||
onExited: function(exitCode) {
|
||||
laptopBacklightAvailable = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: laptopBrightnessProcess
|
||||
running: false
|
||||
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: laptopBrightnessInitProcess
|
||||
command: ["brightnessctl", "get"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
currentRawBrightness = parseInt(text.trim());
|
||||
laptopMaxBrightnessProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("BrightnessService: Failed to read current brightness:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: laptopMaxBrightnessProcess
|
||||
command: ["brightnessctl", "max"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
maxBrightness = parseInt(text.trim());
|
||||
brightnessLevel = Math.round((currentRawBrightness / maxBrightness) * 100);
|
||||
brightnessInitialized = true;
|
||||
console.log("BrightnessService: Initialized with brightness level:", brightnessLevel + "%");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("BrightnessService: Failed to read max brightness:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcBrightnessInitProcess
|
||||
command: ["ddcutil", "getvcp", "10", "--brief"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
const parts = text.trim().split(" ");
|
||||
if (parts.length >= 5) {
|
||||
const current = parseInt(parts[3]) || 50;
|
||||
const max = parseInt(parts[4]) || 100;
|
||||
brightnessLevel = Math.round((current / max) * 100);
|
||||
brightnessInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
if (!laptopBacklightAvailable) {
|
||||
console.warn("BrightnessService: DDC brightness read failed:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IPC Handler for external control
|
||||
IpcHandler {
|
||||
target: "brightness"
|
||||
id: root
|
||||
|
||||
function set(percentage: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available";
|
||||
}
|
||||
|
||||
const value = parseInt(percentage);
|
||||
const clampedValue = Math.max(1, Math.min(100, value));
|
||||
root.setBrightness(clampedValue);
|
||||
return "Brightness set to " + clampedValue + "%";
|
||||
}
|
||||
property bool brightnessAvailable: laptopBacklightAvailable || ddcAvailable
|
||||
property bool laptopBacklightAvailable: false
|
||||
property bool ddcAvailable: false
|
||||
property int brightnessLevel: 50
|
||||
property int maxBrightness: 100
|
||||
property int currentRawBrightness: 0
|
||||
property bool brightnessInitialized: false
|
||||
|
||||
function increment(step: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available";
|
||||
}
|
||||
|
||||
const currentLevel = root.brightnessLevel;
|
||||
const newLevel = Math.max(1, Math.min(100, currentLevel + parseInt(step || "10")));
|
||||
root.setBrightness(newLevel);
|
||||
return "Brightness increased to " + newLevel + "%";
|
||||
}
|
||||
signal brightnessChanged
|
||||
|
||||
function decrement(step: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available";
|
||||
}
|
||||
|
||||
const currentLevel = root.brightnessLevel;
|
||||
const newLevel = Math.max(1, Math.min(100, currentLevel - parseInt(step || "10")));
|
||||
root.setBrightness(newLevel);
|
||||
return "Brightness decreased to " + newLevel + "%";
|
||||
}
|
||||
function setBrightnessInternal(percentage) {
|
||||
brightnessLevel = Math.max(1, Math.min(100, percentage))
|
||||
|
||||
function status(): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available";
|
||||
}
|
||||
|
||||
return "Brightness: " + root.brightnessLevel + "% (" +
|
||||
(root.laptopBacklightAvailable ? "laptop backlight" : "DDC") + ")";
|
||||
}
|
||||
if (laptopBacklightAvailable) {
|
||||
laptopBrightnessProcess.command = ["brightnessctl", "set", brightnessLevel + "%"]
|
||||
laptopBrightnessProcess.running = true
|
||||
} else if (ddcAvailable) {
|
||||
|
||||
Quickshell.execDetached(
|
||||
["ddcutil", "setvcp", "10", brightnessLevel.toString()])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightness(percentage) {
|
||||
setBrightnessInternal(percentage)
|
||||
brightnessChanged()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
ddcAvailabilityChecker.running = true
|
||||
laptopBacklightChecker.running = true
|
||||
}
|
||||
|
||||
onLaptopBacklightAvailableChanged: {
|
||||
if (laptopBacklightAvailable && !brightnessInitialized) {
|
||||
laptopBrightnessInitProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
onDdcAvailableChanged: {
|
||||
if (ddcAvailable && !laptopBacklightAvailable && !brightnessInitialized) {
|
||||
ddcBrightnessInitProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcAvailabilityChecker
|
||||
command: ["which", "ddcutil"]
|
||||
onExited: function (exitCode) {
|
||||
ddcAvailable = (exitCode === 0)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: laptopBacklightChecker
|
||||
command: ["brightnessctl", "--list"]
|
||||
onExited: function (exitCode) {
|
||||
laptopBacklightAvailable = (exitCode === 0)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: laptopBrightnessProcess
|
||||
running: false
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: laptopBrightnessInitProcess
|
||||
command: ["brightnessctl", "get"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
currentRawBrightness = parseInt(text.trim())
|
||||
laptopMaxBrightnessProcess.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("BrightnessService: Failed to read current brightness:",
|
||||
exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: laptopMaxBrightnessProcess
|
||||
command: ["brightnessctl", "max"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
maxBrightness = parseInt(text.trim())
|
||||
brightnessLevel = Math.round(
|
||||
(currentRawBrightness / maxBrightness) * 100)
|
||||
brightnessInitialized = true
|
||||
console.log("BrightnessService: Initialized with brightness level:",
|
||||
brightnessLevel + "%")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("BrightnessService: Failed to read max brightness:",
|
||||
exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcBrightnessInitProcess
|
||||
command: ["ddcutil", "getvcp", "10", "--brief"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
const parts = text.trim().split(" ")
|
||||
if (parts.length >= 5) {
|
||||
const current = parseInt(parts[3]) || 50
|
||||
const max = parseInt(parts[4]) || 100
|
||||
brightnessLevel = Math.round((current / max) * 100)
|
||||
brightnessInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
if (!laptopBacklightAvailable) {
|
||||
console.warn("BrightnessService: DDC brightness read failed:",
|
||||
exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IPC Handler for external control
|
||||
IpcHandler {
|
||||
target: "brightness"
|
||||
|
||||
function set(percentage: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const value = parseInt(percentage)
|
||||
const clampedValue = Math.max(1, Math.min(100, value))
|
||||
root.setBrightness(clampedValue)
|
||||
return "Brightness set to " + clampedValue + "%"
|
||||
}
|
||||
|
||||
function increment(step: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const currentLevel = root.brightnessLevel
|
||||
const newLevel = Math.max(1,
|
||||
Math.min(100,
|
||||
currentLevel + parseInt(step || "10")))
|
||||
root.setBrightness(newLevel)
|
||||
return "Brightness increased to " + newLevel + "%"
|
||||
}
|
||||
|
||||
function decrement(step: string): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const currentLevel = root.brightnessLevel
|
||||
const newLevel = Math.max(1,
|
||||
Math.min(100,
|
||||
currentLevel - parseInt(step || "10")))
|
||||
root.setBrightness(newLevel)
|
||||
return "Brightness decreased to " + newLevel + "%"
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
return "Brightness: " + root.brightnessLevel + "% ("
|
||||
+ (root.laptopBacklightAvailable ? "laptop backlight" : "DDC") + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,245 +1,244 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
property bool khalAvailable: false
|
||||
property var eventsByDate: ({
|
||||
})
|
||||
property bool isLoading: false
|
||||
property string lastError: ""
|
||||
property date lastStartDate
|
||||
property date lastEndDate
|
||||
property bool khalAvailable: false
|
||||
property var eventsByDate: ({})
|
||||
property bool isLoading: false
|
||||
property string lastError: ""
|
||||
property date lastStartDate
|
||||
property date lastEndDate
|
||||
|
||||
function checkKhalAvailability() {
|
||||
if (!khalCheckProcess.running)
|
||||
khalCheckProcess.running = true;
|
||||
function checkKhalAvailability() {
|
||||
if (!khalCheckProcess.running)
|
||||
khalCheckProcess.running = true
|
||||
}
|
||||
|
||||
function loadCurrentMonth() {
|
||||
if (!root.khalAvailable)
|
||||
return
|
||||
|
||||
let today = new Date()
|
||||
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0)
|
||||
// Add padding
|
||||
let startDate = new Date(firstDay)
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7)
|
||||
let endDate = new Date(lastDay)
|
||||
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7)
|
||||
loadEvents(startDate, endDate)
|
||||
}
|
||||
|
||||
function loadEvents(startDate, endDate) {
|
||||
if (!root.khalAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
function loadCurrentMonth() {
|
||||
if (!root.khalAvailable)
|
||||
return ;
|
||||
|
||||
let today = new Date();
|
||||
let firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
let lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
|
||||
// Add padding
|
||||
let startDate = new Date(firstDay);
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay() - 7);
|
||||
let endDate = new Date(lastDay);
|
||||
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7);
|
||||
loadEvents(startDate, endDate);
|
||||
if (eventsProcess.running) {
|
||||
return
|
||||
}
|
||||
// Store last requested date range for refresh timer
|
||||
root.lastStartDate = startDate
|
||||
root.lastEndDate = endDate
|
||||
root.isLoading = true
|
||||
// Format dates for khal (MM/dd/yyyy based on printformats)
|
||||
let startDateStr = Qt.formatDate(startDate, "MM/dd/yyyy")
|
||||
let endDateStr = Qt.formatDate(endDate, "MM/dd/yyyy")
|
||||
eventsProcess.requestStartDate = startDate
|
||||
eventsProcess.requestEndDate = endDate
|
||||
eventsProcess.command = ["khal", "list", "--json", "title", "--json", "description", "--json", "start-date", "--json", "start-time", "--json", "end-date", "--json", "end-time", "--json", "all-day", "--json", "location", "--json", "url", startDateStr, endDateStr]
|
||||
eventsProcess.running = true
|
||||
}
|
||||
|
||||
function loadEvents(startDate, endDate) {
|
||||
if (!root.khalAvailable) {
|
||||
return ;
|
||||
}
|
||||
if (eventsProcess.running) {
|
||||
return ;
|
||||
}
|
||||
// Store last requested date range for refresh timer
|
||||
root.lastStartDate = startDate;
|
||||
root.lastEndDate = endDate;
|
||||
root.isLoading = true;
|
||||
// Format dates for khal (MM/dd/yyyy based on printformats)
|
||||
let startDateStr = Qt.formatDate(startDate, "MM/dd/yyyy");
|
||||
let endDateStr = Qt.formatDate(endDate, "MM/dd/yyyy");
|
||||
eventsProcess.requestStartDate = startDate;
|
||||
eventsProcess.requestEndDate = endDate;
|
||||
eventsProcess.command = ["khal", "list", "--json", "title", "--json", "description", "--json", "start-date", "--json", "start-time", "--json", "end-date", "--json", "end-time", "--json", "all-day", "--json", "location", "--json", "url", startDateStr, endDateStr];
|
||||
eventsProcess.running = true;
|
||||
function getEventsForDate(date) {
|
||||
let dateKey = Qt.formatDate(date, "yyyy-MM-dd")
|
||||
return root.eventsByDate[dateKey] || []
|
||||
}
|
||||
|
||||
function hasEventsForDate(date) {
|
||||
let events = getEventsForDate(date)
|
||||
return events.length > 0
|
||||
}
|
||||
|
||||
// Initialize on component completion
|
||||
Component.onCompleted: {
|
||||
checkKhalAvailability()
|
||||
}
|
||||
|
||||
// Process for checking khal configuration
|
||||
Process {
|
||||
id: khalCheckProcess
|
||||
|
||||
command: ["khal", "list", "today"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
root.khalAvailable = (exitCode === 0)
|
||||
if (exitCode === 0) {
|
||||
loadCurrentMonth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getEventsForDate(date) {
|
||||
let dateKey = Qt.formatDate(date, "yyyy-MM-dd");
|
||||
return root.eventsByDate[dateKey] || [];
|
||||
}
|
||||
// Process for loading events
|
||||
Process {
|
||||
id: eventsProcess
|
||||
|
||||
function hasEventsForDate(date) {
|
||||
let events = getEventsForDate(date);
|
||||
return events.length > 0;
|
||||
}
|
||||
property date requestStartDate
|
||||
property date requestEndDate
|
||||
property string rawOutput: ""
|
||||
|
||||
// Initialize on component completion
|
||||
Component.onCompleted: {
|
||||
checkKhalAvailability();
|
||||
}
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
root.isLoading = false
|
||||
if (exitCode !== 0) {
|
||||
root.lastError = "Failed to load events (exit code: " + exitCode + ")"
|
||||
return
|
||||
}
|
||||
try {
|
||||
let newEventsByDate = {}
|
||||
let lines = eventsProcess.rawOutput.split('\n')
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
if (!line || line === "[]")
|
||||
continue
|
||||
|
||||
// Process for checking khal configuration
|
||||
Process {
|
||||
id: khalCheckProcess
|
||||
// Parse JSON line
|
||||
let dayEvents = JSON.parse(line)
|
||||
// Process each event in this day's array
|
||||
for (let event of dayEvents) {
|
||||
if (!event.title)
|
||||
continue
|
||||
|
||||
command: ["khal", "list", "today"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
root.khalAvailable = (exitCode === 0);
|
||||
if (exitCode === 0) {
|
||||
loadCurrentMonth();
|
||||
// Parse start and end dates
|
||||
let startDate, endDate
|
||||
if (event['start-date']) {
|
||||
let startParts = event['start-date'].split('/')
|
||||
startDate = new Date(parseInt(startParts[2]),
|
||||
parseInt(startParts[0]) - 1,
|
||||
parseInt(startParts[1]))
|
||||
} else {
|
||||
startDate = new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process for loading events
|
||||
Process {
|
||||
id: eventsProcess
|
||||
|
||||
property date requestStartDate
|
||||
property date requestEndDate
|
||||
property string rawOutput: ""
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
root.isLoading = false;
|
||||
if (exitCode !== 0) {
|
||||
root.lastError = "Failed to load events (exit code: " + exitCode + ")";
|
||||
return ;
|
||||
if (event['end-date']) {
|
||||
let endParts = event['end-date'].split('/')
|
||||
endDate = new Date(parseInt(endParts[2]),
|
||||
parseInt(endParts[0]) - 1,
|
||||
parseInt(endParts[1]))
|
||||
} else {
|
||||
endDate = new Date(startDate)
|
||||
}
|
||||
try {
|
||||
let newEventsByDate = {
|
||||
};
|
||||
let lines = eventsProcess.rawOutput.split('\n');
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line || line === "[]")
|
||||
continue;
|
||||
|
||||
// Parse JSON line
|
||||
let dayEvents = JSON.parse(line);
|
||||
// Process each event in this day's array
|
||||
for (let event of dayEvents) {
|
||||
if (!event.title)
|
||||
continue;
|
||||
|
||||
// Parse start and end dates
|
||||
let startDate, endDate;
|
||||
if (event['start-date']) {
|
||||
let startParts = event['start-date'].split('/');
|
||||
startDate = new Date(parseInt(startParts[2]), parseInt(startParts[0]) - 1, parseInt(startParts[1]));
|
||||
} else {
|
||||
startDate = new Date();
|
||||
}
|
||||
if (event['end-date']) {
|
||||
let endParts = event['end-date'].split('/');
|
||||
endDate = new Date(parseInt(endParts[2]), parseInt(endParts[0]) - 1, parseInt(endParts[1]));
|
||||
} else {
|
||||
endDate = new Date(startDate);
|
||||
}
|
||||
// Create start/end times
|
||||
let startTime = new Date(startDate);
|
||||
let endTime = new Date(endDate);
|
||||
if (event['start-time'] && event['all-day'] !== "True") {
|
||||
// Parse time if available and not all-day
|
||||
let timeStr = event['start-time'];
|
||||
if (timeStr) {
|
||||
let timeParts = timeStr.match(/(\d+):(\d+)/);
|
||||
if (timeParts) {
|
||||
startTime.setHours(parseInt(timeParts[1]), parseInt(timeParts[2]));
|
||||
if (event['end-time']) {
|
||||
let endTimeParts = event['end-time'].match(/(\d+):(\d+)/);
|
||||
if (endTimeParts)
|
||||
endTime.setHours(parseInt(endTimeParts[1]), parseInt(endTimeParts[2]));
|
||||
|
||||
} else {
|
||||
// Default to 1 hour duration on same day
|
||||
endTime = new Date(startTime);
|
||||
endTime.setHours(startTime.getHours() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create unique ID for this event (to track multi-day events)
|
||||
let eventId = event.title + "_" + event['start-date'] + "_" + (event['start-time'] || 'allday');
|
||||
// Create event object template
|
||||
let eventTemplate = {
|
||||
"id": eventId,
|
||||
"title": event.title || "Untitled Event",
|
||||
"start": startTime,
|
||||
"end": endTime,
|
||||
"location": event.location || "",
|
||||
"description": event.description || "",
|
||||
"url": event.url || "",
|
||||
"calendar": "",
|
||||
"color": "",
|
||||
"allDay": event['all-day'] === "True",
|
||||
"isMultiDay": startDate.toDateString() !== endDate.toDateString()
|
||||
};
|
||||
// Add event to each day it spans
|
||||
let currentDate = new Date(startDate);
|
||||
while (currentDate <= endDate) {
|
||||
let dateKey = Qt.formatDate(currentDate, "yyyy-MM-dd");
|
||||
if (!newEventsByDate[dateKey])
|
||||
newEventsByDate[dateKey] = [];
|
||||
|
||||
// Check if this exact event is already added to this date (prevent duplicates)
|
||||
let existingEvent = newEventsByDate[dateKey].find((e) => {
|
||||
return e.id === eventId;
|
||||
});
|
||||
if (existingEvent) {
|
||||
// Move to next day without adding duplicate
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
continue;
|
||||
}
|
||||
// Create a copy of the event for this date
|
||||
let dayEvent = Object.assign({
|
||||
}, eventTemplate);
|
||||
// For multi-day events, adjust the display time for this specific day
|
||||
if (currentDate.getTime() === startDate.getTime()) {
|
||||
// First day - use original start time
|
||||
dayEvent.start = new Date(startTime);
|
||||
} else {
|
||||
// Subsequent days - start at beginning of day for all-day events
|
||||
dayEvent.start = new Date(currentDate);
|
||||
if (!dayEvent.allDay)
|
||||
dayEvent.start.setHours(0, 0, 0, 0);
|
||||
|
||||
}
|
||||
if (currentDate.getTime() === endDate.getTime()) {
|
||||
// Last day - use original end time
|
||||
dayEvent.end = new Date(endTime);
|
||||
} else {
|
||||
// Earlier days - end at end of day for all-day events
|
||||
dayEvent.end = new Date(currentDate);
|
||||
if (!dayEvent.allDay)
|
||||
dayEvent.end.setHours(23, 59, 59, 999);
|
||||
|
||||
}
|
||||
newEventsByDate[dateKey].push(dayEvent);
|
||||
// Move to next day
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
}
|
||||
// Create start/end times
|
||||
let startTime = new Date(startDate)
|
||||
let endTime = new Date(endDate)
|
||||
if (event['start-time'] && event['all-day'] !== "True") {
|
||||
// Parse time if available and not all-day
|
||||
let timeStr = event['start-time']
|
||||
if (timeStr) {
|
||||
let timeParts = timeStr.match(/(\d+):(\d+)/)
|
||||
if (timeParts) {
|
||||
startTime.setHours(parseInt(timeParts[1]),
|
||||
parseInt(timeParts[2]))
|
||||
if (event['end-time']) {
|
||||
let endTimeParts = event['end-time'].match(/(\d+):(\d+)/)
|
||||
if (endTimeParts)
|
||||
endTime.setHours(parseInt(endTimeParts[1]),
|
||||
parseInt(endTimeParts[2]))
|
||||
} else {
|
||||
// Default to 1 hour duration on same day
|
||||
endTime = new Date(startTime)
|
||||
endTime.setHours(startTime.getHours() + 1)
|
||||
}
|
||||
}
|
||||
// Sort events by start time within each date
|
||||
for (let dateKey in newEventsByDate) {
|
||||
newEventsByDate[dateKey].sort((a, b) => {
|
||||
return a.start.getTime() - b.start.getTime();
|
||||
});
|
||||
}
|
||||
root.eventsByDate = newEventsByDate;
|
||||
root.lastError = "";
|
||||
} catch (error) {
|
||||
root.lastError = "Failed to parse events JSON: " + error.toString();
|
||||
root.eventsByDate = {
|
||||
};
|
||||
}
|
||||
}
|
||||
// Reset for next run
|
||||
eventsProcess.rawOutput = "";
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
eventsProcess.rawOutput += data + "\n";
|
||||
// Create unique ID for this event (to track multi-day events)
|
||||
let eventId = event.title + "_" + event['start-date'] + "_" + (event['start-time']
|
||||
|| 'allday')
|
||||
// Create event object template
|
||||
let eventTemplate = {
|
||||
"id": eventId,
|
||||
"title": event.title || "Untitled Event",
|
||||
"start": startTime,
|
||||
"end": endTime,
|
||||
"location": event.location || "",
|
||||
"description": event.description || "",
|
||||
"url": event.url || "",
|
||||
"calendar": "",
|
||||
"color": "",
|
||||
"allDay": event['all-day'] === "True",
|
||||
"isMultiDay": startDate.toDateString() !== endDate.toDateString()
|
||||
}
|
||||
}
|
||||
// Add event to each day it spans
|
||||
let currentDate = new Date(startDate)
|
||||
while (currentDate <= endDate) {
|
||||
let dateKey = Qt.formatDate(currentDate, "yyyy-MM-dd")
|
||||
if (!newEventsByDate[dateKey])
|
||||
newEventsByDate[dateKey] = []
|
||||
|
||||
// Check if this exact event is already added to this date (prevent duplicates)
|
||||
let existingEvent = newEventsByDate[dateKey].find(e => {
|
||||
return e.id === eventId
|
||||
})
|
||||
if (existingEvent) {
|
||||
// Move to next day without adding duplicate
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
continue
|
||||
}
|
||||
// Create a copy of the event for this date
|
||||
let dayEvent = Object.assign({}, eventTemplate)
|
||||
// For multi-day events, adjust the display time for this specific day
|
||||
if (currentDate.getTime() === startDate.getTime()) {
|
||||
// First day - use original start time
|
||||
dayEvent.start = new Date(startTime)
|
||||
} else {
|
||||
// Subsequent days - start at beginning of day for all-day events
|
||||
dayEvent.start = new Date(currentDate)
|
||||
if (!dayEvent.allDay)
|
||||
dayEvent.start.setHours(0, 0, 0, 0)
|
||||
}
|
||||
if (currentDate.getTime() === endDate.getTime()) {
|
||||
// Last day - use original end time
|
||||
dayEvent.end = new Date(endTime)
|
||||
} else {
|
||||
// Earlier days - end at end of day for all-day events
|
||||
dayEvent.end = new Date(currentDate)
|
||||
if (!dayEvent.allDay)
|
||||
dayEvent.end.setHours(23, 59, 59, 999)
|
||||
}
|
||||
newEventsByDate[dateKey].push(dayEvent)
|
||||
// Move to next day
|
||||
currentDate.setDate(currentDate.getDate() + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort events by start time within each date
|
||||
for (let dateKey in newEventsByDate) {
|
||||
newEventsByDate[dateKey].sort((a, b) => {
|
||||
return a.start.getTime(
|
||||
) - b.start.getTime()
|
||||
})
|
||||
}
|
||||
root.eventsByDate = newEventsByDate
|
||||
root.lastError = ""
|
||||
} catch (error) {
|
||||
root.lastError = "Failed to parse events JSON: " + error.toString()
|
||||
root.eventsByDate = {}
|
||||
}
|
||||
// Reset for next run
|
||||
eventsProcess.rawOutput = ""
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: data => {
|
||||
eventsProcess.rawOutput += data + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
property list<int> values: Array(6)
|
||||
property int refCount: 0
|
||||
property bool cavaAvailable: false
|
||||
property list<int> values: Array(6)
|
||||
property int refCount: 0
|
||||
property bool cavaAvailable: false
|
||||
|
||||
Process {
|
||||
id: cavaCheck
|
||||
Process {
|
||||
id: cavaCheck
|
||||
|
||||
command: ["which", "cava"]
|
||||
running: true
|
||||
onExited: (exitCode) => {
|
||||
root.cavaAvailable = exitCode === 0;
|
||||
}
|
||||
command: ["which", "cava"]
|
||||
running: true
|
||||
onExited: exitCode => {
|
||||
root.cavaAvailable = exitCode === 0
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProcess
|
||||
|
||||
running: root.cavaAvailable && root.refCount > 0
|
||||
command: ["sh", "-c", `printf '[general]\\nmode=normal\\nframerate=25\\nautosens=0\\nsensitivity=30\\nbars=6\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin`]
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
root.values = Array(6).fill(0)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProcess
|
||||
|
||||
running: root.cavaAvailable && root.refCount > 0
|
||||
command: ["sh", "-c", `printf '[general]\\nmode=normal\\nframerate=25\\nautosens=0\\nsensitivity=30\\nbars=6\\nlower_cutoff_freq=50\\nhigher_cutoff_freq=12000\\n[output]\\nmethod=raw\\nraw_target=/dev/stdout\\ndata_format=ascii\\nchannels=mono\\nmono_option=average\\n[smoothing]\\nnoise_reduction=35\\nintegral=90\\ngravity=95\\nignore=2\\nmonstercat=1.5' | cava -p /dev/stdin`]
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
root.values = Array(6).fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (root.refCount > 0 && data.trim()) {
|
||||
let points = data.split(";").map((p) => {
|
||||
return parseInt(p.trim(), 10);
|
||||
}).filter((p) => {
|
||||
return !isNaN(p);
|
||||
});
|
||||
if (points.length >= 6) {
|
||||
root.values = points.slice(0, 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: data => {
|
||||
if (root.refCount > 0 && data.trim()) {
|
||||
let points = data.split(";").map(p => {
|
||||
return parseInt(p.trim(), 10)
|
||||
}).filter(p => {
|
||||
return !isNaN(p)
|
||||
})
|
||||
if (points.length >= 6) {
|
||||
root.values = points.slice(0, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,96 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
property bool niriAvailable: false
|
||||
property string focusedAppId: ""
|
||||
property string focusedAppName: ""
|
||||
property string focusedWindowTitle: ""
|
||||
property int focusedWindowId: -1
|
||||
property bool niriAvailable: false
|
||||
property string focusedAppId: ""
|
||||
property string focusedAppName: ""
|
||||
property string focusedWindowTitle: ""
|
||||
property int focusedWindowId: -1
|
||||
|
||||
|
||||
function updateFromNiriData() {
|
||||
if (!root.niriAvailable) {
|
||||
clearFocusedWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
let focusedWindow = NiriService.windows.find(w => w.is_focused);
|
||||
|
||||
if (focusedWindow) {
|
||||
root.focusedAppId = focusedWindow.app_id || "";
|
||||
root.focusedWindowTitle = focusedWindow.title || "";
|
||||
root.focusedAppName = getDisplayName(focusedWindow.app_id || "");
|
||||
root.focusedWindowId = parseInt(focusedWindow.id) || -1;
|
||||
} else {
|
||||
clearFocusedWindow();
|
||||
}
|
||||
function updateFromNiriData() {
|
||||
if (!root.niriAvailable) {
|
||||
clearFocusedWindow()
|
||||
return
|
||||
}
|
||||
|
||||
let focusedWindow = NiriService.windows.find(w => w.is_focused)
|
||||
|
||||
function clearFocusedWindow() {
|
||||
root.focusedAppId = "";
|
||||
root.focusedAppName = "";
|
||||
root.focusedWindowTitle = "";
|
||||
if (focusedWindow) {
|
||||
root.focusedAppId = focusedWindow.app_id || ""
|
||||
root.focusedWindowTitle = focusedWindow.title || ""
|
||||
root.focusedAppName = getDisplayName(focusedWindow.app_id || "")
|
||||
root.focusedWindowId = parseInt(focusedWindow.id) || -1
|
||||
} else {
|
||||
clearFocusedWindow()
|
||||
}
|
||||
}
|
||||
|
||||
function clearFocusedWindow() {
|
||||
root.focusedAppId = ""
|
||||
root.focusedAppName = ""
|
||||
root.focusedWindowTitle = ""
|
||||
}
|
||||
|
||||
// Convert app_id to a more user-friendly display name
|
||||
function getDisplayName(appId) {
|
||||
if (!appId)
|
||||
return ""
|
||||
const desktopEntry = DesktopEntries.byId(appId)
|
||||
return desktopEntry && desktopEntry.name ? desktopEntry.name : ""
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.niriAvailable = NiriService.niriAvailable
|
||||
NiriService.onNiriAvailableChanged.connect(() => {
|
||||
root.niriAvailable = NiriService.niriAvailable
|
||||
if (root.niriAvailable)
|
||||
updateFromNiriData()
|
||||
})
|
||||
if (root.niriAvailable)
|
||||
updateFromNiriData()
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onFocusedWindowIdChanged() {
|
||||
const focusedWindowId = NiriService.focusedWindowId
|
||||
if (!focusedWindowId) {
|
||||
clearFocusedWindow()
|
||||
return
|
||||
}
|
||||
|
||||
const focusedWindow = NiriService.windows.find(
|
||||
w => w.id == focusedWindowId)
|
||||
if (focusedWindow) {
|
||||
root.focusedAppId = focusedWindow.app_id || ""
|
||||
root.focusedWindowTitle = focusedWindow.title || ""
|
||||
root.focusedAppName = getDisplayName(focusedWindow.app_id || "")
|
||||
root.focusedWindowId = parseInt(focusedWindow.id) || -1
|
||||
} else {
|
||||
clearFocusedWindow()
|
||||
}
|
||||
}
|
||||
|
||||
// Convert app_id to a more user-friendly display name
|
||||
function getDisplayName(appId) {
|
||||
if (!appId)
|
||||
return "";
|
||||
const desktopEntry = DesktopEntries.byId(appId);
|
||||
return desktopEntry && desktopEntry.name ? desktopEntry.name : "";
|
||||
function onWindowsChanged() {
|
||||
updateFromNiriData()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.niriAvailable = NiriService.niriAvailable;
|
||||
NiriService.onNiriAvailableChanged.connect(() => {
|
||||
root.niriAvailable = NiriService.niriAvailable;
|
||||
if (root.niriAvailable)
|
||||
updateFromNiriData();
|
||||
|
||||
});
|
||||
if (root.niriAvailable)
|
||||
updateFromNiriData();
|
||||
|
||||
function onWindowOpenedOrChanged(windowData) {
|
||||
if (windowData.is_focused) {
|
||||
root.focusedAppId = windowData.app_id || ""
|
||||
root.focusedWindowTitle = windowData.title || ""
|
||||
root.focusedAppName = getDisplayName(windowData.app_id || "")
|
||||
root.focusedWindowId = parseInt(windowData.id) || -1
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onFocusedWindowIdChanged() {
|
||||
const focusedWindowId = NiriService.focusedWindowId;
|
||||
if (!focusedWindowId) {
|
||||
clearFocusedWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
const focusedWindow = NiriService.windows.find(w => w.id == focusedWindowId);
|
||||
if (focusedWindow) {
|
||||
root.focusedAppId = focusedWindow.app_id || "";
|
||||
root.focusedWindowTitle = focusedWindow.title || "";
|
||||
root.focusedAppName = getDisplayName(focusedWindow.app_id || "");
|
||||
root.focusedWindowId = parseInt(focusedWindow.id) || -1;
|
||||
} else {
|
||||
clearFocusedWindow();
|
||||
}
|
||||
}
|
||||
|
||||
function onWindowsChanged() {
|
||||
updateFromNiriData();
|
||||
}
|
||||
|
||||
function onWindowOpenedOrChanged(windowData) {
|
||||
if (windowData.is_focused) {
|
||||
root.focusedAppId = windowData.app_id || "";
|
||||
root.focusedWindowTitle = windowData.title || "";
|
||||
root.focusedAppName = getDisplayName(windowData.app_id || "");
|
||||
root.focusedWindowId = parseInt(windowData.id) || -1;
|
||||
}
|
||||
}
|
||||
|
||||
target: NiriService
|
||||
}
|
||||
|
||||
|
||||
|
||||
target: NiriService
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -8,48 +9,48 @@ import Quickshell.Services.Mpris
|
||||
import Quickshell.Widgets
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
||||
|
||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
|
||||
?? availablePlayers.find(p => p.canControl && p.canPlay)
|
||||
?? null
|
||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
||||
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
|
||||
?? availablePlayers.find(
|
||||
p => p.canControl && p.canPlay) ?? null
|
||||
|
||||
function list(): string {
|
||||
return root.availablePlayers.map(p => p.identity).join("");
|
||||
}
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
|
||||
function play(): void {
|
||||
if (root.activePlayer?.canPlay)
|
||||
root.activePlayer.play();
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
if (root.activePlayer?.canPause)
|
||||
root.activePlayer.pause();
|
||||
}
|
||||
|
||||
function playPause(): void {
|
||||
if (root.activePlayer?.canTogglePlaying)
|
||||
root.activePlayer.togglePlaying();
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
if (root.activePlayer?.canGoPrevious)
|
||||
root.activePlayer.previous();
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
if (root.activePlayer?.canGoNext)
|
||||
root.activePlayer.next();
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
root.activePlayer?.stop();
|
||||
}
|
||||
function list(): string {
|
||||
return root.availablePlayers.map(p => p.identity).join("")
|
||||
}
|
||||
|
||||
function play(): void {
|
||||
if (root.activePlayer?.canPlay)
|
||||
root.activePlayer.play()
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
if (root.activePlayer?.canPause)
|
||||
root.activePlayer.pause()
|
||||
}
|
||||
|
||||
function playPause(): void {
|
||||
if (root.activePlayer?.canTogglePlaying)
|
||||
root.activePlayer.togglePlaying()
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
if (root.activePlayer?.canGoPrevious)
|
||||
root.activePlayer.previous()
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
if (root.activePlayer?.canGoNext)
|
||||
root.activePlayer.next()
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
root.activePlayer?.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,299 +1,303 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
// Workspace management
|
||||
property var workspaces: ({})
|
||||
property var allWorkspaces: []
|
||||
property int focusedWorkspaceIndex: 0
|
||||
property string focusedWorkspaceId: ""
|
||||
property var currentOutputWorkspaces: []
|
||||
property string currentOutput: ""
|
||||
// Workspace management
|
||||
property var workspaces: ({})
|
||||
property var allWorkspaces: []
|
||||
property int focusedWorkspaceIndex: 0
|
||||
property string focusedWorkspaceId: ""
|
||||
property var currentOutputWorkspaces: []
|
||||
property string currentOutput: ""
|
||||
|
||||
// Window management
|
||||
property var windows: []
|
||||
property int focusedWindowIndex: -1
|
||||
property string focusedWindowTitle: "(No active window)"
|
||||
property string focusedWindowId: ""
|
||||
// Window management
|
||||
property var windows: []
|
||||
property int focusedWindowIndex: -1
|
||||
property string focusedWindowTitle: "(No active window)"
|
||||
property string focusedWindowId: ""
|
||||
|
||||
// Overview state
|
||||
property bool inOverview: false
|
||||
// Overview state
|
||||
property bool inOverview: false
|
||||
|
||||
signal windowOpenedOrChanged(var windowData)
|
||||
signal windowOpenedOrChanged(var windowData)
|
||||
|
||||
// Feature availability
|
||||
property bool niriAvailable: false
|
||||
// Feature availability
|
||||
property bool niriAvailable: false
|
||||
|
||||
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
|
||||
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
|
||||
|
||||
Component.onCompleted: checkNiriAvailability()
|
||||
Component.onCompleted: checkNiriAvailability()
|
||||
|
||||
Process {
|
||||
id: niriCheck
|
||||
command: ["test", "-S", root.socketPath]
|
||||
Process {
|
||||
id: niriCheck
|
||||
command: ["test", "-S", root.socketPath]
|
||||
|
||||
onExited: exitCode => {
|
||||
root.niriAvailable = exitCode === 0;
|
||||
if (root.niriAvailable) {
|
||||
eventStreamSocket.connected = true;
|
||||
}
|
||||
onExited: exitCode => {
|
||||
root.niriAvailable = exitCode === 0
|
||||
if (root.niriAvailable) {
|
||||
eventStreamSocket.connected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkNiriAvailability() {
|
||||
niriCheck.running = true
|
||||
}
|
||||
|
||||
Socket {
|
||||
id: eventStreamSocket
|
||||
path: root.socketPath
|
||||
connected: false
|
||||
|
||||
onConnectionStateChanged: {
|
||||
if (connected) {
|
||||
write('"EventStream"\n')
|
||||
}
|
||||
}
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => {
|
||||
try {
|
||||
const event = JSON.parse(line)
|
||||
handleNiriEvent(event)
|
||||
} catch (e) {
|
||||
console.warn("NiriService: Failed to parse event:", line, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Socket {
|
||||
id: requestSocket
|
||||
path: root.socketPath
|
||||
connected: root.niriAvailable
|
||||
}
|
||||
|
||||
function handleNiriEvent(event) {
|
||||
if (event.WorkspacesChanged) {
|
||||
handleWorkspacesChanged(event.WorkspacesChanged)
|
||||
} else if (event.WorkspaceActivated) {
|
||||
handleWorkspaceActivated(event.WorkspaceActivated)
|
||||
} else if (event.WindowsChanged) {
|
||||
handleWindowsChanged(event.WindowsChanged)
|
||||
} else if (event.WindowClosed) {
|
||||
handleWindowClosed(event.WindowClosed)
|
||||
} else if (event.WindowFocusChanged) {
|
||||
handleWindowFocusChanged(event.WindowFocusChanged)
|
||||
} else if (event.WindowOpenedOrChanged) {
|
||||
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
|
||||
} else if (event.OverviewOpenedOrClosed) {
|
||||
handleOverviewChanged(event.OverviewOpenedOrClosed)
|
||||
}
|
||||
}
|
||||
|
||||
function handleWorkspacesChanged(data) {
|
||||
const workspaces = {}
|
||||
|
||||
for (const ws of data.workspaces) {
|
||||
workspaces[ws.id] = ws
|
||||
}
|
||||
|
||||
function checkNiriAvailability() {
|
||||
niriCheck.running = true;
|
||||
root.workspaces = workspaces
|
||||
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx)
|
||||
|
||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused)
|
||||
if (focusedWorkspaceIndex >= 0) {
|
||||
var focusedWs = allWorkspaces[focusedWorkspaceIndex]
|
||||
focusedWorkspaceId = focusedWs.id
|
||||
currentOutput = focusedWs.output || ""
|
||||
} else {
|
||||
focusedWorkspaceIndex = 0
|
||||
focusedWorkspaceId = ""
|
||||
}
|
||||
|
||||
Socket {
|
||||
id: eventStreamSocket
|
||||
path: root.socketPath
|
||||
connected: false
|
||||
updateCurrentOutputWorkspaces()
|
||||
}
|
||||
|
||||
onConnectionStateChanged: {
|
||||
if (connected) {
|
||||
write('"EventStream"\n');
|
||||
}
|
||||
}
|
||||
function handleWorkspaceActivated(data) {
|
||||
const ws = root.workspaces[data.id]
|
||||
if (!ws)
|
||||
return
|
||||
const output = ws.output
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => {
|
||||
try {
|
||||
const event = JSON.parse(line);
|
||||
handleNiriEvent(event);
|
||||
} catch (e) {
|
||||
console.warn("NiriService: Failed to parse event:", line, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const id in root.workspaces) {
|
||||
const workspace = root.workspaces[id]
|
||||
const got_activated = workspace.id === data.id
|
||||
|
||||
if (workspace.output === output) {
|
||||
workspace.is_active = got_activated
|
||||
}
|
||||
|
||||
if (data.focused) {
|
||||
workspace.is_focused = got_activated
|
||||
}
|
||||
}
|
||||
|
||||
Socket {
|
||||
id: requestSocket
|
||||
path: root.socketPath
|
||||
connected: root.niriAvailable
|
||||
focusedWorkspaceId = data.id
|
||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id)
|
||||
|
||||
if (focusedWorkspaceIndex >= 0) {
|
||||
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || ""
|
||||
}
|
||||
|
||||
function handleNiriEvent(event) {
|
||||
if (event.WorkspacesChanged) {
|
||||
handleWorkspacesChanged(event.WorkspacesChanged);
|
||||
} else if (event.WorkspaceActivated) {
|
||||
handleWorkspaceActivated(event.WorkspaceActivated);
|
||||
} else if (event.WindowsChanged) {
|
||||
handleWindowsChanged(event.WindowsChanged);
|
||||
} else if (event.WindowClosed) {
|
||||
handleWindowClosed(event.WindowClosed);
|
||||
} else if (event.WindowFocusChanged) {
|
||||
handleWindowFocusChanged(event.WindowFocusChanged);
|
||||
} else if (event.WindowOpenedOrChanged) {
|
||||
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
|
||||
} else if (event.OverviewOpenedOrClosed) {
|
||||
handleOverviewChanged(event.OverviewOpenedOrClosed);
|
||||
}
|
||||
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx)
|
||||
|
||||
updateCurrentOutputWorkspaces()
|
||||
workspacesChanged()
|
||||
}
|
||||
|
||||
function handleWindowsChanged(data) {
|
||||
windows = [...data.windows].sort((a, b) => a.id - b.id)
|
||||
updateFocusedWindow()
|
||||
}
|
||||
|
||||
function handleWindowClosed(data) {
|
||||
windows = windows.filter(w => w.id !== data.id)
|
||||
updateFocusedWindow()
|
||||
}
|
||||
|
||||
function handleWindowFocusChanged(data) {
|
||||
if (data.id) {
|
||||
focusedWindowId = data.id
|
||||
focusedWindowIndex = windows.findIndex(w => w.id === data.id)
|
||||
} else {
|
||||
focusedWindowId = ""
|
||||
focusedWindowIndex = -1
|
||||
}
|
||||
updateFocusedWindow()
|
||||
}
|
||||
|
||||
function handleWindowOpenedOrChanged(data) {
|
||||
if (!data.window)
|
||||
return
|
||||
|
||||
const window = data.window
|
||||
const existingIndex = windows.findIndex(w => w.id === window.id)
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
let updatedWindows = [...windows]
|
||||
updatedWindows[existingIndex] = window
|
||||
windows = updatedWindows.sort((a, b) => a.id - b.id)
|
||||
} else {
|
||||
windows = [...windows, window].sort((a, b) => a.id - b.id)
|
||||
}
|
||||
|
||||
function handleWorkspacesChanged(data) {
|
||||
const workspaces = {};
|
||||
|
||||
for (const ws of data.workspaces) {
|
||||
workspaces[ws.id] = ws;
|
||||
}
|
||||
|
||||
root.workspaces = workspaces;
|
||||
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx);
|
||||
|
||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused);
|
||||
if (focusedWorkspaceIndex >= 0) {
|
||||
var focusedWs = allWorkspaces[focusedWorkspaceIndex];
|
||||
focusedWorkspaceId = focusedWs.id;
|
||||
currentOutput = focusedWs.output || "";
|
||||
} else {
|
||||
focusedWorkspaceIndex = 0;
|
||||
focusedWorkspaceId = "";
|
||||
}
|
||||
|
||||
updateCurrentOutputWorkspaces();
|
||||
if (window.is_focused) {
|
||||
focusedWindowId = window.id
|
||||
focusedWindowIndex = windows.findIndex(w => w.id === window.id)
|
||||
}
|
||||
|
||||
function handleWorkspaceActivated(data) {
|
||||
const ws = root.workspaces[data.id];
|
||||
if (!ws)
|
||||
return;
|
||||
const output = ws.output;
|
||||
updateFocusedWindow()
|
||||
|
||||
for (const id in root.workspaces) {
|
||||
const workspace = root.workspaces[id];
|
||||
const got_activated = workspace.id === data.id;
|
||||
windowOpenedOrChanged(window)
|
||||
}
|
||||
|
||||
if (workspace.output === output) {
|
||||
workspace.is_active = got_activated;
|
||||
}
|
||||
function handleOverviewChanged(data) {
|
||||
inOverview = data.is_open
|
||||
}
|
||||
|
||||
if (data.focused) {
|
||||
workspace.is_focused = got_activated;
|
||||
}
|
||||
}
|
||||
|
||||
focusedWorkspaceId = data.id;
|
||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id);
|
||||
|
||||
if (focusedWorkspaceIndex >= 0) {
|
||||
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "";
|
||||
}
|
||||
|
||||
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx);
|
||||
|
||||
updateCurrentOutputWorkspaces();
|
||||
workspacesChanged();
|
||||
function updateCurrentOutputWorkspaces() {
|
||||
if (!currentOutput) {
|
||||
currentOutputWorkspaces = allWorkspaces
|
||||
return
|
||||
}
|
||||
|
||||
function handleWindowsChanged(data) {
|
||||
windows = [...data.windows].sort((a, b) => a.id - b.id);
|
||||
updateFocusedWindow();
|
||||
var outputWs = allWorkspaces.filter(w => w.output === currentOutput)
|
||||
currentOutputWorkspaces = outputWs
|
||||
}
|
||||
|
||||
function updateFocusedWindow() {
|
||||
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
||||
var focusedWin = windows[focusedWindowIndex]
|
||||
focusedWindowTitle = focusedWin.title || "(Unnamed window)"
|
||||
} else {
|
||||
focusedWindowTitle = "(No active window)"
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowClosed(data) {
|
||||
windows = windows.filter(w => w.id !== data.id);
|
||||
updateFocusedWindow();
|
||||
}
|
||||
function send(request) {
|
||||
if (!niriAvailable || !requestSocket.connected)
|
||||
return false
|
||||
requestSocket.write(JSON.stringify(request) + "\n")
|
||||
return true
|
||||
}
|
||||
|
||||
function handleWindowFocusChanged(data) {
|
||||
if (data.id) {
|
||||
focusedWindowId = data.id;
|
||||
focusedWindowIndex = windows.findIndex(w => w.id === data.id);
|
||||
} else {
|
||||
focusedWindowId = "";
|
||||
focusedWindowIndex = -1;
|
||||
}
|
||||
updateFocusedWindow();
|
||||
}
|
||||
|
||||
function handleWindowOpenedOrChanged(data) {
|
||||
if (!data.window)
|
||||
return;
|
||||
|
||||
const window = data.window;
|
||||
const existingIndex = windows.findIndex(w => w.id === window.id);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
let updatedWindows = [...windows];
|
||||
updatedWindows[existingIndex] = window;
|
||||
windows = updatedWindows.sort((a, b) => a.id - b.id);
|
||||
} else {
|
||||
windows = [...windows, window].sort((a, b) => a.id - b.id);
|
||||
}
|
||||
|
||||
if (window.is_focused) {
|
||||
focusedWindowId = window.id;
|
||||
focusedWindowIndex = windows.findIndex(w => w.id === window.id);
|
||||
}
|
||||
|
||||
updateFocusedWindow();
|
||||
|
||||
windowOpenedOrChanged(window);
|
||||
}
|
||||
|
||||
function handleOverviewChanged(data) {
|
||||
inOverview = data.is_open;
|
||||
}
|
||||
|
||||
function updateCurrentOutputWorkspaces() {
|
||||
if (!currentOutput) {
|
||||
currentOutputWorkspaces = allWorkspaces;
|
||||
return;
|
||||
}
|
||||
|
||||
var outputWs = allWorkspaces.filter(w => w.output === currentOutput);
|
||||
currentOutputWorkspaces = outputWs;
|
||||
}
|
||||
|
||||
function updateFocusedWindow() {
|
||||
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
|
||||
var focusedWin = windows[focusedWindowIndex];
|
||||
focusedWindowTitle = focusedWin.title || "(Unnamed window)";
|
||||
} else {
|
||||
focusedWindowTitle = "(No active window)";
|
||||
}
|
||||
}
|
||||
|
||||
function send(request) {
|
||||
if (!niriAvailable || !requestSocket.connected)
|
||||
return false;
|
||||
requestSocket.write(JSON.stringify(request) + "\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
function switchToWorkspace(workspaceIndex) {
|
||||
return send({
|
||||
Action: {
|
||||
FocusWorkspace: {
|
||||
reference: {
|
||||
Index: workspaceIndex
|
||||
function switchToWorkspace(workspaceIndex) {
|
||||
return send({
|
||||
"Action": {
|
||||
"FocusWorkspace": {
|
||||
"reference": {
|
||||
"Index": workspaceIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getCurrentOutputWorkspaceNumbers() {
|
||||
return currentOutputWorkspaces.map(w => w.idx + 1); // niri uses 0-based, UI shows 1-based
|
||||
}
|
||||
function getCurrentOutputWorkspaceNumbers() {
|
||||
return currentOutputWorkspaces.map(
|
||||
w => w.idx + 1) // niri uses 0-based, UI shows 1-based
|
||||
}
|
||||
|
||||
function getCurrentWorkspaceNumber() {
|
||||
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
|
||||
return allWorkspaces[focusedWorkspaceIndex].idx + 1;
|
||||
}
|
||||
return 1;
|
||||
function getCurrentWorkspaceNumber() {
|
||||
if (focusedWorkspaceIndex >= 0
|
||||
&& focusedWorkspaceIndex < allWorkspaces.length) {
|
||||
return allWorkspaces[focusedWorkspaceIndex].idx + 1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
function focusWindow(windowId) {
|
||||
return send({
|
||||
Action: {
|
||||
FocusWindow: {
|
||||
id: windowId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function focusWindow(windowId) {
|
||||
return send({
|
||||
"Action": {
|
||||
"FocusWindow": {
|
||||
"id": windowId
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function closeWindow(windowId) {
|
||||
return send({
|
||||
Action: {
|
||||
CloseWindow: {
|
||||
id: windowId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function closeWindow(windowId) {
|
||||
return send({
|
||||
"Action": {
|
||||
"CloseWindow": {
|
||||
"id": windowId
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function quit() {
|
||||
return send({
|
||||
Action: {
|
||||
Quit: {
|
||||
skip_confirmation: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function quit() {
|
||||
return send({
|
||||
"Action": {
|
||||
"Quit": {
|
||||
"skip_confirmation": true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getWindowsByAppId(appId) {
|
||||
if (!appId)
|
||||
return [];
|
||||
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase());
|
||||
}
|
||||
function getWindowsByAppId(appId) {
|
||||
if (!appId)
|
||||
return []
|
||||
return windows.filter(w => w.app_id && w.app_id.toLowerCase(
|
||||
) === appId.toLowerCase())
|
||||
}
|
||||
|
||||
function getRunningAppIds() {
|
||||
var appIds = new Set();
|
||||
windows.forEach(w => {
|
||||
if (w.app_id) {
|
||||
appIds.add(w.app_id.toLowerCase());
|
||||
}
|
||||
});
|
||||
return Array.from(appIds);
|
||||
}
|
||||
function getRunningAppIds() {
|
||||
var appIds = new Set()
|
||||
windows.forEach(w => {
|
||||
if (w.app_id) {
|
||||
appIds.add(w.app_id.toLowerCase())
|
||||
}
|
||||
})
|
||||
return Array.from(appIds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -10,430 +11,449 @@ import qs.Common
|
||||
import "../Common/markdown2html.js" as Markdown2Html
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property list<NotifWrapper> notifications: []
|
||||
readonly property list<NotifWrapper> allWrappers: []
|
||||
readonly property list<NotifWrapper> popups: allWrappers.filter(n => n.popup)
|
||||
|
||||
property list<NotifWrapper> notificationQueue: []
|
||||
property list<NotifWrapper> visibleNotifications: []
|
||||
property int maxVisibleNotifications: 3
|
||||
property bool addGateBusy: false
|
||||
property int enterAnimMs: 400
|
||||
property int seqCounter: 0
|
||||
property bool bulkDismissing: false
|
||||
readonly property list<NotifWrapper> notifications: []
|
||||
readonly property list<NotifWrapper> allWrappers: []
|
||||
readonly property list<NotifWrapper> popups: allWrappers.filter(n => n.popup)
|
||||
|
||||
Timer {
|
||||
id: addGate
|
||||
interval: enterAnimMs + 50
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: { addGateBusy = false; processQueue(); }
|
||||
property list<NotifWrapper> notificationQueue: []
|
||||
property list<NotifWrapper> visibleNotifications: []
|
||||
property int maxVisibleNotifications: 3
|
||||
property bool addGateBusy: false
|
||||
property int enterAnimMs: 400
|
||||
property int seqCounter: 0
|
||||
property bool bulkDismissing: false
|
||||
|
||||
Timer {
|
||||
id: addGate
|
||||
interval: enterAnimMs + 50
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
addGateBusy = false
|
||||
processQueue()
|
||||
}
|
||||
|
||||
// Android 16-style grouped notifications
|
||||
readonly property var groupedNotifications: getGroupedNotifications()
|
||||
readonly property var groupedPopups: getGroupedPopups()
|
||||
|
||||
|
||||
property var expandedGroups: ({})
|
||||
property var expandedMessages: ({})
|
||||
property bool popupsDisabled: false
|
||||
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
// Android 16-style grouped notifications
|
||||
readonly property var groupedNotifications: getGroupedNotifications()
|
||||
readonly property var groupedPopups: getGroupedPopups()
|
||||
|
||||
keepOnReload: false
|
||||
actionsSupported: true
|
||||
actionIconsSupported: true
|
||||
bodyHyperlinksSupported: true
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
imageSupported: true
|
||||
inlineReplySupported: true
|
||||
property var expandedGroups: ({})
|
||||
property var expandedMessages: ({})
|
||||
property bool popupsDisabled: false
|
||||
|
||||
onNotification: notif => {
|
||||
notif.tracked = true;
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb;
|
||||
const wrapper = notifComponent.createObject(root, {
|
||||
popup: shouldShowPopup,
|
||||
notification: notif
|
||||
});
|
||||
keepOnReload: false
|
||||
actionsSupported: true
|
||||
actionIconsSupported: true
|
||||
bodyHyperlinksSupported: true
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
imageSupported: true
|
||||
inlineReplySupported: true
|
||||
|
||||
if (wrapper) {
|
||||
root.allWrappers.push(wrapper);
|
||||
root.notifications.push(wrapper);
|
||||
|
||||
if (shouldShowPopup) {
|
||||
notificationQueue = [...notificationQueue, wrapper];
|
||||
processQueue();
|
||||
}
|
||||
}
|
||||
onNotification: notif => {
|
||||
notif.tracked = true
|
||||
|
||||
const shouldShowPopup = !root.popupsDisabled && !SessionData.doNotDisturb
|
||||
const wrapper = notifComponent.createObject(root, {
|
||||
"popup": shouldShowPopup,
|
||||
"notification": notif
|
||||
})
|
||||
|
||||
if (wrapper) {
|
||||
root.allWrappers.push(wrapper)
|
||||
root.notifications.push(wrapper)
|
||||
|
||||
if (shouldShowPopup) {
|
||||
notificationQueue = [...notificationQueue, wrapper]
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NotifWrapper: QtObject {
|
||||
id: wrapper
|
||||
|
||||
property bool popup: false
|
||||
property bool removedByLimit: false
|
||||
property bool isPersistent: true
|
||||
property int seq: 0
|
||||
|
||||
onPopupChanged: {
|
||||
if (!popup) {
|
||||
removeFromVisibleNotifications(wrapper)
|
||||
}
|
||||
}
|
||||
|
||||
component NotifWrapper: QtObject {
|
||||
id: wrapper
|
||||
readonly property Timer timer: Timer {
|
||||
interval: 5000
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
wrapper.popup = false
|
||||
}
|
||||
}
|
||||
readonly property date time: new Date()
|
||||
readonly property string timeStr: {
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - time.getTime()
|
||||
const m = Math.floor(diff / 60000)
|
||||
const h = Math.floor(m / 60)
|
||||
|
||||
property bool popup: false
|
||||
property bool removedByLimit: false
|
||||
property bool isPersistent: true
|
||||
property int seq: 0
|
||||
|
||||
onPopupChanged: {
|
||||
if (!popup) {
|
||||
removeFromVisibleNotifications(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
readonly property Timer timer: Timer {
|
||||
interval: 5000
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
wrapper.popup = false;
|
||||
}
|
||||
}
|
||||
readonly property date time: new Date()
|
||||
readonly property string timeStr: {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - time.getTime();
|
||||
const m = Math.floor(diff / 60000);
|
||||
const h = Math.floor(m / 60);
|
||||
|
||||
if (h < 1 && m < 1)
|
||||
return "now";
|
||||
if (h < 1)
|
||||
return `${m}m`;
|
||||
return `${h}h`;
|
||||
}
|
||||
|
||||
required property Notification notification
|
||||
readonly property string summary: notification.summary
|
||||
readonly property string body: notification.body
|
||||
readonly property string htmlBody: {
|
||||
if (body && (body.includes('<') && body.includes('>'))) {
|
||||
return body;
|
||||
}
|
||||
return Markdown2Html.markdownToHtml(body);
|
||||
}
|
||||
readonly property string appIcon: notification.appIcon
|
||||
readonly property string appName: {
|
||||
if (notification.appName == "") {
|
||||
// try to get the app name from the desktop entry
|
||||
const entry = DesktopEntries.byId(notification.desktopEntry);
|
||||
if (entry && entry.name) {
|
||||
return entry.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
return notification.appName || "app";
|
||||
}
|
||||
readonly property string desktopEntry: notification.desktopEntry
|
||||
readonly property string image: notification.image
|
||||
readonly property string cleanImage: {
|
||||
if (!image) return "";
|
||||
if (image.startsWith("file://")) {
|
||||
return image.substring(7);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
readonly property int urgency: notification.urgency
|
||||
readonly property list<NotificationAction> actions: notification.actions
|
||||
|
||||
readonly property bool hasImage: image && image.length > 0
|
||||
readonly property bool hasAppIcon: appIcon && appIcon.length > 0
|
||||
|
||||
readonly property Connections conn: Connections {
|
||||
target: wrapper.notification.Retainable
|
||||
|
||||
function onDropped(): void {
|
||||
const notifIndex = root.notifications.indexOf(wrapper);
|
||||
const allIndex = root.allWrappers.indexOf(wrapper);
|
||||
if (allIndex !== -1) root.allWrappers.splice(allIndex, 1);
|
||||
if (notifIndex !== -1) root.notifications.splice(notifIndex, 1);
|
||||
|
||||
if (root.bulkDismissing) return;
|
||||
|
||||
const groupKey = getGroupKey(wrapper);
|
||||
const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey);
|
||||
|
||||
if (remainingInGroup.length <= 1) {
|
||||
clearGroupExpansionState(groupKey);
|
||||
}
|
||||
|
||||
cleanupExpansionStates();
|
||||
}
|
||||
|
||||
function onAboutToDestroy(): void {
|
||||
wrapper.destroy();
|
||||
}
|
||||
}
|
||||
if (h < 1 && m < 1)
|
||||
return "now"
|
||||
if (h < 1)
|
||||
return `${m}m`
|
||||
return `${h}h`
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notifComponent
|
||||
NotifWrapper {}
|
||||
required property Notification notification
|
||||
readonly property string summary: notification.summary
|
||||
readonly property string body: notification.body
|
||||
readonly property string htmlBody: {
|
||||
if (body && (body.includes('<') && body.includes('>'))) {
|
||||
return body
|
||||
}
|
||||
return Markdown2Html.markdownToHtml(body)
|
||||
}
|
||||
readonly property string appIcon: notification.appIcon
|
||||
readonly property string appName: {
|
||||
if (notification.appName == "") {
|
||||
// try to get the app name from the desktop entry
|
||||
const entry = DesktopEntries.byId(notification.desktopEntry)
|
||||
if (entry && entry.name) {
|
||||
return entry.name.toLowerCase()
|
||||
}
|
||||
}
|
||||
return notification.appName || "app"
|
||||
}
|
||||
readonly property string desktopEntry: notification.desktopEntry
|
||||
readonly property string image: notification.image
|
||||
readonly property string cleanImage: {
|
||||
if (!image)
|
||||
return ""
|
||||
if (image.startsWith("file://")) {
|
||||
return image.substring(7)
|
||||
}
|
||||
return image
|
||||
}
|
||||
readonly property int urgency: notification.urgency
|
||||
readonly property list<NotificationAction> actions: notification.actions
|
||||
|
||||
readonly property bool hasImage: image && image.length > 0
|
||||
readonly property bool hasAppIcon: appIcon && appIcon.length > 0
|
||||
|
||||
readonly property Connections conn: Connections {
|
||||
target: wrapper.notification.Retainable
|
||||
|
||||
function onDropped(): void {
|
||||
const notifIndex = root.notifications.indexOf(wrapper)
|
||||
const allIndex = root.allWrappers.indexOf(wrapper)
|
||||
if (allIndex !== -1)
|
||||
root.allWrappers.splice(allIndex, 1)
|
||||
if (notifIndex !== -1)
|
||||
root.notifications.splice(notifIndex, 1)
|
||||
|
||||
if (root.bulkDismissing)
|
||||
return
|
||||
|
||||
const groupKey = getGroupKey(wrapper)
|
||||
const remainingInGroup = root.notifications.filter(n => getGroupKey(
|
||||
n) === groupKey)
|
||||
|
||||
if (remainingInGroup.length <= 1) {
|
||||
clearGroupExpansionState(groupKey)
|
||||
}
|
||||
|
||||
cleanupExpansionStates()
|
||||
}
|
||||
|
||||
function onAboutToDestroy(): void {
|
||||
wrapper.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notifComponent
|
||||
NotifWrapper {}
|
||||
}
|
||||
|
||||
function clearAllNotifications() {
|
||||
bulkDismissing = true
|
||||
popupsDisabled = true
|
||||
addGate.stop()
|
||||
addGateBusy = false
|
||||
notificationQueue = []
|
||||
|
||||
for (const w of visibleNotifications)
|
||||
w.popup = false
|
||||
visibleNotifications = []
|
||||
|
||||
const toDismiss = notifications.slice()
|
||||
|
||||
if (notifications.length)
|
||||
notifications.splice(0, notifications.length)
|
||||
expandedGroups = {}
|
||||
expandedMessages = {}
|
||||
|
||||
for (var i = 0; i < toDismiss.length; ++i) {
|
||||
const w = toDismiss[i]
|
||||
if (w && w.notification) {
|
||||
try {
|
||||
w.notification.dismiss()
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearAllNotifications() {
|
||||
bulkDismissing = true;
|
||||
popupsDisabled = true;
|
||||
addGate.stop();
|
||||
addGateBusy = false;
|
||||
notificationQueue = [];
|
||||
bulkDismissing = false
|
||||
popupsDisabled = false
|
||||
}
|
||||
|
||||
for (const w of visibleNotifications) w.popup = false;
|
||||
visibleNotifications = [];
|
||||
function dismissNotification(wrapper) {
|
||||
if (!wrapper || !wrapper.notification)
|
||||
return
|
||||
wrapper.popup = false
|
||||
wrapper.notification.dismiss()
|
||||
}
|
||||
|
||||
const toDismiss = notifications.slice();
|
||||
function disablePopups(disable) {
|
||||
popupsDisabled = disable
|
||||
if (disable) {
|
||||
notificationQueue = []
|
||||
visibleNotifications = []
|
||||
for (const notif of root.allWrappers) {
|
||||
notif.popup = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notifications.length) notifications.splice(0, notifications.length);
|
||||
expandedGroups = {};
|
||||
expandedMessages = {};
|
||||
function processQueue() {
|
||||
if (addGateBusy)
|
||||
return
|
||||
if (popupsDisabled)
|
||||
return
|
||||
if (SessionData.doNotDisturb)
|
||||
return
|
||||
if (notificationQueue.length === 0)
|
||||
return
|
||||
|
||||
for (let i = 0; i < toDismiss.length; ++i) {
|
||||
const w = toDismiss[i];
|
||||
if (w && w.notification) {
|
||||
try { w.notification.dismiss(); } catch (e) {}
|
||||
}
|
||||
}
|
||||
const next = notificationQueue.shift()
|
||||
|
||||
bulkDismissing = false;
|
||||
popupsDisabled = false;
|
||||
next.seq = ++seqCounter
|
||||
visibleNotifications = [...visibleNotifications, next]
|
||||
next.popup = true
|
||||
|
||||
addGateBusy = true
|
||||
addGate.restart()
|
||||
}
|
||||
|
||||
function removeFromVisibleNotifications(wrapper) {
|
||||
const i = visibleNotifications.findIndex(n => n === wrapper)
|
||||
if (i !== -1) {
|
||||
const v = [...visibleNotifications]
|
||||
v.splice(i, 1)
|
||||
visibleNotifications = v
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
function releaseWrapper(w) {
|
||||
// Remove from visible
|
||||
let v = visibleNotifications.slice()
|
||||
const vi = v.indexOf(w)
|
||||
if (vi !== -1) {
|
||||
v.splice(vi, 1)
|
||||
visibleNotifications = v
|
||||
}
|
||||
|
||||
function dismissNotification(wrapper) {
|
||||
if (!wrapper || !wrapper.notification) return;
|
||||
wrapper.popup = false;
|
||||
wrapper.notification.dismiss();
|
||||
}
|
||||
|
||||
function disablePopups(disable) {
|
||||
popupsDisabled = disable;
|
||||
if (disable) {
|
||||
notificationQueue = [];
|
||||
visibleNotifications = [];
|
||||
for (const notif of root.allWrappers) {
|
||||
notif.popup = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (addGateBusy) return;
|
||||
if (popupsDisabled) return;
|
||||
if (SessionData.doNotDisturb) return;
|
||||
if (notificationQueue.length === 0) return;
|
||||
|
||||
const [next, ...rest] = notificationQueue;
|
||||
notificationQueue = rest;
|
||||
|
||||
next.seq = ++seqCounter;
|
||||
visibleNotifications = [...visibleNotifications, next];
|
||||
next.popup = true;
|
||||
|
||||
addGateBusy = true;
|
||||
addGate.restart();
|
||||
// Remove from queue
|
||||
let q = notificationQueue.slice()
|
||||
const qi = q.indexOf(w)
|
||||
if (qi !== -1) {
|
||||
q.splice(qi, 1)
|
||||
notificationQueue = q
|
||||
}
|
||||
|
||||
function removeFromVisibleNotifications(wrapper) {
|
||||
const i = visibleNotifications.findIndex(n => n === wrapper);
|
||||
if (i !== -1) {
|
||||
const v = [...visibleNotifications]; v.splice(i, 1);
|
||||
visibleNotifications = v;
|
||||
processQueue();
|
||||
}
|
||||
// Destroy wrapper if non-persistent
|
||||
if (w && w.destroy && !w.isPersistent) {
|
||||
w.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// Android 16-style notification grouping functions
|
||||
function getGroupKey(wrapper) {
|
||||
// Priority 1: Use desktopEntry if available
|
||||
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
|
||||
return wrapper.desktopEntry.toLowerCase()
|
||||
}
|
||||
|
||||
function releaseWrapper(w) {
|
||||
// Remove from visible
|
||||
let v = visibleNotifications.slice();
|
||||
const vi = v.indexOf(w);
|
||||
if (vi !== -1) {
|
||||
v.splice(vi, 1);
|
||||
visibleNotifications = v;
|
||||
}
|
||||
// Priority 2: Use appName as fallback
|
||||
return wrapper.appName.toLowerCase()
|
||||
}
|
||||
|
||||
// Remove from queue
|
||||
let q = notificationQueue.slice();
|
||||
const qi = q.indexOf(w);
|
||||
if (qi !== -1) {
|
||||
q.splice(qi, 1);
|
||||
notificationQueue = q;
|
||||
}
|
||||
function getGroupedNotifications() {
|
||||
const groups = {}
|
||||
|
||||
// Destroy wrapper if non-persistent
|
||||
if (w && w.destroy && !w.isPersistent) {
|
||||
w.destroy();
|
||||
for (const notif of notifications) {
|
||||
const groupKey = getGroupKey(notif)
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = {
|
||||
"key": groupKey,
|
||||
"appName": notif.appName,
|
||||
"notifications": [],
|
||||
"latestNotification": null,
|
||||
"count": 0,
|
||||
"hasInlineReply": false
|
||||
}
|
||||
}
|
||||
|
||||
groups[groupKey].notifications.unshift(notif)
|
||||
groups[groupKey].latestNotification = groups[groupKey].notifications[0]
|
||||
groups[groupKey].count = groups[groupKey].notifications.length
|
||||
|
||||
if (notif.notification.hasInlineReply) {
|
||||
groups[groupKey].hasInlineReply = true
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(groups).sort((a, b) => {
|
||||
const aUrgency = a.latestNotification.urgency
|
||||
|| NotificationUrgency.Low
|
||||
const bUrgency = b.latestNotification.urgency
|
||||
|| NotificationUrgency.Low
|
||||
if (aUrgency !== bUrgency) {
|
||||
return bUrgency - aUrgency
|
||||
}
|
||||
return b.latestNotification.time.getTime(
|
||||
) - a.latestNotification.time.getTime(
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Android 16-style notification grouping functions
|
||||
function getGroupKey(wrapper) {
|
||||
// Priority 1: Use desktopEntry if available
|
||||
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
|
||||
return wrapper.desktopEntry.toLowerCase();
|
||||
function getGroupedPopups() {
|
||||
const groups = {}
|
||||
|
||||
for (const notif of popups) {
|
||||
const groupKey = getGroupKey(notif)
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = {
|
||||
"key": groupKey,
|
||||
"appName": notif.appName,
|
||||
"notifications": [],
|
||||
"latestNotification": null,
|
||||
"count": 0,
|
||||
"hasInlineReply": false
|
||||
}
|
||||
|
||||
// Priority 2: Use appName as fallback
|
||||
return wrapper.appName.toLowerCase();
|
||||
}
|
||||
|
||||
groups[groupKey].notifications.unshift(notif)
|
||||
groups[groupKey].latestNotification = groups[groupKey].notifications[0]
|
||||
groups[groupKey].count = groups[groupKey].notifications.length
|
||||
|
||||
if (notif.notification.hasInlineReply) {
|
||||
groups[groupKey].hasInlineReply = true
|
||||
}
|
||||
}
|
||||
|
||||
function getGroupedNotifications() {
|
||||
const groups = {};
|
||||
|
||||
for (const notif of notifications) {
|
||||
const groupKey = getGroupKey(notif);
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = {
|
||||
key: groupKey,
|
||||
appName: notif.appName,
|
||||
notifications: [],
|
||||
latestNotification: null,
|
||||
count: 0,
|
||||
hasInlineReply: false,
|
||||
};
|
||||
}
|
||||
|
||||
groups[groupKey].notifications.unshift(notif);
|
||||
groups[groupKey].latestNotification = groups[groupKey].notifications[0];
|
||||
groups[groupKey].count = groups[groupKey].notifications.length;
|
||||
|
||||
if (notif.notification.hasInlineReply) {
|
||||
groups[groupKey].hasInlineReply = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(groups).sort((a, b) => {
|
||||
const aUrgency = a.latestNotification.urgency || NotificationUrgency.Low;
|
||||
const bUrgency = b.latestNotification.urgency || NotificationUrgency.Low;
|
||||
if (aUrgency !== bUrgency) {
|
||||
return bUrgency - aUrgency;
|
||||
}
|
||||
return b.latestNotification.time.getTime() - a.latestNotification.time.getTime();
|
||||
});
|
||||
}
|
||||
return Object.values(groups).sort((a, b) => {
|
||||
return b.latestNotification.time.getTime(
|
||||
) - a.latestNotification.time.getTime(
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function getGroupedPopups() {
|
||||
const groups = {};
|
||||
|
||||
for (const notif of popups) {
|
||||
const groupKey = getGroupKey(notif);
|
||||
if (!groups[groupKey]) {
|
||||
groups[groupKey] = {
|
||||
key: groupKey,
|
||||
appName: notif.appName,
|
||||
notifications: [],
|
||||
latestNotification: null,
|
||||
count: 0,
|
||||
hasInlineReply: false,
|
||||
};
|
||||
}
|
||||
|
||||
groups[groupKey].notifications.unshift(notif);
|
||||
groups[groupKey].latestNotification = groups[groupKey].notifications[0];
|
||||
groups[groupKey].count = groups[groupKey].notifications.length;
|
||||
|
||||
if (notif.notification.hasInlineReply) {
|
||||
groups[groupKey].hasInlineReply = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(groups).sort((a, b) => {
|
||||
return b.latestNotification.time.getTime() - a.latestNotification.time.getTime();
|
||||
});
|
||||
function toggleGroupExpansion(groupKey) {
|
||||
let newExpandedGroups = {}
|
||||
for (const key in expandedGroups) {
|
||||
newExpandedGroups[key] = expandedGroups[key]
|
||||
}
|
||||
newExpandedGroups[groupKey] = !newExpandedGroups[groupKey]
|
||||
expandedGroups = newExpandedGroups
|
||||
}
|
||||
|
||||
function toggleGroupExpansion(groupKey) {
|
||||
let newExpandedGroups = {};
|
||||
for (const key in expandedGroups) {
|
||||
newExpandedGroups[key] = expandedGroups[key];
|
||||
function dismissGroup(groupKey) {
|
||||
const group = groupedNotifications.find(g => g.key === groupKey)
|
||||
if (group) {
|
||||
for (const notif of group.notifications) {
|
||||
if (notif && notif.notification) {
|
||||
notif.notification.dismiss()
|
||||
}
|
||||
newExpandedGroups[groupKey] = !newExpandedGroups[groupKey];
|
||||
expandedGroups = newExpandedGroups;
|
||||
}
|
||||
} else {
|
||||
for (const notif of allWrappers) {
|
||||
if (notif && notif.notification && getGroupKey(notif) === groupKey) {
|
||||
notif.notification.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dismissGroup(groupKey) {
|
||||
const group = groupedNotifications.find(g => g.key === groupKey);
|
||||
if (group) {
|
||||
for (const notif of group.notifications) {
|
||||
if (notif && notif.notification) {
|
||||
notif.notification.dismiss();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const notif of allWrappers) {
|
||||
if (notif && notif.notification && getGroupKey(notif) === groupKey) {
|
||||
notif.notification.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
function clearGroupExpansionState(groupKey) {
|
||||
let newExpandedGroups = {}
|
||||
for (const key in expandedGroups) {
|
||||
if (key !== groupKey && expandedGroups[key]) {
|
||||
newExpandedGroups[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
function clearGroupExpansionState(groupKey) {
|
||||
let newExpandedGroups = {};
|
||||
for (const key in expandedGroups) {
|
||||
if (key !== groupKey && expandedGroups[key]) {
|
||||
newExpandedGroups[key] = true;
|
||||
}
|
||||
}
|
||||
expandedGroups = newExpandedGroups;
|
||||
expandedGroups = newExpandedGroups
|
||||
}
|
||||
|
||||
function cleanupExpansionStates() {
|
||||
const currentGroupKeys = new Set(groupedNotifications.map(g => g.key))
|
||||
const currentMessageIds = new Set()
|
||||
for (const group of groupedNotifications) {
|
||||
for (const notif of group.notifications) {
|
||||
currentMessageIds.add(notif.notification.id)
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupExpansionStates() {
|
||||
const currentGroupKeys = new Set(groupedNotifications.map(g => g.key));
|
||||
const currentMessageIds = new Set();
|
||||
for (const group of groupedNotifications) {
|
||||
for (const notif of group.notifications) {
|
||||
currentMessageIds.add(notif.notification.id);
|
||||
}
|
||||
}
|
||||
let newExpandedGroups = {};
|
||||
for (const key in expandedGroups) {
|
||||
if (currentGroupKeys.has(key) && expandedGroups[key]) {
|
||||
newExpandedGroups[key] = true;
|
||||
}
|
||||
}
|
||||
expandedGroups = newExpandedGroups;
|
||||
let newExpandedMessages = {};
|
||||
for (const messageId in expandedMessages) {
|
||||
if (currentMessageIds.has(messageId) && expandedMessages[messageId]) {
|
||||
newExpandedMessages[messageId] = true;
|
||||
}
|
||||
}
|
||||
expandedMessages = newExpandedMessages;
|
||||
let newExpandedGroups = {}
|
||||
for (const key in expandedGroups) {
|
||||
if (currentGroupKeys.has(key) && expandedGroups[key]) {
|
||||
newExpandedGroups[key] = true
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMessageExpansion(messageId) {
|
||||
let newExpandedMessages = {};
|
||||
for (const key in expandedMessages) {
|
||||
newExpandedMessages[key] = expandedMessages[key];
|
||||
}
|
||||
newExpandedMessages[messageId] = !newExpandedMessages[messageId];
|
||||
expandedMessages = newExpandedMessages;
|
||||
expandedGroups = newExpandedGroups
|
||||
let newExpandedMessages = {}
|
||||
for (const messageId in expandedMessages) {
|
||||
if (currentMessageIds.has(messageId) && expandedMessages[messageId]) {
|
||||
newExpandedMessages[messageId] = true
|
||||
}
|
||||
}
|
||||
expandedMessages = newExpandedMessages
|
||||
}
|
||||
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onDoNotDisturbChanged() {
|
||||
if (SessionData.doNotDisturb) {
|
||||
// Hide all current popups when DND is enabled
|
||||
for (const notif of visibleNotifications) {
|
||||
notif.popup = false;
|
||||
}
|
||||
visibleNotifications = [];
|
||||
notificationQueue = [];
|
||||
} else {
|
||||
// Re-enable popup processing when DND is disabled
|
||||
processQueue();
|
||||
}
|
||||
}
|
||||
function toggleMessageExpansion(messageId) {
|
||||
let newExpandedMessages = {}
|
||||
for (const key in expandedMessages) {
|
||||
newExpandedMessages[key] = expandedMessages[key]
|
||||
}
|
||||
newExpandedMessages[messageId] = !newExpandedMessages[messageId]
|
||||
expandedMessages = newExpandedMessages
|
||||
}
|
||||
|
||||
}
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onDoNotDisturbChanged() {
|
||||
if (SessionData.doNotDisturb) {
|
||||
// Hide all current popups when DND is enabled
|
||||
for (const notif of visibleNotifications) {
|
||||
notif.popup = false
|
||||
}
|
||||
visibleNotifications = []
|
||||
notificationQueue = []
|
||||
} else {
|
||||
// Re-enable popup processing when DND is disabled
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,180 +1,180 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
property bool accountsServiceAvailable: false
|
||||
property string systemProfileImage: ""
|
||||
property string profileImage: ""
|
||||
property bool settingsPortalAvailable: false
|
||||
property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light
|
||||
|
||||
function getSystemProfileImage() {
|
||||
systemProfileCheckProcess.running = true
|
||||
property bool accountsServiceAvailable: false
|
||||
property string systemProfileImage: ""
|
||||
property string profileImage: ""
|
||||
property bool settingsPortalAvailable: false
|
||||
property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light
|
||||
|
||||
function getSystemProfileImage() {
|
||||
systemProfileCheckProcess.running = true
|
||||
}
|
||||
|
||||
function setProfileImage(imagePath) {
|
||||
profileImage = imagePath
|
||||
if (accountsServiceAvailable && imagePath) {
|
||||
setSystemProfileImage(imagePath)
|
||||
}
|
||||
|
||||
function setProfileImage(imagePath) {
|
||||
profileImage = imagePath
|
||||
if (accountsServiceAvailable && imagePath) {
|
||||
setSystemProfileImage(imagePath)
|
||||
}
|
||||
|
||||
function getSystemColorScheme() {
|
||||
systemColorSchemeCheckProcess.running = true
|
||||
}
|
||||
|
||||
function setLightMode(isLightMode) {
|
||||
if (settingsPortalAvailable) {
|
||||
setSystemColorScheme(isLightMode)
|
||||
}
|
||||
}
|
||||
|
||||
function setSystemColorScheme(isLightMode) {
|
||||
if (!settingsPortalAvailable)
|
||||
return
|
||||
|
||||
var colorScheme = isLightMode ? "prefer-light" : "prefer-dark"
|
||||
var script = "gsettings set org.gnome.desktop.interface color-scheme '" + colorScheme + "'"
|
||||
|
||||
systemColorSchemeSetProcess.command = ["bash", "-c", script]
|
||||
systemColorSchemeSetProcess.running = true
|
||||
}
|
||||
|
||||
function setSystemProfileImage(imagePath) {
|
||||
if (!accountsServiceAvailable || !imagePath)
|
||||
return
|
||||
|
||||
var script = ["dbus-send --system --print-reply --dest=org.freedesktop.Accounts", "/org/freedesktop/Accounts/User$(id -u)", "org.freedesktop.Accounts.User.SetIconFile", "string:'" + imagePath + "'"].join(
|
||||
" ")
|
||||
|
||||
systemProfileSetProcess.command = ["bash", "-c", script]
|
||||
systemProfileSetProcess.running = true
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
checkAccountsService()
|
||||
checkSettingsPortal()
|
||||
}
|
||||
|
||||
function checkAccountsService() {
|
||||
accountsServiceCheckProcess.running = true
|
||||
}
|
||||
|
||||
function checkSettingsPortal() {
|
||||
settingsPortalCheckProcess.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: accountsServiceCheckProcess
|
||||
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts org.freedesktop.Accounts.FindUserByName string:\"$USER\""]
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
root.accountsServiceAvailable = (exitCode === 0)
|
||||
if (root.accountsServiceAvailable) {
|
||||
root.getSystemProfileImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemProfileCheckProcess
|
||||
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var match = text.match(/string\s+"([^"]+)"/)
|
||||
if (match && match[1] && match[1] !== ""
|
||||
&& match[1] !== "/var/lib/AccountsService/icons/") {
|
||||
root.systemProfileImage = match[1]
|
||||
|
||||
if (!root.profileImage || root.profileImage === "") {
|
||||
root.profileImage = root.systemProfileImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemColorScheme() {
|
||||
systemColorSchemeCheckProcess.running = true
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.systemProfileImage = ""
|
||||
}
|
||||
}
|
||||
|
||||
function setLightMode(isLightMode) {
|
||||
if (settingsPortalAvailable) {
|
||||
setSystemColorScheme(isLightMode)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemProfileSetProcess
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
root.getSystemProfileImage()
|
||||
}
|
||||
}
|
||||
|
||||
function setSystemColorScheme(isLightMode) {
|
||||
if (!settingsPortalAvailable) return
|
||||
|
||||
var colorScheme = isLightMode ? "prefer-light" : "prefer-dark"
|
||||
var script = "gsettings set org.gnome.desktop.interface color-scheme '" + colorScheme + "'"
|
||||
|
||||
systemColorSchemeSetProcess.command = ["bash", "-c", script]
|
||||
systemColorSchemeSetProcess.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: settingsPortalCheckProcess
|
||||
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
root.settingsPortalAvailable = (exitCode === 0)
|
||||
if (root.settingsPortalAvailable) {
|
||||
root.getSystemColorScheme()
|
||||
}
|
||||
}
|
||||
|
||||
function setSystemProfileImage(imagePath) {
|
||||
if (!accountsServiceAvailable || !imagePath) return
|
||||
|
||||
var script = [
|
||||
"dbus-send --system --print-reply --dest=org.freedesktop.Accounts",
|
||||
"/org/freedesktop/Accounts/User$(id -u)",
|
||||
"org.freedesktop.Accounts.User.SetIconFile",
|
||||
"string:'" + imagePath + "'"
|
||||
].join(" ")
|
||||
|
||||
systemProfileSetProcess.command = ["bash", "-c", script]
|
||||
systemProfileSetProcess.running = true
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
checkAccountsService()
|
||||
checkSettingsPortal()
|
||||
}
|
||||
|
||||
function checkAccountsService() {
|
||||
accountsServiceCheckProcess.running = true
|
||||
}
|
||||
|
||||
function checkSettingsPortal() {
|
||||
settingsPortalCheckProcess.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: accountsServiceCheckProcess
|
||||
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts org.freedesktop.Accounts.FindUserByName string:\"$USER\""]
|
||||
running: false
|
||||
|
||||
onExited: (exitCode) => {
|
||||
root.accountsServiceAvailable = (exitCode === 0)
|
||||
if (root.accountsServiceAvailable) {
|
||||
root.getSystemProfileImage()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemColorSchemeCheckProcess
|
||||
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var match = text.match(/uint32 (\d+)/)
|
||||
if (match && match[1]) {
|
||||
root.systemColorScheme = parseInt(match[1])
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
var shouldBeLightMode = (root.systemColorScheme === 2)
|
||||
if (Theme.isLightMode !== shouldBeLightMode) {
|
||||
Theme.isLightMode = shouldBeLightMode
|
||||
if (typeof SessionData !== "undefined") {
|
||||
SessionData.setLightMode(shouldBeLightMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemProfileCheckProcess
|
||||
command: ["bash", "-c", "dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.DBus.Properties.Get string:org.freedesktop.Accounts.User string:IconFile"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var match = text.match(/string\s+"([^"]+)"/)
|
||||
if (match && match[1] && match[1] !== "" && match[1] !== "/var/lib/AccountsService/icons/") {
|
||||
root.systemProfileImage = match[1]
|
||||
|
||||
if (!root.profileImage || root.profileImage === "") {
|
||||
root.profileImage = root.systemProfileImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
root.systemProfileImage = ""
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.systemColorScheme = 0
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemProfileSetProcess
|
||||
running: false
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
root.getSystemProfileImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemColorSchemeSetProcess
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
Qt.callLater(() => {
|
||||
root.getSystemColorScheme()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: settingsPortalCheckProcess
|
||||
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
|
||||
running: false
|
||||
|
||||
onExited: (exitCode) => {
|
||||
root.settingsPortalAvailable = (exitCode === 0)
|
||||
if (root.settingsPortalAvailable) {
|
||||
root.getSystemColorScheme()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemColorSchemeCheckProcess
|
||||
command: ["gdbus", "call", "--session", "--dest", "org.freedesktop.portal.Desktop", "--object-path", "/org/freedesktop/portal/desktop", "--method", "org.freedesktop.portal.Settings.ReadOne", "org.freedesktop.appearance", "color-scheme"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var match = text.match(/uint32 (\d+)/)
|
||||
if (match && match[1]) {
|
||||
root.systemColorScheme = parseInt(match[1])
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
var shouldBeLightMode = (root.systemColorScheme === 2)
|
||||
if (Theme.isLightMode !== shouldBeLightMode) {
|
||||
Theme.isLightMode = shouldBeLightMode
|
||||
if (typeof SessionData !== "undefined") {
|
||||
SessionData.setLightMode(shouldBeLightMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
root.systemColorScheme = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemColorSchemeSetProcess
|
||||
running: false
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
Qt.callLater(() => {
|
||||
root.getSystemColorScheme()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -7,119 +8,136 @@ import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property bool microphoneActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values) return false
|
||||
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node) continue
|
||||
|
||||
if ((node.type & PwNodeType.AudioInStream) === PwNodeType.AudioInStream) {
|
||||
if (!looksLikeSystemVirtualMic(node)) {
|
||||
console.log(node.audio)
|
||||
if (node.audio && node.audio.muted) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
readonly property bool microphoneActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values)
|
||||
return false
|
||||
|
||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node)
|
||||
continue
|
||||
|
||||
if ((node.type & PwNodeType.AudioInStream) === PwNodeType.AudioInStream) {
|
||||
if (!looksLikeSystemVirtualMic(node)) {
|
||||
console.log(node.audio)
|
||||
if (node.audio && node.audio.muted) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: Pipewire.nodes.values
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: Pipewire.nodes.values
|
||||
}
|
||||
readonly property bool cameraActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values)
|
||||
return false
|
||||
|
||||
readonly property bool cameraActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values) return false
|
||||
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node || !node.ready) continue
|
||||
|
||||
if (node.properties && node.properties["media.class"] === "Stream/Input/Video") {
|
||||
if (node.properties["stream.is-live"] === "true") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node || !node.ready)
|
||||
continue
|
||||
|
||||
if (node.properties
|
||||
&& node.properties["media.class"] === "Stream/Input/Video") {
|
||||
if (node.properties["stream.is-live"] === "true") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
readonly property bool screensharingActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values) return false
|
||||
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node || !node.ready) continue
|
||||
|
||||
if ((node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource) {
|
||||
if (looksLikeScreencast(node)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") {
|
||||
const mediaName = (node.properties["media.name"] || "").toLowerCase()
|
||||
const appName = (node.properties["application.name"] || "").toLowerCase()
|
||||
|
||||
if (mediaName.includes("desktop") || appName.includes("screen") || appName === "obs") {
|
||||
if (node.properties["stream.is-live"] === "true") {
|
||||
if (node.audio && node.audio.muted) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
readonly property bool screensharingActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values)
|
||||
return false
|
||||
|
||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node || !node.ready)
|
||||
continue
|
||||
|
||||
if ((node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource) {
|
||||
if (looksLikeScreencast(node)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
readonly property bool anyPrivacyActive: microphoneActive || cameraActive || screensharingActive
|
||||
}
|
||||
|
||||
if (node.properties
|
||||
&& node.properties["media.class"] === "Stream/Input/Audio") {
|
||||
const mediaName = (node.properties["media.name"] || "").toLowerCase()
|
||||
const appName = (node.properties["application.name"]
|
||||
|| "").toLowerCase()
|
||||
|
||||
function looksLikeSystemVirtualMic(node) {
|
||||
if (!node) return false
|
||||
const name = (node.name || "").toLowerCase()
|
||||
const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase()
|
||||
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
|
||||
const combined = name + " " + mediaName + " " + appName
|
||||
return /cava|monitor|system/.test(combined)
|
||||
if (mediaName.includes("desktop") || appName.includes("screen")
|
||||
|| appName === "obs") {
|
||||
if (node.properties["stream.is-live"] === "true") {
|
||||
if (node.audio && node.audio.muted) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function looksLikeScreencast(node) {
|
||||
if (!node) return false
|
||||
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
|
||||
const nodeName = (node.name || "").toLowerCase()
|
||||
const combined = appName + " " + nodeName
|
||||
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined)
|
||||
}
|
||||
readonly property bool anyPrivacyActive: microphoneActive || cameraActive
|
||||
|| screensharingActive
|
||||
|
||||
function getMicrophoneStatus() {
|
||||
return microphoneActive ? "active" : "inactive";
|
||||
}
|
||||
function looksLikeSystemVirtualMic(node) {
|
||||
if (!node)
|
||||
return false
|
||||
const name = (node.name || "").toLowerCase()
|
||||
const mediaName = (node.properties && node.properties["media.name"]
|
||||
|| "").toLowerCase()
|
||||
const appName = (node.properties && node.properties["application.name"]
|
||||
|| "").toLowerCase()
|
||||
const combined = name + " " + mediaName + " " + appName
|
||||
return /cava|monitor|system/.test(combined)
|
||||
}
|
||||
|
||||
function getCameraStatus() {
|
||||
return cameraActive ? "active" : "inactive";
|
||||
}
|
||||
function looksLikeScreencast(node) {
|
||||
if (!node)
|
||||
return false
|
||||
const appName = (node.properties && node.properties["application.name"]
|
||||
|| "").toLowerCase()
|
||||
const nodeName = (node.name || "").toLowerCase()
|
||||
const combined = appName + " " + nodeName
|
||||
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(
|
||||
combined)
|
||||
}
|
||||
|
||||
function getScreensharingStatus() {
|
||||
return screensharingActive ? "active" : "inactive";
|
||||
}
|
||||
function getMicrophoneStatus() {
|
||||
return microphoneActive ? "active" : "inactive"
|
||||
}
|
||||
|
||||
function getPrivacySummary() {
|
||||
const active = [];
|
||||
if (microphoneActive) active.push("microphone");
|
||||
if (cameraActive) active.push("camera");
|
||||
if (screensharingActive) active.push("screensharing");
|
||||
|
||||
return active.length > 0 ?
|
||||
"Privacy active: " + active.join(", ") :
|
||||
"No privacy concerns detected";
|
||||
}
|
||||
}
|
||||
function getCameraStatus() {
|
||||
return cameraActive ? "active" : "inactive"
|
||||
}
|
||||
|
||||
function getScreensharingStatus() {
|
||||
return screensharingActive ? "active" : "inactive"
|
||||
}
|
||||
|
||||
function getPrivacySummary() {
|
||||
const active = []
|
||||
if (microphoneActive)
|
||||
active.push("microphone")
|
||||
if (cameraActive)
|
||||
active.push("camera")
|
||||
if (screensharingActive)
|
||||
active.push("screensharing")
|
||||
|
||||
return active.length > 0 ? "Privacy active: " + active.join(
|
||||
", ") : "No privacy concerns detected"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,77 +1,75 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property int levelInfo: 0
|
||||
readonly property int levelWarn: 1
|
||||
readonly property int levelError: 2
|
||||
property string currentMessage: ""
|
||||
property int currentLevel: levelInfo
|
||||
property bool toastVisible: false
|
||||
property var toastQueue: []
|
||||
property string wallpaperErrorStatus: ""
|
||||
readonly property int levelInfo: 0
|
||||
readonly property int levelWarn: 1
|
||||
readonly property int levelError: 2
|
||||
property string currentMessage: ""
|
||||
property int currentLevel: levelInfo
|
||||
property bool toastVisible: false
|
||||
property var toastQueue: []
|
||||
property string wallpaperErrorStatus: ""
|
||||
|
||||
function showToast(message, level = levelInfo) {
|
||||
toastQueue.push({
|
||||
"message": message,
|
||||
"level": level
|
||||
});
|
||||
if (!toastVisible)
|
||||
processQueue();
|
||||
function showToast(message, level = levelInfo) {
|
||||
toastQueue.push({
|
||||
"message": message,
|
||||
"level": level
|
||||
})
|
||||
if (!toastVisible)
|
||||
processQueue()
|
||||
}
|
||||
|
||||
}
|
||||
function showInfo(message) {
|
||||
showToast(message, levelInfo)
|
||||
}
|
||||
|
||||
function showInfo(message) {
|
||||
showToast(message, levelInfo);
|
||||
}
|
||||
function showWarning(message) {
|
||||
showToast(message, levelWarn)
|
||||
}
|
||||
|
||||
function showWarning(message) {
|
||||
showToast(message, levelWarn);
|
||||
}
|
||||
function showError(message) {
|
||||
showToast(message, levelError)
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
showToast(message, levelError);
|
||||
}
|
||||
function hideToast() {
|
||||
toastVisible = false
|
||||
currentMessage = ""
|
||||
currentLevel = levelInfo
|
||||
toastTimer.stop()
|
||||
if (toastQueue.length > 0)
|
||||
processQueue()
|
||||
}
|
||||
|
||||
function hideToast() {
|
||||
toastVisible = false;
|
||||
currentMessage = "";
|
||||
currentLevel = levelInfo;
|
||||
toastTimer.stop();
|
||||
if (toastQueue.length > 0)
|
||||
processQueue();
|
||||
function processQueue() {
|
||||
if (toastQueue.length === 0)
|
||||
return
|
||||
|
||||
}
|
||||
const toast = toastQueue.shift()
|
||||
currentMessage = toast.message
|
||||
currentLevel = toast.level
|
||||
toastVisible = true
|
||||
toastTimer.interval = toast.level
|
||||
=== levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
|
||||
toastTimer.start()
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (toastQueue.length === 0)
|
||||
return ;
|
||||
|
||||
const toast = toastQueue.shift();
|
||||
currentMessage = toast.message;
|
||||
currentLevel = toast.level;
|
||||
toastVisible = true;
|
||||
toastTimer.interval = toast.level === levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000;
|
||||
toastTimer.start();
|
||||
}
|
||||
|
||||
function clearWallpaperError() {
|
||||
wallpaperErrorStatus = "";
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: toastTimer
|
||||
|
||||
interval: 5000
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: hideToast()
|
||||
}
|
||||
function clearWallpaperError() {
|
||||
wallpaperErrorStatus = ""
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: toastTimer
|
||||
|
||||
interval: 5000
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: hideToast()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,100 +1,99 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
property string username: ""
|
||||
property string fullName: ""
|
||||
property string profilePicture: ""
|
||||
property string uptime: ""
|
||||
property string hostname: ""
|
||||
property bool profileAvailable: false
|
||||
property string username: ""
|
||||
property string fullName: ""
|
||||
property string profilePicture: ""
|
||||
property string uptime: ""
|
||||
property string hostname: ""
|
||||
property bool profileAvailable: false
|
||||
|
||||
function getUserInfo() {
|
||||
userInfoProcess.running = true;
|
||||
function getUserInfo() {
|
||||
userInfoProcess.running = true
|
||||
}
|
||||
|
||||
function getUptime() {
|
||||
uptimeProcess.running = true
|
||||
}
|
||||
|
||||
function refreshUserInfo() {
|
||||
getUserInfo()
|
||||
getUptime()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
getUserInfo()
|
||||
getUptime()
|
||||
}
|
||||
|
||||
// Get username and full name
|
||||
Process {
|
||||
id: userInfoProcess
|
||||
|
||||
command: ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
|
||||
root.username = "User"
|
||||
root.fullName = "User"
|
||||
root.hostname = "System"
|
||||
}
|
||||
}
|
||||
|
||||
function getUptime() {
|
||||
uptimeProcess.running = true;
|
||||
}
|
||||
|
||||
function refreshUserInfo() {
|
||||
getUserInfo();
|
||||
getUptime();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
getUserInfo();
|
||||
getUptime();
|
||||
}
|
||||
|
||||
// Get username and full name
|
||||
Process {
|
||||
id: userInfoProcess
|
||||
|
||||
command: ["bash", "-c", "echo \"$USER|$(getent passwd $USER | cut -d: -f5 | cut -d, -f1)|$(hostname)\""]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
|
||||
root.username = "User";
|
||||
root.fullName = "User";
|
||||
root.hostname = "System";
|
||||
}
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const parts = text.trim().split("|")
|
||||
if (parts.length >= 3) {
|
||||
root.username = parts[0] || ""
|
||||
root.fullName = parts[1] || parts[0] || ""
|
||||
root.hostname = parts[2] || ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const parts = text.trim().split("|");
|
||||
if (parts.length >= 3) {
|
||||
root.username = parts[0] || "";
|
||||
root.fullName = parts[1] || parts[0] || "";
|
||||
root.hostname = parts[2] || "";
|
||||
// Get system uptime
|
||||
Process {
|
||||
id: uptimeProcess
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
command: ["cat", "/proc/uptime"]
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.uptime = "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Get system uptime
|
||||
Process {
|
||||
id: uptimeProcess
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const seconds = parseInt(text.split(" ")[0])
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
command: ["cat", "/proc/uptime"]
|
||||
running: false
|
||||
const parts = []
|
||||
if (days > 0)
|
||||
parts.push(`${days} day${days === 1 ? "" : "s"}`)
|
||||
if (hours > 0)
|
||||
parts.push(`${hours} hour${hours === 1 ? "" : "s"}`)
|
||||
if (minutes > 0)
|
||||
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`)
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
root.uptime = "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const seconds = parseInt(text.split(" ")[0]);
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
const parts = [];
|
||||
if (days > 0)
|
||||
parts.push(`${days} day${days === 1 ? "" : "s"}`);
|
||||
if (hours > 0)
|
||||
parts.push(`${hours} hour${hours === 1 ? "" : "s"}`);
|
||||
if (minutes > 0)
|
||||
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`);
|
||||
|
||||
if (parts.length > 0)
|
||||
root.uptime = "up " + parts.join(", ");
|
||||
else
|
||||
root.uptime = `up ${seconds} seconds`;
|
||||
}
|
||||
}
|
||||
if (parts.length > 0)
|
||||
root.uptime = "up " + parts.join(", ")
|
||||
else
|
||||
root.uptime = `up ${seconds} seconds`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -8,334 +9,347 @@ import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int refCount: 0
|
||||
|
||||
property var weather: ({
|
||||
available: false,
|
||||
loading: true,
|
||||
temp: 0,
|
||||
tempF: 0,
|
||||
city: "",
|
||||
wCode: "113",
|
||||
humidity: 0,
|
||||
wind: "",
|
||||
sunrise: "06:00",
|
||||
sunset: "18:00",
|
||||
uv: 0,
|
||||
pressure: 0
|
||||
})
|
||||
|
||||
property int updateInterval: 600000 // 10 minutes
|
||||
property int retryAttempts: 0
|
||||
property int maxRetryAttempts: 3
|
||||
property int retryDelay: 30000 // 30 seconds
|
||||
property int lastFetchTime: 0
|
||||
property int minFetchInterval: 30000 // 30 seconds minimum between fetches
|
||||
property int persistentRetryCount: 0 // Track persistent retry attempts for backoff
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
function getWeatherUrl() {
|
||||
if (SettingsData.useAutoLocation) {
|
||||
const url = "wttr.in/?format=j1"
|
||||
console.log("Using auto location, URL:", url)
|
||||
return url
|
||||
}
|
||||
|
||||
const location = SettingsData.weatherCoordinates || "40.7128,-74.0060"
|
||||
const url = `wttr.in/${encodeURIComponent(location)}?format=j1`
|
||||
console.log("Using manual location:", location, "URL:", url)
|
||||
return url
|
||||
}
|
||||
|
||||
function addRef() {
|
||||
refCount++;
|
||||
|
||||
if (refCount === 1 && !weather.available && SettingsData.weatherEnabled) {
|
||||
// Start fetching when first consumer appears and weather is enabled
|
||||
fetchWeather();
|
||||
}
|
||||
}
|
||||
|
||||
function removeRef() {
|
||||
refCount = Math.max(0, refCount - 1);
|
||||
|
||||
}
|
||||
|
||||
function fetchWeather() {
|
||||
// Only fetch if someone is consuming the data and weather is enabled
|
||||
if (root.refCount === 0 || !SettingsData.weatherEnabled) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (weatherFetcher.running) {
|
||||
console.log("Weather fetch already in progress, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we've fetched recently to prevent spam
|
||||
const now = Date.now()
|
||||
if (now - root.lastFetchTime < root.minFetchInterval) {
|
||||
console.log("Weather fetch throttled, too soon since last fetch")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Fetching weather from:", getWeatherUrl())
|
||||
root.lastFetchTime = now
|
||||
root.weather.loading = true
|
||||
weatherFetcher.command = ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${getWeatherUrl()}'`]
|
||||
weatherFetcher.running = true
|
||||
}
|
||||
|
||||
function forceRefresh() {
|
||||
console.log("Force refreshing weather")
|
||||
root.lastFetchTime = 0 // Reset throttle
|
||||
fetchWeather()
|
||||
}
|
||||
|
||||
function handleWeatherSuccess() {
|
||||
root.retryAttempts = 0
|
||||
root.persistentRetryCount = 0 // Reset persistent retry count on success
|
||||
// Stop any persistent retry timer if running
|
||||
if (persistentRetryTimer.running) {
|
||||
persistentRetryTimer.stop()
|
||||
}
|
||||
// Don't restart the timer - let it continue its normal interval
|
||||
if (updateTimer.interval !== root.updateInterval) {
|
||||
updateTimer.interval = root.updateInterval
|
||||
}
|
||||
}
|
||||
|
||||
function handleWeatherFailure() {
|
||||
root.retryAttempts++
|
||||
if (root.retryAttempts < root.maxRetryAttempts) {
|
||||
console.log(`Weather fetch failed, retrying in ${root.retryDelay/1000}s (attempt ${root.retryAttempts}/${root.maxRetryAttempts})`)
|
||||
retryTimer.start()
|
||||
} else {
|
||||
console.warn("Weather fetch failed after maximum retry attempts, will keep trying...")
|
||||
root.weather.available = false
|
||||
root.weather.loading = false
|
||||
// Reset retry count but keep trying with exponential backoff
|
||||
root.retryAttempts = 0
|
||||
// Use exponential backoff: 1min, 2min, 4min, then cap at 5min
|
||||
const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000)
|
||||
persistentRetryCount++
|
||||
console.log(`Scheduling persistent retry in ${backoffDelay/1000}s`)
|
||||
persistentRetryTimer.interval = backoffDelay
|
||||
persistentRetryTimer.start()
|
||||
}
|
||||
id: root
|
||||
|
||||
property int refCount: 0
|
||||
|
||||
property var weather: ({
|
||||
"available": false,
|
||||
"loading": true,
|
||||
"temp": 0,
|
||||
"tempF": 0,
|
||||
"city": "",
|
||||
"wCode": "113",
|
||||
"humidity": 0,
|
||||
"wind": "",
|
||||
"sunrise": "06:00",
|
||||
"sunset": "18:00",
|
||||
"uv": 0,
|
||||
"pressure": 0
|
||||
})
|
||||
|
||||
property int updateInterval: 600000 // 10 minutes
|
||||
property int retryAttempts: 0
|
||||
property int maxRetryAttempts: 3
|
||||
property int retryDelay: 30000 // 30 seconds
|
||||
property int lastFetchTime: 0
|
||||
property int minFetchInterval: 30000 // 30 seconds minimum between fetches
|
||||
property int persistentRetryCount: 0 // Track persistent retry attempts for backoff
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
function getWeatherUrl() {
|
||||
if (SettingsData.useAutoLocation) {
|
||||
const url = "wttr.in/?format=j1"
|
||||
console.log("Using auto location, URL:", url)
|
||||
return url
|
||||
}
|
||||
|
||||
Process {
|
||||
id: weatherFetcher
|
||||
command: ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${root.getWeatherUrl()}'`]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const raw = text.trim()
|
||||
if (!raw || raw[0] !== "{") {
|
||||
console.warn("No valid weather data received")
|
||||
root.handleWeatherFailure()
|
||||
return
|
||||
}
|
||||
const location = SettingsData.weatherCoordinates || "40.7128,-74.0060"
|
||||
const url = `wttr.in/${encodeURIComponent(location)}?format=j1`
|
||||
console.log("Using manual location:", location, "URL:", url)
|
||||
return url
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
function addRef() {
|
||||
refCount++
|
||||
|
||||
const current = data.current_condition?.[0] || {}
|
||||
const location = data.nearest_area?.[0] || {}
|
||||
const astronomy = data.weather?.[0]?.astronomy?.[0] || {}
|
||||
|
||||
if (!Object.keys(current).length || !Object.keys(location).length) {
|
||||
throw new Error("Required fields missing")
|
||||
}
|
||||
|
||||
root.weather = {
|
||||
available: true,
|
||||
loading: false,
|
||||
temp: Number(current.temp_C) || 0,
|
||||
tempF: Number(current.temp_F) || 0,
|
||||
city: location.areaName?.[0]?.value || "Unknown",
|
||||
wCode: current.weatherCode || "113",
|
||||
humidity: Number(current.humidity) || 0,
|
||||
wind: `${current.windspeedKmph || 0} km/h`,
|
||||
sunrise: astronomy.sunrise || "06:00",
|
||||
sunset: astronomy.sunset || "18:00",
|
||||
uv: Number(current.uvIndex) || 0,
|
||||
pressure: Number(current.pressure) || 0
|
||||
}
|
||||
|
||||
console.log("Weather updated:", root.weather.city,
|
||||
`${root.weather.temp}°C`)
|
||||
|
||||
root.handleWeatherSuccess()
|
||||
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse weather data:", e.message)
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Weather fetch failed with exit code:", exitCode)
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
if (refCount === 1 && !weather.available && SettingsData.weatherEnabled) {
|
||||
// Start fetching when first consumer appears and weather is enabled
|
||||
fetchWeather()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: root.updateInterval
|
||||
running: root.refCount > 0 && SettingsData.weatherEnabled
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
root.fetchWeather()
|
||||
}
|
||||
|
||||
function removeRef() {
|
||||
refCount = Math.max(0, refCount - 1)
|
||||
}
|
||||
|
||||
function fetchWeather() {
|
||||
// Only fetch if someone is consuming the data and weather is enabled
|
||||
if (root.refCount === 0 || !SettingsData.weatherEnabled) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (weatherFetcher.running) {
|
||||
console.log("Weather fetch already in progress, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we've fetched recently to prevent spam
|
||||
const now = Date.now()
|
||||
if (now - root.lastFetchTime < root.minFetchInterval) {
|
||||
console.log("Weather fetch throttled, too soon since last fetch")
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Fetching weather from:", getWeatherUrl())
|
||||
root.lastFetchTime = now
|
||||
root.weather.loading = true
|
||||
weatherFetcher.command
|
||||
= ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${getWeatherUrl(
|
||||
)}'`]
|
||||
weatherFetcher.running = true
|
||||
}
|
||||
|
||||
function forceRefresh() {
|
||||
console.log("Force refreshing weather")
|
||||
root.lastFetchTime = 0 // Reset throttle
|
||||
fetchWeather()
|
||||
}
|
||||
|
||||
function handleWeatherSuccess() {
|
||||
root.retryAttempts = 0
|
||||
root.persistentRetryCount = 0 // Reset persistent retry count on success
|
||||
// Stop any persistent retry timer if running
|
||||
if (persistentRetryTimer.running) {
|
||||
persistentRetryTimer.stop()
|
||||
}
|
||||
// Don't restart the timer - let it continue its normal interval
|
||||
if (updateTimer.interval !== root.updateInterval) {
|
||||
updateTimer.interval = root.updateInterval
|
||||
}
|
||||
}
|
||||
|
||||
function handleWeatherFailure() {
|
||||
root.retryAttempts++
|
||||
if (root.retryAttempts < root.maxRetryAttempts) {
|
||||
console.log(
|
||||
`Weather fetch failed, retrying in ${root.retryDelay
|
||||
/ 1000}s (attempt ${root.retryAttempts}/${root.maxRetryAttempts})`)
|
||||
retryTimer.start()
|
||||
} else {
|
||||
console.warn(
|
||||
"Weather fetch failed after maximum retry attempts, will keep trying...")
|
||||
root.weather.available = false
|
||||
root.weather.loading = false
|
||||
// Reset retry count but keep trying with exponential backoff
|
||||
root.retryAttempts = 0
|
||||
// Use exponential backoff: 1min, 2min, 4min, then cap at 5min
|
||||
const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount),
|
||||
300000)
|
||||
persistentRetryCount++
|
||||
console.log(`Scheduling persistent retry in ${backoffDelay / 1000}s`)
|
||||
persistentRetryTimer.interval = backoffDelay
|
||||
persistentRetryTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: weatherFetcher
|
||||
command: ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${root.getWeatherUrl(
|
||||
)}'`]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const raw = text.trim()
|
||||
if (!raw || raw[0] !== "{") {
|
||||
console.warn("No valid weather data received")
|
||||
root.handleWeatherFailure()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: retryTimer
|
||||
interval: root.retryDelay
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.fetchWeather()
|
||||
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
|
||||
const current = data.current_condition[0] || {}
|
||||
const location = data.nearest_area[0] || {}
|
||||
const astronomy = data.weather[0]?.astronomy[0] || {}
|
||||
|
||||
if (!Object.keys(current).length || !Object.keys(location).length) {
|
||||
throw new Error("Required fields missing")
|
||||
}
|
||||
|
||||
root.weather = {
|
||||
"available": true,
|
||||
"loading": false,
|
||||
"temp": Number(current.temp_C) || 0,
|
||||
"tempF": Number(current.temp_F) || 0,
|
||||
"city": location.areaName[0]?.value || "Unknown",
|
||||
"wCode": current.weatherCode || "113",
|
||||
"humidity": Number(current.humidity) || 0,
|
||||
"wind": `${current.windspeedKmph || 0} km/h`,
|
||||
"sunrise": astronomy.sunrise || "06:00",
|
||||
"sunset": astronomy.sunset || "18:00",
|
||||
"uv": Number(current.uvIndex) || 0,
|
||||
"pressure": Number(current.pressure) || 0
|
||||
}
|
||||
|
||||
console.log("Weather updated:", root.weather.city,
|
||||
`${root.weather.temp}°C`)
|
||||
|
||||
root.handleWeatherSuccess()
|
||||
} catch (e) {
|
||||
console.warn("Failed to parse weather data:", e.message)
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: persistentRetryTimer
|
||||
interval: 60000 // Will be dynamically set
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
console.log("Persistent retry attempt...")
|
||||
root.fetchWeather()
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Weather fetch failed with exit code:", exitCode)
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
SettingsData.weatherCoordinatesChanged.connect(() => {
|
||||
console.log("Weather location changed, force refreshing weather")
|
||||
root.weather = {
|
||||
available: false,
|
||||
loading: true,
|
||||
temp: 0,
|
||||
tempF: 0,
|
||||
city: "",
|
||||
wCode: "113",
|
||||
humidity: 0,
|
||||
wind: "",
|
||||
sunrise: "06:00",
|
||||
sunset: "18:00",
|
||||
uv: 0,
|
||||
pressure: 0
|
||||
}
|
||||
root.lastFetchTime = 0
|
||||
root.forceRefresh()
|
||||
})
|
||||
|
||||
SettingsData.weatherLocationChanged.connect(() => {
|
||||
console.log("Weather location display name changed")
|
||||
const currentWeather = Object.assign({}, root.weather)
|
||||
root.weather = currentWeather
|
||||
})
|
||||
|
||||
SettingsData.useAutoLocationChanged.connect(() => {
|
||||
console.log("Auto location setting changed, force refreshing weather")
|
||||
root.weather = {
|
||||
available: false,
|
||||
loading: true,
|
||||
temp: 0,
|
||||
tempF: 0,
|
||||
city: "",
|
||||
wCode: "113",
|
||||
humidity: 0,
|
||||
wind: "",
|
||||
sunrise: "06:00",
|
||||
sunset: "18:00",
|
||||
uv: 0,
|
||||
pressure: 0
|
||||
}
|
||||
root.lastFetchTime = 0
|
||||
root.forceRefresh()
|
||||
})
|
||||
|
||||
SettingsData.weatherEnabledChanged.connect(() => {
|
||||
console.log("Weather enabled setting changed:", SettingsData.weatherEnabled)
|
||||
if (SettingsData.weatherEnabled && root.refCount > 0 && !root.weather.available) {
|
||||
// Start fetching when weather is re-enabled
|
||||
root.forceRefresh()
|
||||
} else if (!SettingsData.weatherEnabled) {
|
||||
// Stop all timers when weather is disabled
|
||||
updateTimer.stop()
|
||||
retryTimer.stop()
|
||||
persistentRetryTimer.stop()
|
||||
if (weatherFetcher.running) {
|
||||
weatherFetcher.running = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: root.updateInterval
|
||||
running: root.refCount > 0 && SettingsData.weatherEnabled
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
root.fetchWeather()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: retryTimer
|
||||
interval: root.retryDelay
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.fetchWeather()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: persistentRetryTimer
|
||||
interval: 60000 // Will be dynamically set
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
console.log("Persistent retry attempt...")
|
||||
root.fetchWeather()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
SettingsData.weatherCoordinatesChanged.connect(() => {
|
||||
console.log(
|
||||
"Weather location changed, force refreshing weather")
|
||||
root.weather = {
|
||||
"available": false,
|
||||
"loading": true,
|
||||
"temp": 0,
|
||||
"tempF": 0,
|
||||
"city": "",
|
||||
"wCode": "113",
|
||||
"humidity": 0,
|
||||
"wind": "",
|
||||
"sunrise": "06:00",
|
||||
"sunset": "18:00",
|
||||
"uv": 0,
|
||||
"pressure": 0
|
||||
}
|
||||
root.lastFetchTime = 0
|
||||
root.forceRefresh()
|
||||
})
|
||||
|
||||
SettingsData.weatherLocationChanged.connect(() => {
|
||||
console.log(
|
||||
"Weather location display name changed")
|
||||
const currentWeather = Object.assign(
|
||||
{}, root.weather)
|
||||
root.weather = currentWeather
|
||||
})
|
||||
|
||||
SettingsData.useAutoLocationChanged.connect(() => {
|
||||
console.log(
|
||||
"Auto location setting changed, force refreshing weather")
|
||||
root.weather = {
|
||||
"available": false,
|
||||
"loading": true,
|
||||
"temp": 0,
|
||||
"tempF": 0,
|
||||
"city": "",
|
||||
"wCode": "113",
|
||||
"humidity": 0,
|
||||
"wind": "",
|
||||
"sunrise": "06:00",
|
||||
"sunset": "18:00",
|
||||
"uv": 0,
|
||||
"pressure": 0
|
||||
}
|
||||
root.lastFetchTime = 0
|
||||
root.forceRefresh()
|
||||
})
|
||||
|
||||
SettingsData.weatherEnabledChanged.connect(() => {
|
||||
console.log(
|
||||
"Weather enabled setting changed:",
|
||||
SettingsData.weatherEnabled)
|
||||
if (SettingsData.weatherEnabled
|
||||
&& root.refCount > 0
|
||||
&& !root.weather.available) {
|
||||
// Start fetching when weather is re-enabled
|
||||
root.forceRefresh()
|
||||
} else if (!SettingsData.weatherEnabled) {
|
||||
// Stop all timers when weather is disabled
|
||||
updateTimer.stop()
|
||||
retryTimer.stop()
|
||||
persistentRetryTimer.stop()
|
||||
if (weatherFetcher.running) {
|
||||
weatherFetcher.running = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user