Merge tag 'v2.8.0'

Release v2.8.0

We've been busy squashing bugs and adding some nice improvements based on your feedback.
What's New
New Icon Set - Swapped out Material Symbols for Tabler icons. They look great and load faster since they're built right in.
Works on Any Linux Distro - Dropped the Arch-specific update checker so this works properly on whatever distro you're running. You can build your own update notifications with Custom Buttons if you want.
Icon Picker - Added a proper icon picker for custom button widgets. No more guessing icon names.
Smarter Audio Visualizer - The Cava visualizer actually pays attention now - it only kicks in when you're playing music or videos instead of running all the time.
Better Notifications - Notifications now show actual app names like "Firefox" instead of cryptic IDs like "org.mozilla.firefox".
Less Noise - Turned a bunch of those persistent notification popups into toast notifications so they don't stick around cluttering your screen.
Fixes

Active Window widget finally shows the right app icon and title consistently
Fixed a nasty crash on Hyprland
Screen recorder button disables itself if the recording software isn't installed
Added a force-enable option for Night Light so you can turn it on manually whenever
This commit is contained in:
Never Gude 2025-09-11 19:10:35 +02:00
commit 9792f401f7
102 changed files with 7930 additions and 1626 deletions

View file

@ -1,19 +1,19 @@
{ {
"dark": { "dark": {
"mPrimary": "#ebbcba", "mPrimary": "#ebbcba",
"mOnPrimary": "#1f1d2e", "mOnPrimary": "#191724",
"mSecondary": "#9ccfd8", "mSecondary": "#9ccfd8",
"mOnSecondary": "#1f1d2e", "mOnSecondary": "#191724",
"mTertiary": "#f6c177", "mTertiary": "#f6c177",
"mOnTertiary": "#1f1d2e", "mOnTertiary": "#191724",
"mError": "#eb6f92", "mError": "#eb6f92",
"mOnError": "#1f1d2e", "mOnError": "#191724",
"mSurface": "#1f1d2e", "mSurface": "#191724",
"mOnSurface": "#e0def4", "mOnSurface": "#e0def4",
"mSurfaceVariant": "#26233a", "mSurfaceVariant": "#26233a",
"mOnSurfaceVariant": "#908caa", "mOnSurfaceVariant": "#908caa",
"mOutline": "#403d52", "mOutline": "#403d52",
"mShadow": "#1f1d2e" "mShadow": "#191724"
}, },
"light": { "light": {
"mPrimary": "#d46e6b", "mPrimary": "#d46e6b",

View file

@ -0,0 +1,16 @@
Tabler Licenses - Detailed Usage Rights and Guidelines
This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Admin Template, Tabler Icons, Tabler Emails, Tabler Illustrations), constitutes your acceptance of the terms of this license, Tabler terms of service and Tabler private policy.
Tabler Admin Template and Tabler Icons License*
Tabler Admin Template and Tabler Icons are available under MIT License.
Copyright (c) 2018-2025 Tabler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
See more at Tabler Admin Template MIT License See more at Tabler Icons MIT License

Binary file not shown.

View file

@ -1,270 +0,0 @@
#!/usr/bin/env -S bash
# A Bash script to monitor system stats and output them in JSON format.
# --- Configuration ---
# Default sleep duration in seconds. Can be overridden by the first argument.
SLEEP_DURATION=3
# --- Argument Parsing ---
# Check if a command-line argument is provided for the sleep duration.
if [[ -n "$1" ]]; then
# Basic validation to ensure the argument is a number (integer or float).
if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
SLEEP_DURATION=$1
else
# Output to stderr if the format is invalid.
echo "Warning: Invalid duration format '$1'. Using default of ${SLEEP_DURATION}s." >&2
fi
fi
# --- Global Cache Variables ---
# These variables will store the discovered CPU temperature sensor path and type
# to avoid searching for it on every loop iteration.
TEMP_SENSOR_PATH=""
TEMP_SENSOR_TYPE=""
# Network speed monitoring variables
PREV_RX_BYTES=0
PREV_TX_BYTES=0
PREV_TIME=0
# --- Data Collection Functions ---
#
# Gets memory usage in GB, MB, and as a percentage.
#
get_memory_info() {
awk '
/MemTotal/ {total=$2}
/MemAvailable/ {available=$2}
END {
if (total > 0) {
usage_kb = total - available
usage_gb = usage_kb / 1000000
usage_percent = (usage_kb / total) * 100
printf "%.1f %.0f\n", usage_gb, usage_percent
} else {
# Fallback if /proc/meminfo is unreadable or empty.
print "0.0 0 0"
}
}
' /proc/meminfo
}
#
# Gets the usage percentage of the root filesystem ("/").
#
get_disk_usage() {
# df gets disk usage. --output=pcent shows only the percentage for the root path.
# tail -1 gets the data line, and tr removes the '%' sign and whitespace.
df --output=pcent / | tail -1 | tr -d ' %'
}
#
# Calculates current CPU usage over a short interval.
#
get_cpu_usage() {
# Read all 10 CPU time fields to prevent errors on newer kernels.
read -r cpu prev_user prev_nice prev_system prev_idle prev_iowait prev_irq prev_softirq prev_steal prev_guest prev_guest_nice < /proc/stat
# Calculate previous total and idle times.
local prev_total_idle=$((prev_idle + prev_iowait))
local prev_total=$((prev_user + prev_nice + prev_system + prev_idle + prev_iowait + prev_irq + prev_softirq + prev_steal + prev_guest + prev_guest_nice))
# Wait for a short period.
sleep 0.05
# Read all 10 CPU time fields again for the second measurement.
read -r cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat
# Calculate new total and idle times.
local total_idle=$((idle + iowait))
local total=$((user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice))
# Add a check to prevent division by zero if total hasn't changed.
if (( total <= prev_total )); then
echo "0.0"
return
fi
# Calculate the difference over the interval.
local diff_total=$((total - prev_total))
local diff_idle=$((total_idle - prev_total_idle))
# Use awk for floating-point calculation and print the percentage.
awk -v total="$diff_total" -v idle="$diff_idle" '
BEGIN {
if (total > 0) {
# Formula: 100 * (Total - Idle) / Total
usage = 100 * (total - idle) / total
printf "%.1f\n", usage
} else {
print "0.0"
}
}'
}
#
# Finds and returns the CPU temperature in degrees Celsius.
# Caches the sensor path for efficiency.
#
get_cpu_temp() {
# If the sensor path hasn't been found yet, search for it.
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
for dir in /sys/class/hwmon/hwmon*; do
# Check if the 'name' file exists and read it.
if [[ -f "$dir/name" ]]; then
local name
name=$(<"$dir/name")
# Check for supported sensor types.
if [[ "$name" == "coretemp" || "$name" == "k10temp" || "$name" == "zenpower" ]]; then
TEMP_SENSOR_PATH=$dir
TEMP_SENSOR_TYPE=$name
break # Found it, no need to keep searching.
fi
fi
done
fi
# If after searching no sensor was found, return 0.
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
echo 0
return
fi
# --- Get temp based on sensor type ---
if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then
# For Intel 'coretemp', average all available temperature sensors.
local total_temp=0
local sensor_count=0
# Use a for loop with a glob to iterate over all temp input files.
# This is more efficient than 'find' for this simple case.
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
# The glob returns the pattern itself if no files match,
# so we must check if the file actually exists.
if [[ -f "$temp_file" ]]; then
total_temp=$((total_temp + $(<"$temp_file")))
sensor_count=$((sensor_count + 1))
fi
done
if (( sensor_count > 0 )); then
# Use awk for the final division to handle potential floating point numbers
# and convert from millidegrees to integer degrees Celsius.
awk -v total="$total_temp" -v count="$sensor_count" 'BEGIN { print int(total / count / 1000) }'
else
# If no sensor files were found, return 0.
echo 0
fi
elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then
# For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature.
local tctl_input=""
for label_file in "$TEMP_SENSOR_PATH"/temp*_label; do
if [[ -f "$label_file" ]] && [[ $(<"$label_file") == "Tctl" ]]; then
# The input file has the same name but with '_input' instead of '_label'.
tctl_input="${label_file%_label}_input"
break
fi
done
if [[ -f "$tctl_input" ]]; then
# Read the temperature and convert from millidegrees to degrees.
echo "$(( $(<"$tctl_input") / 1000 ))"
else
echo 0 # Fallback
fi
elif [[ "$TEMP_SENSOR_TYPE" == "zenpower" ]]; then
# For zenpower, read the first available temp sensor
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
if [[ -f "$temp_file" ]]; then
local temp_value
temp_value=$(cat "$temp_file" | tr -d '\n\r') # Remove any newlines
echo "$((temp_value / 1000))"
return
fi
done
echo 0
if [[ -f "$tctl_input" ]]; then
# Read the temperature and convert from millidegrees to degrees.
echo "$(($(<"$tctl_input") / 1000))"
else
echo 0 # Fallback
fi
else
echo 0 # Should not happen if cache logic is correct.
fi
}
# --- Main Loop ---
# This loop runs indefinitely, gathering and printing stats.
while true; do
# Call the functions to gather all the data.
# get_memory_info
read -r mem_gb mem_per <<< "$(get_memory_info)"
# Command substitution captures the single output from the other functions.
disk_per=$(get_disk_usage)
cpu_usage=$(get_cpu_usage)
cpu_temp=$(get_cpu_temp)
# Get network speeds
current_time=$(date +%s.%N)
total_rx=0
total_tx=0
# Read total bytes from /proc/net/dev for all interfaces
while IFS=: read -r interface stats; do
# Skip only loopback interface, allow other interfaces
if [[ "$interface" =~ ^lo[[:space:]]*$ ]]; then
continue
fi
# Extract rx and tx bytes (fields 1 and 9 in the stats part)
rx_bytes=$(echo "$stats" | awk '{print $1}')
tx_bytes=$(echo "$stats" | awk '{print $9}')
# Add to totals if they are valid numbers
if [[ "$rx_bytes" =~ ^[0-9]+$ ]] && [[ "$tx_bytes" =~ ^[0-9]+$ ]]; then
total_rx=$((total_rx + rx_bytes))
total_tx=$((total_tx + tx_bytes))
fi
done < <(tail -n +3 /proc/net/dev)
# Calculate speeds if we have previous data
rx_speed=0
tx_speed=0
if [[ "$PREV_TIME" != "0" ]]; then
time_diff=$(awk -v current="$current_time" -v prev="$PREV_TIME" 'BEGIN { printf "%.3f", current - prev }')
rx_diff=$((total_rx - PREV_RX_BYTES))
tx_diff=$((total_tx - PREV_TX_BYTES))
# Calculate speeds in bytes per second using awk
rx_speed=$(awk -v rx="$rx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", rx / time }')
tx_speed=$(awk -v tx="$tx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", tx / time }')
fi
# Update previous values for next iteration
PREV_RX_BYTES=$total_rx
PREV_TX_BYTES=$total_tx
PREV_TIME=$current_time
# Use printf to format the final JSON output string, adding the mem_mb key.
printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s", "rx_speed": "%s", "tx_speed": "%s"}\n' \
"$cpu_usage" \
"$cpu_temp" \
"$mem_gb" \
"$mem_per" \
"$disk_per" \
"$rx_speed" \
"$tx_speed"
# Wait for the specified duration before the next update.
sleep "$SLEEP_DURATION"
done

54
Commons/AppIcons.qml Normal file
View file

@ -0,0 +1,54 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Services
Singleton {
id: root
function iconFromName(iconName, fallbackName) {
const fallback = fallbackName || "application-x-executable"
try {
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
const p = Quickshell.iconPath(iconName, fallback)
if (p && p !== "")
return p
}
} catch (e) {
// ignore and fall back
}
try {
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
} catch (e2) {
return ""
}
}
// Resolve icon path for a DesktopEntries appId - safe on missing entries
function iconForAppId(appId, fallbackName) {
const fallback = fallbackName || "application-x-executable"
if (!appId)
return iconFromName(fallback, fallback)
try {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return iconFromName(fallback, fallback)
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(
appId) : DesktopEntries.byId(appId)
const name = entry && entry.icon ? entry.icon : ""
return iconFromName(name || fallback, fallback)
} catch (e) {
return iconFromName(fallback, fallback)
}
}
// Distro logo helper (absolute path or empty string)
function distroLogoPath() {
try {
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
} catch (e) {
return ""
}
}
}

View file

@ -102,7 +102,8 @@ Singleton {
// FileView to load custom colors data from colors.json // FileView to load custom colors data from colors.json
FileView { FileView {
id: customColorsFile id: customColorsFile
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : "" path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : undefined
printErrors: false
watchChanges: true watchChanges: true
onFileChanged: { onFileChanged: {
Logger.log("Color", "Reloading colors from disk") Logger.log("Color", "Reloading colors from disk")
@ -115,7 +116,7 @@ Singleton {
// Trigger initial load when path changes from empty to actual path // Trigger initial load when path changes from empty to actual path
onPathChanged: { onPathChanged: {
if (path === Settings.configDir + "colors.json") { if (path !== undefined) {
reload() reload()
} }
} }

View file

@ -1,54 +1,49 @@
pragma Singleton pragma Singleton
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import qs.Services import qs.Commons
import qs.Commons.IconsSets
Singleton { Singleton {
id: icons id: root
function iconFromName(iconName, fallbackName) { // Expose the font family name for easy access
const fallback = fallbackName || "application-x-executable" readonly property string fontFamily: fontLoader.name
try { readonly property string defaultIcon: TablerIcons.defaultIcon
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) { readonly property var icons: TablerIcons.icons
const p = Quickshell.iconPath(iconName, fallback) readonly property var aliases: TablerIcons.aliases
if (p && p !== "") readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.woff2"
return p
Component.onCompleted: {
Logger.log("Icons", "Service started")
}
function get(iconName) {
// Check in aliases first
if (aliases[iconName] !== undefined) {
iconName = aliases[iconName]
}
// Find the appropriate codepoint
return icons[iconName]
}
FontLoader {
id: fontLoader
source: Quickshell.shellDir + fontPath
}
// Monitor font loading status
Connections {
target: fontLoader
function onStatusChanged() {
if (fontLoader.status === FontLoader.Ready) {
Logger.log("Icons", "Font loaded successfully:", fontFamily)
} else if (fontLoader.status === FontLoader.Error) {
Logger.error("Icons", "Font failed to load")
} }
} catch (e) {
// ignore and fall back
}
try {
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
} catch (e2) {
return ""
}
}
// Resolve icon path for a DesktopEntries appId - safe on missing entries
function iconForAppId(appId, fallbackName) {
const fallback = fallbackName || "application-x-executable"
if (!appId)
return iconFromName(fallback, fallback)
try {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return iconFromName(fallback, fallback)
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(
appId) : DesktopEntries.byId(appId)
const name = entry && entry.icon ? entry.icon : ""
return iconFromName(name || fallback, fallback)
} catch (e) {
return iconFromName(fallback, fallback)
}
}
// Distro logo helper (absolute path or empty string)
function distroLogoPath() {
try {
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
} catch (e) {
return ""
} }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -23,9 +23,9 @@ Singleton {
property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json")
property string defaultAvatar: Quickshell.env("HOME") + "/.face" property string defaultAvatar: Quickshell.env("HOME") + "/.face"
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos" property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
property string defaultLocation: "Tokyo" property string defaultLocation: "Tokyo"
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png" property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
// Used to access via Settings.data.xxx.yyy // Used to access via Settings.data.xxx.yyy
@ -93,7 +93,22 @@ Singleton {
} }
// ----------------- // -----------------
// 2nd. migrate global settings to user settings // 2nd. remove any non existing widget type
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const widgets = adapter.bar.widgets[sectionName]
// Iterate backward through the widgets array, so it does not break when removing a widget
for (var i = widgets.length - 1; i >= 0; i--) {
var widget = widgets[i]
if (!BarWidgetRegistry.hasWidget(widget.id)) {
widgets.splice(i, 1)
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
}
}
}
// -----------------
// 3nd. migrate global settings to user settings
for (var s = 0; s < sections.length; s++) { for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s] const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) { for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
@ -105,60 +120,63 @@ Singleton {
continue continue
} }
// Check that the widget was not previously migrated and skip if necessary if (upgradeWidget(widget)) {
const keys = Object.keys(widget) Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
if (keys.length > 1) {
continue
} }
migrateWidget(widget)
Logger.log("Settings", JSON.stringify(widget))
} }
} }
} }
// ----------------------------------------------------- // -----------------------------------------------------
function migrateWidget(widget) { function upgradeWidget(widget) {
Logger.log("Settings", `Migrating '${widget.id}' widget`) // Backup the widget definition before altering
const widgetBefore = JSON.stringify(widget)
// Migrate old bar settings to proper per widget settings
switch (widget.id) { switch (widget.id) {
case "ActiveWindow": case "ActiveWindow":
widget.showIcon = adapter.bar.showActiveWindowIcon widget.showIcon = widget.showIcon !== undefined ? widget.showIcon : adapter.bar.showActiveWindowIcon
break break
case "Battery": case "Battery":
widget.alwaysShowPercentage = adapter.bar.alwaysShowBatteryPercentage widget.alwaysShowPercentage = widget.alwaysShowPercentage
break !== undefined ? widget.alwaysShowPercentage : adapter.bar.alwaysShowBatteryPercentage
case "Brightness":
widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage
break break
case "Clock": case "Clock":
widget.showDate = adapter.location.showDateWithClock widget.showDate = widget.showDate !== undefined ? widget.showDate : adapter.location.showDateWithClock
widget.use12HourClock = adapter.location.use12HourClock widget.use12HourClock = widget.use12HourClock !== undefined ? widget.use12HourClock : adapter.location.use12HourClock
widget.reverseDayMonth = adapter.location.reverseDayMonth widget.reverseDayMonth = widget.reverseDayMonth !== undefined ? widget.reverseDayMonth : adapter.location.reverseDayMonth
widget.showSeconds = BarWidgetRegistry.widgetMetadata[widget.id].showSeconds
break break
case "MediaMini": case "MediaMini":
widget.showAlbumArt = adapter.audio.showMiniplayerAlbumArt widget.showAlbumArt = widget.showAlbumArt !== undefined ? widget.showAlbumArt : adapter.audio.showMiniplayerAlbumArt
widget.showVisualizer = adapter.audio.showMiniplayerCava widget.showVisualizer = widget.showVisualizer !== undefined ? widget.showVisualizer : adapter.audio.showMiniplayerCava
widget.visualizerType = BarWidgetRegistry.widgetMetadata[widget.id].visualizerType
break
case "NotificationHistory":
widget.showUnreadBadge = BarWidgetRegistry.widgetMetadata[widget.id].showUnreadBadge
widget.hideWhenZero = BarWidgetRegistry.widgetMetadata[widget.id].hideWhenZero
break break
case "SidePanelToggle": case "SidePanelToggle":
widget.useDistroLogo = adapter.bar.useDistroLogo widget.useDistroLogo = widget.useDistroLogo !== undefined ? widget.useDistroLogo : adapter.bar.useDistroLogo
break break
case "SystemMonitor": case "SystemMonitor":
widget.showNetworkStats = adapter.bar.showNetworkStats widget.showNetworkStats = widget.showNetworkStats !== undefined ? widget.showNetworkStats : adapter.bar.showNetworkStats
break
case "Volume":
widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage
break break
case "Workspace": case "Workspace":
widget.labelMode = adapter.bar.showWorkspaceLabel widget.labelMode = widget.labelMode !== undefined ? widget.labelMode : adapter.bar.showWorkspaceLabel
break break
} }
// Inject missing default setting (metaData) from BarWidgetRegistry
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
for (var i = 0; i < keys.length; i++) {
const k = keys[i]
if (k === "id" || k === "allowUserSettings") {
continue
}
if (widget[k] === undefined) {
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
}
}
// Backup the widget definition before altering
const widgetAfter = JSON.stringify(widget)
return (widgetAfter !== widgetBefore)
} }
// ----------------------------------------------------- // -----------------------------------------------------
// Kickoff essential services // Kickoff essential services
@ -175,6 +193,8 @@ Singleton {
FontService.init() FontService.init()
HooksService.init() HooksService.init()
BluetoothService.init()
} }
// ----------------------------------------------------- // -----------------------------------------------------
@ -200,14 +220,15 @@ Singleton {
FileView { FileView {
id: settingsFileView id: settingsFileView
path: directoriesCreated ? settingsFile : "" path: directoriesCreated ? settingsFile : undefined
printErrors: false
watchChanges: true watchChanges: true
onFileChanged: reload() onFileChanged: reload()
onAdapterUpdated: saveTimer.start() onAdapterUpdated: saveTimer.start()
// Trigger initial load when path changes from empty to actual path // Trigger initial load when path changes from empty to actual path
onPathChanged: { onPathChanged: {
if (path === settingsFile) { if (path !== undefined) {
reload() reload()
} }
} }
@ -215,7 +236,6 @@ Singleton {
if (!isLoaded) { if (!isLoaded) {
Logger.log("Settings", "----------------------------") Logger.log("Settings", "----------------------------")
Logger.log("Settings", "Settings loaded successfully") Logger.log("Settings", "Settings loaded successfully")
isLoaded = true
upgradeSettingsData() upgradeSettingsData()
@ -223,6 +243,8 @@ Singleton {
kickOffServices() kickOffServices()
isLoaded = true
// Emit the signal // Emit the signal
root.settingsLoaded() root.settingsLoaded()
} }
@ -335,7 +357,6 @@ Singleton {
property int transitionDuration: 1500 // 1500 ms property int transitionDuration: 1500 // 1500 ms
property string transitionType: "random" property string transitionType: "random"
property real transitionEdgeSmoothness: 0.05 property real transitionEdgeSmoothness: 0.05
property string defaultWallpaper: root.defaultWallpaper
property list<var> monitors: [] property list<var> monitors: []
} }
@ -422,6 +443,7 @@ Singleton {
// night light // night light
property JsonObject nightLight: JsonObject { property JsonObject nightLight: JsonObject {
property bool enabled: false property bool enabled: false
property bool forced: false
property bool autoSchedule: true property bool autoSchedule: true
property string nightTemp: "4000" property string nightTemp: "4000"
property string dayTemp: "6500" property string dayTemp: "6500"

View file

@ -57,10 +57,10 @@ Singleton {
property real opacityFull: 1.0 property real opacityFull: 1.0
// Animation duration (ms) // Animation duration (ms)
property int animationFast: Math.round(150 * Settings.data.general.animationSpeed) property int animationFast: Math.round(150 / Settings.data.general.animationSpeed)
property int animationNormal: Math.round(300 * Settings.data.general.animationSpeed) property int animationNormal: Math.round(300 / Settings.data.general.animationSpeed)
property int animationSlow: Math.round(450 * Settings.data.general.animationSpeed) property int animationSlow: Math.round(450 / Settings.data.general.animationSpeed)
property int animationSlowest: Math.round(750 * Settings.data.general.animationSpeed) property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
// Dimensions // Dimensions
property int barHeight: 36 property int barHeight: 36

View file

@ -1,527 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
panelWidth: 380 * scaling
panelHeight: 500 * scaling
panelAnchorRight: true
panelContent: Rectangle {
color: Color.mSurface
radius: Style.radiusL * scaling
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// Header
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
text: "system_update_alt"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "System Updates"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
// Reset button (only show if update failed)
NIconButton {
visible: ArchUpdaterService.updateFailed
icon: "refresh"
tooltipText: "Reset update state"
sizeRatio: 0.8
colorBg: Color.mError
colorFg: Color.mOnError
onClicked: {
ArchUpdaterService.resetUpdateState()
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeRatio: 0.8
onClicked: root.close()
}
}
NDivider {
Layout.fillWidth: true
}
// Update summary (only show when packages are available and terminal is configured)
NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
Layout.fillWidth: true
}
// Package selection info (only show when not updating and have packages and terminal is configured)
NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected"
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
// Update in progress state
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.updateInProgress
spacing: Style.marginM * scaling
Item {
Layout.fillHeight: true
} // Spacer
NIcon {
text: "hourglass_empty"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update in progress"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Please check your terminal window for update progress and prompts."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
Item {
Layout.fillHeight: true
} // Spacer
}
// Terminal not available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !ArchUpdaterService.terminalAvailable && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "terminal"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Terminal not configured"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "The TERMINAL environment variable is not set. Please set it to your preferred terminal (e.g., kitty, alacritty, foot) in your shell configuration."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// AUR helper not available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.terminalAvailable && !ArchUpdaterService.aurHelperAvailable
&& !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "package"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "AUR helper not found"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No AUR helper (yay or paru) is installed. Please install either yay or paru to manage AUR packages. yay is recommended."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Check failed state (AUR down, network issues, etc.)
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.checkFailed && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "error"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Cannot check for updates"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: ArchUpdaterService.lastCheckError
|| "AUR helper is unavailable or network connection failed. This could be due to AUR being down, network issues, or missing AUR helper (yay/paru)."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
// Prominent refresh button
NIconButton {
icon: "refresh"
tooltipText: "Try checking again"
sizeRatio: 1.2
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: {
ArchUpdaterService.forceRefresh()
}
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginL * scaling
}
}
}
// Update failed state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "error_outline"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update failed"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Check your terminal for error details and try again."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
// Prominent refresh button
NIconButton {
icon: "refresh"
tooltipText: "Refresh and try again"
sizeRatio: 1.2
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: {
ArchUpdaterService.resetUpdateState()
}
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginL * scaling
}
}
}
// No updates available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates === 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "check_circle"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "System is up to date"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "All packages are current. Check back later for updates."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Checking for updates state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.aurBusy && !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& ArchUpdaterService.terminalAvailable && ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NBusyIndicator {
Layout.alignment: Qt.AlignHCenter
size: Style.fontSizeXXXL * scaling
color: Color.mPrimary
}
NText {
text: "Checking for updates"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Scanning package databases for available updates..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Package list (only show when not in any special state)
NBox {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
Layout.fillWidth: true
Layout.fillHeight: true
// Combine repo and AUR lists in order: repos first, then AUR
property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || [])
ListView {
id: unifiedList
anchors.fill: parent
anchors.margins: Style.marginM * scaling
cacheBuffer: Math.round(300 * scaling)
clip: true
model: parent.items
delegate: Rectangle {
width: unifiedList.width
height: 44 * scaling
color: Color.transparent
radius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
spacing: Style.marginS * scaling
// Checkbox for selection
NCheckbox {
id: checkbox
label: ""
description: ""
checked: ArchUpdaterService.isPackageSelected(modelData.name)
baseSize: Math.max(Style.baseWidgetSize * 0.7, 14)
onToggled: function (checked) {
ArchUpdaterService.togglePackageSelection(modelData.name)
// Force refresh of the checked property
checkbox.checked = ArchUpdaterService.isPackageSelected(modelData.name)
}
}
// Package info
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
NText {
text: modelData.name
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
NText {
text: modelData.oldVersion + " → " + modelData.newVersion
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
// Source tag (AUR vs PAC)
Rectangle {
visible: !!modelData.source
radius: width * 0.5
color: modelData.source === "aur" ? Color.mTertiary : Color.mSecondary
Layout.alignment: Qt.AlignVCenter
implicitHeight: Style.fontSizeS * 1.8 * scaling
// Width based on label content + horizontal padding
implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling)
NText {
id: badgeText
anchors.centerIn: parent
text: modelData.source === "aur" ? "AUR" : "PAC"
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: modelData.source === "aur" ? Color.mOnTertiary : Color.mOnSecondary
}
}
}
}
}
}
// Action buttons (only show when not updating)
RowLayout {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
Layout.fillWidth: true
spacing: Style.marginL * scaling
NIconButton {
icon: "refresh"
tooltipText: ArchUpdaterService.aurBusy ? "Checking for updates..." : (!ArchUpdaterService.canPoll ? "Refresh available soon" : "Refresh package lists")
onClicked: {
ArchUpdaterService.forceRefresh()
}
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
Layout.fillWidth: true
enabled: !ArchUpdaterService.aurBusy
}
NIconButton {
icon: "system_update_alt"
tooltipText: "Update all packages"
enabled: ArchUpdaterService.totalUpdates > 0
onClicked: {
ArchUpdaterService.runUpdate()
root.close()
}
colorBg: ArchUpdaterService.totalUpdates > 0 ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ArchUpdaterService.totalUpdates > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant
Layout.fillWidth: true
}
NIconButton {
icon: "check_box"
tooltipText: "Update selected packages"
enabled: ArchUpdaterService.selectedPackagesCount > 0
onClicked: {
if (ArchUpdaterService.selectedPackagesCount > 0) {
ArchUpdaterService.runSelectiveUpdate()
root.close()
}
}
colorBg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
}
}
}

View file

@ -22,8 +22,6 @@ Variants {
// Internal state management // Internal state management
property string transitionType: "fade" property string transitionType: "fade"
property real transitionProgress: 0 property real transitionProgress: 0
// Scaling support for widgets that rely on it
property real scaling: ScalingService.getScreenScale(screen)
readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness
readonly property var allTransitions: WallpaperService.allTransitions readonly property var allTransitions: WallpaperService.allTransitions
@ -91,15 +89,6 @@ Variants {
left: true left: true
} }
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((screen !== null) && (screenName === screen.name)) {
scaling = scale
}
}
}
Timer { Timer {
id: debounceTimer id: debounceTimer
interval: 333 interval: 333
@ -150,7 +139,7 @@ Variants {
property real screenWidth: width property real screenWidth: width
property real screenHeight: height property real screenHeight: height
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_fade.frag.qsb") fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_fade.frag.qsb")
} }
// Wipe transition shader // Wipe transition shader
@ -175,7 +164,7 @@ Variants {
property real screenWidth: width property real screenWidth: width
property real screenHeight: height property real screenHeight: height
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_wipe.frag.qsb") fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_wipe.frag.qsb")
} }
// Disc reveal transition shader // Disc reveal transition shader
@ -202,7 +191,7 @@ Variants {
property real screenWidth: width property real screenWidth: width
property real screenHeight: height property real screenHeight: height
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_disc.frag.qsb") fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_disc.frag.qsb")
} }
// Diagonal stripes transition shader // Diagonal stripes transition shader
@ -229,7 +218,7 @@ Variants {
property real screenWidth: width property real screenWidth: width
property real screenHeight: height property real screenHeight: height
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_stripes.frag.qsb") fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_stripes.frag.qsb")
} }
// Animation for the transition progress // Animation for the transition progress

View file

@ -176,7 +176,7 @@ PopupWindow {
} }
NIcon { NIcon {
text: modelData?.hasChildren ? "menu" : "" icon: modelData?.hasChildren ? "menu" : ""
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
visible: modelData?.hasChildren ?? false visible: modelData?.hasChildren ?? false

View file

@ -33,28 +33,34 @@ RowLayout {
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
readonly property real minWidth: 160 // 6% of total width
readonly property real maxWidth: 400 readonly property real minWidth: Math.max(1, screen.width * 0.06)
Layout.alignment: Qt.AlignVCenter readonly property real maxWidth: minWidth * 2
spacing: Style.marginS * scaling
visible: getTitle() !== ""
function getTitle() { function getTitle() {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
} }
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
visible: getTitle() !== ""
function getAppIcon() { function getAppIcon() {
// Try CompositorService first // Try CompositorService first
const focusedWindow = CompositorService.getFocusedWindow() const focusedWindow = CompositorService.getFocusedWindow()
if (focusedWindow && focusedWindow.appId) { if (focusedWindow && focusedWindow.appId) {
return Icons.iconForAppId(focusedWindow.appId.toLowerCase()) const idValue = focusedWindow.appId
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
return AppIcons.iconForAppId(normalizedId.toLowerCase())
} }
// Fallback to ToplevelManager // Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) { if (ToplevelManager && ToplevelManager.activeToplevel) {
const activeToplevel = ToplevelManager.activeToplevel const activeToplevel = ToplevelManager.activeToplevel
if (activeToplevel.appId) { if (activeToplevel.appId) {
return Icons.iconForAppId(activeToplevel.appId.toLowerCase()) const idValue2 = activeToplevel.appId
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
return AppIcons.iconForAppId(normalizedId2.toLowerCase())
} }
} }
@ -123,7 +129,7 @@ RowLayout {
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Color.mSecondary color: Color.mPrimary
clip: true clip: true
Behavior on Layout.preferredWidth { Behavior on Layout.preferredWidth {

View file

@ -1,80 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
property real scaling: 1.0
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorBorder: Color.transparent
colorBorderHover: Color.transparent
colorFg: {
if (!ArchUpdaterService.terminalAvailable || !ArchUpdaterService.aurHelperAvailable) {
return Color.mError
}
return (ArchUpdaterService.totalUpdates === 0) ? Color.mOnSurface : Color.mPrimary
}
// Icon states
icon: {
if (!ArchUpdaterService.terminalAvailable) {
return "terminal"
}
if (!ArchUpdaterService.aurHelperAvailable) {
return "package"
}
if (ArchUpdaterService.aurBusy) {
return "sync"
}
if (ArchUpdaterService.totalUpdates > 0) {
return "system_update_alt"
}
return "task_alt"
}
// Tooltip with repo vs AUR breakdown and sample lists
tooltipText: {
if (!ArchUpdaterService.terminalAvailable) {
return "Terminal not configured\nSet TERMINAL environment variable"
}
if (!ArchUpdaterService.aurHelperAvailable) {
return "AUR helper not found\nInstall yay or paru"
}
if (ArchUpdaterService.aurBusy) {
return "Checking for updates…"
}
const total = ArchUpdaterService.totalUpdates
if (total === 0) {
return "System is up to date ✓"
}
let header = (total === 1) ? "1 package can be updated" : (total + " packages can be updated")
const pacCount = ArchUpdaterService.updates
const aurCount = ArchUpdaterService.aurUpdates
const pacmanTooltip = (pacCount > 0) ? ((pacCount === 1) ? "1 system package" : pacCount + " system packages") : ""
const aurTooltip = (aurCount > 0) ? ((aurCount === 1) ? "1 AUR package" : aurCount + " AUR packages") : ""
let tooltip = header
if (pacmanTooltip !== "") {
tooltip += "\n" + pacmanTooltip
}
if (aurTooltip !== "") {
tooltip += "\n" + aurTooltip
}
return tooltip
}
onClicked: {
// Always allow panel to open, never block
PanelService.getPanel("archUpdaterPanel").toggle(screen, this)
}
}

View file

@ -38,8 +38,8 @@ Item {
// Test mode // Test mode
readonly property bool testMode: false readonly property bool testMode: false
readonly property int testPercent: 50 readonly property int testPercent: 90
readonly property bool testCharging: true readonly property bool testCharging: false
// Main properties // Main properties
readonly property var battery: UPower.displayDevice readonly property var battery: UPower.displayDevice
@ -57,9 +57,7 @@ Item {
// Only notify once we are a below threshold // Only notify once we are a below threshold
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) { if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
root.hasNotifiedLowBattery = true root.hasNotifiedLowBattery = true
// Maybe go with toast ? ToastService.showWarning("Low Battery", `Battery is at ${Math.round(percent)}%. Please connect the charger.`)
Quickshell.execDetached(
["notify-send", "-u", "critical", "-i", "battery-caution", "Low Battery", `Battery is at ${p}%. Please connect charger.`])
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) { } else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
// Reset when charging starts or when battery recovers 5% above threshold // Reset when charging starts or when battery recovers 5% above threshold
root.hasNotifiedLowBattery = false root.hasNotifiedLowBattery = false
@ -70,14 +68,20 @@ Item {
Connections { Connections {
target: UPower.displayDevice target: UPower.displayDevice
function onPercentageChanged() { function onPercentageChanged() {
root.maybeNotify(percent, charging) var currentPercent = UPower.displayDevice.percentage * 100
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
root.maybeNotify(currentPercent, isCharging)
} }
function onStateChanged() { function onStateChanged() {
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
// Reset notification flag when charging starts // Reset notification flag when charging starts
if (charging) { if (isCharging) {
root.hasNotifiedLowBattery = false root.hasNotifiedLowBattery = false
} }
// Also re-evaluate maybeNotify, as state might have changed
var currentPercent = UPower.displayDevice.percentage * 100
root.maybeNotify(currentPercent, isCharging)
} }
} }
@ -87,11 +91,7 @@ Item {
rightOpen: BarWidgetRegistry.getNPillDirection(root) rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent,
charging, isReady) charging, isReady)
iconRotation: -90 text: (isReady || testMode) ? Math.round(percent) + "%" : "-"
text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-"
textColor: charging ? Color.mPrimary : Color.mOnSurface
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && alwaysShowPercentage forceOpen: isReady && (testMode || battery.isLaptopBattery) && alwaysShowPercentage
disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery)) disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))

View file

@ -13,14 +13,13 @@ NIconButton {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
visible: Settings.data.network.bluetoothEnabled
sizeRatio: 0.8 sizeRatio: 0.8
colorBg: Color.mSurfaceVariant colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface colorFg: Color.mOnSurface
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
icon: "bluetooth" icon: Settings.data.network.bluetoothEnabled ? "bluetooth" : "bluetooth-off"
tooltipText: "Bluetooth" tooltipText: "Bluetooth devices."
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this) onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this)
} }

View file

@ -46,8 +46,7 @@ Item {
function getIcon() { function getIcon() {
var monitor = getMonitor() var monitor = getMonitor()
var brightness = monitor ? monitor.brightness : 0 var brightness = monitor ? monitor.brightness : 0
return brightness <= 0 ? "brightness_1" : brightness < 0.33 ? "brightness_low" : brightness return brightness <= 0.5 ? "brightness-low" : "brightness-high"
< 0.66 ? "brightness_medium" : "brightness_high"
} }
// Connection used to open the pill when brightness changes // Connection used to open the pill when brightness changes
@ -80,8 +79,6 @@ Item {
rightOpen: BarWidgetRegistry.getNPillDirection(root) rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon() icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want autoHide: false // Important to be false so we can hover as long as we want
text: { text: {
var monitor = getMonitor() var monitor = getMonitor()

View file

@ -60,6 +60,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mPrimary
} }
NTooltip { NTooltip {

View file

@ -38,6 +38,10 @@ NIconButton {
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec) readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
enabled: hasExec
allowClickWhenDisabled: true // we want to be able to open config with left click when its not setup properly
colorBorder: Color.transparent
colorBorderHover: Color.transparent
sizeRatio: 0.8 sizeRatio: 0.8
icon: customIcon icon: customIcon
tooltipText: { tooltipText: {
@ -57,7 +61,6 @@ NIconButton {
return lines.join("<br/>") return lines.join("<br/>")
} }
} }
opacity: hasExec ? Style.opacityFull : Style.opacityMedium
onClicked: { onClicked: {
if (leftClickExec) { if (leftClickExec) {

View file

@ -9,12 +9,12 @@ NIconButton {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
icon: "contrast" icon: "dark-mode"
tooltipText: "Toggle light/dark mode" tooltipText: "Toggle light/dark mode"
sizeRatio: 0.8 sizeRatio: 0.8
colorBg: Color.mSurfaceVariant colorBg: Settings.data.colorSchemes.darkMode ? Color.mSurfaceVariant : Color.mPrimary
colorFg: Color.mOnSurface colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: Color.transparent colorBorderHover: Color.transparent

View file

@ -13,10 +13,10 @@ NIconButton {
sizeRatio: 0.8 sizeRatio: 0.8
icon: "coffee" icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake" tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
colorBg: Color.mSurfaceVariant colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mOnSurface colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mOnSurface
colorBorder: Color.transparent colorBorder: Color.transparent
onClicked: { onClicked: {
IdleInhibitorService.manualToggle() IdleInhibitorService.manualToggle()

View file

@ -24,9 +24,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
rightOpen: BarWidgetRegistry.getNPillDirection(root) rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: "keyboard_alt" icon: "keyboard"
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want autoHide: false // Important to be false so we can hover as long as we want
text: currentLayout text: currentLayout
tooltipText: "Keyboard layout: " + currentLayout tooltipText: "Keyboard layout: " + currentLayout

View file

@ -38,8 +38,9 @@ RowLayout {
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType
!== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType !== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
readonly property real minWidth: 160 // 6% of total width
readonly property real maxWidth: 400 readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2
function getTitle() { function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
@ -134,7 +135,7 @@ RowLayout {
NIcon { NIcon {
id: windowIcon id: windowIcon
text: MediaService.isPlaying ? "pause" : "play_arrow" icon: MediaService.isPlaying ? "media-pause" : "media-play"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@ -154,7 +155,8 @@ RowLayout {
id: trackArt id: trackArt
anchors.fill: parent anchors.fill: parent
imagePath: MediaService.trackArtUrl imagePath: MediaService.trackArtUrl
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow" fallbackIcon: MediaService.isPlaying ? "media-pause" : "media-play"
fallbackIconSize: 10 * scaling
borderWidth: 0 borderWidth: 0
border.color: Color.transparent border.color: Color.transparent
} }
@ -178,7 +180,7 @@ RowLayout {
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
elide: Text.ElideRight elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Color.mTertiary color: Color.mSecondary
Behavior on Layout.preferredWidth { Behavior on Layout.preferredWidth {
NumberAnimation { NumberAnimation {

View file

@ -43,9 +43,9 @@ Item {
function getIcon() { function getIcon() {
if (AudioService.inputMuted) { if (AudioService.inputMuted) {
return "mic_off" return "microphone-mute"
} }
return AudioService.inputVolume <= Number.EPSILON ? "mic_off" : (AudioService.inputVolume < 0.33 ? "mic" : "mic") return (AudioService.inputVolume <= Number.EPSILON) ? "microphone-mute" : "microphone"
} }
// Connection used to open the pill when input volume changes // Connection used to open the pill when input volume changes
@ -92,8 +92,6 @@ Item {
rightOpen: BarWidgetRegistry.getNPillDirection(root) rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon() icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.inputVolume * 100) + "%" text: Math.floor(AudioService.inputVolume * 100) + "%"
forceOpen: alwaysShowPercentage forceOpen: alwaysShowPercentage

View file

@ -15,14 +15,24 @@ NIconButton {
property real scaling: 1.0 property real scaling: 1.0
sizeRatio: 0.8 sizeRatio: 0.8
colorBg: Color.mSurfaceVariant colorBg: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? Color.mTertiary : Color.mPrimary) : Color.mSurfaceVariant
colorFg: Color.mOnSurface colorFg: Settings.data.nightLight.enabled ? Color.mOnPrimary : Color.mOnSurface
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off" icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled." : "disabled."}\nLeft click to toggle.\nRight click to access settings.` tooltipText: `Night light: ${Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "forced." : "enabled.") : "disabled."}\nLeft click to cycle (disabled normal forced).\nRight click to access settings.`
onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled onClicked: {
if (!Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = true
Settings.data.nightLight.forced = false
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
Settings.data.nightLight.forced = true
} else {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
}
}
onRightClicked: { onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel") var settingsPanel = PanelService.getPanel("settingsPanel")

View file

@ -53,10 +53,10 @@ NIconButton {
} }
sizeRatio: 0.8 sizeRatio: 0.8
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications" icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'." tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
colorBg: Color.mSurfaceVariant colorBg: Color.mSurfaceVariant
colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface colorFg: Color.mOnSurface
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: Color.transparent colorBorderHover: Color.transparent

View file

@ -11,49 +11,43 @@ NIconButton {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
property var powerProfiles: PowerProfiles readonly property bool hasPP: PowerProfileService.available
readonly property bool hasPP: powerProfiles.hasPerformanceProfile
sizeRatio: 0.8 sizeRatio: 0.8
visible: hasPP visible: hasPP
function profileIcon() { function profileIcon() {
if (!hasPP) if (!hasPP)
return "balance" return "balanced"
if (powerProfiles.profile === PowerProfile.Performance) if (PowerProfileService.profile === PowerProfile.Performance)
return "speed" return "performance"
if (powerProfiles.profile === PowerProfile.Balanced) if (PowerProfileService.profile === PowerProfile.Balanced)
return "balance" return "balanced"
if (powerProfiles.profile === PowerProfile.PowerSaver) if (PowerProfileService.profile === PowerProfile.PowerSaver)
return "eco" return "powersaver"
} }
function profileName() { function profileName() {
if (!hasPP) if (!hasPP)
return "Unknown" return "Unknown"
if (powerProfiles.profile === PowerProfile.Performance) if (PowerProfileService.profile === PowerProfile.Performance)
return "Performance" return "Performance"
if (powerProfiles.profile === PowerProfile.Balanced) if (PowerProfileService.profile === PowerProfile.Balanced)
return "Balanced" return "Balanced"
if (powerProfiles.profile === PowerProfile.PowerSaver) if (PowerProfileService.profile === PowerProfile.PowerSaver)
return "Power Saver" return "Power Saver"
} }
function changeProfile() { function changeProfile() {
if (!hasPP) if (!hasPP)
return return
if (powerProfiles.profile === PowerProfile.Performance) PowerProfileService.cycleProfile()
powerProfiles.profile = PowerProfile.PowerSaver
else if (powerProfiles.profile === PowerProfile.Balanced)
powerProfiles.profile = PowerProfile.Performance
else if (powerProfiles.profile === PowerProfile.PowerSaver)
powerProfiles.profile = PowerProfile.Balanced
} }
icon: root.profileIcon() icon: root.profileIcon()
tooltipText: root.profileName() tooltipText: root.profileName()
colorBg: Color.mSurfaceVariant colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mSurfaceVariant : Color.mPrimary
colorFg: Color.mOnSurface colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
onClicked: root.changeProfile() onClicked: root.changeProfile()

View file

@ -13,7 +13,7 @@ NIconButton {
sizeRatio: 0.8 sizeRatio: 0.8
icon: "power_settings_new" icon: "power"
tooltipText: "Power Settings" tooltipText: "Power Settings"
colorBg: Color.mSurfaceVariant colorBg: Color.mSurfaceVariant
colorFg: Color.mError colorFg: Color.mError

View file

@ -11,7 +11,7 @@ NIconButton {
property real scaling: 1.0 property real scaling: 1.0
visible: ScreenRecorderService.isRecording visible: ScreenRecorderService.isRecording
icon: "videocam" icon: "camera-video"
tooltipText: "Screen recording is active\nClick to stop recording" tooltipText: "Screen recording is active\nClick to stop recording"
sizeRatio: 0.8 sizeRatio: 0.8
colorBg: Color.mPrimary colorBg: Color.mPrimary

View file

@ -33,7 +33,7 @@ NIconButton {
readonly property bool useDistroLogo: (widgetSettings.useDistroLogo readonly property bool useDistroLogo: (widgetSettings.useDistroLogo
!== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo !== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
icon: useDistroLogo ? "" : "widgets" icon: useDistroLogo ? "" : "apps"
tooltipText: "Open side panel." tooltipText: "Open side panel."
sizeRatio: 0.8 sizeRatio: 0.8

View file

@ -38,12 +38,4 @@ Item {
implicitHeight: Style.barHeight * scaling implicitHeight: Style.barHeight * scaling
width: implicitWidth width: implicitWidth
height: implicitHeight height: implicitHeight
// Optional: Add a subtle visual indicator in debug mode
Rectangle {
anchors.fill: parent
color: Qt.rgba(1, 0, 0, 0.1) // Very subtle red tint
visible: Settings.data.general.debugMode || false
radius: Style.radiusXXS * scaling
}
} }

View file

@ -38,6 +38,10 @@ RowLayout {
!== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats readonly property bool showNetworkStats: (widgetSettings.showNetworkStats
!== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage
!== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
readonly property bool showGpuTemp: (widgetSettings.showGpuTemp !== undefined) ? widgetSettings.showGpuTemp : (widgetMetadata.showGpuTemp
|| false)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
@ -52,126 +56,218 @@ RowLayout {
RowLayout { RowLayout {
id: mainLayout id: mainLayout
anchors.fill: parent anchors.centerIn: parent // Better centering than margins
anchors.leftMargin: Style.marginS * scaling width: parent.width - Style.marginM * scaling * 2
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// CPU Usage Component // CPU Usage Component
RowLayout { Item {
id: cpuUsageLayout Layout.preferredWidth: cpuUsageRow.implicitWidth
spacing: Style.marginXS * scaling Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: showCpuUsage visible: showCpuUsage
NIcon { RowLayout {
id: cpuUsageIcon id: cpuUsageRow
text: "speed" anchors.centerIn: parent
Layout.alignment: Qt.AlignVCenter spacing: Style.marginXS * scaling
}
NText { NIcon {
id: cpuUsageText icon: "cpu-usage"
text: `${SystemStatService.cpuUsage}%` font.pointSize: Style.fontSizeM * scaling
font.family: Settings.data.ui.fontFixed Layout.alignment: Qt.AlignVCenter
font.pointSize: Style.fontSizeS * scaling }
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter NText {
verticalAlignment: Text.AlignVCenter text: `${SystemStatService.cpuUsage}%`
color: Color.mPrimary font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
} }
} }
// CPU Temperature Component // CPU Temperature Component
RowLayout { Item {
id: cpuTempLayout Layout.preferredWidth: cpuTempRow.implicitWidth
// spacing is thin here to compensate for the vertical thermometer icon Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
spacing: Style.marginXXS * scaling
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: showCpuTemp visible: showCpuTemp
NIcon { RowLayout {
text: "thermometer" id: cpuTempRow
Layout.alignment: Qt.AlignVCenter anchors.centerIn: parent
} spacing: Style.marginXS * scaling
NText { NIcon {
text: `${SystemStatService.cpuTemp}°C` icon: "cpu-temperature"
font.family: Settings.data.ui.fontFixed // Fire is so tall, we need to make it smaller
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium Layout.alignment: Qt.AlignVCenter
Layout.alignment: Qt.AlignVCenter }
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary NText {
text: `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
}
// GPU Temperature Component
Item {
Layout.preferredWidth: gpuTempRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showGpuTemp
RowLayout {
id: gpuTempRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NIcon {
icon: "gpu-temperature"
font.pointSize: Style.fontSizeS * scaling
Layout.alignment: Qt.AlignVCenter
}
NText {
text: `${SystemStatService.gpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
} }
} }
// Memory Usage Component // Memory Usage Component
RowLayout { Item {
id: memoryUsageLayout Layout.preferredWidth: memoryUsageRow.implicitWidth
spacing: Style.marginXS * scaling Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: showMemoryUsage visible: showMemoryUsage
NIcon { RowLayout {
text: "memory" id: memoryUsageRow
Layout.alignment: Qt.AlignVCenter anchors.centerIn: parent
} spacing: Style.marginXS * scaling
NText { NIcon {
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G` icon: "memory"
font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeM * scaling
font.pointSize: Style.fontSizeS * scaling Layout.alignment: Qt.AlignVCenter
font.weight: Style.fontWeightMedium }
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter NText {
color: Color.mPrimary text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
} }
} }
// Network Download Speed Component // Network Download Speed Component
RowLayout { Item {
id: networkDownloadLayout Layout.preferredWidth: networkDownloadRow.implicitWidth
spacing: Style.marginXS * scaling Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: showNetworkStats visible: showNetworkStats
NIcon { RowLayout {
text: "download" id: networkDownloadRow
Layout.alignment: Qt.AlignVCenter anchors.centerIn: parent
} spacing: Style.marginXS * scaling
NText { NIcon {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) icon: "download-speed"
font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeM * scaling
font.pointSize: Style.fontSizeS * scaling Layout.alignment: Qt.AlignVCenter
font.weight: Style.fontWeightMedium }
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter NText {
color: Color.mPrimary text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
} }
} }
// Network Upload Speed Component // Network Upload Speed Component
RowLayout { Item {
id: networkUploadLayout Layout.preferredWidth: networkUploadRow.implicitWidth
spacing: Style.marginXS * scaling Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: showNetworkStats visible: showNetworkStats
NIcon { RowLayout {
text: "upload" id: networkUploadRow
Layout.alignment: Qt.AlignVCenter anchors.centerIn: parent
} spacing: Style.marginXS * scaling
NText { NIcon {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed) icon: "upload-speed"
font.family: Settings.data.ui.fontFixed font.pointSize: Style.fontSizeM * scaling
font.pointSize: Style.fontSizeS * scaling Layout.alignment: Qt.AlignVCenter
font.weight: Style.fontWeightMedium }
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter NText {
color: Color.mPrimary text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
}
}
// Disk Usage Component (primary drive)
Item {
Layout.preferredWidth: diskUsageRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showDiskUsage
RowLayout {
id: diskUsageRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NIcon {
icon: "storage"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
NText {
text: `${SystemStatService.diskPercent}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
} }
} }
} }

View file

@ -56,7 +56,7 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
width: Style.marginL * root.scaling width: Style.marginL * root.scaling
height: Style.marginL * root.scaling height: Style.marginL * root.scaling
source: Icons.iconForAppId(taskbarItem.modelData.appId) source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true smooth: true
} }
} }

View file

@ -43,9 +43,9 @@ Item {
function getIcon() { function getIcon() {
if (AudioService.muted) { if (AudioService.muted) {
return "volume_off" return "volume-mute"
} }
return AudioService.volume <= Number.EPSILON ? "volume_off" : (AudioService.volume < 0.33 ? "volume_down" : "volume_up") return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
} }
// Connection used to open the pill when volume changes // Connection used to open the pill when volume changes
@ -77,8 +77,6 @@ Item {
rightOpen: BarWidgetRegistry.getNPillDirection(root) rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon() icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100) + "%" text: Math.floor(AudioService.volume * 100) + "%"
forceOpen: alwaysShowPercentage forceOpen: alwaysShowPercentage

View file

@ -23,7 +23,7 @@ NIconButton {
icon: { icon: {
try { try {
if (NetworkService.ethernetConnected) { if (NetworkService.ethernetConnected) {
return "lan" return "ethernet"
} }
let connected = false let connected = false
let signalStrength = 0 let signalStrength = 0
@ -34,7 +34,7 @@ NIconButton {
break break
} }
} }
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find" return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
} catch (error) { } catch (error) {
Logger.error("Wi-Fi", "Error getting icon:", error) Logger.error("Wi-Fi", "Error getting icon:", error)
return "signal_wifi_bad" return "signal_wifi_bad"

View file

@ -66,7 +66,7 @@ ColumnLayout {
// One device BT icon // One device BT icon
NIcon { NIcon {
text: BluetoothService.getDeviceIcon(modelData) icon: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: getContentColor(Color.mOnSurface) color: getContentColor(Color.mOnSurface)
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@ -164,7 +164,7 @@ ColumnLayout {
} }
return "Connect" return "Connect"
} }
icon: (isBusy ? "hourglass_full" : null) icon: (isBusy ? "hourglass-split" : null)
onClicked: { onClicked: {
if (modelData.connected) { if (modelData.connected) {
BluetoothService.disconnectDevice(modelData) BluetoothService.disconnectDevice(modelData)

View file

@ -28,7 +28,7 @@ NPanel {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { NIcon {
text: "bluetooth" icon: "bluetooth"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary color: Color.mPrimary
} }
@ -41,8 +41,16 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
} }
NToggle {
id: wifiSwitch
checked: Settings.data.network.bluetoothEnabled
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65 * scaling
}
NIconButton { NIconButton {
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh" enabled: Settings.data.network.bluetoothEnabled
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
tooltipText: "Refresh Devices" tooltipText: "Refresh Devices"
sizeRatio: 0.8 sizeRatio: 0.8
onClicked: { onClicked: {
@ -66,7 +74,42 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
} }
Rectangle {
visible: !Settings.data.network.bluetoothEnabled
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.transparent
// Center the content within this rectangle
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
icon: "bluetooth-off"
font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Bluetooth is disabled"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Bluetooth to see available devices."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
ScrollView { ScrollView {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
@ -75,7 +118,6 @@ NPanel {
contentWidth: availableWidth contentWidth: availableWidth
ColumnLayout { ColumnLayout {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
width: parent.width width: parent.width
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
@ -146,7 +188,7 @@ NPanel {
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
NIcon { NIcon {
text: "sync" icon: "refresh"
font.pointSize: Style.fontSizeXXL * 1.5 * scaling font.pointSize: Style.fontSizeXXL * 1.5 * scaling
color: Color.mPrimary color: Color.mPrimary

View file

@ -28,7 +28,7 @@ NPanel {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NIconButton { NIconButton {
icon: "chevron_left" icon: "chevron-left"
tooltipText: "Previous month" tooltipText: "Previous month"
onClicked: { onClicked: {
let newDate = new Date(grid.year, grid.month - 1, 1) let newDate = new Date(grid.year, grid.month - 1, 1)
@ -47,7 +47,7 @@ NPanel {
} }
NIconButton { NIconButton {
icon: "chevron_right" icon: "chevron-right"
tooltipText: "Next month" tooltipText: "Next month"
onClicked: { onClicked: {
let newDate = new Date(grid.year, grid.month + 1, 1) let newDate = new Date(grid.year, grid.month + 1, 1)

View file

@ -197,7 +197,7 @@ Variants {
function getAppIcon(toplevel: Toplevel): string { function getAppIcon(toplevel: Toplevel): string {
if (!toplevel) if (!toplevel)
return "" return ""
return Icons.iconForAppId(toplevel.appId?.toLowerCase()) return AppIcons.iconForAppId(toplevel.appId?.toLowerCase())
} }
RowLayout { RowLayout {
@ -256,11 +256,10 @@ Variants {
} }
// Fall back if no icon // Fall back if no icon
NText { NIcon {
anchors.centerIn: parent anchors.centerIn: parent
visible: !appIcon.visible visible: !appIcon.visible
text: "question_mark" icon: "question-mark"
font.family: "Material Symbols Rounded"
font.pointSize: iconSize * 0.7 font.pointSize: iconSize * 0.7
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
scale: appButton.hovered ? 1.15 : 1.0 scale: appButton.hovered ? 1.15 : 1.0

View file

@ -22,7 +22,9 @@ Item {
IpcHandler { IpcHandler {
target: "screenRecorder" target: "screenRecorder"
function toggle() { function toggle() {
ScreenRecorderService.toggleRecording() if (ScreenRecorderService.isAvailable) {
ScreenRecorderService.toggleRecording()
}
} }
} }

View file

@ -404,7 +404,7 @@ NPanel {
sourceComponent: Component { sourceComponent: Component {
IconImage { IconImage {
anchors.fill: parent anchors.fill: parent
source: modelData.icon ? Icons.iconFromName(modelData.icon, "application-x-executable") : "" source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== "" visible: modelData.icon && source !== ""
asynchronous: true asynchronous: true
} }

View file

@ -136,7 +136,7 @@ Item {
const items = ClipboardService.items || [] const items = ClipboardService.items || []
// If no items and we haven't tried loading yet, trigger a load // If no items and we haven't tried loading yet, trigger a load
if (items.length === 0 && !ClipboardService.loading) { if (items.count === 0 && !ClipboardService.loading) {
isWaitingForData = true isWaitingForData = true
ClipboardService.list(100) ClipboardService.list(100)
return [{ return [{

View file

@ -418,7 +418,7 @@ Loader {
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
} }
NIcon { NIcon {
text: "keyboard_alt" icon: "keyboard"
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface color: Color.mOnSurface
} }
@ -428,7 +428,7 @@ Loader {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible visible: batteryIndicator.batteryVisible
NIcon { NIcon {
text: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging, icon: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging,
batteryIndicator.isReady) batteryIndicator.isReady)
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
@ -718,21 +718,47 @@ Loader {
anchors.margins: 50 * scaling anchors.margins: 50 * scaling
spacing: 20 * scaling spacing: 20 * scaling
// Shutdown
Rectangle { Rectangle {
Layout.preferredWidth: 60 * scaling Layout.preferredWidth: iconPower.implicitWidth + Style.marginXL * scaling
Layout.preferredHeight: 60 * scaling Layout.preferredHeight: Layout.preferredWidth
radius: width * 0.5 radius: width * 0.5
color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2) color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2)
border.color: Color.mError border.color: Color.mError
border.width: Math.max(1, Style.borderM * scaling) border.width: Math.max(1, Style.borderM * scaling)
NIcon { NIcon {
id: iconPower
anchors.centerIn: parent anchors.centerIn: parent
text: "power_settings_new" icon: "shutdown"
font.pointSize: Style.fontSizeXL * scaling font.pointSize: Style.fontSizeXXXL * scaling
color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError color: powerButtonArea.containsMouse ? Color.mOnError : Color.mError
} }
// Tooltip (inline rectangle to avoid separate Window during lock)
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: powerButtonArea.containsMouse
z: 1
NText {
id: shutdownTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Shut down the computer."
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
implicitWidth: shutdownTooltipText.implicitWidth + Style.marginM * 2 * scaling
implicitHeight: shutdownTooltipText.implicitHeight + Style.marginM * 2 * scaling
}
MouseArea { MouseArea {
id: powerButtonArea id: powerButtonArea
anchors.fill: parent anchors.fill: parent
@ -743,21 +769,47 @@ Loader {
} }
} }
// Reboot
Rectangle { Rectangle {
Layout.preferredWidth: 60 * scaling Layout.preferredWidth: iconReboot.implicitWidth + Style.marginXL * scaling
Layout.preferredHeight: 60 * scaling Layout.preferredHeight: Layout.preferredWidth
radius: width * 0.5 radius: width * 0.5
color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight) color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight)
border.color: Color.mPrimary border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling) border.width: Math.max(1, Style.borderM * scaling)
NIcon { NIcon {
id: iconReboot
anchors.centerIn: parent anchors.centerIn: parent
text: "restart_alt" icon: "reboot"
font.pointSize: Style.fontSizeXL * scaling font.pointSize: Style.fontSizeXXXL * scaling
color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary color: restartButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
} }
// Tooltip
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: restartButtonArea.containsMouse
z: 1
NText {
id: restartTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Restart the computer."
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
implicitWidth: restartTooltipText.implicitWidth + Style.marginM * 2 * scaling
implicitHeight: restartTooltipText.implicitHeight + Style.marginM * 2 * scaling
}
MouseArea { MouseArea {
id: restartButtonArea id: restartButtonArea
anchors.fill: parent anchors.fill: parent
@ -765,24 +817,51 @@ Loader {
onClicked: { onClicked: {
CompositorService.reboot() CompositorService.reboot()
} }
// Tooltip handled via inline rectangle visibility
} }
} }
// Suspend
Rectangle { Rectangle {
Layout.preferredWidth: 60 * scaling Layout.preferredWidth: iconSuspend.implicitWidth + Style.marginXL * scaling
Layout.preferredHeight: 60 * scaling Layout.preferredHeight: Layout.preferredWidth
radius: width * 0.5 radius: width * 0.5
color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2) color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2)
border.color: Color.mSecondary border.color: Color.mSecondary
border.width: Math.max(1, Style.borderM * scaling) border.width: Math.max(1, Style.borderM * scaling)
NIcon { NIcon {
id: iconSuspend
anchors.centerIn: parent anchors.centerIn: parent
text: "bedtime" icon: "suspend"
font.pointSize: Style.fontSizeXL * scaling font.pointSize: Style.fontSizeXXXL * scaling
color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary color: suspendButtonArea.containsMouse ? Color.mOnSecondary : Color.mSecondary
} }
// Tooltip
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.top
anchors.bottomMargin: 12 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: suspendButtonArea.containsMouse
z: 1
NText {
id: suspendTooltipText
anchors.margins: Style.marginM * scaling
anchors.fill: parent
text: "Suspend the system."
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
implicitWidth: suspendTooltipText.implicitWidth + Style.marginM * 2 * scaling
implicitHeight: suspendTooltipText.implicitHeight + Style.marginM * 2 * scaling
}
MouseArea { MouseArea {
id: suspendButtonArea id: suspendButtonArea
anchors.fill: parent anchors.fill: parent
@ -790,6 +869,7 @@ Loader {
onClicked: { onClicked: {
CompositorService.suspend() CompositorService.suspend()
} }
// Tooltip handled via inline rectangle visibility
} }
} }
} }

View file

@ -205,14 +205,12 @@ Variants {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
// Avatar // Image
NImageCircled { NImageCircled {
id: appAvatar
Layout.preferredWidth: 40 * scaling Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop
imagePath: model.image && model.image !== "" ? model.image : "" imagePath: model.image && model.image !== "" ? model.image : ""
fallbackIcon: ""
borderColor: Color.transparent borderColor: Color.transparent
borderWidth: 0 borderWidth: 0
visible: (model.image && model.image !== "") visible: (model.image && model.image !== "")

View file

@ -31,7 +31,7 @@ NPanel {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { NIcon {
text: "notifications" icon: "bell"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary color: Color.mPrimary
} }
@ -45,14 +45,15 @@ NPanel {
} }
NIconButton { NIconButton {
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications_active" icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled." tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled."
sizeRatio: 0.8 sizeRatio: 0.8
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
} }
NIconButton { NIconButton {
icon: "delete" icon: "trash"
tooltipText: "Clear history" tooltipText: "Clear history"
sizeRatio: 0.8 sizeRatio: 0.8
onClicked: NotificationService.clearHistory() onClicked: NotificationService.clearHistory()
@ -85,7 +86,7 @@ NPanel {
} }
NIcon { NIcon {
text: "notifications_off" icon: "bell-off"
font.pointSize: 64 * scaling font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@ -103,6 +104,9 @@ NPanel {
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
} }
Item { Item {
@ -135,10 +139,29 @@ NPanel {
anchors.margins: Style.marginM * scaling anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
// App icon (same style as popup)
NImageCircled {
Layout.preferredWidth: 28 * scaling
Layout.preferredHeight: 28 * scaling
Layout.alignment: Qt.AlignVCenter
// Prefer stable themed icons over transient image paths
imagePath: (appIcon
&& appIcon !== "") ? (AppIcons.iconFromName(appIcon, "application-x-executable")
|| appIcon) : ((AppIcons.iconForAppId(desktopEntry
|| appName, "application-x-executable")
|| (image && image
!== "" ? image : AppIcons.iconFromName("application-x-executable",
"application-x-executable"))))
borderColor: Color.transparent
borderWidth: 0
visible: true
}
// Notification content column // Notification content column
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: notificationList.width - (Style.marginM * scaling * 4) // Account for margins and delete button
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
NText { NText {
@ -148,7 +171,6 @@ NPanel {
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary
wrapMode: Text.Wrap wrapMode: Text.Wrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: parent.width
maximumLineCount: 2 maximumLineCount: 2
elide: Text.ElideRight elide: Text.ElideRight
} }
@ -159,7 +181,6 @@ NPanel {
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
wrapMode: Text.Wrap wrapMode: Text.Wrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.maximumWidth: parent.width
maximumLineCount: 3 maximumLineCount: 3
elide: Text.ElideRight elide: Text.ElideRight
visible: text.length > 0 visible: text.length > 0
@ -175,7 +196,7 @@ NPanel {
// Delete button // Delete button
NIconButton { NIconButton {
icon: "delete" icon: "trash"
tooltipText: "Delete notification" tooltipText: "Delete notification"
sizeRatio: 0.7 sizeRatio: 0.7
Layout.alignment: Qt.AlignTop Layout.alignment: Qt.AlignTop

View file

@ -29,27 +29,27 @@ NPanel {
property int selectedIndex: 0 property int selectedIndex: 0
readonly property var powerOptions: [{ readonly property var powerOptions: [{
"action": "lock", "action": "lock",
"icon": "lock_outline", "icon": "lock",
"title": "Lock", "title": "Lock",
"subtitle": "Lock your session" "subtitle": "Lock your session"
}, { }, {
"action": "suspend", "action": "suspend",
"icon": "bedtime", "icon": "suspend",
"title": "Suspend", "title": "Suspend",
"subtitle": "Put the system to sleep" "subtitle": "Put the system to sleep"
}, { }, {
"action": "reboot", "action": "reboot",
"icon": "refresh", "icon": "reboot",
"title": "Reboot", "title": "Reboot",
"subtitle": "Restart the system" "subtitle": "Restart the system"
}, { }, {
"action": "logout", "action": "logout",
"icon": "exit_to_app", "icon": "logout",
"title": "Logout", "title": "Logout",
"subtitle": "End your session" "subtitle": "End your session"
}, { }, {
"action": "shutdown", "action": "shutdown",
"icon": "power_settings_new", "icon": "shutdown",
"title": "Shutdown", "title": "Shutdown",
"subtitle": "Turn off the system", "subtitle": "Turn off the system",
"isShutdown": true "isShutdown": true
@ -276,7 +276,7 @@ NPanel {
} }
NIconButton { NIconButton {
icon: timerActive ? "back_hand" : "close" icon: timerActive ? "stop" : "close"
tooltipText: timerActive ? "Cancel Timer" : "Close" tooltipText: timerActive ? "Cancel Timer" : "Close"
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
@ -360,7 +360,7 @@ NPanel {
id: iconElement id: iconElement
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: buttonRoot.icon icon: buttonRoot.icon
color: { color: {
if (buttonRoot.pending) if (buttonRoot.pending)
return Color.mPrimary return Color.mPrimary

View file

@ -18,7 +18,11 @@ NBox {
signal removeWidget(string section, int index) signal removeWidget(string section, int index)
signal reorderWidget(string section, int fromIndex, int toIndex) signal reorderWidget(string section, int fromIndex, int toIndex)
signal updateWidgetSettings(string section, int index, var settings) signal updateWidgetSettings(string section, int index, var settings)
signal dragPotentialStarted
// Emitted when a widget is pressed (potential drag start)
signal dragPotentialEnded
// Emitted when interaction ends (drag or click)
color: Color.mSurface color: Color.mSurface
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumHeight: { Layout.minimumHeight: {
@ -105,13 +109,11 @@ NBox {
} }
// Drag and Drop Widget Area // Drag and Drop Widget Area
// Replace your Flow section with this:
// Drag and Drop Widget Area - use Item container
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.minimumHeight: 65 * scaling Layout.minimumHeight: 65 * scaling
clip: false // Don't clip children so ghost can move freely
Flow { Flow {
id: widgetFlow id: widgetFlow
@ -139,13 +141,18 @@ NBox {
readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id) readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id)
// Visual feedback during drag // Visual feedback during drag
states: State { opacity: flowDragArea.draggedIndex === index ? 0.5 : 1.0
when: flowDragArea.draggedIndex === index scale: flowDragArea.draggedIndex === index ? 0.95 : 1.0
PropertyChanges { z: flowDragArea.draggedIndex === index ? 1000 : 0
target: widgetItem
scale: 1.1 Behavior on opacity {
opacity: 0.9 NumberAnimation {
z: 1000 duration: 150
}
}
Behavior on scale {
NumberAnimation {
duration: 150
} }
} }
@ -227,31 +234,186 @@ NBox {
} }
} }
// MouseArea outside Flow, covering the same area // Ghost/Clone widget for dragging
Rectangle {
id: dragGhost
width: 0
height: Style.baseWidgetSize * 1.15 * scaling
radius: Style.radiusL * scaling
color: "transparent"
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
opacity: 0.7
visible: flowDragArea.dragStarted
z: 2000
clip: false // Ensure ghost isn't clipped
Text {
id: ghostText
anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnPrimary
}
}
// Drop indicator - visual feedback for where the widget will be inserted
Rectangle {
id: dropIndicator
width: 3 * scaling
height: Style.baseWidgetSize * 1.15 * scaling
radius: width / 2
color: Color.mPrimary
opacity: 0
visible: opacity > 0
z: 1999
SequentialAnimation on opacity {
id: pulseAnimation
running: false
loops: Animation.Infinite
NumberAnimation {
to: 1
duration: 400
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 0.6
duration: 400
easing.type: Easing.InOutQuad
}
}
Behavior on x {
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
Behavior on y {
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
}
// MouseArea for drag and drop
MouseArea { MouseArea {
id: flowDragArea id: flowDragArea
anchors.fill: parent anchors.fill: parent
z: -1 // Ensure this mouse area is below the Settings and Close buttons z: -1
// Critical properties for proper event handling
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
preventStealing: false // Prevent child items from stealing events preventStealing: false
propagateComposedEvents: draggedIndex != -1 // Don't propagate to children during drag propagateComposedEvents: !dragStarted
hoverEnabled: draggedIndex != -1 hoverEnabled: true // Always track mouse for drag operations
property point startPos: Qt.point(0, 0) property point startPos: Qt.point(0, 0)
property bool dragStarted: false property bool dragStarted: false
property bool potentialDrag: false // Track if we're in a potential drag interaction
property int draggedIndex: -1 property int draggedIndex: -1
property real dragThreshold: 15 * scaling property real dragThreshold: 15 * scaling
property Item draggedWidget: null property Item draggedWidget: null
property point clickOffsetInWidget: Qt.point(0, 0) property int dropTargetIndex: -1
property point originalWidgetPos: Qt.point(0, 0) // ADD THIS: Store original position property var draggedModelData: null
// Drop position calculation
function updateDropIndicator(mouseX, mouseY) {
if (!dragStarted || draggedIndex === -1) {
dropIndicator.opacity = 0
pulseAnimation.running = false
return
}
let bestIndex = -1
let bestPosition = null
let minDistance = Infinity
// Check position relative to each widget
for (var i = 0; i < widgetModel.length; i++) {
if (i === draggedIndex)
continue
const widget = widgetFlow.children[i]
if (!widget || widget.widgetIndex === undefined)
continue
// Check distance to left edge (insert before)
const leftDist = Math.sqrt(Math.pow(mouseX - widget.x,
2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2))
// Check distance to right edge (insert after)
const rightDist = Math.sqrt(Math.pow(mouseX - (widget.x + widget.width),
2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2))
if (leftDist < minDistance) {
minDistance = leftDist
bestIndex = i
bestPosition = Qt.point(widget.x - dropIndicator.width / 2 - Style.marginXS * scaling, widget.y)
}
if (rightDist < minDistance) {
minDistance = rightDist
bestIndex = i + 1
bestPosition = Qt.point(widget.x + widget.width + Style.marginXS * scaling - dropIndicator.width / 2,
widget.y)
}
}
// Check if we should insert at position 0 (very beginning)
if (widgetModel.length > 0 && draggedIndex !== 0) {
const firstWidget = widgetFlow.children[0]
if (firstWidget) {
const dist = Math.sqrt(Math.pow(mouseX, 2) + Math.pow(mouseY - firstWidget.y, 2))
if (dist < minDistance && mouseX < firstWidget.x + firstWidget.width / 2) {
minDistance = dist
bestIndex = 0
bestPosition = Qt.point(Math.max(0, firstWidget.x - dropIndicator.width - Style.marginS * scaling),
firstWidget.y)
}
}
}
// Only show indicator if we're close enough and it's a different position
if (minDistance < 80 * scaling && bestIndex !== -1) {
// Adjust index if we're moving forward
let adjustedIndex = bestIndex
if (bestIndex > draggedIndex) {
adjustedIndex = bestIndex - 1
}
// Don't show if it's the same position
if (adjustedIndex === draggedIndex) {
dropIndicator.opacity = 0
pulseAnimation.running = false
dropTargetIndex = -1
return
}
dropTargetIndex = adjustedIndex
if (bestPosition) {
dropIndicator.x = bestPosition.x
dropIndicator.y = bestPosition.y
dropIndicator.opacity = 1
if (!pulseAnimation.running) {
pulseAnimation.running = true
}
}
} else {
dropIndicator.opacity = 0
pulseAnimation.running = false
dropTargetIndex = -1
}
}
onPressed: mouse => { onPressed: mouse => {
startPos = Qt.point(mouse.x, mouse.y) startPos = Qt.point(mouse.x, mouse.y)
dragStarted = false dragStarted = false
potentialDrag = false
draggedIndex = -1 draggedIndex = -1
draggedWidget = null draggedWidget = null
dropTargetIndex = -1
draggedModelData = null
// Find which widget was clicked // Find which widget was clicked
for (var i = 0; i < widgetModel.length; i++) { for (var i = 0; i < widgetModel.length; i++) {
@ -264,22 +426,18 @@ NBox {
const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth) const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth)
if (localX < buttonsStartX) { if (localX < buttonsStartX) {
// This is a draggable area - prevent panel close immediately
draggedIndex = widget.widgetIndex draggedIndex = widget.widgetIndex
draggedWidget = widget draggedWidget = widget
draggedModelData = widget.modelData
// Calculate and store where within the widget the user clicked potentialDrag = true
const clickOffsetX = mouse.x - widget.x
const clickOffsetY = mouse.y - widget.y
clickOffsetInWidget = Qt.point(clickOffsetX, clickOffsetY)
// STORE ORIGINAL POSITION
originalWidgetPos = Qt.point(widget.x, widget.y)
// Immediately set prevent stealing to true when drag candidate is found
preventStealing = true preventStealing = true
// Signal that interaction started (prevents panel close)
root.dragPotentialStarted()
break break
} else { } else {
// Click was on buttons - allow event propagation // This is a button area - let the click through
mouse.accepted = false mouse.accepted = false
return return
} }
@ -289,154 +447,83 @@ NBox {
} }
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (draggedIndex !== -1) { if (draggedIndex !== -1 && potentialDrag) {
const deltaX = mouse.x - startPos.x const deltaX = mouse.x - startPos.x
const deltaY = mouse.y - startPos.y const deltaY = mouse.y - startPos.y
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY) const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
if (!dragStarted && distance > dragThreshold) { if (!dragStarted && distance > dragThreshold) {
dragStarted = true dragStarted = true
//Logger.log("BarSectionEditor", "Drag started")
// Enable visual feedback // Setup ghost widget
if (draggedWidget) { if (draggedWidget) {
draggedWidget.z = 1000 dragGhost.width = draggedWidget.width
dragGhost.color = root.getWidgetColor(draggedModelData)
ghostText.text = draggedModelData.id
} }
} }
if (dragStarted && draggedWidget) { if (dragStarted) {
// Adjust position to account for where within the widget the user clicked // Move ghost widget
draggedWidget.x = mouse.x - clickOffsetInWidget.x dragGhost.x = mouse.x - dragGhost.width / 2
draggedWidget.y = mouse.y - clickOffsetInWidget.y dragGhost.y = mouse.y - dragGhost.height / 2
// Update drop indicator
updateDropIndicator(mouse.x, mouse.y)
} }
} }
} }
onReleased: mouse => { onReleased: mouse => {
if (dragStarted && draggedWidget) { if (dragStarted && dropTargetIndex !== -1 && dropTargetIndex !== draggedIndex) {
// Find drop target using improved logic // Perform the reorder
let targetIndex = -1 reorderWidget(sectionId, draggedIndex, dropTargetIndex)
let minDistance = Infinity }
const mouseX = mouse.x
const mouseY = mouse.y
// Check if we should insert at the beginning // Always signal end of interaction if we started one
let insertAtBeginning = true if (potentialDrag) {
let insertAtEnd = true root.dragPotentialEnded()
// Check if the dragged item is already the last item
let isLastItem = true
for (var k = 0; k < widgetModel.length; k++) {
if (k !== draggedIndex && k > draggedIndex) {
isLastItem = false
break
}
}
for (var i = 0; i < widgetModel.length; i++) {
if (i !== draggedIndex) {
const widget = widgetFlow.children[i]
if (widget && widget.widgetIndex !== undefined) {
const centerX = widget.x + widget.width / 2
const centerY = widget.y + widget.height / 2
const distance = Math.sqrt(Math.pow(mouseX - centerX, 2) + Math.pow(mouseY - centerY, 2))
// Check if mouse is to the right of this widget
if (mouseX > widget.x + widget.width / 2) {
insertAtBeginning = false
}
// Check if mouse is to the left of this widget
if (mouseX < widget.x + widget.width / 2) {
insertAtEnd = false
}
if (distance < minDistance) {
minDistance = distance
targetIndex = widget.widgetIndex
}
}
}
}
// If dragging the last item to the right, don't reorder
if (isLastItem && insertAtEnd) {
insertAtEnd = false
targetIndex = -1
//Logger.log("BarSectionEditor", "Last item dropped to right - no reordering needed")
}
// Determine final target index based on position
let finalTargetIndex = targetIndex
if (insertAtBeginning && widgetModel.length > 1) {
// Insert at the very beginning (position 0)
finalTargetIndex = 0
//Logger.log("BarSectionEditor", "Inserting at beginning")
} else if (insertAtEnd && widgetModel.length > 1) {
// Insert at the very end
let maxIndex = -1
for (var j = 0; j < widgetModel.length; j++) {
if (j !== draggedIndex) {
maxIndex = Math.max(maxIndex, j)
}
}
finalTargetIndex = maxIndex
//Logger.log("BarSectionEditor", "Inserting at end, target:", finalTargetIndex)
} else if (targetIndex !== -1) {
// Normal case - determine if we should insert before or after the target
const targetWidget = widgetFlow.children[targetIndex]
if (targetWidget) {
const targetCenterX = targetWidget.x + targetWidget.width / 2
if (mouseX > targetCenterX) {
// Mouse is to the right of target center, insert after
//Logger.log("BarSectionEditor", "Inserting after widget at index:", targetIndex)
} else {
// Mouse is to the left of target center, insert before
finalTargetIndex = targetIndex
//Logger.log("BarSectionEditor", "Inserting before widget at index:", targetIndex)
}
}
}
//Logger.log("BarSectionEditor", "Final drop target index:", finalTargetIndex)
// Check if reordering is needed
if (finalTargetIndex !== -1 && finalTargetIndex !== draggedIndex) {
// Reordering will happen - reset position for the Flow to handle
draggedWidget.x = 0
draggedWidget.y = 0
draggedWidget.z = 0
reorderWidget(sectionId, draggedIndex, finalTargetIndex)
} else {
// No reordering - restore original position
draggedWidget.x = originalWidgetPos.x
draggedWidget.y = originalWidgetPos.y
draggedWidget.z = 0
//Logger.log("BarSectionEditor", "No reordering - restoring original position")
}
} else if (draggedIndex !== -1 && !dragStarted) {
// This was a click without drag - could add click handling here if needed
} }
// Reset everything // Reset everything
dragStarted = false dragStarted = false
potentialDrag = false
draggedIndex = -1 draggedIndex = -1
draggedWidget = null draggedWidget = null
preventStealing = false // Allow normal event propagation again dropTargetIndex = -1
originalWidgetPos = Qt.point(0, 0) // Reset stored position draggedModelData = null
preventStealing = false
dropIndicator.opacity = 0
pulseAnimation.running = false
dragGhost.width = 0
} }
// Handle case where mouse leaves the area during drag
onExited: { onExited: {
if (dragStarted && draggedWidget) { if (dragStarted) {
// Restore original position when mouse leaves area // Hide drop indicator when mouse leaves, but keep ghost visible
draggedWidget.x = originalWidgetPos.x dropIndicator.opacity = 0
draggedWidget.y = originalWidgetPos.y pulseAnimation.running = false
draggedWidget.z = 0
} }
} }
onCanceled: {
// Handle cancel (e.g., ESC key pressed during drag)
if (potentialDrag) {
root.dragPotentialEnded()
}
// Reset everything
dragStarted = false
potentialDrag = false
draggedIndex = -1
draggedWidget = null
dropTargetIndex = -1
draggedModelData = null
preventStealing = false
dropIndicator.opacity = 0
pulseAnimation.running = false
dragGhost.width = 0
}
} }
} }
} }

View file

@ -107,6 +107,7 @@ Popup {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling Layout.topMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -16,10 +16,13 @@ ColumnLayout {
// Local state // Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage !== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
property int valueWarningThreshold: widgetData.warningThreshold
!== undefined ? widgetData.warningThreshold : widgetMetadata.warningThreshold
function saveSettings() { function saveSettings() {
var settings = Object.assign({}, widgetData || {}) var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage settings.alwaysShowPercentage = valueAlwaysShowPercentage
settings.warningThreshold = valueWarningThreshold
return settings return settings
} }
@ -28,4 +31,14 @@ ColumnLayout {
checked: root.valueAlwaysShowPercentage checked: root.valueAlwaysShowPercentage
onToggled: checked => root.valueAlwaysShowPercentage = checked onToggled: checked => root.valueAlwaysShowPercentage = checked
} }
NSpinBox {
label: "Low battery warning threshold"
description: "Show a warning when battery falls below this percentage."
value: valueWarningThreshold
suffix: "%"
minimum: 5
maximum: 50
onValueChanged: valueWarningThreshold = value
}
} }

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Window
import qs.Commons import qs.Commons
import qs.Widgets import qs.Widgets
import qs.Services import qs.Services
@ -9,7 +10,6 @@ ColumnLayout {
id: root id: root
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null property var widgetData: null
property var widgetMetadata: null property var widgetMetadata: null
@ -22,16 +22,189 @@ ColumnLayout {
return settings return settings
} }
// Icon setting
NTextInput { NTextInput {
id: iconInput id: iconInput
Layout.fillWidth: true Layout.fillWidth: true
label: "Icon Name" label: "Icon Name"
description: "Choose a name from the Material Icon set." description: "Select an icon from the library."
placeholderText: "Enter icon name (e.g., favorite, home, settings)" placeholderText: "Enter icon name (e.g., cat, gear, house, ...)"
text: widgetData?.icon || widgetMetadata.icon text: widgetData?.icon || widgetMetadata.icon
} }
RowLayout {
spacing: Style.marginS * scaling
Layout.alignment: Qt.AlignLeft
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: iconInput.text
visible: iconInput.text !== ""
}
NButton {
text: "Browse"
onClicked: iconPicker.open()
}
}
Popup {
id: iconPicker
modal: true
property real panelWidth: {
var w = Math.round(Math.max(Screen.width * 0.35, 900) * scaling)
w = Math.min(w, Screen.width - Style.marginL * 2)
return w
}
property real panelHeight: {
var h = Math.round(Math.max(Screen.height * 0.65, 700) * scaling)
h = Math.min(h, Screen.height - Style.barHeight * scaling - Style.marginL * 2)
return h
}
width: panelWidth
height: panelHeight
anchors.centerIn: Overlay.overlay
padding: Style.marginXL * scaling
property string query: ""
property string selectedIcon: ""
property var allIcons: Object.keys(Icons.icons)
property var filteredIcons: allIcons.filter(function (name) {
return query === "" || name.toLowerCase().indexOf(query.toLowerCase()) !== -1
})
readonly property int columns: 6
readonly property int cellW: Math.floor(grid.width / columns)
readonly property int cellH: Math.round(cellW * 0.7 + 36 * scaling)
background: Rectangle {
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM * scaling
// Title row
RowLayout {
Layout.fillWidth: true
NText {
text: "Icon Picker"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: iconPicker.close()
}
}
NDivider {
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
NTextInput {
Layout.fillWidth: true
label: "Search"
placeholderText: "Search (e.g., arrow, battery, cloud)"
text: iconPicker.query
onTextChanged: iconPicker.query = text.trim().toLowerCase()
}
}
// Icon grid
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
GridView {
id: grid
anchors.fill: parent
anchors.margins: Style.marginM * scaling
cellWidth: iconPicker.cellW
cellHeight: iconPicker.cellH
model: iconPicker.filteredIcons
delegate: Rectangle {
width: grid.cellWidth
height: grid.cellHeight
radius: Style.radiusS * scaling
clip: true
color: (iconPicker.selectedIcon === modelData) ? Qt.alpha(Color.mPrimary, 0.15) : "transparent"
border.color: (iconPicker.selectedIcon === modelData) ? Color.mPrimary : Qt.rgba(0, 0, 0, 0)
border.width: (iconPicker.selectedIcon === modelData) ? Style.borderS * scaling : 0
MouseArea {
anchors.fill: parent
onClicked: iconPicker.selectedIcon = modelData
onDoubleClicked: {
iconPicker.selectedIcon = modelData
iconInput.text = iconPicker.selectedIcon
iconPicker.close()
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
Item {
Layout.fillHeight: true
Layout.preferredHeight: 4 * scaling
}
NIcon {
Layout.alignment: Qt.AlignHCenter
icon: modelData
font.pointSize: 42 * scaling
}
NText {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: Style.marginXS * scaling
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeXS * scaling
text: modelData
}
Item {
Layout.fillHeight: true
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
Item {
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: iconPicker.close()
}
NButton {
text: "Apply"
icon: "check"
enabled: iconPicker.selectedIcon !== ""
onClicked: {
iconInput.text = iconPicker.selectedIcon
iconPicker.close()
}
}
}
}
}
NTextInput { NTextInput {
id: leftClickExecInput id: leftClickExecInput
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -16,19 +16,24 @@ ColumnLayout {
// Local, editable state for checkboxes // Local, editable state for checkboxes
property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage
property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp
property bool valueShowGpuTemp: widgetData.showGpuTemp !== undefined ? widgetData.showGpuTemp : (widgetMetadata.showGpuTemp
|| false)
property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage
property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent
!== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
property bool valueShowNetworkStats: widgetData.showNetworkStats property bool valueShowNetworkStats: widgetData.showNetworkStats
!== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats
property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage
function saveSettings() { function saveSettings() {
var settings = Object.assign({}, widgetData || {}) var settings = Object.assign({}, widgetData || {})
settings.showCpuUsage = valueShowCpuUsage settings.showCpuUsage = valueShowCpuUsage
settings.showCpuTemp = valueShowCpuTemp settings.showCpuTemp = valueShowCpuTemp
settings.showGpuTemp = valueShowGpuTemp
settings.showMemoryUsage = valueShowMemoryUsage settings.showMemoryUsage = valueShowMemoryUsage
settings.showMemoryAsPercent = valueShowMemoryAsPercent settings.showMemoryAsPercent = valueShowMemoryAsPercent
settings.showNetworkStats = valueShowNetworkStats settings.showNetworkStats = valueShowNetworkStats
settings.showDiskUsage = valueShowDiskUsage
return settings return settings
} }
@ -48,6 +53,14 @@ ColumnLayout {
onToggled: checked => valueShowCpuTemp = checked onToggled: checked => valueShowCpuTemp = checked
} }
NToggle {
id: showGpuTemp
Layout.fillWidth: true
label: "GPU temperature"
checked: valueShowGpuTemp
onToggled: checked => valueShowGpuTemp = checked
}
NToggle { NToggle {
id: showMemoryUsage id: showMemoryUsage
Layout.fillWidth: true Layout.fillWidth: true
@ -59,7 +72,7 @@ ColumnLayout {
NToggle { NToggle {
id: showMemoryAsPercent id: showMemoryAsPercent
Layout.fillWidth: true Layout.fillWidth: true
label: "Show memory as percentage" label: "Memory as percentage"
checked: valueShowMemoryAsPercent checked: valueShowMemoryAsPercent
onToggled: checked => valueShowMemoryAsPercent = checked onToggled: checked => valueShowMemoryAsPercent = checked
} }
@ -71,4 +84,12 @@ ColumnLayout {
checked: valueShowNetworkStats checked: valueShowNetworkStats
onToggled: checked => valueShowNetworkStats = checked onToggled: checked => valueShowNetworkStats = checked
} }
NToggle {
id: showDiskUsage
Layout.fillWidth: true
label: "Storage usage"
checked: valueShowDiskUsage
onToggled: checked => valueShowDiskUsage = checked
}
} }

View file

@ -123,52 +123,52 @@ NPanel {
let newTabs = [{ let newTabs = [{
"id": SettingsPanel.Tab.General, "id": SettingsPanel.Tab.General,
"label": "General", "label": "General",
"icon": "tune", "icon": "settings-general",
"source": generalTab "source": generalTab
}, { }, {
"id": SettingsPanel.Tab.Bar, "id": SettingsPanel.Tab.Bar,
"label": "Bar", "label": "Bar",
"icon": "web_asset", "icon": "settings-bar",
"source": barTab "source": barTab
}, { }, {
"id": SettingsPanel.Tab.Launcher, "id": SettingsPanel.Tab.Launcher,
"label": "Launcher", "label": "Launcher",
"icon": "apps", "icon": "settings-launcher",
"source": launcherTab "source": launcherTab
}, { }, {
"id": SettingsPanel.Tab.Audio, "id": SettingsPanel.Tab.Audio,
"label": "Audio", "label": "Audio",
"icon": "volume_up", "icon": "settings-audio",
"source": audioTab "source": audioTab
}, { }, {
"id": SettingsPanel.Tab.Display, "id": SettingsPanel.Tab.Display,
"label": "Display", "label": "Display",
"icon": "monitor", "icon": "settings-display",
"source": displayTab "source": displayTab
}, { }, {
"id": SettingsPanel.Tab.Network, "id": SettingsPanel.Tab.Network,
"label": "Network", "label": "Network",
"icon": "lan", "icon": "settings-network",
"source": networkTab "source": networkTab
}, { }, {
"id": SettingsPanel.Tab.Brightness, "id": SettingsPanel.Tab.Brightness,
"label": "Brightness", "label": "Brightness",
"icon": "brightness_6", "icon": "settings-brightness",
"source": brightnessTab "source": brightnessTab
}, { }, {
"id": SettingsPanel.Tab.Weather, "id": SettingsPanel.Tab.Weather,
"label": "Weather", "label": "Weather",
"icon": "partly_cloudy_day", "icon": "settings-weather",
"source": weatherTab "source": weatherTab
}, { }, {
"id": SettingsPanel.Tab.ColorScheme, "id": SettingsPanel.Tab.ColorScheme,
"label": "Color Scheme", "label": "Color Scheme",
"icon": "palette", "icon": "settings-color-scheme",
"source": colorSchemeTab "source": colorSchemeTab
}, { }, {
"id": SettingsPanel.Tab.Wallpaper, "id": SettingsPanel.Tab.Wallpaper,
"label": "Wallpaper", "label": "Wallpaper",
"icon": "image", "icon": "settings-wallpaper",
"source": wallpaperTab "source": wallpaperTab
}] }]
@ -177,7 +177,7 @@ NPanel {
newTabs.push({ newTabs.push({
"id": SettingsPanel.Tab.WallpaperSelector, "id": SettingsPanel.Tab.WallpaperSelector,
"label": "Wallpaper Selector", "label": "Wallpaper Selector",
"icon": "wallpaper_slideshow", "icon": "settings-wallpaper-selector",
"source": wallpaperSelectorTab "source": wallpaperSelectorTab
}) })
} }
@ -185,17 +185,17 @@ NPanel {
newTabs.push({ newTabs.push({
"id": SettingsPanel.Tab.ScreenRecorder, "id": SettingsPanel.Tab.ScreenRecorder,
"label": "Screen Recorder", "label": "Screen Recorder",
"icon": "videocam", "icon": "settings-screen-recorder",
"source": screenRecorderTab "source": screenRecorderTab
}, { }, {
"id": SettingsPanel.Tab.Hooks, "id": SettingsPanel.Tab.Hooks,
"label": "Hooks", "label": "Hooks",
"icon": "cable", "icon": "settings-hooks",
"source": hooksTab "source": hooksTab
}, { }, {
"id": SettingsPanel.Tab.About, "id": SettingsPanel.Tab.About,
"label": "About", "label": "About",
"icon": "info", "icon": "settings-about",
"source": aboutTab "source": aboutTab
}) })
@ -400,13 +400,13 @@ NPanel {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling spacing: Style.marginM * scaling
// Tab icon // Tab icon
NIcon { NIcon {
text: modelData.icon icon: modelData.icon
color: tabTextColor color: tabTextColor
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeXL * scaling
} }
// Tab label // Tab label
@ -416,6 +416,7 @@ NPanel {
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
} }
} }
@ -461,7 +462,14 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Tab title // Main icon
NIcon {
icon: root.tabsModel[currentTabIndex]?.icon
color: Color.mPrimary
font.pointSize: Style.fontSizeXL * scaling
}
// Main title
NText { NText {
text: root.tabsModel[currentTabIndex]?.label || "" text: root.tabsModel[currentTabIndex]?.label || ""
font.pointSize: Style.fontSizeXL * scaling font.pointSize: Style.fontSizeXL * scaling

View file

@ -90,7 +90,7 @@ ColumnLayout {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NIcon { NIcon {
text: "download" icon: "download"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary color: updateArea.containsMouse ? Color.mSurface : Color.mPrimary
} }

View file

@ -9,6 +9,22 @@ import qs.Modules.SettingsPanel.Bar
ColumnLayout { ColumnLayout {
id: root id: root
// Handler for drag start - disables panel background clicks
function handleDragStart() {
var panel = PanelService.getPanel("settingsPanel")
if (panel && panel.disableBackgroundClick) {
panel.disableBackgroundClick()
}
}
// Handler for drag end - re-enables panel background clicks
function handleDragEnd() {
var panel = PanelService.getPanel("settingsPanel")
if (panel && panel.enableBackgroundClick) {
panel.enableBackgroundClick()
}
}
ColumnLayout { ColumnLayout {
spacing: Style.marginL * scaling spacing: Style.marginL * scaling
@ -116,6 +132,8 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index) onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
} }
// Center Section // Center Section
@ -128,6 +146,8 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index) onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
} }
// Right Section // Right Section
@ -140,6 +160,8 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index) onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
} }
} }
} }

View file

@ -194,6 +194,7 @@ ColumnLayout {
wlsunsetCheck.running = true wlsunsetCheck.running = true
} else { } else {
Settings.data.nightLight.enabled = false Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
NightLightService.apply() NightLightService.apply()
ToastService.showNotice("Night Light", "Disabled") ToastService.showNotice("Night Light", "Disabled")
} }
@ -276,6 +277,7 @@ ColumnLayout {
ColumnLayout { ColumnLayout {
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule
&& !Settings.data.nightLight.forced
RowLayout { RowLayout {
Layout.fillWidth: false Layout.fillWidth: false
@ -319,4 +321,21 @@ ColumnLayout {
} }
} }
} }
// Force activation toggle
NToggle {
label: "Force activation"
description: "Immediately apply night temperature without scheduling or fade."
checked: Settings.data.nightLight.forced
onToggled: checked => {
Settings.data.nightLight.forced = checked
if (checked && !Settings.data.nightLight.enabled) {
// Ensure enabled when forcing
wlsunsetCheck.running = true
} else {
NightLightService.apply()
}
}
visible: Settings.data.nightLight.enabled
}
} }

View file

@ -187,7 +187,8 @@ ColumnLayout {
color: getSchemeColor(modelData, "mSurface") color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderL * scaling) border.width: Math.max(1, Style.borderL * scaling)
border.color: (!Settings.data.colorSchemes.useWallpaperColors border.color: (!Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline && (Settings.data.colorSchemes.predefinedScheme === modelData.split("/").pop().replace(
".json", ""))) ? Color.mPrimary : Color.mOutline
scale: root.cardScaleLow scale: root.cardScaleLow
// Mouse area for selection // Mouse area for selection
@ -198,8 +199,8 @@ ColumnLayout {
Settings.data.colorSchemes.useWallpaperColors = false Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting") Logger.log("ColorSchemeTab", "Disabled matugen setting")
Settings.data.colorSchemes.predefinedScheme = schemePath Settings.data.colorSchemes.predefinedScheme = schemePath.split("/").pop().replace(".json", "")
ColorSchemeService.applyScheme(schemePath) ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
} }
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@ -281,7 +282,8 @@ ColumnLayout {
// Selection indicator (Checkmark) // Selection indicator (Checkmark)
Rectangle { Rectangle {
visible: !Settings.data.colorSchemes.useWallpaperColors visible: !Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === schemePath) && (Settings.data.colorSchemes.predefinedScheme === schemePath.split("/").pop().replace(".json",
""))
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginS * scaling

View file

@ -22,15 +22,7 @@ ColumnLayout {
label: "Enable Bluetooth" label: "Enable Bluetooth"
description: "Enable Bluetooth connectivity." description: "Enable Bluetooth connectivity."
checked: Settings.data.network.bluetoothEnabled checked: Settings.data.network.bluetoothEnabled
onToggled: checked => { onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
Settings.data.network.bluetoothEnabled = checked
BluetoothService.setBluetoothEnabled(checked)
if (checked) {
ToastService.showNotice("Bluetooth", "Enabled")
} else {
ToastService.showNotice("Bluetooth", "Disabled")
}
}
} }
NDivider { NDivider {

View file

@ -181,7 +181,7 @@ ColumnLayout {
visible: isSelected visible: isSelected
NIcon { NIcon {
text: "check" icon: "check"
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mOnSecondary color: Color.mOnSecondary
@ -246,8 +246,8 @@ ColumnLayout {
} }
NIcon { NIcon {
text: "folder_open" icon: "folder-open"
font.pointSize: Style.fontSizeXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurface color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }

View file

@ -18,7 +18,7 @@ NBox {
Layout.fillHeight: true Layout.fillHeight: true
anchors.margins: Style.marginL * scaling anchors.margins: Style.marginL * scaling
// Fallback // No media player detected
ColumnLayout { ColumnLayout {
id: fallback id: fallback
@ -31,8 +31,8 @@ NBox {
} }
NIcon { NIcon {
text: "album" icon: "disc"
font.pointSize: Style.fontSizeXXXL * 2.5 * scaling font.pointSize: Style.fontSizeXXXL * 3 * scaling
color: Color.mPrimary color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
} }
@ -89,7 +89,7 @@ NBox {
indicator: NIcon { indicator: NIcon {
x: playerSelector.width - width x: playerSelector.width - width
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2 y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
text: "arrow_drop_down" icon: "caret-down"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurface color: Color.mOnSurface
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
@ -156,22 +156,22 @@ NBox {
color: trackArt.visible ? Color.mPrimary : Color.transparent color: trackArt.visible ? Color.mPrimary : Color.transparent
clip: true clip: true
// Can't use fallback icon here, as we have a big disc behind
NImageCircled { NImageCircled {
id: trackArt id: trackArt
visible: MediaService.trackArtUrl.toString() !== "" visible: MediaService.trackArtUrl !== ""
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginXS * scaling anchors.margins: Style.marginXS * scaling
imagePath: MediaService.trackArtUrl imagePath: MediaService.trackArtUrl
fallbackIcon: "music_note"
borderColor: Color.mOutline borderColor: Color.mOutline
borderWidth: Math.max(1, Style.borderS * scaling) borderWidth: Math.max(1, Style.borderS * scaling)
} }
// Fallback icon when no album art available // Fallback icon when no album art available
NIcon { NIcon {
text: "album" icon: "disc"
color: Color.mPrimary color: Color.mPrimary
font.pointSize: Style.fontSizeL * 12 * scaling font.pointSize: Style.fontSizeXXXL * 3 * scaling
visible: !trackArt.visible visible: !trackArt.visible
anchors.centerIn: parent anchors.centerIn: parent
} }
@ -307,7 +307,7 @@ NBox {
// Previous button // Previous button
NIconButton { NIconButton {
icon: "skip_previous" icon: "media-prev"
tooltipText: "Previous Media" tooltipText: "Previous Media"
visible: MediaService.canGoPrevious visible: MediaService.canGoPrevious
onClicked: MediaService.canGoPrevious ? MediaService.previous() : {} onClicked: MediaService.canGoPrevious ? MediaService.previous() : {}
@ -315,7 +315,7 @@ NBox {
// Play/Pause button // Play/Pause button
NIconButton { NIconButton {
icon: MediaService.isPlaying ? "pause" : "play_arrow" icon: MediaService.isPlaying ? "media-pause" : "media-play"
tooltipText: MediaService.isPlaying ? "Pause" : "Play" tooltipText: MediaService.isPlaying ? "Pause" : "Play"
visible: (MediaService.canPlay || MediaService.canPause) visible: (MediaService.canPlay || MediaService.canPause)
onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {} onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {}
@ -323,7 +323,7 @@ NBox {
// Next button // Next button
NIconButton { NIconButton {
icon: "skip_next" icon: "media-next"
tooltipText: "Next media" tooltipText: "Next media"
visible: MediaService.canGoNext visible: MediaService.canGoNext
onClicked: MediaService.canGoNext ? MediaService.next() : {} onClicked: MediaService.canGoNext ? MediaService.next() : {}

View file

@ -13,9 +13,8 @@ NBox {
Layout.preferredWidth: 1 Layout.preferredWidth: 1
implicitHeight: powerRow.implicitHeight + Style.marginM * 2 * scaling implicitHeight: powerRow.implicitHeight + Style.marginM * 2 * scaling
// PowerProfiles service // Centralized service
property var powerProfiles: PowerProfiles readonly property bool hasPP: PowerProfileService.available
readonly property bool hasPP: powerProfiles.hasPerformanceProfile
property real spacing: 0 property real spacing: 0
RowLayout { RowLayout {
@ -28,43 +27,46 @@ NBox {
} }
// Performance // Performance
NIconButton { NIconButton {
icon: "speed" icon: "performance"
tooltipText: "Set performance power profile." tooltipText: "Set performance power profile."
enabled: hasPP enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && powerProfiles.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant colorBg: (enabled
colorFg: (enabled && powerProfiles.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary
onClicked: { onClicked: {
if (enabled) { if (enabled) {
powerProfiles.profile = PowerProfile.Performance PowerProfileService.setProfile(PowerProfile.Performance)
} }
} }
} }
// Balanced // Balanced
NIconButton { NIconButton {
icon: "balance" icon: "balanced"
tooltipText: "Set balanced power profile." tooltipText: "Set balanced power profile."
enabled: hasPP enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && powerProfiles.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant colorBg: (enabled
colorFg: (enabled && powerProfiles.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary
onClicked: { onClicked: {
if (enabled) { if (enabled) {
powerProfiles.profile = PowerProfile.Balanced PowerProfileService.setProfile(PowerProfile.Balanced)
} }
} }
} }
// Eco // Eco
NIconButton { NIconButton {
icon: "eco" icon: "powersaver"
tooltipText: "Set eco power profile." tooltipText: "Set eco power profile."
enabled: hasPP enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && powerProfiles.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant colorBg: (enabled
colorFg: (enabled && powerProfiles.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary
onClicked: { onClicked: {
if (enabled) { if (enabled) {
powerProfiles.profile = PowerProfile.PowerSaver PowerProfileService.setProfile(PowerProfile.PowerSaver)
} }
} }
} }

View file

@ -47,7 +47,8 @@ NBox {
} }
NText { NText {
text: `System uptime: ${uptimeText}` text: `System uptime: ${uptimeText}`
color: Color.mOnSurface font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
} }
} }
@ -68,7 +69,7 @@ NBox {
NIconButton { NIconButton {
id: powerButton id: powerButton
icon: "power_settings_new" icon: "power"
tooltipText: "Power menu." tooltipText: "Power menu."
onClicked: { onClicked: {
powerPanel.open(screen) powerPanel.open(screen)

View file

@ -24,7 +24,7 @@ NBox {
NCircleStat { NCircleStat {
value: SystemStatService.cpuUsage value: SystemStatService.cpuUsage
icon: "speed" icon: "cpu-usage"
flat: true flat: true
contentScale: 0.8 contentScale: 0.8
width: 72 * scaling width: 72 * scaling
@ -33,7 +33,7 @@ NBox {
NCircleStat { NCircleStat {
value: SystemStatService.cpuTemp value: SystemStatService.cpuTemp
suffix: "°C" suffix: "°C"
icon: "device_thermostat" icon: "cpu-temperature"
flat: true flat: true
contentScale: 0.8 contentScale: 0.8
width: 72 * scaling width: 72 * scaling
@ -49,7 +49,7 @@ NBox {
} }
NCircleStat { NCircleStat {
value: SystemStatService.diskPercent value: SystemStatService.diskPercent
icon: "hard_drive" icon: "storage"
flat: true flat: true
contentScale: 0.8 contentScale: 0.8
width: 72 * scaling width: 72 * scaling

View file

@ -25,11 +25,14 @@ NBox {
} }
// Screen Recorder // Screen Recorder
NIconButton { NIconButton {
icon: "videocam" icon: "camera-video"
tooltipText: ScreenRecorderService.isRecording ? "Stop screen recording." : "Start screen recording." enabled: ScreenRecorderService.isAvailable
tooltipText: ScreenRecorderService.isAvailable ? (ScreenRecorderService.isRecording ? "Stop screen recording." : "Start screen recording.") : "Screen recorder not installed."
colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant colorBg: ScreenRecorderService.isRecording ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary colorFg: ScreenRecorderService.isRecording ? Color.mOnPrimary : Color.mPrimary
onClicked: { onClicked: {
if (!ScreenRecorderService.isAvailable)
return
ScreenRecorderService.toggleRecording() ScreenRecorderService.toggleRecording()
// If we were not recording and we just initiated a start, close the panel // If we were not recording and we just initiated a start, close the panel
if (!ScreenRecorderService.isRecording) { if (!ScreenRecorderService.isRecording) {
@ -41,7 +44,7 @@ NBox {
// Idle Inhibitor // Idle Inhibitor
NIconButton { NIconButton {
icon: "coffee" icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake." : "Enable keep awake." tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake." : "Enable keep awake."
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
@ -53,7 +56,7 @@ NBox {
// Wallpaper // Wallpaper
NIconButton { NIconButton {
visible: Settings.data.wallpaper.enabled visible: Settings.data.wallpaper.enabled
icon: "image" icon: "wallpaper-selector"
tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper." tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper."
onClicked: { onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel") var settingsPanel = PanelService.getPanel("settingsPanel")

View file

@ -27,7 +27,8 @@ NBox {
RowLayout { RowLayout {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NIcon { NIcon {
text: weatherReady ? LocationService.weatherSymbolFromCode( Layout.alignment: Qt.AlignVCenter
icon: weatherReady ? LocationService.weatherSymbolFromCode(
LocationService.data.weather.current_weather.weathercode) : "" LocationService.data.weather.current_weather.weathercode) : ""
font.pointSize: Style.fontSizeXXXL * 1.75 * scaling font.pointSize: Style.fontSizeXXXL * 1.75 * scaling
color: Color.mPrimary color: Color.mPrimary
@ -89,20 +90,23 @@ NBox {
model: weatherReady ? LocationService.data.weather.daily.time : [] model: weatherReady ? LocationService.data.weather.daily.time : []
delegate: ColumnLayout { delegate: ColumnLayout {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
spacing: Style.marginS * scaling spacing: Style.marginL * scaling
NText { NText {
text: { text: {
var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/")) var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/"))
return Qt.formatDateTime(weatherDate, "ddd") return Qt.formatDateTime(weatherDate, "ddd")
} }
color: Color.mOnSurface color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
} }
NIcon { NIcon {
text: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index]) Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
font.pointSize: Style.fontSizeXXL * scaling icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index])
font.pointSize: Style.fontSizeXXL * 1.6 * scaling
color: Color.mPrimary color: Color.mPrimary
} }
NText { NText {
Layout.alignment: Qt.AlignHCenter
text: { text: {
var max = LocationService.data.weather.daily.temperature_2m_max[index] var max = LocationService.data.weather.daily.temperature_2m_max[index]
var min = LocationService.data.weather.daily.temperature_2m_min[index] var min = LocationService.data.weather.daily.temperature_2m_min[index]

View file

@ -34,7 +34,7 @@ NPanel {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { NIcon {
text: Settings.data.network.wifiEnabled ? "wifi" : "wifi_off" icon: Settings.data.network.wifiEnabled ? "wifi" : "wifi-off"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
} }
@ -91,7 +91,7 @@ NPanel {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NIcon { NIcon {
text: "error" icon: "warning"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
color: Color.mError color: Color.mError
} }
@ -129,7 +129,7 @@ NPanel {
} }
NIcon { NIcon {
text: "wifi_off" icon: "wifi-off"
font.pointSize: 64 * scaling font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
@ -245,7 +245,7 @@ NPanel {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NIcon { NIcon {
text: NetworkService.signalIcon(modelData.signal) icon: NetworkService.signalIcon(modelData.signal)
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: modelData.connected ? Color.mPrimary : Color.mOnSurface color: modelData.connected ? Color.mPrimary : Color.mOnSurface
} }
@ -377,7 +377,7 @@ NPanel {
&& NetworkService.connectingTo !== modelData.ssid && NetworkService.connectingTo !== modelData.ssid
&& NetworkService.forgettingNetwork !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid
&& NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.disconnectingFrom !== modelData.ssid
icon: "delete" icon: "trash"
tooltipText: "Forget network" tooltipText: "Forget network"
sizeRatio: 0.7 sizeRatio: 0.7
onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid onClicked: expandedSsid = expandedSsid === modelData.ssid ? "" : modelData.ssid
@ -521,7 +521,7 @@ NPanel {
RowLayout { RowLayout {
NIcon { NIcon {
text: "delete_outline" icon: "trash"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
color: Color.mError color: Color.mError
} }
@ -571,7 +571,7 @@ NPanel {
} }
NIcon { NIcon {
text: "wifi_find" icon: "search"
font.pointSize: 64 * scaling font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter

View file

@ -21,6 +21,8 @@
</a> </a>
</p> </p>
---
A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell. A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.
Features a modern modular architecture with a status bar, notification system, control panel, comprehensive system integration, and more — all styled with a warm lavender palette, or your favorite color scheme! Features a modern modular architecture with a status bar, notification system, control panel, comprehensive system integration, and more — all styled with a warm lavender palette, or your favorite color scheme!
@ -66,7 +68,6 @@ Features a modern modular architecture with a status bar, notification system, c
- `quickshell-git` - Core shell framework - `quickshell-git` - Core shell framework
- `ttf-roboto` - The default font used for most of the UI - `ttf-roboto` - The default font used for most of the UI
- `inter-font` - The default font used for Headers (ex: clock on the LockScreen) - `inter-font` - The default font used for Headers (ex: clock on the LockScreen)
- `ttf-material-symbols-variable-git` - Icon font for UI elements
- `gpu-screen-recorder` - Screen recording functionality - `gpu-screen-recorder` - Screen recording functionality
- `brightnessctl` - For internal/laptop monitor brightness - `brightnessctl` - For internal/laptop monitor brightness
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors) - `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
@ -341,7 +342,7 @@ Special thanks to the creators of [**Caelestia**](https://github.com/caelestia-d
While all donations are greatly appreciated, they are completely voluntary. While all donations are greatly appreciated, they are completely voluntary.
<a href="https://ko-fi.com/soramane"> <a href="https://ko-fi.com/lysec">
<img src="https://img.shields.io/badge/donate-ko--fi-A8AEFF?style=for-the-badge&logo=kofi&logoColor=FFFFFF&labelColor=0C0D11" alt="Ko-Fi" /> <img src="https://img.shields.io/badge/donate-ko--fi-A8AEFF?style=for-the-badge&logo=kofi&logoColor=FFFFFF&labelColor=0C0D11" alt="Ko-Fi" />
</a> </a>

View file

@ -62,6 +62,8 @@ Singleton {
function onMutedChanged() { function onMutedChanged() {
root._muted = (sink?.audio.muted ?? true) root._muted = (sink?.audio.muted ?? true)
Logger.log("AudioService", "OnMuteChanged:", root._muted) Logger.log("AudioService", "OnMuteChanged:", root._muted)
// Toast: audio output mute toggle
ToastService.showNotice("Audio Output", root._muted ? "Muted" : "Unmuted")
} }
} }
@ -79,6 +81,8 @@ Singleton {
function onMutedChanged() { function onMutedChanged() {
root._inputMuted = (source?.audio.muted ?? true) root._inputMuted = (source?.audio.muted ?? true)
Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted) Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted)
// Toast: microphone mute toggle
ToastService.showNotice("Microphone", root._inputMuted ? "Muted" : "Unmuted")
} }
} }

View file

@ -11,7 +11,6 @@ Singleton {
// Widget registry object mapping widget names to components // Widget registry object mapping widget names to components
property var widgets: ({ property var widgets: ({
"ActiveWindow": activeWindowComponent, "ActiveWindow": activeWindowComponent,
"ArchUpdater": archUpdaterComponent,
"Battery": batteryComponent, "Battery": batteryComponent,
"Bluetooth": bluetoothComponent, "Bluetooth": bluetoothComponent,
"Brightness": brightnessComponent, "Brightness": brightnessComponent,
@ -60,7 +59,7 @@ Singleton {
}, },
"CustomButton": { "CustomButton": {
"allowUserSettings": true, "allowUserSettings": true,
"icon": "favorite", "icon": "heart",
"leftClickExec": "", "leftClickExec": "",
"rightClickExec": "", "rightClickExec": "",
"middleClickExec": "" "middleClickExec": ""
@ -82,9 +81,11 @@ Singleton {
"allowUserSettings": true, "allowUserSettings": true,
"showCpuUsage": true, "showCpuUsage": true,
"showCpuTemp": true, "showCpuTemp": true,
"showGpuTemp": false,
"showMemoryUsage": true, "showMemoryUsage": true,
"showMemoryAsPercent": false, "showMemoryAsPercent": false,
"showNetworkStats": false "showNetworkStats": false,
"showDiskUsage": false
}, },
"Workspace": { "Workspace": {
"allowUserSettings": true, "allowUserSettings": true,
@ -110,9 +111,6 @@ Singleton {
property Component activeWindowComponent: Component { property Component activeWindowComponent: Component {
ActiveWindow {} ActiveWindow {}
} }
property Component archUpdaterComponent: Component {
ArchUpdater {}
}
property Component batteryComponent: Component { property Component batteryComponent: Component {
Battery {} Battery {}
} }

View file

@ -2,6 +2,8 @@ pragma Singleton
import Quickshell import Quickshell
import Quickshell.Services.UPower import Quickshell.Services.UPower
import qs.Commons
import qs.Services
Singleton { Singleton {
id: root id: root
@ -9,41 +11,21 @@ Singleton {
// Choose icon based on charge and charging state // Choose icon based on charge and charging state
function getIcon(percent, charging, isReady) { function getIcon(percent, charging, isReady) {
if (!isReady) { if (!isReady) {
return "battery_error" return "battery-exclamation"
} }
if (charging) { if (charging) {
if (percent >= 95) return "battery-charging"
return "battery_full"
if (percent >= 85)
return "battery_charging_90"
if (percent >= 65)
return "battery_charging_80"
if (percent >= 55)
return "battery_charging_60"
if (percent >= 45)
return "battery_charging_50"
if (percent >= 25)
return "battery_charging_30"
if (percent >= 0)
return "battery_charging_20"
} else { } else {
if (percent >= 95) if (percent >= 90)
return "battery_full" return "battery-4"
if (percent >= 85) if (percent >= 50)
return "battery_6_bar" return "battery-3"
if (percent >= 70)
return "battery_5_bar"
if (percent >= 55)
return "battery_4_bar"
if (percent >= 40)
return "battery_3_bar"
if (percent >= 25) if (percent >= 25)
return "battery_2_bar" return "battery-2"
if (percent >= 10)
return "battery_1_bar"
if (percent >= 0) if (percent >= 0)
return "battery_0_bar" return "battery-1"
return "battery"
} }
} }
} }

View file

@ -30,6 +30,31 @@ Singleton {
}) })
} }
function init() {
Logger.log("Bluetooth", "Service initialized")
}
Timer {
id: delayDiscovery
interval: 1000
repeat: false
onTriggered: adapter.discovering = true
}
Connections {
target: adapter
function onEnabledChanged() {
Settings.data.network.bluetoothEnabled = adapter.enabled
if (adapter.enabled) {
ToastService.showNotice("Bluetooth", "Enabled")
// Using a timer to give a little time so the adapter is really enabled
delayDiscovery.running = true
} else {
ToastService.showNotice("Bluetooth", "Disabled")
}
}
}
function sortDevices(devices) { function sortDevices(devices) {
return devices.sort((a, b) => { return devices.sort((a, b) => {
var aName = a.name || a.deviceName || "" var aName = a.name || a.deviceName || ""
@ -51,36 +76,36 @@ Singleton {
function getDeviceIcon(device) { function getDeviceIcon(device) {
if (!device) { if (!device) {
return "bluetooth" return "bt-device-generic"
} }
var name = (device.name || device.deviceName || "").toLowerCase() var name = (device.name || device.deviceName || "").toLowerCase()
var icon = (device.icon || "").toLowerCase() var icon = (device.icon || "").toLowerCase()
if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod")
|| name.includes("headset") || name.includes("arctis")) { || name.includes("headset") || name.includes("arctis")) {
return "headset" return "bt-device-headphones"
} }
if (icon.includes("mouse") || name.includes("mouse")) { if (icon.includes("mouse") || name.includes("mouse")) {
return "mouse" return "bt-device-mouse"
} }
if (icon.includes("keyboard") || name.includes("keyboard")) { if (icon.includes("keyboard") || name.includes("keyboard")) {
return "keyboard" return "bt-device-keyboard"
} }
if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android")
|| name.includes("samsung")) { || name.includes("samsung")) {
return "smartphone" return "bt-device-phone"
} }
if (icon.includes("watch") || name.includes("watch")) { if (icon.includes("watch") || name.includes("watch")) {
return "watch" return "bt-device-watch"
} }
if (icon.includes("speaker") || name.includes("speaker")) { if (icon.includes("speaker") || name.includes("speaker")) {
return "speaker" return "bt-device-speaker"
} }
if (icon.includes("display") || name.includes("tv")) { if (icon.includes("display") || name.includes("tv")) {
return "tv" return "bt-device-tv"
} }
return "bluetooth" return "bt-device-generic"
} }
function canConnect(device) { function canConnect(device) {

View file

@ -37,7 +37,7 @@ Singleton {
Process { Process {
id: process id: process
stdinEnabled: true stdinEnabled: true
running: true running: MediaService.isPlaying
command: ["cava", "-p", "/dev/stdin"] command: ["cava", "-p", "/dev/stdin"]
onExited: { onExited: {
stdinEnabled = true stdinEnabled = true

View file

@ -23,6 +23,11 @@ Singleton {
// Re-apply current scheme to pick the right variant // Re-apply current scheme to pick the right variant
applyScheme(Settings.data.colorSchemes.predefinedScheme) applyScheme(Settings.data.colorSchemes.predefinedScheme)
} }
// Toast: dark/light mode switched
const enabled = !!Settings.data.colorSchemes.darkMode
const label = enabled ? "Dark Mode" : "Light Mode"
const description = enabled ? "Enabled" : "Enabled"
ToastService.showNotice(label, description)
} }
} }
@ -43,8 +48,26 @@ Singleton {
folderModel.folder = "file://" + schemesDirectory folderModel.folder = "file://" + schemesDirectory
} }
function applyScheme(filePath) { function getBasename(path) {
if (!path)
return ""
var chunks = path.split("/")
var last = chunks[chunks.length - 1]
return last.endsWith(".json") ? last.slice(0, -5) : last
}
function resolveSchemePath(nameOrPath) {
if (!nameOrPath)
return ""
if (nameOrPath.indexOf("/") !== -1) {
return nameOrPath
}
return schemesDirectory + "/" + nameOrPath.replace(".json", "") + ".json"
}
function applyScheme(nameOrPath) {
// Force reload by bouncing the path // Force reload by bouncing the path
var filePath = resolveSchemePath(nameOrPath)
schemeReader.path = "" schemeReader.path = ""
schemeReader.path = filePath schemeReader.path = filePath
} }
@ -64,6 +87,17 @@ Singleton {
schemes = files schemes = files
scanning = false scanning = false
Logger.log("ColorScheme", "Listed", schemes.length, "schemes") Logger.log("ColorScheme", "Listed", schemes.length, "schemes")
// Normalize stored scheme to basename and re-apply if necessary
var stored = Settings.data.colorSchemes.predefinedScheme
if (stored) {
var basename = getBasename(stored)
if (basename !== stored) {
Settings.data.colorSchemes.predefinedScheme = basename
}
if (!Settings.data.colorSchemes.useWallpaperColors) {
applyScheme(basename)
}
}
} }
} }
} }
@ -84,7 +118,7 @@ Singleton {
} }
} }
writeColorsToDisk(variant) writeColorsToDisk(variant)
Logger.log("ColorScheme", "Applying color scheme:", path) Logger.log("ColorScheme", "Applying color scheme:", getBasename(path))
} catch (e) { } catch (e) {
Logger.error("ColorScheme", "Failed to parse scheme JSON:", e) Logger.error("ColorScheme", "Failed to parse scheme JSON:", e)
} }

View file

@ -192,9 +192,11 @@ Singleton {
} }
windowsList.push({ windowsList.push({
"id": toplevel.address || "", "id": (toplevel.address !== undefined
"title": toplevel.title || "", && toplevel.address !== null) ? String(toplevel.address) : "",
"appId": appId, "title": (toplevel.title !== undefined && toplevel.title !== null) ? String(
toplevel.title) : "",
"appId": (appId !== undefined && appId !== null) ? String(appId) : "",
"workspaceId": toplevel.workspace?.id || null, "workspaceId": toplevel.workspace?.id || null,
"isFocused": toplevel.activated === true "isFocused": toplevel.activated === true
}) })

View file

@ -13,6 +13,7 @@ Singleton {
property ListModel displayFonts: ListModel {} property ListModel displayFonts: ListModel {}
property bool fontsLoaded: false property bool fontsLoaded: false
// -------------------------------------------
function init() { function init() {
Logger.log("Font", "Service started") Logger.log("Font", "Service started")
loadSystemFonts() loadSystemFonts()

View file

@ -25,6 +25,7 @@ Singleton {
FileView { FileView {
id: locationFileView id: locationFileView
path: locationFile path: locationFile
printErrors: false
onAdapterUpdated: saveTimer.start() onAdapterUpdated: saveTimer.start()
onLoaded: { onLoaded: {
Logger.log("Location", "Loaded cached data") Logger.log("Location", "Loaded cached data")
@ -230,22 +231,24 @@ Singleton {
// -------------------------------- // --------------------------------
function weatherSymbolFromCode(code) { function weatherSymbolFromCode(code) {
if (code === 0) if (code === 0)
return "sunny" return "weather-sun"
if (code === 1 || code === 2) if (code === 1 || code === 2)
return "partly_cloudy_day" return "weather-cloud-sun"
if (code === 3) if (code === 3)
return "cloud" return "weather-cloud"
if (code >= 45 && code <= 48) if (code >= 45 && code <= 48)
return "foggy" return "weather-cloud-haze"
if (code >= 51 && code <= 67) if (code >= 51 && code <= 67)
return "rainy" return "weather-cloud-rain"
if (code >= 71 && code <= 77) if (code >= 71 && code <= 77)
return "weather_snowy" return "weather-cloud-snow"
if (code >= 80 && code <= 82) if (code >= 71 && code <= 77)
return "rainy" return "weather-cloud-snow"
if (code >= 85 && code <= 86)
return "weather-cloud-snow"
if (code >= 95 && code <= 99) if (code >= 95 && code <= 99)
return "thunderstorm" return "weather-cloud-lightning"
return "cloud" return "weather-cloud"
} }
// -------------------------------- // --------------------------------

View file

@ -30,6 +30,7 @@ Singleton {
FileView { FileView {
id: cacheFileView id: cacheFileView
path: root.cacheFile path: root.cacheFile
printErrors: false
JsonAdapter { JsonAdapter {
id: cacheAdapter id: cacheAdapter
@ -95,6 +96,7 @@ Singleton {
function setWifiEnabled(enabled) { function setWifiEnabled(enabled) {
Settings.data.network.wifiEnabled = enabled Settings.data.network.wifiEnabled = enabled
wifiStateEnableProcess.running = true
} }
function scan() { function scan() {
@ -201,14 +203,12 @@ Singleton {
// Helper functions // Helper functions
function signalIcon(signal) { function signalIcon(signal) {
if (signal >= 80) if (signal >= 80)
return "network_wifi" return "wifi"
if (signal >= 60) if (signal >= 50)
return "network_wifi_3_bar" return "wifi-2"
if (signal >= 40)
return "network_wifi_2_bar"
if (signal >= 20) if (signal >= 20)
return "network_wifi_1_bar" return "wifi-1"
return "signal_wifi_0_bar" return "wifi-0"
} }
function isSecured(security) { function isSecured(security) {
@ -235,6 +235,8 @@ Singleton {
} }
} }
// Only check the state of the actual interface
// and update our setting to be in sync.
Process { Process {
id: wifiStateProcess id: wifiStateProcess
running: false running: false
@ -243,7 +245,7 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const enabled = text.trim() === "enabled" const enabled = text.trim() === "enabled"
Logger.log("Network", "Wi-Fi enabled:", enabled) Logger.log("Network", "Wi-Fi adapter was detect as enabled:", enabled)
if (Settings.data.network.wifiEnabled !== enabled) { if (Settings.data.network.wifiEnabled !== enabled) {
Settings.data.network.wifiEnabled = enabled Settings.data.network.wifiEnabled = enabled
} }
@ -251,6 +253,29 @@ Singleton {
} }
} }
// Process to enable/disable the Wi-Fi interface
Process {
id: wifiStateEnableProcess
running: false
command: ["nmcli", "radio", "wifi", Settings.data.network.wifiEnabled ? "on" : "off"]
stdout: StdioCollector {
onStreamFinished: {
Logger.log("Network", "Wi-Fi state change command executed.")
// Re-check the state to ensure it's in sync
syncWifiState()
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim()) {
Logger.warn("Network", "Error changing Wi-Fi state: " + text)
}
}
}
}
// Helper process to get existing profiles // Helper process to get existing profiles
Process { Process {
id: profileCheckProcess id: profileCheckProcess

View file

@ -15,7 +15,7 @@ Singleton {
function apply() { function apply() {
// If using LocationService, wait for it to be ready // If using LocationService, wait for it to be ready
if (params.autoSchedule && !LocationService.coordinatesReady) { if (!params.forced && params.autoSchedule && !LocationService.coordinatesReady) {
return return
} }
@ -34,14 +34,25 @@ Singleton {
function buildCommand() { function buildCommand() {
var cmd = ["wlsunset"] var cmd = ["wlsunset"]
cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`) if (params.forced) {
if (params.autoSchedule) { // Force immediate full night temperature regardless of time
cmd.push("-l", `${LocationService.stableLatitude}`, "-L", `${LocationService.stableLongitude}`) // Keep distinct day/night temps but set times so we're effectively always in "night"
cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`)
// Night spans from sunset (00:00) to sunrise (23:59) covering almost the full day
cmd.push("-S", "23:59") // sunrise very late
cmd.push("-s", "00:00") // sunset at midnight
// Near-instant transition
cmd.push("-d", 1)
} else { } else {
cmd.push("-S", params.manualSunrise) cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`)
cmd.push("-s", params.manualSunset) if (params.autoSchedule) {
cmd.push("-l", `${LocationService.stableLatitude}`, "-L", `${LocationService.stableLongitude}`)
} else {
cmd.push("-S", params.manualSunrise)
cmd.push("-s", params.manualSunset)
}
cmd.push("-d", 60 * 15) // 15min progressive fade at sunset/sunrise
} }
cmd.push("-d", 60 * 15) // 15min progressive fade at sunset/sunrise
return cmd return cmd
} }
@ -50,6 +61,15 @@ Singleton {
target: Settings.data.nightLight target: Settings.data.nightLight
function onEnabledChanged() { function onEnabledChanged() {
apply() apply()
// Toast: night light toggled
const enabled = !!Settings.data.nightLight.enabled
ToastService.showNotice("Night Light", enabled ? "Enabled" : "Disabled")
}
function onForcedChanged() {
apply()
if (Settings.data.nightLight.enabled) {
ToastService.showNotice("Night Light", Settings.data.nightLight.forced ? "Forced activation" : "Normal mode")
}
} }
function onNightTempChanged() { function onNightTempChanged() {
apply() apply()

View file

@ -65,6 +65,7 @@ Singleton {
id: historyFileView id: historyFileView
objectName: "notificationHistoryFileView" objectName: "notificationHistoryFileView"
path: historyFile path: historyFile
printErrors: false
watchChanges: true watchChanges: true
onFileChanged: reload() onFileChanged: reload()
onAdapterUpdated: writeAdapter() onAdapterUpdated: writeAdapter()
@ -116,14 +117,51 @@ Singleton {
} }
} }
// Function to resolve app name from notification
function resolveAppName(notification) {
try {
const appName = notification.appName || ""
// If it's already a clean name (no dots or reverse domain notation), use it
if (!appName.includes(".") || appName.length < 10) {
return appName
}
// Try to find a desktop entry for this app ID
const desktopEntries = DesktopEntries.byId(appName)
if (desktopEntries && desktopEntries.length > 0) {
const entry = desktopEntries[0]
// Prefer name over genericName, fallback to original appName
return entry.name || entry.genericName || appName
}
// If no desktop entry found, try to clean up the app ID
// Convert "org.gnome.Nautilus" to "Nautilus"
const parts = appName.split(".")
if (parts.length > 1) {
// Take the last part and capitalize it
const lastPart = parts[parts.length - 1]
return lastPart.charAt(0).toUpperCase() + lastPart.slice(1)
}
return appName
} catch (e) {
// Fallback to original app name on any error
return notification.appName || ""
}
}
// Function to add notification to model // Function to add notification to model
function addNotification(notification) { function addNotification(notification) {
const resolvedImage = resolveNotificationImage(notification) const resolvedImage = resolveNotificationImage(notification)
const resolvedAppName = resolveAppName(notification)
notificationModel.insert(0, { notificationModel.insert(0, {
"rawNotification": notification, "rawNotification": notification,
"summary": notification.summary, "summary": notification.summary,
"body": notification.body, "body": notification.body,
"appName": notification.appName, "appName": resolvedAppName,
"desktopEntry": notification.desktopEntry,
"image": resolvedImage, "image": resolvedImage,
"appIcon": notification.appIcon, "appIcon": notification.appIcon,
"urgency": notification.urgency, "urgency": notification.urgency,
@ -164,7 +202,7 @@ Singleton {
// Resolve themed icon names to absolute paths // Resolve themed icon names to absolute paths
try { try {
const p = Icons.iconFromName(icon, "") const p = AppIcons.iconFromName(icon, "")
return p || "" return p || ""
} catch (e2) { } catch (e2) {
return "" return ""
@ -174,12 +212,17 @@ Singleton {
} }
} }
// Add a simplified copy into persistent history
function addToHistory(notification) { function addToHistory(notification) {
const resolvedAppName = resolveAppName(notification)
const resolvedImage = resolveNotificationImage(notification)
historyModel.insert(0, { historyModel.insert(0, {
"summary": notification.summary, "summary": notification.summary,
"body": notification.body, "body": notification.body,
"appName": notification.appName, "appName": resolvedAppName,
"desktopEntry": notification.desktopEntry || "",
"image": resolvedImage,
"appIcon": notification.appIcon || "",
"urgency": notification.urgency, "urgency": notification.urgency,
"timestamp": new Date() "timestamp": new Date()
}) })
@ -210,6 +253,9 @@ Singleton {
"summary": it.summary || "", "summary": it.summary || "",
"body": it.body || "", "body": it.body || "",
"appName": it.appName || "", "appName": it.appName || "",
"desktopEntry": it.desktopEntry || "",
"image": it.image || "",
"appIcon": it.appIcon || "",
"urgency": it.urgency, "urgency": it.urgency,
"timestamp": ts ? new Date(ts) : new Date() "timestamp": ts ? new Date(ts) : new Date()
}) })
@ -229,6 +275,9 @@ Singleton {
"summary": n.summary, "summary": n.summary,
"body": n.body, "body": n.body,
"appName": n.appName, "appName": n.appName,
"desktopEntry": n.desktopEntry,
"image": n.image,
"appIcon": n.appIcon,
"urgency": n.urgency, "urgency": n.urgency,
"timestamp"// Always persist in milliseconds "timestamp"// Always persist in milliseconds
: (n.timestamp instanceof Date) ? n.timestamp.getTime( : (n.timestamp instanceof Date) ? n.timestamp.getTime(

View file

@ -0,0 +1,62 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Services
Singleton {
id: root
readonly property var powerProfiles: PowerProfiles
readonly property bool available: powerProfiles && powerProfiles.hasPerformanceProfile
property int profile: powerProfiles ? powerProfiles.profile : PowerProfile.Balanced
function profileName(p) {
const prof = (p !== undefined) ? p : profile
if (!available)
return "Unknown"
if (prof === PowerProfile.Performance)
return "Performance"
if (prof === PowerProfile.Balanced)
return "Balanced"
if (prof === PowerProfile.PowerSaver)
return "Power Saver"
return "Unknown"
}
function setProfile(p) {
if (!available)
return
try {
powerProfiles.profile = p
} catch (e) {
Logger.error("PowerProfileService", "Failed to set profile:", e)
}
}
function cycleProfile() {
if (!available)
return
const current = powerProfiles.profile
if (current === PowerProfile.Performance)
setProfile(PowerProfile.PowerSaver)
else if (current === PowerProfile.Balanced)
setProfile(PowerProfile.Performance)
else if (current === PowerProfile.PowerSaver)
setProfile(PowerProfile.Balanced)
}
Connections {
target: powerProfiles
function onProfileChanged() {
root.profile = powerProfiles.profile
// Only show toast if we have a valid profile name (not "Unknown")
const profileName = root.profileName()
if (profileName !== "Unknown") {
ToastService.showNotice("Power Profile", profileName)
}
}
}
}

View file

@ -13,6 +13,17 @@ Singleton {
property bool isRecording: false property bool isRecording: false
property bool isPending: false property bool isPending: false
property string outputPath: "" property string outputPath: ""
property bool isAvailable: false
Component.onCompleted: {
checkAvailability()
}
function checkAvailability() {
// Detect native or Flatpak gpu-screen-recorder
availabilityCheckProcess.command = ["sh", "-c", "command -v gpu-screen-recorder >/dev/null 2>&1 || (command -v flatpak >/dev/null 2>&1 && flatpak list --app | grep -q 'com.dec05eba.gpu_screen_recorder')"]
availabilityCheckProcess.running = true
}
// Start or Stop recording // Start or Stop recording
function toggleRecording() { function toggleRecording() {
@ -21,6 +32,9 @@ Singleton {
// Start screen recording using Quickshell.execDetached // Start screen recording using Quickshell.execDetached
function startRecording() { function startRecording() {
if (!isAvailable) {
return
}
if (isRecording || isPending) { if (isRecording || isPending) {
return return
} }
@ -88,6 +102,18 @@ Singleton {
} }
} }
// Availability check process
Process {
id: availabilityCheckProcess
command: ["sh", "-c", "true"]
onExited: function (exitCode, exitStatus) {
// exitCode 0 means available, non-zero means unavailable
root.isAvailable = (exitCode === 0)
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
Timer { Timer {
id: pendingTimer id: pendingTimer
interval: 2000 // Wait 2 seconds to see if process stays alive interval: 2000 // Wait 2 seconds to see if process stays alive

View file

@ -12,6 +12,7 @@ Singleton {
// Public values // Public values
property real cpuUsage: 0 property real cpuUsage: 0
property real cpuTemp: 0 property real cpuTemp: 0
property real gpuTemp: 0
property real memGb: 0 property real memGb: 0
property real memPercent: 0 property real memPercent: 0
property real diskPercent: 0 property real diskPercent: 0
@ -35,6 +36,12 @@ Singleton {
readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"] readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
property string cpuTempSensorName: "" property string cpuTempSensorName: ""
property string cpuTempHwmonPath: "" property string cpuTempHwmonPath: ""
// Gpu temperature (simple hwmon read if available)
readonly property var supportedTempGpuSensorNames: ["amdgpu", "nvidia", "radeon"]
property string gpuTempSensorName: ""
property string gpuTempHwmonPath: ""
property bool gpuIsDedicated: false
property string _gpuPendingAmdPath: ""
// For Intel coretemp averaging of all cores/sensors // For Intel coretemp averaging of all cores/sensors
property var intelTempValues: [] property var intelTempValues: []
property int intelTempFilesChecked: 0 property int intelTempFilesChecked: 0
@ -66,6 +73,7 @@ Singleton {
dfProcess.running = true dfProcess.running = true
updateCpuTemperature() updateCpuTemperature()
updateGpuTemperature()
} }
} }
@ -107,6 +115,7 @@ Singleton {
} }
} }
// --------------------------------------------
// -------------------------------------------- // --------------------------------------------
// CPU Temperature // CPU Temperature
// It's more complex. // It's more complex.
@ -115,9 +124,10 @@ Singleton {
FileView { FileView {
id: cpuTempNameReader id: cpuTempNameReader
property int currentIndex: 0 property int currentIndex: 0
printErrors: false
function checkNext() { function checkNext() {
if (currentIndex >= 10) { if (currentIndex >= 16) {
// Check up to hwmon10 // Check up to hwmon10
Logger.warn("No supported temperature sensor found") Logger.warn("No supported temperature sensor found")
return return
@ -182,6 +192,109 @@ Singleton {
} }
} }
// --------------------------------------------
// --------------------------------------------
// ---- GPU temperature detection (hwmon)
FileView {
id: gpuTempNameReader
property int currentIndex: 0
printErrors: false
function checkNext() {
if (currentIndex >= 16) {
// Check up to hwmon10
Logger.warn("SystemStat", "No supported GPU temperature sensor found")
return
}
gpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`
gpuTempNameReader.reload()
}
Component.onCompleted: checkNext()
onLoaded: {
const name = text().trim()
if (root.supportedTempGpuSensorNames.includes(name)) {
const hwPath = `/sys/class/hwmon/hwmon${currentIndex}`
if (name === "nvidia") {
// Treat NVIDIA as dedicated by default
root.gpuTempSensorName = name
root.gpuTempHwmonPath = hwPath
root.gpuIsDedicated = true
Logger.log("SystemStat", `Selected NVIDIA GPU thermal sensor at ${root.gpuTempHwmonPath}`)
} else if (name === "amdgpu") {
// Probe VRAM to distinguish dGPU vs iGPU
root._gpuPendingAmdPath = hwPath
vramReader.requestCheck(hwPath)
} else if (!root.gpuTempHwmonPath) {
// Fallback to first supported sensor (e.g., radeon)
root.gpuTempSensorName = name
root.gpuTempHwmonPath = hwPath
Logger.log("SystemStat", `Selected GPU thermal sensor at ${root.gpuTempHwmonPath}`)
}
} else {
currentIndex++
Qt.callLater(() => {
checkNext()
})
}
}
onLoadFailed: function (error) {
currentIndex++
Qt.callLater(() => {
checkNext()
})
}
}
// Reader to detect AMD dGPU by checking VRAM presence
FileView {
id: vramReader
property string targetHwmonPath: ""
function requestCheck(hwPath) {
targetHwmonPath = hwPath
vramReader.path = `${hwPath}/device/mem_info_vram_total`
vramReader.reload()
}
printErrors: false
onLoaded: {
const val = parseInt(text().trim())
// If VRAM present (>0), prefer this as dGPU
if (!isNaN(val) && val > 0) {
root.gpuTempSensorName = "amdgpu"
root.gpuTempHwmonPath = targetHwmonPath
root.gpuIsDedicated = true
Logger.log("SystemStat",
`Selected AMD dGPU (VRAM=${Math.round(val / (1024 * 1024 * 1024))}GB) at ${root.gpuTempHwmonPath}`)
} else if (!root.gpuTempHwmonPath) {
// Use as fallback iGPU if nothing selected yet
root.gpuTempSensorName = "amdgpu"
root.gpuTempHwmonPath = targetHwmonPath
root.gpuIsDedicated = false
Logger.log("SystemStat", `Selected AMD GPU (no VRAM) at ${root.gpuTempHwmonPath}`)
}
// Continue scanning other hwmon entries
gpuTempNameReader.currentIndex++
Qt.callLater(() => {
gpuTempNameReader.checkNext()
})
}
onLoadFailed: function (error) {
// If failed to read VRAM, consider as fallback if none selected
if (!root.gpuTempHwmonPath) {
root.gpuTempSensorName = "amdgpu"
root.gpuTempHwmonPath = targetHwmonPath
}
gpuTempNameReader.currentIndex++
Qt.callLater(() => {
gpuTempNameReader.checkNext()
})
}
}
// -------------------------------------------------------
// ------------------------------------------------------- // -------------------------------------------------------
// Parse memory info from /proc/meminfo // Parse memory info from /proc/meminfo
function parseMemoryInfo(text) { function parseMemoryInfo(text) {
@ -321,10 +434,8 @@ Singleton {
// ------------------------------------------------------- // -------------------------------------------------------
// Helper function to format network speeds // Helper function to format network speeds
function formatSpeed(bytesPerSecond) { function formatSpeed(bytesPerSecond) {
if (bytesPerSecond < 1024) { if (bytesPerSecond < 1024 * 1024) {
return bytesPerSecond.toFixed(0) + "B/s" return (bytesPerSecond / 1024).toFixed(1) + "KB/s"
} else if (bytesPerSecond < 1024 * 1024) {
return (bytesPerSecond / 1024).toFixed(0) + "KB/s"
} else if (bytesPerSecond < 1024 * 1024 * 1024) { } else if (bytesPerSecond < 1024 * 1024 * 1024) {
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s" return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s"
} else { } else {
@ -348,6 +459,26 @@ Singleton {
} }
} }
// -------------------------------------------------------
// Function to start/refresh the GPU temperature
function updateGpuTemperature() {
if (!root.gpuTempHwmonPath)
return
gpuTempReader.path = `${root.gpuTempHwmonPath}/temp1_input`
gpuTempReader.reload()
}
FileView {
id: gpuTempReader
printErrors: false
onLoaded: {
const data = parseInt(text().trim())
if (!isNaN(data)) {
root.gpuTemp = Math.round(data / 1000.0)
}
}
}
// ------------------------------------------------------- // -------------------------------------------------------
// Function to check next Intel temperature sensor // Function to check next Intel temperature sensor
function checkNextIntelTemp() { function checkNextIntelTemp() {

View file

@ -185,6 +185,11 @@ Singleton {
// Process the message queue // Process the message queue
function processQueue() { function processQueue() {
if (messageQueue.length === 0 || allToasts.length === 0) { if (messageQueue.length === 0 || allToasts.length === 0) {
// Added this so we don't accidentally get duplicate toasts
// if it causes issues, remove it and we'll find a different solution
if (allToasts.length === 0 && messageQueue.length > 0) {
messageQueue = []
}
isShowingToast = false isShowingToast = false
return return
} }

View file

@ -8,8 +8,8 @@ Singleton {
id: root id: root
// Public properties // Public properties
property string baseVersion: "2.7.0" property string baseVersion: "2.8.0"
property bool isDevelopment: true property bool isDevelopment: false
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}` property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`

View file

@ -216,11 +216,7 @@ Singleton {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Get specific monitor wallpaper - now from cache // Get specific monitor wallpaper - now from cache
function getWallpaper(screenName) { function getWallpaper(screenName) {
var path = currentWallpapers[screenName] || "" return currentWallpapers[screenName] || Settings.defaultWallpaper
if (path === "") {
return Settings.data.wallpaper.defaultWallpaper || ""
}
return path
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View file

@ -82,9 +82,9 @@ Rectangle {
// Icon (optional) // Icon (optional)
NIcon { NIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
layoutTopMargin: 1 * scaling
visible: root.icon !== "" visible: root.icon !== ""
text: root.icon
icon: root.icon
font.pointSize: root.iconSize font.pointSize: root.iconSize
color: { color: {
if (!root.enabled) if (!root.enabled)

View file

@ -57,7 +57,7 @@ RowLayout {
NIcon { NIcon {
visible: root.checked visible: root.checked
anchors.centerIn: parent anchors.centerIn: parent
text: "check" icon: "check"
color: root.activeOnColor color: root.activeOnColor
font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling
} }

View file

@ -88,20 +88,21 @@ Rectangle {
// Tiny circular badge for the icon, positioned using anchors within the gauge // Tiny circular badge for the icon, positioned using anchors within the gauge
Rectangle { Rectangle {
id: iconBadge id: iconBadge
width: 28 * scaling * contentScale width: iconText.implicitWidth + Style.marginXS * scaling
height: width height: width
radius: width / 2 radius: width / 2
color: Color.mSurface color: Color.mPrimary
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.rightMargin: -6 * scaling * contentScale anchors.rightMargin: -2 * scaling
anchors.topMargin: Style.marginXXS * scaling * contentScale anchors.topMargin: -2 * scaling
NIcon { NIcon {
id: iconText
anchors.centerIn: parent anchors.centerIn: parent
text: root.icon icon: root.icon
font.pointSize: Style.fontSizeLargeXL * scaling * contentScale color: Color.mOnPrimary
color: Color.mOnSurface font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import qs.Commons import qs.Commons
import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
@ -58,7 +59,7 @@ Rectangle {
} }
NIcon { NIcon {
text: "palette" icon: "color-picker"
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
} }
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import qs.Commons import qs.Commons
import qs.Services
import qs.Widgets import qs.Widgets
Popup { Popup {
@ -129,7 +130,7 @@ Popup {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NIcon { NIcon {
text: "palette" icon: "color-picker"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary color: Color.mPrimary
} }
@ -491,7 +492,6 @@ Popup {
NButton { NButton {
id: cancelButton id: cancelButton
text: "Cancel" text: "Cancel"
icon: "close"
outlined: cancelButton.hovered ? false : true outlined: cancelButton.hovered ? false : true
customHeight: 36 * scaling customHeight: 36 * scaling
customWidth: 100 * scaling customWidth: 100 * scaling

View file

@ -85,8 +85,8 @@ RowLayout {
indicator: NIcon { indicator: NIcon {
x: combo.width - width - Style.marginM * scaling x: combo.width - width - Style.marginM * scaling
y: combo.topPadding + (combo.availableHeight - height) / 2 y: combo.topPadding + (combo.availableHeight - height) / 2
text: "arrow_drop_down" icon: "caret-down"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeL * scaling
} }
popup: Popup { popup: Popup {

View file

@ -1,19 +1,27 @@
import QtQuick import QtQuick
import QtQuick.Layouts
import qs.Commons import qs.Commons
import qs.Widgets import qs.Widgets
import QtQuick.Layouts
Text { Text {
// Optional layout nudge for optical alignment when used inside Layouts id: root
property real layoutTopMargin: 0
text: "question_mark" property string icon: Icons.defaultIcon
font.family: "Material Symbols Rounded"
font.pointSize: Style.fontSizeL * scaling visible: (icon !== undefined) && (icon !== "")
font.variableAxes: { text: {
"wght"// slightly bold to ensure all lines looks good if ((icon === undefined) || (icon === "")) {
: (Font.Normal + Font.Bold) / 2.5 return ""
}
if (Icons.get(icon) === undefined) {
Logger.warn("Icon", `"${icon}"`, "doesn't exist in the icons font")
Logger.callStack()
return Icons.get(Icons.defaultIcon)
}
return Icons.get(icon)
} }
font.family: Icons.fontFamily
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
Layout.topMargin: layoutTopMargin
} }

View file

@ -14,6 +14,7 @@ Rectangle {
property string icon property string icon
property string tooltipText property string tooltipText
property bool enabled: true property bool enabled: true
property bool allowClickWhenDisabled: false
property bool hovering: false property bool hovering: false
property color colorBg: Color.mSurfaceVariant property color colorBg: Color.mSurfaceVariant
@ -35,17 +36,31 @@ Rectangle {
opacity: root.enabled ? Style.opacityFull : Style.opacityMedium opacity: root.enabled ? Style.opacityFull : Style.opacityMedium
color: root.enabled && root.hovering ? colorBgHover : colorBg color: root.enabled && root.hovering ? colorBgHover : colorBg
radius: width * 0.5 radius: width * 0.5
border.color: root.hovering ? colorBorderHover : colorBorder border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
NIcon { NIcon {
text: root.icon icon: root.icon
font.pointSize: Style.fontSizeM * scaling font.pointSize: Math.max(1, root.width * 0.47)
color: root.hovering ? colorFgHover : colorFg color: root.enabled && root.hovering ? colorFgHover : colorFg
// Center horizontally // Center horizontally
x: (root.width - width) / 2 x: (root.width - width) / 2
// Center vertically accounting for font metrics // Center vertically accounting for font metrics
y: (root.height - height) / 2 + (height - contentHeight) / 2 y: (root.height - height) / 2 + (height - contentHeight) / 2
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutQuad
}
}
} }
NTooltip { NTooltip {
@ -56,13 +71,14 @@ Rectangle {
} }
MouseArea { MouseArea {
enabled: root.enabled // Always enabled to allow hover/tooltip even when the button is disabled
enabled: true
anchors.fill: parent anchors.fill: parent
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true hoverEnabled: true
onEntered: { onEntered: {
hovering = true hovering = root.enabled ? true : false
if (tooltipText) { if (tooltipText) {
tooltip.show() tooltip.show()
} }
@ -79,6 +95,9 @@ Rectangle {
if (tooltipText) { if (tooltipText) {
tooltip.hide() tooltip.hide()
} }
if (!root.enabled && !allowClickWhenDisabled) {
return
}
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
root.clicked() root.clicked()
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {

View file

@ -9,9 +9,10 @@ Rectangle {
id: root id: root
property string imagePath: "" property string imagePath: ""
property string fallbackIcon: ""
property color borderColor: Color.transparent property color borderColor: Color.transparent
property real borderWidth: 0 property real borderWidth: 0
property string fallbackIcon: ""
property real fallbackIconSize: Style.fontSizeXXL * scaling
color: Color.transparent color: Color.transparent
radius: parent.width * 0.5 radius: parent.width * 0.5
@ -45,18 +46,20 @@ Rectangle {
} }
property real imageOpacity: root.opacity property real imageOpacity: root.opacity
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/circled_image.frag.qsb") fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/circled_image.frag.qsb")
supportsAtlasTextures: false supportsAtlasTextures: false
blending: true blending: true
} }
// Fallback icon // Fallback icon
NIcon { Loader {
anchors.centerIn: parent active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
text: fallbackIcon sourceComponent: NIcon {
font.pointSize: Style.fontSizeXXL * scaling anchors.centerIn: parent
visible: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "") icon: fallbackIcon
z: 0 font.pointSize: fallbackIconSize
z: 0
}
} }
} }

View file

@ -9,10 +9,11 @@ Rectangle {
id: root id: root
property string imagePath: "" property string imagePath: ""
property string fallbackIcon: ""
property color borderColor: Color.transparent property color borderColor: Color.transparent
property real borderWidth: 0 property real borderWidth: 0
property real imageRadius: width * 0.5 property real imageRadius: width * 0.5
property string fallbackIcon: ""
property real fallbackIconSize: Style.fontSizeXXL * scaling
property real scaledRadius: imageRadius * Settings.data.general.radiusRatio property real scaledRadius: imageRadius * Settings.data.general.radiusRatio
@ -56,7 +57,7 @@ Rectangle {
property real itemHeight: root.height property real itemHeight: root.height
property real cornerRadius: root.radius property real cornerRadius: root.radius
property real imageOpacity: root.opacity property real imageOpacity: root.opacity
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/rounded_image.frag.qsb") fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb")
// Qt6 specific properties - ensure proper blending // Qt6 specific properties - ensure proper blending
supportsAtlasTextures: false supportsAtlasTextures: false
@ -71,12 +72,14 @@ Rectangle {
} }
// Fallback icon // Fallback icon
NIcon { Loader {
anchors.centerIn: parent active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
text: fallbackIcon sourceComponent: NIcon {
font.pointSize: Style.fontSizeXXL * scaling anchors.centerIn: parent
visible: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "") icon: fallbackIcon
z: 0 font.pointSize: fallbackIconSize
z: 0
}
} }
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Commons import qs.Commons
import qs.Widgets import qs.Widgets
import qs.Services
// Input and button row // Input and button row
RowLayout { RowLayout {
@ -13,7 +14,7 @@ RowLayout {
property string placeholderText: "" property string placeholderText: ""
property string text: "" property string text: ""
property string actionButtonText: "Test" property string actionButtonText: "Test"
property string actionButtonIcon: "play_arrow" property string actionButtonIcon: "media-play"
property bool actionButtonEnabled: text !== "" property bool actionButtonEnabled: text !== ""
// Signals // Signals

View file

@ -40,8 +40,8 @@ Loader {
property int buttonWidth: 0 property int buttonWidth: 0
property int buttonHeight: 0 property int buttonHeight: 0
// Whether this panel should accept keyboard focus
property bool panelKeyboardFocus: false property bool panelKeyboardFocus: false
property bool backgroundClickEnabled: true
// Animation properties // Animation properties
readonly property real originalScale: 0.7 readonly property real originalScale: 0.7
@ -62,6 +62,24 @@ Loader {
PanelService.registerPanel(root) PanelService.registerPanel(root)
} }
// -----------------------------------------
// Functions to control background click behavior
function disableBackgroundClick() {
backgroundClickEnabled = false
}
function enableBackgroundClick() {
// Add a small delay to prevent immediate close after drag release
enableBackgroundClickTimer.restart()
}
Timer {
id: enableBackgroundClickTimer
interval: 100
repeat: false
onTriggered: backgroundClickEnabled = true
}
// ----------------------------------------- // -----------------------------------------
function toggle(aScreen, buttonItem) { function toggle(aScreen, buttonItem) {
// Don't toggle if screen is null or invalid // Don't toggle if screen is null or invalid
@ -110,6 +128,7 @@ Loader {
PanelService.willOpenPanel(root) PanelService.willOpenPanel(root)
backgroundClickEnabled = true
active = true active = true
root.opened() root.opened()
} }
@ -125,7 +144,8 @@ Loader {
function closeCompleted() { function closeCompleted() {
root.closed() root.closed()
active = false active = false
useButtonPosition = false // Reset button position usage useButtonPosition = false
backgroundClickEnabled = true
PanelService.closedPanel(root) PanelService.closedPanel(root)
} }
@ -179,6 +199,7 @@ Loader {
// Clicking outside of the rectangle to close // Clicking outside of the rectangle to close
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.backgroundClickEnabled
onClicked: root.close() onClicked: root.close()
} }
@ -208,7 +229,7 @@ Loader {
return Math.round(Math.max(minX, Math.min(targetX, maxX))) return Math.round(Math.max(minX, Math.min(targetX, maxX)))
} else if (!panelAnchorHorizontalCenter && panelAnchorLeft) { } else if (!panelAnchorHorizontalCenter && panelAnchorLeft) {
return Math.round(marginS * scaling) return Math.round(Style.marginS * scaling)
} else if (!panelAnchorHorizontalCenter && panelAnchorRight) { } else if (!panelAnchorHorizontalCenter && panelAnchorRight) {
return Math.round(panelWindow.width - panelWidth - (Style.marginS * scaling)) return Math.round(panelWindow.width - panelWidth - (Style.marginS * scaling))
} else { } else {

View file

@ -9,21 +9,15 @@ Item {
property string icon: "" property string icon: ""
property string text: "" property string text: ""
property string tooltipText: "" property string tooltipText: ""
property color pillColor: Color.mSurfaceVariant
property color textColor: Color.mOnSurface
property color iconCircleColor: Color.mPrimary
property color iconTextColor: Color.mSurface
property color collapsedIconColor: Color.mOnSurface
property real iconRotation: 0
property real sizeRatio: 0.8 property real sizeRatio: 0.8
property bool autoHide: false property bool autoHide: false
property bool forceOpen: false property bool forceOpen: false
property bool disableOpen: false property bool disableOpen: false
property bool rightOpen: false property bool rightOpen: false
property bool hovered: false
// Effective shown state (true if hovered/animated open or forced) // Effective shown state (true if hovered/animated open or forced)
readonly property bool effectiveShown: forceOpen || showPill readonly property bool revealed: forceOpen || showPill
signal shown signal shown
signal hidden signal hidden
@ -50,14 +44,14 @@ Item {
Rectangle { Rectangle {
id: pill id: pill
width: effectiveShown ? maxPillWidth : 1 width: revealed ? maxPillWidth : 1
height: pillHeight height: pillHeight
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
(iconCircle.x + iconCircle.width / 2) - width // Opens left (iconCircle.x + iconCircle.width / 2) - width // Opens left
opacity: effectiveShown ? Style.opacityFull : Style.opacityNone opacity: revealed ? Style.opacityFull : Style.opacityNone
color: pillColor color: Color.mSurfaceVariant
topLeftRadius: rightOpen ? 0 : pillHeight * 0.5 topLeftRadius: rightOpen ? 0 : pillHeight * 0.5
bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5 bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5
@ -77,8 +71,8 @@ Item {
text: root.text text: root.text
font.pointSize: Style.fontSizeXS * scaling font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: textColor color: Color.mPrimary
visible: effectiveShown visible: revealed
} }
Behavior on width { Behavior on width {
@ -102,11 +96,8 @@ Item {
width: iconSize width: iconSize
height: iconSize height: iconSize
radius: width * 0.5 radius: width * 0.5
// When forced shown, match pill background; otherwise use accent when hovered color: hovered && !forceOpen ? Color.mPrimary : Color.mSurfaceVariant
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
border.width: Math.max(1, Style.borderS * scaling)
border.color: forceOpen ? Qt.alpha(Color.mOutline, 0.5) : Color.transparent
x: rightOpen ? 0 : (parent.width - width) x: rightOpen ? 0 : (parent.width - width)
@ -118,11 +109,9 @@ Item {
} }
NIcon { NIcon {
text: root.icon icon: root.icon
rotation: root.iconRotation
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
// When forced shown, use pill text color; otherwise accent color when hovered color: hovered && !forceOpen ? Color.mOnPrimary : Color.mOnSurface
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
// Center horizontally // Center horizontally
x: (iconCircle.width - width) / 2 x: (iconCircle.width - width) / 2
// Center vertically accounting for font metrics // Center vertically accounting for font metrics
@ -220,6 +209,7 @@ Item {
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: { onEntered: {
hovered = true
root.entered() root.entered()
tooltip.show() tooltip.show()
if (disableOpen) { if (disableOpen) {
@ -230,6 +220,7 @@ Item {
} }
} }
onExited: { onExited: {
hovered = false
root.exited() root.exited()
if (!forceOpen) { if (!forceOpen) {
hide() hide()

View file

@ -95,7 +95,7 @@ RowLayout {
NIcon { NIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: "remove" icon: "chevron-left"
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
color: decreaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary color: decreaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
} }
@ -130,7 +130,7 @@ RowLayout {
NIcon { NIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: "add" icon: "chevron-right"
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
color: increaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary color: increaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
} }

View file

@ -112,23 +112,13 @@ Item {
RowLayout { RowLayout {
id: contentLayout id: contentLayout
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginM * scaling anchors.margins: Style.marginL * scaling
spacing: Style.marginS * scaling spacing: Style.marginL * scaling
// Icon // Icon
NIcon { NIcon {
id: icon id: icon
text: { icon: (root.type == "warning") ? "toast-warning" : "toast-notice"
switch (root.type) {
case "warning":
return "warning"
case "notice":
return "info"
default:
return "info"
}
}
color: { color: {
switch (root.type) { switch (root.type) {
case "warning": case "warning":

Some files were not shown because too many files have changed in this diff Show more