mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-11 07:52:50 -05:00
qmlfmt with 4 space
This commit is contained in:
@@ -9,149 +9,153 @@ import Quickshell.Widgets
|
||||
import "../Common/fuzzysort.js" as Fuzzy
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
property var applications: DesktopEntries.applications.values
|
||||
property var applications: DesktopEntries.applications.values
|
||||
|
||||
property var preppedApps: applications.map(app => ({
|
||||
"name": Fuzzy.prepare(
|
||||
app.name
|
||||
|| ""),
|
||||
"comment": Fuzzy.prepare(
|
||||
app.comment
|
||||
|| ""),
|
||||
"entry": app
|
||||
}))
|
||||
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
|
||||
}
|
||||
function searchApplications(query) {
|
||||
if (!query || query.length === 0) {
|
||||
return applications
|
||||
}
|
||||
|
||||
if (preppedApps.length === 0) {
|
||||
return []
|
||||
}
|
||||
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
|
||||
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 (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
|
||||
}
|
||||
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"
|
||||
}
|
||||
|
||||
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"
|
||||
return finalScore
|
||||
},
|
||||
"limit": 50
|
||||
})
|
||||
|
||||
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 results.map(r => r.obj.entry)
|
||||
}
|
||||
|
||||
return Array.from(categories).sort()
|
||||
}
|
||||
function getCategoriesForApp(app) {
|
||||
if (!app || !app.categories)
|
||||
return []
|
||||
|
||||
function getAppsInCategory(category) {
|
||||
if (category === "All") {
|
||||
return applications
|
||||
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)
|
||||
}
|
||||
|
||||
return applications.filter(app => {
|
||||
var appCategories = getCategoriesForApp(app)
|
||||
return appCategories.includes(category)
|
||||
})
|
||||
}
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,180 +8,183 @@ import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
|
||||
signal volumeChanged
|
||||
signal micMuteChanged
|
||||
signal volumeChanged
|
||||
signal micMuteChanged
|
||||
|
||||
function displayName(node) {
|
||||
if (!node)
|
||||
return ""
|
||||
function displayName(node) {
|
||||
if (!node)
|
||||
return ""
|
||||
|
||||
if (node.properties && node.properties["device.description"]) {
|
||||
return node.properties["device.description"]
|
||||
}
|
||||
|
||||
if (node.description && node.description !== node.name) {
|
||||
return node.description
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
// 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 increment(step: string): string {
|
||||
if (root.sink && root.sink.audio) {
|
||||
if (root.sink.audio.muted) {
|
||||
root.sink.audio.muted = false
|
||||
if (node.properties && node.properties["device.description"]) {
|
||||
return node.properties["device.description"]
|
||||
}
|
||||
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) {
|
||||
if (root.sink.audio.muted) {
|
||||
root.sink.audio.muted = false
|
||||
if (node.description && node.description !== node.name) {
|
||||
return node.description
|
||||
}
|
||||
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"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function mute(): string {
|
||||
const result = root.toggleMute()
|
||||
root.volumeChanged()
|
||||
return result
|
||||
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 ""
|
||||
}
|
||||
|
||||
function setmic(percentage: string): string {
|
||||
return root.setMicVolume(parseInt(percentage))
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
|
||||
}
|
||||
|
||||
function micmute(): string {
|
||||
const result = root.toggleMicMute()
|
||||
root.micMuteChanged()
|
||||
return result
|
||||
// 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 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 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 increment(step: string): string {
|
||||
if (root.sink && root.sink.audio) {
|
||||
if (root.sink.audio.muted) {
|
||||
root.sink.audio.muted = false
|
||||
}
|
||||
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) {
|
||||
if (root.sink.audio.muted) {
|
||||
root.sink.audio.muted = false
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -9,50 +10,71 @@ Singleton {
|
||||
id: root
|
||||
|
||||
readonly property UPowerDevice device: UPower.displayDevice
|
||||
readonly property bool batteryAvailable: device && device.ready && device.isLaptopBattery
|
||||
readonly property real batteryLevel: batteryAvailable ? Math.round(device.percentage * 100) : 0
|
||||
readonly property bool isCharging: batteryAvailable && device.state === UPowerDeviceState.Charging && device.changeRate > 0
|
||||
readonly property bool isPluggedIn: batteryAvailable && (device.state !== UPowerDeviceState.Discharging && device.state !== UPowerDeviceState.Empty)
|
||||
readonly property bool batteryAvailable: device && device.ready
|
||||
&& device.isLaptopBattery
|
||||
readonly property real batteryLevel: batteryAvailable ? Math.round(
|
||||
device.percentage * 100) : 0
|
||||
readonly property bool isCharging: batteryAvailable
|
||||
&& device.state === UPowerDeviceState.Charging
|
||||
&& device.changeRate > 0
|
||||
readonly property bool isPluggedIn: batteryAvailable
|
||||
&& (device.state !== UPowerDeviceState.Discharging
|
||||
&& device.state !== UPowerDeviceState.Empty)
|
||||
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
|
||||
readonly property string batteryHealth: {
|
||||
if (!batteryAvailable)
|
||||
return "N/A"
|
||||
|
||||
return "N/A"
|
||||
|
||||
if (device.healthSupported && device.healthPercentage > 0)
|
||||
return Math.round(device.healthPercentage) + "%"
|
||||
|
||||
return Math.round(device.healthPercentage) + "%"
|
||||
|
||||
// Calculate health from energy capacity vs design capacity
|
||||
if (device.energyCapacity > 0 && device.energy > 0) {
|
||||
// energyCapacity is current full capacity, we need design capacity
|
||||
// Use a rough estimate based on typical battery degradation patterns
|
||||
var healthPercent = (device.energyCapacity / 90.0045) * 100 // your design capacity from upower
|
||||
var healthPercent = (device.energyCapacity / 90.0045)
|
||||
* 100 // your design capacity from upower
|
||||
return Math.round(healthPercent) + "%"
|
||||
}
|
||||
|
||||
|
||||
return "N/A"
|
||||
}
|
||||
readonly property real batteryCapacity: batteryAvailable && device.energyCapacity > 0 ? device.energyCapacity : 0
|
||||
readonly property real batteryCapacity: batteryAvailable
|
||||
&& device.energyCapacity > 0 ? device.energyCapacity : 0
|
||||
readonly property string batteryStatus: {
|
||||
if (!batteryAvailable)
|
||||
return "No Battery"
|
||||
|
||||
if (device.state === UPowerDeviceState.Charging && device.changeRate <= 0)
|
||||
return "Plugged In"
|
||||
|
||||
return "No Battery"
|
||||
|
||||
if (device.state === UPowerDeviceState.Charging
|
||||
&& device.changeRate <= 0)
|
||||
return "Plugged In"
|
||||
|
||||
return UPowerDeviceState.toString(device.state)
|
||||
}
|
||||
readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== PowerProfile.PowerSaver)
|
||||
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)) {
|
||||
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
|
||||
})
|
||||
"name": dev.model
|
||||
|| UPowerDeviceType.toString(
|
||||
dev.type),
|
||||
"percentage": Math.round(dev.percentage),
|
||||
"type": dev.type
|
||||
})
|
||||
}
|
||||
}
|
||||
return btDevices
|
||||
@@ -63,13 +85,13 @@ Singleton {
|
||||
return "Unknown"
|
||||
|
||||
var timeSeconds = isCharging ? device.timeToFull : device.timeToEmpty
|
||||
|
||||
|
||||
if (!timeSeconds || timeSeconds <= 0 || timeSeconds > 86400)
|
||||
return "Unknown"
|
||||
|
||||
var hours = Math.floor(timeSeconds / 3600)
|
||||
var minutes = Math.floor((timeSeconds % 3600) / 60)
|
||||
|
||||
|
||||
if (hours > 0)
|
||||
return hours + "h " + minutes + "m"
|
||||
else
|
||||
|
||||
@@ -7,149 +7,153 @@ 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 || ""
|
||||
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
|
||||
var aHasRealName = aName.includes(" ")
|
||||
&& aName.length > 3
|
||||
var bHasRealName = bName.includes(" ")
|
||||
&& bName.length > 3
|
||||
|
||||
if (aHasRealName && !bHasRealName)
|
||||
return -1
|
||||
if (!aHasRealName && bHasRealName)
|
||||
return 1
|
||||
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
|
||||
})
|
||||
}
|
||||
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 getDeviceIcon(device) {
|
||||
if (!device)
|
||||
return "bluetooth"
|
||||
function getDeviceIcon(device) {
|
||||
if (!device)
|
||||
return "bluetooth"
|
||||
|
||||
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"
|
||||
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("mouse") || name.includes("mouse"))
|
||||
return "mouse"
|
||||
if (icon.includes("mouse") || name.includes("mouse"))
|
||||
return "mouse"
|
||||
|
||||
if (icon.includes("keyboard") || name.includes("keyboard"))
|
||||
return "keyboard"
|
||||
if (icon.includes("keyboard") || name.includes("keyboard"))
|
||||
return "keyboard"
|
||||
|
||||
if (icon.includes("phone") || name.includes("phone") || name.includes(
|
||||
"iphone") || name.includes("android") || name.includes("samsung"))
|
||||
return "smartphone"
|
||||
if (icon.includes("phone") || name.includes("phone") || name.includes(
|
||||
"iphone") || name.includes("android") || name.includes(
|
||||
"samsung"))
|
||||
return "smartphone"
|
||||
|
||||
if (icon.includes("watch") || name.includes("watch"))
|
||||
return "watch"
|
||||
if (icon.includes("watch") || name.includes("watch"))
|
||||
return "watch"
|
||||
|
||||
if (icon.includes("speaker") || name.includes("speaker"))
|
||||
return "speaker"
|
||||
if (icon.includes("speaker") || name.includes("speaker"))
|
||||
return "speaker"
|
||||
|
||||
if (icon.includes("display") || name.includes("tv"))
|
||||
return "tv"
|
||||
if (icon.includes("display") || name.includes("tv"))
|
||||
return "tv"
|
||||
|
||||
return "bluetooth"
|
||||
}
|
||||
return "bluetooth"
|
||||
}
|
||||
|
||||
function canConnect(device) {
|
||||
if (!device)
|
||||
return false
|
||||
function canConnect(device) {
|
||||
if (!device)
|
||||
return false
|
||||
|
||||
return !device.paired && !device.pairing && !device.blocked
|
||||
}
|
||||
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 isDeviceBusy(device) {
|
||||
if (!device)
|
||||
return false
|
||||
return device.pairing
|
||||
|| device.state === BluetoothDeviceState.Disconnecting
|
||||
|| device.state === BluetoothDeviceState.Connecting
|
||||
}
|
||||
|
||||
function connectDeviceWithTrust(device) {
|
||||
if (!device)
|
||||
return
|
||||
function connectDeviceWithTrust(device) {
|
||||
if (!device)
|
||||
return
|
||||
|
||||
device.trusted = true
|
||||
device.connect()
|
||||
}
|
||||
device.trusted = true
|
||||
device.connect()
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,238 +7,245 @@ 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 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 checkKhalAvailability() {
|
||||
if (!khalCheckProcess.running)
|
||||
khalCheckProcess.running = true
|
||||
}
|
||||
if (eventsProcess.running) {
|
||||
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)
|
||||
}
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
|
||||
// Process for loading events
|
||||
Process {
|
||||
id: eventsProcess
|
||||
function getEventsForDate(date) {
|
||||
let dateKey = Qt.formatDate(date, "yyyy-MM-dd")
|
||||
return root.eventsByDate[dateKey] || []
|
||||
}
|
||||
|
||||
property date requestStartDate
|
||||
property date requestEndDate
|
||||
property string rawOutput: ""
|
||||
function hasEventsForDate(date) {
|
||||
let events = getEventsForDate(date)
|
||||
return events.length > 0
|
||||
}
|
||||
|
||||
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
|
||||
// Initialize on component completion
|
||||
Component.onCompleted: {
|
||||
checkKhalAvailability()
|
||||
}
|
||||
|
||||
// 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
|
||||
// Process for checking khal configuration
|
||||
Process {
|
||||
id: khalCheckProcess
|
||||
|
||||
// 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()
|
||||
command: ["khal", "list", "today"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
root.khalAvailable = (exitCode === 0)
|
||||
if (exitCode === 0) {
|
||||
loadCurrentMonth()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 = {}
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
// Reset for next run
|
||||
eventsProcess.rawOutput = ""
|
||||
}
|
||||
// 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"
|
||||
}
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: data => {
|
||||
eventsProcess.rawOutput += data + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,52 +7,54 @@ 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: false
|
||||
onExited: exitCode => {
|
||||
root.cavaAvailable = exitCode === 0
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
cavaCheck.running = true
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
command: ["which", "cava"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
root.cavaAvailable = exitCode === 0
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
cavaCheck.running = true
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -74,21 +75,28 @@ Singleton {
|
||||
property int historySize: 60
|
||||
property var cpuHistory: []
|
||||
property var memoryHistory: []
|
||||
property var networkHistory: ({ "rx": [], "tx": [] })
|
||||
property var diskHistory: ({ "read": [], "write": [] })
|
||||
property var networkHistory: ({
|
||||
"rx": [],
|
||||
"tx": []
|
||||
})
|
||||
property var diskHistory: ({
|
||||
"read": [],
|
||||
"write": []
|
||||
})
|
||||
|
||||
function addRef(modules = null) {
|
||||
refCount++
|
||||
let modulesChanged = false
|
||||
|
||||
|
||||
if (modules) {
|
||||
const modulesToAdd = Array.isArray(modules) ? modules : [modules]
|
||||
for (const module of modulesToAdd) {
|
||||
// Increment reference count for this module
|
||||
const currentCount = moduleRefCounts[module] || 0
|
||||
moduleRefCounts[module] = currentCount + 1
|
||||
console.log("Adding ref for module:", module, "count:", moduleRefCounts[module])
|
||||
|
||||
console.log("Adding ref for module:", module, "count:",
|
||||
moduleRefCounts[module])
|
||||
|
||||
// Add to enabled modules if not already there
|
||||
if (enabledModules.indexOf(module) === -1) {
|
||||
enabledModules.push(module)
|
||||
@@ -99,7 +107,8 @@ Singleton {
|
||||
|
||||
if (modulesChanged || refCount === 1) {
|
||||
enabledModules = enabledModules.slice() // Force property change
|
||||
moduleRefCounts = Object.assign({}, moduleRefCounts) // Force property change
|
||||
moduleRefCounts = Object.assign(
|
||||
{}, moduleRefCounts) // Force property change
|
||||
updateAllStats()
|
||||
} else if (gpuPciIds.length > 0 && refCount > 0) {
|
||||
// If we have GPU PCI IDs and active modules, make sure to update
|
||||
@@ -111,7 +120,7 @@ Singleton {
|
||||
function removeRef(modules = null) {
|
||||
refCount = Math.max(0, refCount - 1)
|
||||
let modulesChanged = false
|
||||
|
||||
|
||||
if (modules) {
|
||||
const modulesToRemove = Array.isArray(modules) ? modules : [modules]
|
||||
for (const module of modulesToRemove) {
|
||||
@@ -119,7 +128,8 @@ Singleton {
|
||||
if (currentCount > 1) {
|
||||
// Decrement reference count
|
||||
moduleRefCounts[module] = currentCount - 1
|
||||
console.log("Removing ref for module:", module, "count:", moduleRefCounts[module])
|
||||
console.log("Removing ref for module:", module, "count:",
|
||||
moduleRefCounts[module])
|
||||
} else if (currentCount === 1) {
|
||||
// Remove completely when count reaches 0
|
||||
delete moduleRefCounts[module]
|
||||
@@ -127,7 +137,8 @@ Singleton {
|
||||
if (index > -1) {
|
||||
enabledModules.splice(index, 1)
|
||||
modulesChanged = true
|
||||
console.log("Disabling module:", module, "(no more refs)")
|
||||
console.log("Disabling module:", module,
|
||||
"(no more refs)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,8 +146,9 @@ Singleton {
|
||||
|
||||
if (modulesChanged) {
|
||||
enabledModules = enabledModules.slice() // Force property change
|
||||
moduleRefCounts = Object.assign({}, moduleRefCounts) // Force property change
|
||||
|
||||
moduleRefCounts = Object.assign(
|
||||
{}, moduleRefCounts) // Force property change
|
||||
|
||||
// Clear cursor data when CPU or process modules are no longer active
|
||||
if (!enabledModules.includes("cpu")) {
|
||||
cpuCursor = ""
|
||||
@@ -156,13 +168,14 @@ Singleton {
|
||||
function addGpuPciId(pciId) {
|
||||
const currentCount = gpuPciIdRefCounts[pciId] || 0
|
||||
gpuPciIdRefCounts[pciId] = currentCount + 1
|
||||
|
||||
|
||||
// Add to gpuPciIds array if not already there
|
||||
if (!gpuPciIds.includes(pciId)) {
|
||||
gpuPciIds = gpuPciIds.concat([pciId])
|
||||
}
|
||||
|
||||
console.log("Adding GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId])
|
||||
|
||||
console.log("Adding GPU PCI ID ref:", pciId, "count:",
|
||||
gpuPciIdRefCounts[pciId])
|
||||
// Force property change notification
|
||||
gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts)
|
||||
}
|
||||
@@ -172,7 +185,8 @@ Singleton {
|
||||
if (currentCount > 1) {
|
||||
// Decrement reference count
|
||||
gpuPciIdRefCounts[pciId] = currentCount - 1
|
||||
console.log("Removing GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId])
|
||||
console.log("Removing GPU PCI ID ref:", pciId, "count:",
|
||||
gpuPciIdRefCounts[pciId])
|
||||
} else if (currentCount === 1) {
|
||||
// Remove completely when count reaches 0
|
||||
delete gpuPciIdRefCounts[pciId]
|
||||
@@ -181,21 +195,23 @@ Singleton {
|
||||
gpuPciIds = gpuPciIds.slice()
|
||||
gpuPciIds.splice(index, 1)
|
||||
}
|
||||
|
||||
|
||||
// Clear temperature data for this GPU when no longer monitored
|
||||
if (availableGpus && availableGpus.length > 0) {
|
||||
const updatedGpus = availableGpus.slice()
|
||||
for (let i = 0; i < updatedGpus.length; i++) {
|
||||
for (var i = 0; i < updatedGpus.length; i++) {
|
||||
if (updatedGpus[i].pciId === pciId) {
|
||||
updatedGpus[i] = Object.assign({}, updatedGpus[i], { temperature: 0 })
|
||||
updatedGpus[i] = Object.assign({}, updatedGpus[i], {
|
||||
"temperature": 0
|
||||
})
|
||||
}
|
||||
}
|
||||
availableGpus = updatedGpus
|
||||
}
|
||||
|
||||
|
||||
console.log("Removing GPU PCI ID completely:", pciId)
|
||||
}
|
||||
|
||||
|
||||
// Force property change notification
|
||||
gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts)
|
||||
}
|
||||
@@ -216,19 +232,20 @@ Singleton {
|
||||
}
|
||||
|
||||
function initializeGpuMetadata() {
|
||||
if (!dgopAvailable) return
|
||||
if (!dgopAvailable)
|
||||
return
|
||||
// Load GPU metadata once at startup for basic info
|
||||
gpuInitProcess.running = true
|
||||
}
|
||||
|
||||
function buildDgopCommand() {
|
||||
const cmd = ["dgop", "meta", "--json"]
|
||||
|
||||
|
||||
if (enabledModules.length === 0) {
|
||||
// Don't run if no modules are needed
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
// Replace 'gpu' with 'gpu-temp' when we have PCI IDs to monitor
|
||||
const finalModules = []
|
||||
for (const module of enabledModules) {
|
||||
@@ -238,12 +255,12 @@ Singleton {
|
||||
finalModules.push(module)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add gpu-temp module automatically when we have PCI IDs to monitor
|
||||
if (gpuPciIds.length > 0 && finalModules.indexOf("gpu-temp") === -1) {
|
||||
finalModules.push("gpu-temp")
|
||||
}
|
||||
|
||||
|
||||
if (enabledModules.indexOf("all") !== -1) {
|
||||
cmd.push("--modules", "all")
|
||||
} else if (finalModules.length > 0) {
|
||||
@@ -254,10 +271,12 @@ Singleton {
|
||||
}
|
||||
|
||||
// Add cursor data if available for accurate CPU percentages
|
||||
if ((enabledModules.includes("cpu") || enabledModules.includes("all")) && cpuCursor) {
|
||||
if ((enabledModules.includes("cpu") || enabledModules.includes("all"))
|
||||
&& cpuCursor) {
|
||||
cmd.push("--cpu-cursor", cpuCursor)
|
||||
}
|
||||
if ((enabledModules.includes("processes") || enabledModules.includes("all")) && procCursor) {
|
||||
if ((enabledModules.includes("processes") || enabledModules.includes(
|
||||
"all")) && procCursor) {
|
||||
cmd.push("--proc-cursor", procCursor)
|
||||
}
|
||||
|
||||
@@ -265,7 +284,8 @@ Singleton {
|
||||
cmd.push("--gpu-pci-ids", gpuPciIds.join(","))
|
||||
}
|
||||
|
||||
if (enabledModules.indexOf("processes") !== -1 || enabledModules.indexOf("all") !== -1) {
|
||||
if (enabledModules.indexOf("processes") !== -1
|
||||
|| enabledModules.indexOf("all") !== -1) {
|
||||
cmd.push("--limit", "100") // Get more data for client sorting
|
||||
cmd.push("--sort", "cpu") // Always get CPU sorted data
|
||||
if (noCpu) {
|
||||
@@ -280,7 +300,7 @@ Singleton {
|
||||
if (data.cpu) {
|
||||
const cpu = data.cpu
|
||||
cpuSampleCount++
|
||||
|
||||
|
||||
// Use dgop CPU numbers directly without modification
|
||||
cpuUsage = cpu.usage || 0
|
||||
cpuFrequency = cpu.frequency || 0
|
||||
@@ -301,34 +321,34 @@ Singleton {
|
||||
const totalKB = mem.total || 0
|
||||
const availableKB = mem.available || 0
|
||||
const freeKB = mem.free || 0
|
||||
|
||||
|
||||
// Update MB properties
|
||||
totalMemoryMB = totalKB / 1024
|
||||
availableMemoryMB = availableKB / 1024
|
||||
freeMemoryMB = freeKB / 1024
|
||||
usedMemoryMB = totalMemoryMB - availableMemoryMB
|
||||
memoryUsage = totalKB > 0 ? ((totalKB - availableKB) / totalKB) * 100 : 0
|
||||
|
||||
|
||||
// Update KB properties for compatibility
|
||||
totalMemoryKB = totalKB
|
||||
usedMemoryKB = totalKB - availableKB
|
||||
totalSwapKB = mem.swaptotal || 0
|
||||
usedSwapKB = (mem.swaptotal || 0) - (mem.swapfree || 0)
|
||||
|
||||
|
||||
addToHistory(memoryHistory, memoryUsage)
|
||||
}
|
||||
|
||||
if (data.network && Array.isArray(data.network)) {
|
||||
// Store raw network interface data
|
||||
networkInterfaces = data.network
|
||||
|
||||
|
||||
let totalRx = 0
|
||||
let totalTx = 0
|
||||
for (const iface of data.network) {
|
||||
totalRx += iface.rx || 0
|
||||
totalTx += iface.tx || 0
|
||||
}
|
||||
|
||||
|
||||
if (lastNetworkStats) {
|
||||
const timeDiff = updateInterval / 1000
|
||||
const rxDiff = totalRx - lastNetworkStats.rx
|
||||
@@ -338,20 +358,23 @@ Singleton {
|
||||
addToHistory(networkHistory.rx, networkRxRate / 1024)
|
||||
addToHistory(networkHistory.tx, networkTxRate / 1024)
|
||||
}
|
||||
lastNetworkStats = { "rx": totalRx, "tx": totalTx }
|
||||
lastNetworkStats = {
|
||||
"rx": totalRx,
|
||||
"tx": totalTx
|
||||
}
|
||||
}
|
||||
|
||||
if (data.disk && Array.isArray(data.disk)) {
|
||||
// Store raw disk device data
|
||||
diskDevices = data.disk
|
||||
|
||||
|
||||
let totalRead = 0
|
||||
let totalWrite = 0
|
||||
for (const disk of data.disk) {
|
||||
totalRead += (disk.read || 0) * 512
|
||||
totalWrite += (disk.write || 0) * 512
|
||||
}
|
||||
|
||||
|
||||
if (lastDiskStats) {
|
||||
const timeDiff = updateInterval / 1000
|
||||
const readDiff = totalRead - lastDiskStats.read
|
||||
@@ -361,7 +384,10 @@ Singleton {
|
||||
addToHistory(diskHistory.read, diskReadRate / (1024 * 1024))
|
||||
addToHistory(diskHistory.write, diskWriteRate / (1024 * 1024))
|
||||
}
|
||||
lastDiskStats = { "read": totalRead, "write": totalWrite }
|
||||
lastDiskStats = {
|
||||
"read": totalRead,
|
||||
"write": totalWrite
|
||||
}
|
||||
}
|
||||
|
||||
if (data.diskmounts) {
|
||||
@@ -371,26 +397,31 @@ Singleton {
|
||||
if (data.processes && Array.isArray(data.processes)) {
|
||||
const newProcesses = []
|
||||
processSampleCount++
|
||||
|
||||
|
||||
for (const proc of data.processes) {
|
||||
// Only show CPU usage if we have had at least 2 samples (first sample is inaccurate)
|
||||
const cpuUsage = processSampleCount >= 2 ? (proc.cpu || 0) : 0
|
||||
|
||||
|
||||
newProcesses.push({
|
||||
"pid": proc.pid || 0,
|
||||
"ppid": proc.ppid || 0,
|
||||
"cpu": cpuUsage,
|
||||
"memoryPercent": proc.memoryPercent || proc.pssPercent || 0,
|
||||
"memoryKB": proc.memoryKB || proc.pssKB || 0,
|
||||
"command": proc.command || "",
|
||||
"fullCommand": proc.fullCommand || "",
|
||||
"displayName": (proc.command && proc.command.length > 15) ?
|
||||
proc.command.substring(0, 15) + "..." : (proc.command || "")
|
||||
})
|
||||
"pid": proc.pid || 0,
|
||||
"ppid": proc.ppid || 0,
|
||||
"cpu": cpuUsage,
|
||||
"memoryPercent": proc.memoryPercent
|
||||
|| proc.pssPercent || 0,
|
||||
"memoryKB": proc.memoryKB
|
||||
|| proc.pssKB || 0,
|
||||
"command": proc.command || "",
|
||||
"fullCommand": proc.fullCommand || "",
|
||||
"displayName": (proc.command
|
||||
&& proc.command.length
|
||||
> 15) ? proc.command.substring(
|
||||
0,
|
||||
15) + "..." : (proc.command || "")
|
||||
})
|
||||
}
|
||||
allProcesses = newProcesses
|
||||
applySorting()
|
||||
|
||||
|
||||
// Store the single opaque cursor string for the entire process list
|
||||
if (data.cursor) {
|
||||
procCursor = data.cursor
|
||||
@@ -398,18 +429,24 @@ Singleton {
|
||||
}
|
||||
|
||||
// Handle both gpu and gpu-temp module data
|
||||
const gpuData = (data.gpu && data.gpu.gpus) || data.gpus // Handle both meta format and direct gpu command format
|
||||
const gpuData = (data.gpu && data.gpu.gpus)
|
||||
|| data.gpus // Handle both meta format and direct gpu command format
|
||||
if (gpuData && Array.isArray(gpuData)) {
|
||||
// Check if this is temperature update data (has PCI IDs being monitored)
|
||||
if (gpuPciIds.length > 0 && availableGpus && availableGpus.length > 0) {
|
||||
if (gpuPciIds.length > 0 && availableGpus
|
||||
&& availableGpus.length > 0) {
|
||||
// This is temperature data - merge with existing GPU metadata
|
||||
const updatedGpus = availableGpus.slice()
|
||||
for (let i = 0; i < updatedGpus.length; i++) {
|
||||
for (var i = 0; i < updatedGpus.length; i++) {
|
||||
const existingGpu = updatedGpus[i]
|
||||
const tempGpu = gpuData.find(g => g.pciId === existingGpu.pciId)
|
||||
const tempGpu = gpuData.find(
|
||||
g => g.pciId === existingGpu.pciId)
|
||||
// Only update temperature if this GPU's PCI ID is being monitored
|
||||
if (tempGpu && gpuPciIds.includes(existingGpu.pciId)) {
|
||||
updatedGpus[i] = Object.assign({}, existingGpu, { temperature: tempGpu.temperature || 0 })
|
||||
updatedGpus[i] = Object.assign({}, existingGpu, {
|
||||
"temperature": tempGpu.temperature
|
||||
|| 0
|
||||
})
|
||||
}
|
||||
}
|
||||
availableGpus = updatedGpus
|
||||
@@ -417,17 +454,18 @@ Singleton {
|
||||
// This is initial GPU metadata - set the full list
|
||||
const gpuList = []
|
||||
for (const gpu of gpuData) {
|
||||
let displayName = gpu.displayName || gpu.name || "Unknown GPU"
|
||||
let displayName = gpu.displayName || gpu.name
|
||||
|| "Unknown GPU"
|
||||
let fullName = gpu.fullName || gpu.name || "Unknown GPU"
|
||||
|
||||
gpuList.push({
|
||||
"driver": gpu.driver || "",
|
||||
"vendor": gpu.vendor || "",
|
||||
"displayName": displayName,
|
||||
"fullName": fullName,
|
||||
"pciId": gpu.pciId || "",
|
||||
"temperature": gpu.temperature || 0
|
||||
})
|
||||
"driver": gpu.driver || "",
|
||||
"vendor": gpu.vendor || "",
|
||||
"displayName": displayName,
|
||||
"fullName": fullName,
|
||||
"pciId": gpu.pciId || "",
|
||||
"temperature": gpu.temperature || 0
|
||||
})
|
||||
}
|
||||
availableGpus = gpuList
|
||||
}
|
||||
@@ -463,17 +501,22 @@ Singleton {
|
||||
|
||||
function getProcessIcon(command) {
|
||||
const cmd = command.toLowerCase()
|
||||
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser"))
|
||||
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes(
|
||||
"browser"))
|
||||
return "web"
|
||||
if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim"))
|
||||
if (cmd.includes("code") || cmd.includes("editor")
|
||||
|| cmd.includes("vim"))
|
||||
return "code"
|
||||
if (cmd.includes("terminal") || cmd.includes("bash") || cmd.includes("zsh"))
|
||||
if (cmd.includes("terminal") || cmd.includes("bash")
|
||||
|| cmd.includes("zsh"))
|
||||
return "terminal"
|
||||
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify"))
|
||||
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes(
|
||||
"spotify"))
|
||||
return "music_note"
|
||||
if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv"))
|
||||
return "play_circle"
|
||||
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread"))
|
||||
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes(
|
||||
"kthread"))
|
||||
return "settings"
|
||||
return "memory"
|
||||
}
|
||||
@@ -514,43 +557,45 @@ Singleton {
|
||||
applySorting()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function applySorting() {
|
||||
if (!allProcesses || allProcesses.length === 0) return
|
||||
|
||||
if (!allProcesses || allProcesses.length === 0)
|
||||
return
|
||||
|
||||
const sorted = allProcesses.slice()
|
||||
sorted.sort((a, b) => {
|
||||
let valueA, valueB
|
||||
|
||||
switch (currentSort) {
|
||||
case "cpu":
|
||||
valueA = a.cpu || 0
|
||||
valueB = b.cpu || 0
|
||||
return valueB - valueA
|
||||
case "memory":
|
||||
valueA = a.memoryKB || 0
|
||||
valueB = b.memoryKB || 0
|
||||
return valueB - valueA
|
||||
case "name":
|
||||
valueA = (a.command || "").toLowerCase()
|
||||
valueB = (b.command || "").toLowerCase()
|
||||
return valueA.localeCompare(valueB)
|
||||
case "pid":
|
||||
valueA = a.pid || 0
|
||||
valueB = b.pid || 0
|
||||
return valueA - valueB
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
let valueA, valueB
|
||||
|
||||
switch (currentSort) {
|
||||
case "cpu":
|
||||
valueA = a.cpu || 0
|
||||
valueB = b.cpu || 0
|
||||
return valueB - valueA
|
||||
case "memory":
|
||||
valueA = a.memoryKB || 0
|
||||
valueB = b.memoryKB || 0
|
||||
return valueB - valueA
|
||||
case "name":
|
||||
valueA = (a.command || "").toLowerCase()
|
||||
valueB = (b.command || "").toLowerCase()
|
||||
return valueA.localeCompare(valueB)
|
||||
case "pid":
|
||||
valueA = a.pid || 0
|
||||
valueB = b.pid || 0
|
||||
return valueA - valueB
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
processes = sorted.slice(0, processLimit)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: root.updateInterval
|
||||
running: root.dgopAvailable && root.refCount > 0 && root.enabledModules.length > 0
|
||||
running: root.dgopAvailable && root.refCount > 0
|
||||
&& root.enabledModules.length > 0
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.updateAllStats()
|
||||
@@ -561,14 +606,16 @@ Singleton {
|
||||
command: root.buildDgopCommand()
|
||||
running: false
|
||||
onCommandChanged: {
|
||||
|
||||
//console.log("DgopService command:", JSON.stringify(command))
|
||||
}
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Dgop process failed with exit code:", exitCode)
|
||||
isUpdating = false
|
||||
}
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Dgop process failed with exit code:",
|
||||
exitCode)
|
||||
isUpdating = false
|
||||
}
|
||||
}
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
@@ -590,10 +637,12 @@ Singleton {
|
||||
command: ["dgop", "gpu", "--json"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("GPU init process failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
console.warn(
|
||||
"GPU init process failed with exit code:",
|
||||
exitCode)
|
||||
}
|
||||
}
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
@@ -613,23 +662,24 @@ Singleton {
|
||||
command: ["which", "dgop"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
dgopAvailable = (exitCode === 0)
|
||||
if (dgopAvailable) {
|
||||
initializeGpuMetadata()
|
||||
// Load persisted GPU PCI IDs from session state
|
||||
if (SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.length > 0) {
|
||||
for (const pciId of SessionData.enabledGpuPciIds) {
|
||||
addGpuPciId(pciId)
|
||||
}
|
||||
// Trigger update if we already have active modules
|
||||
if (refCount > 0 && enabledModules.length > 0) {
|
||||
updateAllStats()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn("dgop is not installed or not in PATH")
|
||||
}
|
||||
}
|
||||
dgopAvailable = (exitCode === 0)
|
||||
if (dgopAvailable) {
|
||||
initializeGpuMetadata()
|
||||
// Load persisted GPU PCI IDs from session state
|
||||
if (SessionData.enabledGpuPciIds
|
||||
&& SessionData.enabledGpuPciIds.length > 0) {
|
||||
for (const pciId of SessionData.enabledGpuPciIds) {
|
||||
addGpuPciId(pciId)
|
||||
}
|
||||
// Trigger update if we already have active modules
|
||||
if (refCount > 0 && enabledModules.length > 0) {
|
||||
updateAllStats()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn("dgop is not installed or not in PATH")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
@@ -637,10 +687,10 @@ Singleton {
|
||||
command: ["cat", "/etc/os-release"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Failed to read /etc/os-release")
|
||||
}
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Failed to read /etc/os-release")
|
||||
}
|
||||
}
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
@@ -648,16 +698,18 @@ Singleton {
|
||||
const lines = text.trim().split('\n')
|
||||
let prettyName = ""
|
||||
let name = ""
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.startsWith('PRETTY_NAME=')) {
|
||||
prettyName = trimmedLine.substring(12).replace(/^["']|["']$/g, '')
|
||||
prettyName = trimmedLine.substring(12).replace(
|
||||
/^["']|["']$/g, '')
|
||||
} else if (trimmedLine.startsWith('NAME=')) {
|
||||
name = trimmedLine.substring(5).replace(/^["']|["']$/g, '')
|
||||
name = trimmedLine.substring(5).replace(
|
||||
/^["']|["']$/g, '')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Prefer PRETTY_NAME, fallback to NAME
|
||||
const distroName = prettyName || name || "Linux"
|
||||
distribution = distroName
|
||||
@@ -675,4 +727,4 @@ Singleton {
|
||||
dgopCheckProcess.running = true
|
||||
osReleaseProcess.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -11,100 +12,98 @@ Singleton {
|
||||
property bool inhibitorAvailable: true
|
||||
property bool idleInhibited: false
|
||||
property string inhibitReason: "Keep system awake"
|
||||
|
||||
signal inhibitorChanged()
|
||||
|
||||
signal inhibitorChanged
|
||||
|
||||
function enableIdleInhibit() {
|
||||
if (idleInhibited) return;
|
||||
idleInhibited = true;
|
||||
inhibitorChanged();
|
||||
if (idleInhibited)
|
||||
return
|
||||
idleInhibited = true
|
||||
inhibitorChanged()
|
||||
}
|
||||
|
||||
|
||||
function disableIdleInhibit() {
|
||||
if (!idleInhibited) return;
|
||||
idleInhibited = false;
|
||||
inhibitorChanged();
|
||||
if (!idleInhibited)
|
||||
return
|
||||
idleInhibited = false
|
||||
inhibitorChanged()
|
||||
}
|
||||
|
||||
|
||||
function toggleIdleInhibit() {
|
||||
if (idleInhibited) {
|
||||
disableIdleInhibit();
|
||||
disableIdleInhibit()
|
||||
} else {
|
||||
enableIdleInhibit();
|
||||
enableIdleInhibit()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setInhibitReason(reason) {
|
||||
inhibitReason = reason;
|
||||
|
||||
inhibitReason = reason
|
||||
|
||||
if (idleInhibited) {
|
||||
const wasActive = idleInhibited;
|
||||
idleInhibited = false;
|
||||
|
||||
const wasActive = idleInhibited
|
||||
idleInhibited = false
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (wasActive) idleInhibited = true;
|
||||
});
|
||||
if (wasActive)
|
||||
idleInhibited = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: idleInhibitProcess
|
||||
|
||||
|
||||
command: {
|
||||
if (!idleInhibited) {
|
||||
return ["true"];
|
||||
return ["true"]
|
||||
}
|
||||
|
||||
return [
|
||||
"systemd-inhibit",
|
||||
"--what=idle",
|
||||
"--who=quickshell",
|
||||
"--why=" + inhibitReason,
|
||||
"--mode=block",
|
||||
"sleep", "infinity"
|
||||
];
|
||||
|
||||
return ["systemd-inhibit", "--what=idle", "--who=quickshell", "--why="
|
||||
+ inhibitReason, "--mode=block", "sleep", "infinity"]
|
||||
}
|
||||
|
||||
|
||||
running: idleInhibited
|
||||
|
||||
onExited: function(exitCode) {
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (idleInhibited && exitCode !== 0) {
|
||||
console.warn("IdleInhibitorService: Inhibitor process crashed with exit code:", exitCode);
|
||||
idleInhibited = false;
|
||||
ToastService.showWarning("Idle inhibitor failed");
|
||||
console.warn("IdleInhibitorService: Inhibitor process crashed with exit code:",
|
||||
exitCode)
|
||||
idleInhibited = false
|
||||
ToastService.showWarning("Idle inhibitor failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
IpcHandler {
|
||||
function toggle() : string {
|
||||
root.toggleIdleInhibit();
|
||||
return root.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled";
|
||||
function toggle(): string {
|
||||
root.toggleIdleInhibit()
|
||||
return root.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
|
||||
}
|
||||
|
||||
function enable() : string {
|
||||
root.enableIdleInhibit();
|
||||
return "Idle inhibit enabled";
|
||||
|
||||
function enable(): string {
|
||||
root.enableIdleInhibit()
|
||||
return "Idle inhibit enabled"
|
||||
}
|
||||
|
||||
function disable() : string {
|
||||
root.disableIdleInhibit();
|
||||
return "Idle inhibit disabled";
|
||||
|
||||
function disable(): string {
|
||||
root.disableIdleInhibit()
|
||||
return "Idle inhibit disabled"
|
||||
}
|
||||
|
||||
function status() : string {
|
||||
return root.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled";
|
||||
|
||||
function status(): string {
|
||||
return root.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled"
|
||||
}
|
||||
|
||||
function reason(newReason: string) : string {
|
||||
|
||||
function reason(newReason: string): string {
|
||||
if (!newReason) {
|
||||
return "Current reason: " + root.inhibitReason;
|
||||
return "Current reason: " + root.inhibitReason
|
||||
}
|
||||
|
||||
root.setInhibitReason(newReason);
|
||||
return "Inhibit reason set to: " + newReason;
|
||||
|
||||
root.setInhibitReason(newReason)
|
||||
return "Inhibit reason set to: " + newReason
|
||||
}
|
||||
|
||||
|
||||
target: "inhibit"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -13,93 +14,55 @@ Singleton {
|
||||
property bool powerDialogVisible: false
|
||||
property bool rebootDialogVisible: false
|
||||
property bool logoutDialogVisible: false
|
||||
|
||||
property var facts: [
|
||||
"A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.",
|
||||
"A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.",
|
||||
"Right now, 100 trillion solar neutrinos are passing through your body every second.",
|
||||
"The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.",
|
||||
"The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.",
|
||||
"There's a nebula out there that's actually colder than empty space itself.",
|
||||
"We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.",
|
||||
"Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.",
|
||||
"Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.",
|
||||
"Distant galaxies can move away from us faster than light because space itself is stretching.",
|
||||
"The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.",
|
||||
"The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.",
|
||||
"A day on Venus lasts longer than its entire year around the Sun.",
|
||||
"On Mercury, the time between sunrises is 176 Earth days long.",
|
||||
"In about 4.5 billion years, our galaxy will smash into Andromeda.",
|
||||
"Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.",
|
||||
"PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.",
|
||||
"Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.",
|
||||
"Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.",
|
||||
"Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.",
|
||||
"Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.",
|
||||
"Counting to a billion at one number per second would take over 31 years.",
|
||||
"Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.",
|
||||
"Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.",
|
||||
"Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.",
|
||||
"Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.",
|
||||
"Even at light-speed, you'd never catch up to most galaxies—space expands faster.",
|
||||
"Only around 5% of galaxies are ever reachable—even at light-speed.",
|
||||
"If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.",
|
||||
"If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.",
|
||||
"Our oldest radio signals will reach the Milky Way's center in 26,000 years.",
|
||||
"Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.",
|
||||
"The Moon moves 3.8 centimeters farther from Earth every year.",
|
||||
"The universe creates 275 million new stars every single day.",
|
||||
"Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.",
|
||||
"If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.",
|
||||
"The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."
|
||||
]
|
||||
|
||||
property var facts: ["A photon takes 100,000 to 200,000 years bouncing through the Sun's dense core, then races to Earth in just 8 minutes 20 seconds.", "A teaspoon of neutron star matter would weigh a billion metric tons here on Earth.", "Right now, 100 trillion solar neutrinos are passing through your body every second.", "The Sun converts 4 million metric tons of matter into pure energy every second—enough to power Earth for 500,000 years.", "The universe still glows with leftover heat from the Big Bang—just 2.7 degrees above absolute zero.", "There's a nebula out there that's actually colder than empty space itself.", "We've detected black holes crashing together by measuring spacetime stretch by less than 1/10,000th the width of a proton.", "Fast radio bursts can release more energy in 5 milliseconds than our Sun produces in 3 days.", "Our galaxy might be crawling with billions of rogue planets drifting alone in the dark.", "Distant galaxies can move away from us faster than light because space itself is stretching.", "The edge of what we can see is 46.5 billion light-years away, even though the universe is only 13.8 billion years old.", "The universe is mostly invisible: 5% regular matter, 27% dark matter, 68% dark energy.", "A day on Venus lasts longer than its entire year around the Sun.", "On Mercury, the time between sunrises is 176 Earth days long.", "In about 4.5 billion years, our galaxy will smash into Andromeda.", "Most of the gold in your jewelry was forged when neutron stars collided somewhere in space.", "PSR J1748-2446ad, the fastest spinning star, rotates 716 times per second—its equator moves at 24% the speed of light.", "Cosmic rays create particles that shouldn't make it to Earth's surface, but time dilation lets them sneak through.", "Jupiter's magnetic field is so huge that if we could see it, it would look bigger than the Moon in our sky.", "Interstellar space is so empty it's like a cube 32 kilometers wide containing just a single grain of sand.", "Voyager 1 is 24 billion kilometers away but won't leave the Sun's gravitational influence for another 30,000 years.", "Counting to a billion at one number per second would take over 31 years.", "Space is so vast, even speeding at light-speed, you'd never return past the cosmic horizon.", "Astronauts on the ISS age about 0.01 seconds less each year than people on Earth.", "Sagittarius B2, a dust cloud near our galaxy's center, contains ethyl formate—the compound that gives raspberries their flavor and rum its smell.", "Beyond 16 billion light-years, the cosmic event horizon marks where space expands too fast for light to ever reach us again.", "Even at light-speed, you'd never catch up to most galaxies—space expands faster.", "Only around 5% of galaxies are ever reachable—even at light-speed.", "If the Sun vanished, we'd still orbit it for 8 minutes before drifting away.", "If a planet 65 million light-years away looked at Earth now, it'd see dinosaurs.", "Our oldest radio signals will reach the Milky Way's center in 26,000 years.", "Every atom in your body heavier than hydrogen was forged in the nuclear furnace of a dying star.", "The Moon moves 3.8 centimeters farther from Earth every year.", "The universe creates 275 million new stars every single day.", "Jupiter's Great Red Spot is a storm twice the size of Earth that has been raging for at least 350 years.", "If you watched someone fall into a black hole, they'd appear frozen at the event horizon forever—time effectively stops from your perspective.", "The Boötes Supervoid is a cosmic desert 1.8 billion light-years across with 60% fewer galaxies than it should have."]
|
||||
|
||||
function pickRandomFact() {
|
||||
randomFact = facts[Math.floor(Math.random() * facts.length)];
|
||||
randomFact = facts[Math.floor(Math.random() * facts.length)]
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
unlocking = false;
|
||||
pamState = "";
|
||||
powerDialogVisible = false;
|
||||
rebootDialogVisible = false;
|
||||
logoutDialogVisible = false;
|
||||
pickRandomFact();
|
||||
unlocking = false
|
||||
pamState = ""
|
||||
powerDialogVisible = false
|
||||
rebootDialogVisible = false
|
||||
logoutDialogVisible = false
|
||||
pickRandomFact()
|
||||
}
|
||||
|
||||
function setPamState(state) {
|
||||
pamState = state;
|
||||
pamState = state
|
||||
}
|
||||
|
||||
function setUnlocking(value) {
|
||||
unlocking = value;
|
||||
unlocking = value
|
||||
}
|
||||
|
||||
function showPowerDialog() {
|
||||
powerDialogVisible = true;
|
||||
powerDialogVisible = true
|
||||
}
|
||||
|
||||
function hidePowerDialog() {
|
||||
powerDialogVisible = false;
|
||||
powerDialogVisible = false
|
||||
}
|
||||
|
||||
function showRebootDialog() {
|
||||
rebootDialogVisible = true;
|
||||
rebootDialogVisible = true
|
||||
}
|
||||
|
||||
function hideRebootDialog() {
|
||||
rebootDialogVisible = false;
|
||||
rebootDialogVisible = false
|
||||
}
|
||||
|
||||
function showLogoutDialog() {
|
||||
logoutDialogVisible = true;
|
||||
logoutDialogVisible = true
|
||||
}
|
||||
|
||||
function hideLogoutDialog() {
|
||||
logoutDialogVisible = false;
|
||||
logoutDialogVisible = false
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
pickRandomFact();
|
||||
pickRandomFact()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,48 +9,49 @@ import Quickshell.Services.Mpris
|
||||
import Quickshell.Widgets
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
||||
|
||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
|
||||
?? availablePlayers.find(
|
||||
p => p.canControl && p.canPlay) ?? null
|
||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
|
||||
?? availablePlayers.find(
|
||||
p => p.canControl
|
||||
&& p.canPlay) ?? null
|
||||
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
|
||||
function list(): string {
|
||||
return root.availablePlayers.map(p => p.identity).join("")
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -10,514 +10,519 @@ 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)
|
||||
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
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timeUpdateTimer
|
||||
interval: 30000
|
||||
repeat: true
|
||||
running: root.allWrappers.length > 0
|
||||
triggeredOnStart: false
|
||||
onTriggered: {
|
||||
root.timeUpdateTick = !root.timeUpdateTick
|
||||
}
|
||||
}
|
||||
|
||||
property bool timeUpdateTick: false
|
||||
property bool clockFormatChanged: false
|
||||
|
||||
readonly property var groupedNotifications: getGroupedNotifications()
|
||||
readonly property var groupedPopups: getGroupedPopups()
|
||||
|
||||
property var expandedGroups: ({})
|
||||
property var expandedMessages: ({})
|
||||
property bool popupsDisabled: false
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
keepOnReload: false
|
||||
actionsSupported: true
|
||||
actionIconsSupported: true
|
||||
bodyHyperlinksSupported: true
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
imageSupported: true
|
||||
inlineReplySupported: true
|
||||
persistenceSupported: true
|
||||
|
||||
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()
|
||||
Timer {
|
||||
id: addGate
|
||||
interval: enterAnimMs + 50
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
addGateBusy = false
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Timer timer: Timer {
|
||||
interval: {
|
||||
if (!wrapper.notification) return 5000
|
||||
|
||||
switch (wrapper.notification.urgency) {
|
||||
case NotificationUrgency.Low:
|
||||
return SettingsData.notificationTimeoutLow
|
||||
case NotificationUrgency.Critical:
|
||||
return SettingsData.notificationTimeoutCritical
|
||||
default:
|
||||
return SettingsData.notificationTimeoutNormal
|
||||
Timer {
|
||||
id: timeUpdateTimer
|
||||
interval: 30000
|
||||
repeat: true
|
||||
running: root.allWrappers.length > 0
|
||||
triggeredOnStart: false
|
||||
onTriggered: {
|
||||
root.timeUpdateTick = !root.timeUpdateTick
|
||||
}
|
||||
}
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
if (interval > 0) {
|
||||
wrapper.popup = false
|
||||
}
|
||||
|
||||
property bool timeUpdateTick: false
|
||||
property bool clockFormatChanged: false
|
||||
|
||||
readonly property var groupedNotifications: getGroupedNotifications()
|
||||
readonly property var groupedPopups: getGroupedPopups()
|
||||
|
||||
property var expandedGroups: ({})
|
||||
property var expandedMessages: ({})
|
||||
property bool popupsDisabled: false
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
keepOnReload: false
|
||||
actionsSupported: true
|
||||
actionIconsSupported: true
|
||||
bodyHyperlinksSupported: true
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
imageSupported: true
|
||||
inlineReplySupported: true
|
||||
persistenceSupported: true
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
readonly property date time: new Date()
|
||||
readonly property string timeStr: {
|
||||
root.timeUpdateTick
|
||||
root.clockFormatChanged
|
||||
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - time.getTime()
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
|
||||
if (hours < 1) {
|
||||
if (minutes < 1)
|
||||
return "now"
|
||||
return `${minutes}m ago`
|
||||
}
|
||||
|
||||
const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate())
|
||||
const daysDiff = Math.floor((nowDate - timeDate) / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (daysDiff === 0) {
|
||||
return formatTime(time)
|
||||
}
|
||||
|
||||
if (daysDiff === 1) {
|
||||
return `yesterday, ${formatTime(time)}`
|
||||
}
|
||||
|
||||
return `${daysDiff} days ago`
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
let use24Hour = true
|
||||
try {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.use24HourClock !== undefined) {
|
||||
use24Hour = SettingsData.use24HourClock
|
||||
}
|
||||
} catch (e) {
|
||||
use24Hour = true
|
||||
}
|
||||
|
||||
if (use24Hour) {
|
||||
return date.toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
} else {
|
||||
return date.toLocaleTimeString(Qt.locale(), "h:mm AP")
|
||||
}
|
||||
}
|
||||
|
||||
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 == "") {
|
||||
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
|
||||
component NotifWrapper: QtObject {
|
||||
id: wrapper
|
||||
|
||||
property bool popup: false
|
||||
property bool removedByLimit: false
|
||||
property bool isPersistent: true
|
||||
property int seq: 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)
|
||||
onPopupChanged: {
|
||||
if (!popup) {
|
||||
removeFromVisibleNotifications(wrapper)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
readonly property Timer timer: Timer {
|
||||
interval: {
|
||||
if (!wrapper.notification)
|
||||
return 5000
|
||||
|
||||
switch (wrapper.notification.urgency) {
|
||||
case NotificationUrgency.Low:
|
||||
return SettingsData.notificationTimeoutLow
|
||||
case NotificationUrgency.Critical:
|
||||
return SettingsData.notificationTimeoutCritical
|
||||
default:
|
||||
return SettingsData.notificationTimeoutNormal
|
||||
}
|
||||
}
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
if (interval > 0) {
|
||||
wrapper.popup = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bulkDismissing = false
|
||||
popupsDisabled = false
|
||||
}
|
||||
readonly property date time: new Date()
|
||||
readonly property string timeStr: {
|
||||
root.timeUpdateTick
|
||||
root.clockFormatChanged
|
||||
|
||||
function dismissNotification(wrapper) {
|
||||
if (!wrapper || !wrapper.notification)
|
||||
return
|
||||
wrapper.popup = false
|
||||
wrapper.notification.dismiss()
|
||||
}
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - time.getTime()
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
|
||||
function disablePopups(disable) {
|
||||
popupsDisabled = disable
|
||||
if (disable) {
|
||||
notificationQueue = []
|
||||
visibleNotifications = []
|
||||
for (const notif of root.allWrappers) {
|
||||
notif.popup = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hours < 1) {
|
||||
if (minutes < 1)
|
||||
return "now"
|
||||
return `${minutes}m ago`
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (addGateBusy)
|
||||
return
|
||||
if (popupsDisabled)
|
||||
return
|
||||
if (SessionData.doNotDisturb)
|
||||
return
|
||||
if (notificationQueue.length === 0)
|
||||
return
|
||||
const nowDate = new Date(now.getFullYear(), now.getMonth(),
|
||||
now.getDate())
|
||||
const timeDate = new Date(time.getFullYear(), time.getMonth(),
|
||||
time.getDate())
|
||||
const daysDiff = Math.floor(
|
||||
(nowDate - timeDate) / (1000 * 60 * 60 * 24))
|
||||
|
||||
const next = notificationQueue.shift()
|
||||
if (daysDiff === 0) {
|
||||
return formatTime(time)
|
||||
}
|
||||
|
||||
next.seq = ++seqCounter
|
||||
visibleNotifications = [...visibleNotifications, next]
|
||||
next.popup = true
|
||||
if (daysDiff === 1) {
|
||||
return `yesterday, ${formatTime(time)}`
|
||||
}
|
||||
|
||||
if (next.timer.interval > 0) {
|
||||
next.timer.start()
|
||||
}
|
||||
|
||||
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) {
|
||||
let v = visibleNotifications.slice()
|
||||
const vi = v.indexOf(w)
|
||||
if (vi !== -1) {
|
||||
v.splice(vi, 1)
|
||||
visibleNotifications = v
|
||||
}
|
||||
|
||||
let q = notificationQueue.slice()
|
||||
const qi = q.indexOf(w)
|
||||
if (qi !== -1) {
|
||||
q.splice(qi, 1)
|
||||
notificationQueue = q
|
||||
}
|
||||
|
||||
if (w && w.destroy && !w.isPersistent) {
|
||||
w.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
function getGroupKey(wrapper) {
|
||||
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
|
||||
return wrapper.desktopEntry.toLowerCase()
|
||||
}
|
||||
|
||||
return wrapper.appName.toLowerCase()
|
||||
}
|
||||
|
||||
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
|
||||
return `${daysDiff} days ago`
|
||||
}
|
||||
}
|
||||
|
||||
groups[groupKey].notifications.unshift(notif)
|
||||
groups[groupKey].latestNotification = groups[groupKey].notifications[0]
|
||||
groups[groupKey].count = groups[groupKey].notifications.length
|
||||
function formatTime(date) {
|
||||
let use24Hour = true
|
||||
try {
|
||||
if (typeof SettingsData !== "undefined"
|
||||
&& SettingsData.use24HourClock !== undefined) {
|
||||
use24Hour = SettingsData.use24HourClock
|
||||
}
|
||||
} catch (e) {
|
||||
use24Hour = true
|
||||
}
|
||||
|
||||
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(
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
if (use24Hour) {
|
||||
return date.toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
} else {
|
||||
return date.toLocaleTimeString(Qt.locale(), "h:mm AP")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const notif of allWrappers) {
|
||||
if (notif && notif.notification && getGroupKey(notif) === groupKey) {
|
||||
notif.notification.dismiss()
|
||||
readonly property string appIcon: notification.appIcon
|
||||
readonly property string appName: {
|
||||
if (notification.appName == "") {
|
||||
const entry = DesktopEntries.byId(notification.desktopEntry)
|
||||
if (entry && entry.name) {
|
||||
return entry.name.toLowerCase()
|
||||
}
|
||||
}
|
||||
return notification.appName || "app"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearGroupExpansionState(groupKey) {
|
||||
let newExpandedGroups = {}
|
||||
for (const key in expandedGroups) {
|
||||
if (key !== groupKey && expandedGroups[key]) {
|
||||
newExpandedGroups[key] = true
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
visibleNotifications = []
|
||||
readonly property int urgency: notification.urgency
|
||||
readonly property list<NotificationAction> actions: notification.actions
|
||||
|
||||
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 = []
|
||||
} else {
|
||||
// Re-enable popup processing when DND is disabled
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: typeof SettingsData !== "undefined" ? SettingsData : null
|
||||
function onUse24HourClockChanged() {
|
||||
root.clockFormatChanged = !root.clockFormatChanged
|
||||
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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bulkDismissing = false
|
||||
popupsDisabled = false
|
||||
}
|
||||
|
||||
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 = notificationQueue.shift()
|
||||
|
||||
next.seq = ++seqCounter
|
||||
visibleNotifications = [...visibleNotifications, next]
|
||||
next.popup = true
|
||||
|
||||
if (next.timer.interval > 0) {
|
||||
next.timer.start()
|
||||
}
|
||||
|
||||
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) {
|
||||
let v = visibleNotifications.slice()
|
||||
const vi = v.indexOf(w)
|
||||
if (vi !== -1) {
|
||||
v.splice(vi, 1)
|
||||
visibleNotifications = v
|
||||
}
|
||||
|
||||
let q = notificationQueue.slice()
|
||||
const qi = q.indexOf(w)
|
||||
if (qi !== -1) {
|
||||
q.splice(qi, 1)
|
||||
notificationQueue = q
|
||||
}
|
||||
|
||||
if (w && w.destroy && !w.isPersistent) {
|
||||
w.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
function getGroupKey(wrapper) {
|
||||
if (wrapper.desktopEntry && wrapper.desktopEntry !== "") {
|
||||
return wrapper.desktopEntry.toLowerCase()
|
||||
}
|
||||
|
||||
return wrapper.appName.toLowerCase()
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: typeof SettingsData !== "undefined" ? SettingsData : null
|
||||
function onUse24HourClockChanged() {
|
||||
root.clockFormatChanged = !root.clockFormatChanged
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,174 +7,174 @@ 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
|
||||
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 getSystemProfileImage() {
|
||||
systemProfileCheckProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
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 setProfileImage(imagePath) {
|
||||
profileImage = imagePath
|
||||
if (accountsServiceAvailable && imagePath) {
|
||||
setSystemProfileImage(imagePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.systemProfileImage = ""
|
||||
}
|
||||
function getSystemColorScheme() {
|
||||
systemColorSchemeCheckProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemProfileSetProcess
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
root.getSystemProfileImage()
|
||||
}
|
||||
function setLightMode(isLightMode) {
|
||||
if (settingsPortalAvailable) {
|
||||
setSystemColorScheme(isLightMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
function setSystemColorScheme(isLightMode) {
|
||||
if (!settingsPortalAvailable)
|
||||
return
|
||||
|
||||
onExited: exitCode => {
|
||||
root.settingsPortalAvailable = (exitCode === 0)
|
||||
if (root.settingsPortalAvailable) {
|
||||
root.getSystemColorScheme()
|
||||
}
|
||||
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: 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
|
||||
function setSystemProfileImage(imagePath) {
|
||||
if (!accountsServiceAvailable || !imagePath)
|
||||
return
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var match = text.match(/uint32 (\d+)/)
|
||||
if (match && match[1]) {
|
||||
root.systemColorScheme = parseInt(match[1])
|
||||
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(
|
||||
" ")
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
var shouldBeLightMode = (root.systemColorScheme === 2)
|
||||
if (Theme.isLightMode !== shouldBeLightMode) {
|
||||
Theme.isLightMode = shouldBeLightMode
|
||||
if (typeof SessionData !== "undefined") {
|
||||
SessionData.setLightMode(shouldBeLightMode)
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.systemColorScheme = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
Process {
|
||||
id: systemColorSchemeSetProcess
|
||||
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]
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
Qt.callLater(() => {
|
||||
root.getSystemColorScheme()
|
||||
})
|
||||
}
|
||||
if (!root.profileImage || root.profileImage === "") {
|
||||
root.profileImage = root.systemProfileImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.systemProfileImage = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: systemProfileSetProcess
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
root.getSystemProfileImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,136 +8,137 @@ import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
readonly property bool microphoneActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values)
|
||||
return false
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: Pipewire.nodes.values
|
||||
}
|
||||
|
||||
readonly property bool cameraActive: {
|
||||
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.properties
|
||||
&& node.properties["media.class"] === "Stream/Input/Video") {
|
||||
if (node.properties["stream.is-live"] === "true") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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 true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
readonly property bool anyPrivacyActive: microphoneActive || cameraActive
|
||||
|| screensharingActive
|
||||
PwObjectTracker {
|
||||
objects: Pipewire.nodes.values
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
readonly property bool cameraActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values)
|
||||
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)
|
||||
}
|
||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node || !node.ready)
|
||||
continue
|
||||
|
||||
function getMicrophoneStatus() {
|
||||
return microphoneActive ? "active" : "inactive"
|
||||
}
|
||||
if (node.properties
|
||||
&& node.properties["media.class"] === "Stream/Input/Video") {
|
||||
if (node.properties["stream.is-live"] === "true") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function getCameraStatus() {
|
||||
return cameraActive ? "active" : "inactive"
|
||||
}
|
||||
readonly property bool screensharingActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values)
|
||||
return false
|
||||
|
||||
function getScreensharingStatus() {
|
||||
return screensharingActive ? "active" : "inactive"
|
||||
}
|
||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node || !node.ready)
|
||||
continue
|
||||
|
||||
function getPrivacySummary() {
|
||||
const active = []
|
||||
if (microphoneActive)
|
||||
active.push("microphone")
|
||||
if (cameraActive)
|
||||
active.push("camera")
|
||||
if (screensharingActive)
|
||||
active.push("screensharing")
|
||||
if ((node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource) {
|
||||
if (looksLikeScreencast(node)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return active.length > 0 ? "Privacy active: " + active.join(
|
||||
", ") : "No privacy concerns detected"
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
readonly property bool anyPrivacyActive: microphoneActive || cameraActive
|
||||
|| screensharingActive
|
||||
|
||||
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 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 getMicrophoneStatus() {
|
||||
return microphoneActive ? "active" : "inactive"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,97 +6,98 @@ 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 currentDetails: ""
|
||||
property bool hasDetails: false
|
||||
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 currentDetails: ""
|
||||
property bool hasDetails: false
|
||||
property string wallpaperErrorStatus: ""
|
||||
|
||||
function showToast(message, level = levelInfo, details = "") {
|
||||
toastQueue.push({
|
||||
"message": message,
|
||||
"level": level,
|
||||
"details": details
|
||||
})
|
||||
if (!toastVisible)
|
||||
processQueue()
|
||||
}
|
||||
|
||||
function showInfo(message, details = "") {
|
||||
showToast(message, levelInfo, details)
|
||||
}
|
||||
|
||||
function showWarning(message, details = "") {
|
||||
showToast(message, levelWarn, details)
|
||||
}
|
||||
|
||||
function showError(message, details = "") {
|
||||
showToast(message, levelError, details)
|
||||
}
|
||||
|
||||
function hideToast() {
|
||||
toastVisible = false
|
||||
currentMessage = ""
|
||||
currentDetails = ""
|
||||
hasDetails = false
|
||||
currentLevel = levelInfo
|
||||
toastTimer.stop()
|
||||
resetToastState()
|
||||
if (toastQueue.length > 0)
|
||||
processQueue()
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (toastQueue.length === 0)
|
||||
return
|
||||
|
||||
const toast = toastQueue.shift()
|
||||
currentMessage = toast.message
|
||||
currentLevel = toast.level
|
||||
currentDetails = toast.details || ""
|
||||
hasDetails = currentDetails.length > 0
|
||||
toastVisible = true
|
||||
resetToastState()
|
||||
|
||||
if (toast.level === levelError && hasDetails) {
|
||||
toastTimer.interval = 8000
|
||||
toastTimer.start()
|
||||
} else {
|
||||
toastTimer.interval = toast.level === levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
|
||||
toastTimer.start()
|
||||
function showToast(message, level = levelInfo, details = "") {
|
||||
toastQueue.push({
|
||||
"message": message,
|
||||
"level": level,
|
||||
"details": details
|
||||
})
|
||||
if (!toastVisible)
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
signal resetToastState
|
||||
|
||||
function stopTimer() {
|
||||
toastTimer.stop()
|
||||
}
|
||||
|
||||
function restartTimer() {
|
||||
if (hasDetails && currentLevel === levelError) {
|
||||
toastTimer.interval = 8000
|
||||
toastTimer.restart()
|
||||
function showInfo(message, details = "") {
|
||||
showToast(message, levelInfo, details)
|
||||
}
|
||||
}
|
||||
|
||||
function clearWallpaperError() {
|
||||
wallpaperErrorStatus = ""
|
||||
}
|
||||
function showWarning(message, details = "") {
|
||||
showToast(message, levelWarn, details)
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: toastTimer
|
||||
function showError(message, details = "") {
|
||||
showToast(message, levelError, details)
|
||||
}
|
||||
|
||||
interval: 5000
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: hideToast()
|
||||
}
|
||||
function hideToast() {
|
||||
toastVisible = false
|
||||
currentMessage = ""
|
||||
currentDetails = ""
|
||||
hasDetails = false
|
||||
currentLevel = levelInfo
|
||||
toastTimer.stop()
|
||||
resetToastState()
|
||||
if (toastQueue.length > 0)
|
||||
processQueue()
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (toastQueue.length === 0)
|
||||
return
|
||||
|
||||
const toast = toastQueue.shift()
|
||||
currentMessage = toast.message
|
||||
currentLevel = toast.level
|
||||
currentDetails = toast.details || ""
|
||||
hasDetails = currentDetails.length > 0
|
||||
toastVisible = true
|
||||
resetToastState()
|
||||
|
||||
if (toast.level === levelError && hasDetails) {
|
||||
toastTimer.interval = 8000
|
||||
toastTimer.start()
|
||||
} else {
|
||||
toastTimer.interval = toast.level
|
||||
=== levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
|
||||
toastTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
signal resetToastState
|
||||
|
||||
function stopTimer() {
|
||||
toastTimer.stop()
|
||||
}
|
||||
|
||||
function restartTimer() {
|
||||
if (hasDetails && currentLevel === levelError) {
|
||||
toastTimer.interval = 8000
|
||||
toastTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
function clearWallpaperError() {
|
||||
wallpaperErrorStatus = ""
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: toastTimer
|
||||
|
||||
interval: 5000
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: hideToast()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,93 +7,93 @@ 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 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 getUserInfo() {
|
||||
userInfoProcess.running = true
|
||||
}
|
||||
|
||||
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] || ""
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get system uptime
|
||||
Process {
|
||||
id: uptimeProcess
|
||||
|
||||
command: ["cat", "/proc/uptime"]
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
root.uptime = "Unknown"
|
||||
}
|
||||
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 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)
|
||||
// Get system uptime
|
||||
Process {
|
||||
id: uptimeProcess
|
||||
|
||||
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"}`)
|
||||
command: ["cat", "/proc/uptime"]
|
||||
running: false
|
||||
|
||||
if (parts.length > 0)
|
||||
root.uptime = "up " + parts.join(", ")
|
||||
else
|
||||
root.uptime = `up ${seconds} seconds`
|
||||
}
|
||||
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`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -7,189 +8,203 @@ import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
property bool cyclingActive: false
|
||||
property string cachedCyclingTime: SessionData.wallpaperCyclingTime
|
||||
property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
|
||||
property string lastTimeCheck: ""
|
||||
property bool cyclingActive: false
|
||||
property string cachedCyclingTime: SessionData.wallpaperCyclingTime
|
||||
property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
|
||||
property string lastTimeCheck: ""
|
||||
|
||||
Component.onCompleted: {
|
||||
updateCyclingState()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
|
||||
function onWallpaperCyclingEnabledChanged() {
|
||||
updateCyclingState()
|
||||
}
|
||||
|
||||
function onWallpaperCyclingModeChanged() {
|
||||
updateCyclingState()
|
||||
}
|
||||
|
||||
function onWallpaperCyclingIntervalChanged() {
|
||||
cachedCyclingInterval = SessionData.wallpaperCyclingInterval
|
||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
||||
Component.onCompleted: {
|
||||
updateCyclingState()
|
||||
}
|
||||
}
|
||||
|
||||
function onWallpaperCyclingTimeChanged() {
|
||||
cachedCyclingTime = SessionData.wallpaperCyclingTime
|
||||
if (SessionData.wallpaperCyclingMode === "time") {
|
||||
updateCyclingState()
|
||||
}
|
||||
Connections {
|
||||
target: SessionData
|
||||
|
||||
function onWallpaperCyclingEnabledChanged() {
|
||||
updateCyclingState()
|
||||
}
|
||||
|
||||
function onWallpaperCyclingModeChanged() {
|
||||
updateCyclingState()
|
||||
}
|
||||
|
||||
function onWallpaperCyclingIntervalChanged() {
|
||||
cachedCyclingInterval = SessionData.wallpaperCyclingInterval
|
||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
||||
updateCyclingState()
|
||||
}
|
||||
}
|
||||
|
||||
function onWallpaperCyclingTimeChanged() {
|
||||
cachedCyclingTime = SessionData.wallpaperCyclingTime
|
||||
if (SessionData.wallpaperCyclingMode === "time") {
|
||||
updateCyclingState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateCyclingState() {
|
||||
if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath) {
|
||||
startCycling()
|
||||
} else {
|
||||
stopCycling()
|
||||
function updateCyclingState() {
|
||||
if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath) {
|
||||
startCycling()
|
||||
} else {
|
||||
stopCycling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startCycling() {
|
||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
||||
intervalTimer.interval = cachedCyclingInterval * 1000
|
||||
intervalTimer.start()
|
||||
cyclingActive = true
|
||||
} else if (SessionData.wallpaperCyclingMode === "time") {
|
||||
cyclingActive = true
|
||||
checkTimeBasedCycling()
|
||||
}
|
||||
}
|
||||
|
||||
function stopCycling() {
|
||||
intervalTimer.stop()
|
||||
cyclingActive = false
|
||||
}
|
||||
|
||||
function cycleToNextWallpaper() {
|
||||
if (!SessionData.wallpaperPath) return
|
||||
|
||||
const wallpaperDir = SessionData.wallpaperPath.substring(0, SessionData.wallpaperPath.lastIndexOf('/'))
|
||||
cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]
|
||||
cyclingProcess.running = true
|
||||
}
|
||||
|
||||
function cycleToPrevWallpaper() {
|
||||
if (!SessionData.wallpaperPath) return
|
||||
|
||||
const wallpaperDir = SessionData.wallpaperPath.substring(0, SessionData.wallpaperPath.lastIndexOf('/'))
|
||||
prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]
|
||||
prevCyclingProcess.running = true
|
||||
}
|
||||
|
||||
function cycleNextManually() {
|
||||
if (SessionData.wallpaperPath) {
|
||||
cycleToNextWallpaper()
|
||||
// Restart timers if cycling is active
|
||||
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
|
||||
function startCycling() {
|
||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
||||
intervalTimer.interval = cachedCyclingInterval * 1000
|
||||
intervalTimer.restart()
|
||||
intervalTimer.interval = cachedCyclingInterval * 1000
|
||||
intervalTimer.start()
|
||||
cyclingActive = true
|
||||
} else if (SessionData.wallpaperCyclingMode === "time") {
|
||||
cyclingActive = true
|
||||
checkTimeBasedCycling()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cyclePrevManually() {
|
||||
if (SessionData.wallpaperPath) {
|
||||
cycleToPrevWallpaper()
|
||||
// Restart timers if cycling is active
|
||||
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
|
||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
||||
intervalTimer.interval = cachedCyclingInterval * 1000
|
||||
intervalTimer.restart()
|
||||
function stopCycling() {
|
||||
intervalTimer.stop()
|
||||
cyclingActive = false
|
||||
}
|
||||
|
||||
function cycleToNextWallpaper() {
|
||||
if (!SessionData.wallpaperPath)
|
||||
return
|
||||
|
||||
const wallpaperDir = SessionData.wallpaperPath.substring(
|
||||
0, SessionData.wallpaperPath.lastIndexOf('/'))
|
||||
cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]
|
||||
cyclingProcess.running = true
|
||||
}
|
||||
|
||||
function cycleToPrevWallpaper() {
|
||||
if (!SessionData.wallpaperPath)
|
||||
return
|
||||
|
||||
const wallpaperDir = SessionData.wallpaperPath.substring(
|
||||
0, SessionData.wallpaperPath.lastIndexOf('/'))
|
||||
prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`]
|
||||
prevCyclingProcess.running = true
|
||||
}
|
||||
|
||||
function cycleNextManually() {
|
||||
if (SessionData.wallpaperPath) {
|
||||
cycleToNextWallpaper()
|
||||
// Restart timers if cycling is active
|
||||
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
|
||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
||||
intervalTimer.interval = cachedCyclingInterval * 1000
|
||||
intervalTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkTimeBasedCycling() {
|
||||
const currentTime = Qt.formatTime(systemClock.date, "hh:mm")
|
||||
|
||||
if (currentTime === cachedCyclingTime && currentTime !== lastTimeCheck) {
|
||||
lastTimeCheck = currentTime
|
||||
cycleToNextWallpaper()
|
||||
} else if (currentTime !== cachedCyclingTime) {
|
||||
lastTimeCheck = ""
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: intervalTimer
|
||||
interval: cachedCyclingInterval * 1000
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered: cycleToNextWallpaper()
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: systemClock
|
||||
precision: SystemClock.Minutes
|
||||
onDateChanged: {
|
||||
if (SessionData.wallpaperCyclingMode === "time" && cyclingActive) {
|
||||
checkTimeBasedCycling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cyclingProcess
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
const files = text.trim().split('\n').filter(file => file.length > 0)
|
||||
if (files.length <= 1) return
|
||||
|
||||
const wallpaperList = files.sort()
|
||||
const currentPath = SessionData.wallpaperPath
|
||||
let currentIndex = wallpaperList.findIndex(path => path === currentPath)
|
||||
if (currentIndex === -1) currentIndex = 0
|
||||
|
||||
// Get next wallpaper
|
||||
const nextIndex = (currentIndex + 1) % wallpaperList.length
|
||||
const nextWallpaper = wallpaperList[nextIndex]
|
||||
|
||||
if (nextWallpaper && nextWallpaper !== currentPath) {
|
||||
SessionData.setWallpaper(nextWallpaper)
|
||||
}
|
||||
function cyclePrevManually() {
|
||||
if (SessionData.wallpaperPath) {
|
||||
cycleToPrevWallpaper()
|
||||
// Restart timers if cycling is active
|
||||
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
|
||||
if (SessionData.wallpaperCyclingMode === "interval") {
|
||||
intervalTimer.interval = cachedCyclingInterval * 1000
|
||||
intervalTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: prevCyclingProcess
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
const files = text.trim().split('\n').filter(file => file.length > 0)
|
||||
if (files.length <= 1) return
|
||||
|
||||
const wallpaperList = files.sort()
|
||||
const currentPath = SessionData.wallpaperPath
|
||||
let currentIndex = wallpaperList.findIndex(path => path === currentPath)
|
||||
if (currentIndex === -1) currentIndex = 0
|
||||
|
||||
// Get previous wallpaper
|
||||
const prevIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1
|
||||
const prevWallpaper = wallpaperList[prevIndex]
|
||||
|
||||
if (prevWallpaper && prevWallpaper !== currentPath) {
|
||||
SessionData.setWallpaper(prevWallpaper)
|
||||
}
|
||||
function checkTimeBasedCycling() {
|
||||
const currentTime = Qt.formatTime(systemClock.date, "hh:mm")
|
||||
|
||||
if (currentTime === cachedCyclingTime
|
||||
&& currentTime !== lastTimeCheck) {
|
||||
lastTimeCheck = currentTime
|
||||
cycleToNextWallpaper()
|
||||
} else if (currentTime !== cachedCyclingTime) {
|
||||
lastTimeCheck = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: intervalTimer
|
||||
interval: cachedCyclingInterval * 1000
|
||||
running: false
|
||||
repeat: true
|
||||
onTriggered: cycleToNextWallpaper()
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: systemClock
|
||||
precision: SystemClock.Minutes
|
||||
onDateChanged: {
|
||||
if (SessionData.wallpaperCyclingMode === "time" && cyclingActive) {
|
||||
checkTimeBasedCycling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cyclingProcess
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
const files = text.trim().split('\n').filter(
|
||||
file => file.length > 0)
|
||||
if (files.length <= 1)
|
||||
return
|
||||
|
||||
const wallpaperList = files.sort()
|
||||
const currentPath = SessionData.wallpaperPath
|
||||
let currentIndex = wallpaperList.findIndex(
|
||||
path => path === currentPath)
|
||||
if (currentIndex === -1)
|
||||
currentIndex = 0
|
||||
|
||||
// Get next wallpaper
|
||||
const nextIndex = (currentIndex + 1) % wallpaperList.length
|
||||
const nextWallpaper = wallpaperList[nextIndex]
|
||||
|
||||
if (nextWallpaper && nextWallpaper !== currentPath) {
|
||||
SessionData.setWallpaper(nextWallpaper)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: prevCyclingProcess
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
const files = text.trim().split('\n').filter(
|
||||
file => file.length > 0)
|
||||
if (files.length <= 1)
|
||||
return
|
||||
|
||||
const wallpaperList = files.sort()
|
||||
const currentPath = SessionData.wallpaperPath
|
||||
let currentIndex = wallpaperList.findIndex(
|
||||
path => path === currentPath)
|
||||
if (currentIndex === -1)
|
||||
currentIndex = 0
|
||||
|
||||
// Get previous wallpaper
|
||||
const prevIndex = currentIndex
|
||||
=== 0 ? wallpaperList.length - 1 : currentIndex - 1
|
||||
const prevWallpaper = wallpaperList[prevIndex]
|
||||
|
||||
if (prevWallpaper && prevWallpaper !== currentPath) {
|
||||
SessionData.setWallpaper(prevWallpaper)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,346 +8,346 @@ import Quickshell.Io
|
||||
import qs.Common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
property int refCount: 0
|
||||
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 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
|
||||
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"
|
||||
})
|
||||
// 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
|
||||
function getWeatherIcon(code) {
|
||||
return weatherIcons[code] || "cloud"
|
||||
}
|
||||
|
||||
const location = SettingsData.weatherCoordinates || "40.7128,-74.0060"
|
||||
const url = `wttr.in/${encodeURIComponent(location)}?format=j1`
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
function getWeatherUrl() {
|
||||
if (SettingsData.useAutoLocation) {
|
||||
const url = "wttr.in/?format=j1"
|
||||
console.log("Using auto location, URL:", url)
|
||||
return url
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(raw)
|
||||
const location = SettingsData.weatherCoordinates || "40.7128,-74.0060"
|
||||
const url = `wttr.in/${encodeURIComponent(location)}?format=j1`
|
||||
return url
|
||||
}
|
||||
|
||||
const current = data.current_condition[0] || {}
|
||||
const location = data.nearest_area[0] || {}
|
||||
const astronomy = data.weather[0]?.astronomy[0] || {}
|
||||
function addRef() {
|
||||
refCount++
|
||||
|
||||
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()
|
||||
if (refCount === 1 && !weather.available
|
||||
&& SettingsData.weatherEnabled) {
|
||||
// Start fetching when first consumer appears and weather is enabled
|
||||
fetchWeather()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Weather fetch failed with exit code:", exitCode)
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
function removeRef() {
|
||||
refCount = Math.max(0, refCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: root.updateInterval
|
||||
running: root.refCount > 0 && SettingsData.weatherEnabled
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
root.fetchWeather()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: retryTimer
|
||||
interval: root.retryDelay
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
root.fetchWeather()
|
||||
function forceRefresh() {
|
||||
console.log("Force refreshing weather")
|
||||
root.lastFetchTime = 0 // Reset throttle
|
||||
fetchWeather()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: persistentRetryTimer
|
||||
interval: 60000 // Will be dynamically set
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
console.log("Persistent retry attempt...")
|
||||
root.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Weather fetch failed with exit code:", exitCode)
|
||||
root.handleWeatherFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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