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": {
"mPrimary": "#ebbcba",
"mOnPrimary": "#1f1d2e",
"mOnPrimary": "#191724",
"mSecondary": "#9ccfd8",
"mOnSecondary": "#1f1d2e",
"mOnSecondary": "#191724",
"mTertiary": "#f6c177",
"mOnTertiary": "#1f1d2e",
"mOnTertiary": "#191724",
"mError": "#eb6f92",
"mOnError": "#1f1d2e",
"mSurface": "#1f1d2e",
"mOnError": "#191724",
"mSurface": "#191724",
"mOnSurface": "#e0def4",
"mSurfaceVariant": "#26233a",
"mOnSurfaceVariant": "#908caa",
"mOutline": "#403d52",
"mShadow": "#1f1d2e"
"mShadow": "#191724"
},
"light": {
"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 {
id: customColorsFile
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : ""
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : undefined
printErrors: false
watchChanges: true
onFileChanged: {
Logger.log("Color", "Reloading colors from disk")
@ -115,7 +116,7 @@ Singleton {
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path === Settings.configDir + "colors.json") {
if (path !== undefined) {
reload()
}
}

View file

@ -1,54 +1,49 @@
pragma Singleton
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Services
import qs.Commons
import qs.Commons.IconsSets
Singleton {
id: icons
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
// Expose the font family name for easy access
readonly property string fontFamily: fontLoader.name
readonly property string defaultIcon: TablerIcons.defaultIcon
readonly property var icons: TablerIcons.icons
readonly property var aliases: TablerIcons.aliases
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.woff2"
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 defaultAvatar: Quickshell.env("HOME") + "/.face"
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
property string defaultLocation: "Tokyo"
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
// 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++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
@ -105,60 +120,63 @@ Singleton {
continue
}
// Check that the widget was not previously migrated and skip if necessary
const keys = Object.keys(widget)
if (keys.length > 1) {
continue
if (upgradeWidget(widget)) {
Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
}
migrateWidget(widget)
Logger.log("Settings", JSON.stringify(widget))
}
}
}
// -----------------------------------------------------
function migrateWidget(widget) {
Logger.log("Settings", `Migrating '${widget.id}' widget`)
function upgradeWidget(widget) {
// Backup the widget definition before altering
const widgetBefore = JSON.stringify(widget)
// Migrate old bar settings to proper per widget settings
switch (widget.id) {
case "ActiveWindow":
widget.showIcon = adapter.bar.showActiveWindowIcon
widget.showIcon = widget.showIcon !== undefined ? widget.showIcon : adapter.bar.showActiveWindowIcon
break
case "Battery":
widget.alwaysShowPercentage = adapter.bar.alwaysShowBatteryPercentage
break
case "Brightness":
widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage
widget.alwaysShowPercentage = widget.alwaysShowPercentage
!== undefined ? widget.alwaysShowPercentage : adapter.bar.alwaysShowBatteryPercentage
break
case "Clock":
widget.showDate = adapter.location.showDateWithClock
widget.use12HourClock = adapter.location.use12HourClock
widget.reverseDayMonth = adapter.location.reverseDayMonth
widget.showSeconds = BarWidgetRegistry.widgetMetadata[widget.id].showSeconds
widget.showDate = widget.showDate !== undefined ? widget.showDate : adapter.location.showDateWithClock
widget.use12HourClock = widget.use12HourClock !== undefined ? widget.use12HourClock : adapter.location.use12HourClock
widget.reverseDayMonth = widget.reverseDayMonth !== undefined ? widget.reverseDayMonth : adapter.location.reverseDayMonth
break
case "MediaMini":
widget.showAlbumArt = adapter.audio.showMiniplayerAlbumArt
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
widget.showAlbumArt = widget.showAlbumArt !== undefined ? widget.showAlbumArt : adapter.audio.showMiniplayerAlbumArt
widget.showVisualizer = widget.showVisualizer !== undefined ? widget.showVisualizer : adapter.audio.showMiniplayerCava
break
case "SidePanelToggle":
widget.useDistroLogo = adapter.bar.useDistroLogo
widget.useDistroLogo = widget.useDistroLogo !== undefined ? widget.useDistroLogo : adapter.bar.useDistroLogo
break
case "SystemMonitor":
widget.showNetworkStats = adapter.bar.showNetworkStats
break
case "Volume":
widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage
widget.showNetworkStats = widget.showNetworkStats !== undefined ? widget.showNetworkStats : adapter.bar.showNetworkStats
break
case "Workspace":
widget.labelMode = adapter.bar.showWorkspaceLabel
widget.labelMode = widget.labelMode !== undefined ? widget.labelMode : adapter.bar.showWorkspaceLabel
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
@ -175,6 +193,8 @@ Singleton {
FontService.init()
HooksService.init()
BluetoothService.init()
}
// -----------------------------------------------------
@ -200,14 +220,15 @@ Singleton {
FileView {
id: settingsFileView
path: directoriesCreated ? settingsFile : ""
path: directoriesCreated ? settingsFile : undefined
printErrors: false
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: saveTimer.start()
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path === settingsFile) {
if (path !== undefined) {
reload()
}
}
@ -215,7 +236,6 @@ Singleton {
if (!isLoaded) {
Logger.log("Settings", "----------------------------")
Logger.log("Settings", "Settings loaded successfully")
isLoaded = true
upgradeSettingsData()
@ -223,6 +243,8 @@ Singleton {
kickOffServices()
isLoaded = true
// Emit the signal
root.settingsLoaded()
}
@ -335,7 +357,6 @@ Singleton {
property int transitionDuration: 1500 // 1500 ms
property string transitionType: "random"
property real transitionEdgeSmoothness: 0.05
property string defaultWallpaper: root.defaultWallpaper
property list<var> monitors: []
}
@ -422,6 +443,7 @@ Singleton {
// night light
property JsonObject nightLight: JsonObject {
property bool enabled: false
property bool forced: false
property bool autoSchedule: true
property string nightTemp: "4000"
property string dayTemp: "6500"

View file

@ -57,10 +57,10 @@ Singleton {
property real opacityFull: 1.0
// Animation duration (ms)
property int animationFast: Math.round(150 * 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 animationSlowest: Math.round(750 * 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 animationSlow: Math.round(450 / Settings.data.general.animationSpeed)
property int animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
// Dimensions
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
property string transitionType: "fade"
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 var allTransitions: WallpaperService.allTransitions
@ -91,15 +89,6 @@ Variants {
left: true
}
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((screen !== null) && (screenName === screen.name)) {
scaling = scale
}
}
}
Timer {
id: debounceTimer
interval: 333
@ -150,7 +139,7 @@ Variants {
property real screenWidth: width
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
@ -175,7 +164,7 @@ Variants {
property real screenWidth: width
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
@ -202,7 +191,7 @@ Variants {
property real screenWidth: width
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
@ -229,7 +218,7 @@ Variants {
property real screenWidth: width
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

View file

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

View file

@ -33,28 +33,34 @@ RowLayout {
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
readonly property real minWidth: 160
readonly property real maxWidth: 400
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
visible: getTitle() !== ""
// 6% of total width
readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2
function getTitle() {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
}
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
visible: getTitle() !== ""
function getAppIcon() {
// Try CompositorService first
const focusedWindow = CompositorService.getFocusedWindow()
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
if (ToplevelManager && ToplevelManager.activeToplevel) {
const activeToplevel = ToplevelManager.activeToplevel
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
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
verticalAlignment: Text.AlignVCenter
color: Color.mSecondary
color: Color.mPrimary
clip: true
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
readonly property bool testMode: false
readonly property int testPercent: 50
readonly property bool testCharging: true
readonly property int testPercent: 90
readonly property bool testCharging: false
// Main properties
readonly property var battery: UPower.displayDevice
@ -57,9 +57,7 @@ Item {
// Only notify once we are a below threshold
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
root.hasNotifiedLowBattery = true
// Maybe go with toast ?
Quickshell.execDetached(
["notify-send", "-u", "critical", "-i", "battery-caution", "Low Battery", `Battery is at ${p}%. Please connect charger.`])
ToastService.showWarning("Low Battery", `Battery is at ${Math.round(percent)}%. Please connect the charger.`)
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
// Reset when charging starts or when battery recovers 5% above threshold
root.hasNotifiedLowBattery = false
@ -70,14 +68,20 @@ Item {
Connections {
target: UPower.displayDevice
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() {
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
// Reset notification flag when charging starts
if (charging) {
if (isCharging) {
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)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent,
charging, isReady)
iconRotation: -90
text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-"
textColor: charging ? Color.mPrimary : Color.mOnSurface
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
text: (isReady || testMode) ? Math.round(percent) + "%" : "-"
autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && alwaysShowPercentage
disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))

View file

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

View file

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

View file

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

View file

@ -38,6 +38,10 @@ NIconButton {
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.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
icon: customIcon
tooltipText: {
@ -57,7 +61,6 @@ NIconButton {
return lines.join("<br/>")
}
}
opacity: hasExec ? Style.opacityFull : Style.opacityMedium
onClicked: {
if (leftClickExec) {

View file

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

View file

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

View file

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

View file

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

View file

@ -43,9 +43,9 @@ Item {
function getIcon() {
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
@ -92,8 +92,6 @@ Item {
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.inputVolume * 100) + "%"
forceOpen: alwaysShowPercentage

View file

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

View file

@ -53,10 +53,10 @@ NIconButton {
}
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'."
colorBg: Color.mSurfaceVariant
colorFg: Settings.data.notifications.doNotDisturb ? Color.mError : Color.mOnSurface
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent

View file

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

View file

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

View file

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

View file

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

View file

@ -38,12 +38,4 @@ Item {
implicitHeight: Style.barHeight * scaling
width: implicitWidth
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
readonly property bool showNetworkStats: (widgetSettings.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
spacing: Style.marginS * scaling
@ -52,126 +56,218 @@ RowLayout {
RowLayout {
id: mainLayout
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
anchors.centerIn: parent // Better centering than margins
width: parent.width - Style.marginM * scaling * 2
spacing: Style.marginS * scaling
// CPU Usage Component
RowLayout {
id: cpuUsageLayout
spacing: Style.marginXS * scaling
Item {
Layout.preferredWidth: cpuUsageRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showCpuUsage
NIcon {
id: cpuUsageIcon
text: "speed"
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
id: cpuUsageRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NText {
id: cpuUsageText
text: `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
NIcon {
icon: "cpu-usage"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
NText {
text: `${SystemStatService.cpuUsage}%`
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
RowLayout {
id: cpuTempLayout
// spacing is thin here to compensate for the vertical thermometer icon
spacing: Style.marginXXS * scaling
Item {
Layout.preferredWidth: cpuTempRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showCpuTemp
NIcon {
text: "thermometer"
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
id: cpuTempRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NText {
text: `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
NIcon {
icon: "cpu-temperature"
// Fire is so tall, we need to make it smaller
font.pointSize: Style.fontSizeS * scaling
Layout.alignment: Qt.AlignVCenter
}
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
RowLayout {
id: memoryUsageLayout
spacing: Style.marginXS * scaling
Item {
Layout.preferredWidth: memoryUsageRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showMemoryUsage
NIcon {
text: "memory"
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
id: memoryUsageRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NText {
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
NIcon {
icon: "memory"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
NText {
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
RowLayout {
id: networkDownloadLayout
spacing: Style.marginXS * scaling
Item {
Layout.preferredWidth: networkDownloadRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showNetworkStats
NIcon {
text: "download"
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
id: networkDownloadRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
NIcon {
icon: "download-speed"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
NText {
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
RowLayout {
id: networkUploadLayout
spacing: Style.marginXS * scaling
Item {
Layout.preferredWidth: networkUploadRow.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: Qt.AlignVCenter
visible: showNetworkStats
NIcon {
text: "upload"
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
id: networkUploadRow
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
NIcon {
icon: "upload-speed"
font.pointSize: Style.fontSizeM * scaling
Layout.alignment: Qt.AlignVCenter
}
NText {
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
width: Style.marginL * root.scaling
height: Style.marginL * root.scaling
source: Icons.iconForAppId(taskbarItem.modelData.appId)
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
}
}

View file

@ -43,9 +43,9 @@ Item {
function getIcon() {
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
@ -77,8 +77,6 @@ Item {
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100) + "%"
forceOpen: alwaysShowPercentage

View file

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

View file

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

View file

@ -28,7 +28,7 @@ NPanel {
spacing: Style.marginM * scaling
NIcon {
text: "bluetooth"
icon: "bluetooth"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
@ -41,8 +41,16 @@ NPanel {
Layout.fillWidth: true
}
NToggle {
id: wifiSwitch
checked: Settings.data.network.bluetoothEnabled
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65 * scaling
}
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"
sizeRatio: 0.8
onClicked: {
@ -66,7 +74,42 @@ NPanel {
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 {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
@ -75,7 +118,6 @@ NPanel {
contentWidth: availableWidth
ColumnLayout {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
width: parent.width
spacing: Style.marginM * scaling
@ -146,7 +188,7 @@ NPanel {
spacing: Style.marginXS * scaling
NIcon {
text: "sync"
icon: "refresh"
font.pointSize: Style.fontSizeXXL * 1.5 * scaling
color: Color.mPrimary

View file

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

View file

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

View file

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

View file

@ -404,7 +404,7 @@ NPanel {
sourceComponent: Component {
IconImage {
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 !== ""
asynchronous: true
}

View file

@ -136,7 +136,7 @@ Item {
const items = ClipboardService.items || []
// 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
ClipboardService.list(100)
return [{

View file

@ -418,7 +418,7 @@ Loader {
font.weight: Style.fontWeightBold
}
NIcon {
text: "keyboard_alt"
icon: "keyboard"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
}
@ -428,7 +428,7 @@ Loader {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging,
icon: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging,
batteryIndicator.isReady)
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
@ -718,21 +718,47 @@ Loader {
anchors.margins: 50 * scaling
spacing: 20 * scaling
// Shutdown
Rectangle {
Layout.preferredWidth: 60 * scaling
Layout.preferredHeight: 60 * scaling
Layout.preferredWidth: iconPower.implicitWidth + Style.marginXL * scaling
Layout.preferredHeight: Layout.preferredWidth
radius: width * 0.5
color: powerButtonArea.containsMouse ? Color.mError : Qt.alpha(Color.mError, 0.2)
border.color: Color.mError
border.width: Math.max(1, Style.borderM * scaling)
NIcon {
id: iconPower
anchors.centerIn: parent
text: "power_settings_new"
font.pointSize: Style.fontSizeXL * scaling
icon: "shutdown"
font.pointSize: Style.fontSizeXXXL * scaling
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 {
id: powerButtonArea
anchors.fill: parent
@ -743,21 +769,47 @@ Loader {
}
}
// Reboot
Rectangle {
Layout.preferredWidth: 60 * scaling
Layout.preferredHeight: 60 * scaling
Layout.preferredWidth: iconReboot.implicitWidth + Style.marginXL * scaling
Layout.preferredHeight: Layout.preferredWidth
radius: width * 0.5
color: restartButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mPrimary, Style.opacityLight)
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderM * scaling)
NIcon {
id: iconReboot
anchors.centerIn: parent
text: "restart_alt"
font.pointSize: Style.fontSizeXL * scaling
icon: "reboot"
font.pointSize: Style.fontSizeXXXL * scaling
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 {
id: restartButtonArea
anchors.fill: parent
@ -765,24 +817,51 @@ Loader {
onClicked: {
CompositorService.reboot()
}
// Tooltip handled via inline rectangle visibility
}
}
// Suspend
Rectangle {
Layout.preferredWidth: 60 * scaling
Layout.preferredHeight: 60 * scaling
Layout.preferredWidth: iconSuspend.implicitWidth + Style.marginXL * scaling
Layout.preferredHeight: Layout.preferredWidth
radius: width * 0.5
color: suspendButtonArea.containsMouse ? Color.mSecondary : Qt.alpha(Color.mSecondary, 0.2)
border.color: Color.mSecondary
border.width: Math.max(1, Style.borderM * scaling)
NIcon {
id: iconSuspend
anchors.centerIn: parent
text: "bedtime"
font.pointSize: Style.fontSizeXL * scaling
icon: "suspend"
font.pointSize: Style.fontSizeXXXL * scaling
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 {
id: suspendButtonArea
anchors.fill: parent
@ -790,6 +869,7 @@ Loader {
onClicked: {
CompositorService.suspend()
}
// Tooltip handled via inline rectangle visibility
}
}
}

View file

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

View file

@ -31,7 +31,7 @@ NPanel {
spacing: Style.marginM * scaling
NIcon {
text: "notifications"
icon: "bell"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
@ -45,14 +45,15 @@ NPanel {
}
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."
sizeRatio: 0.8
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
NIconButton {
icon: "delete"
icon: "trash"
tooltipText: "Clear history"
sizeRatio: 0.8
onClicked: NotificationService.clearHistory()
@ -85,7 +86,7 @@ NPanel {
}
NIcon {
text: "notifications_off"
icon: "bell-off"
font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
@ -103,6 +104,9 @@ NPanel {
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
Item {
@ -135,10 +139,29 @@ NPanel {
anchors.margins: 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
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: notificationList.width - (Style.marginM * scaling * 4) // Account for margins and delete button
spacing: Style.marginXXS * scaling
NText {
@ -148,7 +171,6 @@ NPanel {
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary
wrapMode: Text.Wrap
Layout.fillWidth: true
Layout.maximumWidth: parent.width
maximumLineCount: 2
elide: Text.ElideRight
}
@ -159,7 +181,6 @@ NPanel {
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
wrapMode: Text.Wrap
Layout.fillWidth: true
Layout.maximumWidth: parent.width
maximumLineCount: 3
elide: Text.ElideRight
visible: text.length > 0
@ -175,7 +196,7 @@ NPanel {
// Delete button
NIconButton {
icon: "delete"
icon: "trash"
tooltipText: "Delete notification"
sizeRatio: 0.7
Layout.alignment: Qt.AlignTop

View file

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

View file

@ -18,7 +18,11 @@ NBox {
signal removeWidget(string section, int index)
signal reorderWidget(string section, int fromIndex, int toIndex)
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
Layout.fillWidth: true
Layout.minimumHeight: {
@ -105,13 +109,11 @@ NBox {
}
// Drag and Drop Widget Area
// Replace your Flow section with this:
// Drag and Drop Widget Area - use Item container
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 65 * scaling
clip: false // Don't clip children so ghost can move freely
Flow {
id: widgetFlow
@ -139,13 +141,18 @@ NBox {
readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id)
// Visual feedback during drag
states: State {
when: flowDragArea.draggedIndex === index
PropertyChanges {
target: widgetItem
scale: 1.1
opacity: 0.9
z: 1000
opacity: flowDragArea.draggedIndex === index ? 0.5 : 1.0
scale: flowDragArea.draggedIndex === index ? 0.95 : 1.0
z: flowDragArea.draggedIndex === index ? 1000 : 0
Behavior on opacity {
NumberAnimation {
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 {
id: flowDragArea
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
preventStealing: false // Prevent child items from stealing events
propagateComposedEvents: draggedIndex != -1 // Don't propagate to children during drag
hoverEnabled: draggedIndex != -1
preventStealing: false
propagateComposedEvents: !dragStarted
hoverEnabled: true // Always track mouse for drag operations
property point startPos: Qt.point(0, 0)
property bool dragStarted: false
property bool potentialDrag: false // Track if we're in a potential drag interaction
property int draggedIndex: -1
property real dragThreshold: 15 * scaling
property Item draggedWidget: null
property point clickOffsetInWidget: Qt.point(0, 0)
property point originalWidgetPos: Qt.point(0, 0) // ADD THIS: Store original position
property int dropTargetIndex: -1
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 => {
startPos = Qt.point(mouse.x, mouse.y)
dragStarted = false
potentialDrag = false
draggedIndex = -1
draggedWidget = null
dropTargetIndex = -1
draggedModelData = null
// Find which widget was clicked
for (var i = 0; i < widgetModel.length; i++) {
@ -264,22 +426,18 @@ NBox {
const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth)
if (localX < buttonsStartX) {
// This is a draggable area - prevent panel close immediately
draggedIndex = widget.widgetIndex
draggedWidget = widget
// Calculate and store where within the widget the user clicked
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
draggedModelData = widget.modelData
potentialDrag = true
preventStealing = true
// Signal that interaction started (prevents panel close)
root.dragPotentialStarted()
break
} else {
// Click was on buttons - allow event propagation
// This is a button area - let the click through
mouse.accepted = false
return
}
@ -289,154 +447,83 @@ NBox {
}
onPositionChanged: mouse => {
if (draggedIndex !== -1) {
if (draggedIndex !== -1 && potentialDrag) {
const deltaX = mouse.x - startPos.x
const deltaY = mouse.y - startPos.y
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
if (!dragStarted && distance > dragThreshold) {
dragStarted = true
//Logger.log("BarSectionEditor", "Drag started")
// Enable visual feedback
// Setup ghost widget
if (draggedWidget) {
draggedWidget.z = 1000
dragGhost.width = draggedWidget.width
dragGhost.color = root.getWidgetColor(draggedModelData)
ghostText.text = draggedModelData.id
}
}
if (dragStarted && draggedWidget) {
// Adjust position to account for where within the widget the user clicked
draggedWidget.x = mouse.x - clickOffsetInWidget.x
draggedWidget.y = mouse.y - clickOffsetInWidget.y
if (dragStarted) {
// Move ghost widget
dragGhost.x = mouse.x - dragGhost.width / 2
dragGhost.y = mouse.y - dragGhost.height / 2
// Update drop indicator
updateDropIndicator(mouse.x, mouse.y)
}
}
}
onReleased: mouse => {
if (dragStarted && draggedWidget) {
// Find drop target using improved logic
let targetIndex = -1
let minDistance = Infinity
const mouseX = mouse.x
const mouseY = mouse.y
if (dragStarted && dropTargetIndex !== -1 && dropTargetIndex !== draggedIndex) {
// Perform the reorder
reorderWidget(sectionId, draggedIndex, dropTargetIndex)
}
// Check if we should insert at the beginning
let insertAtBeginning = true
let insertAtEnd = true
// 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
// Always signal end of interaction if we started one
if (potentialDrag) {
root.dragPotentialEnded()
}
// Reset everything
dragStarted = false
potentialDrag = false
draggedIndex = -1
draggedWidget = null
preventStealing = false // Allow normal event propagation again
originalWidgetPos = Qt.point(0, 0) // Reset stored position
dropTargetIndex = -1
draggedModelData = null
preventStealing = false
dropIndicator.opacity = 0
pulseAnimation.running = false
dragGhost.width = 0
}
// Handle case where mouse leaves the area during drag
onExited: {
if (dragStarted && draggedWidget) {
// Restore original position when mouse leaves area
draggedWidget.x = originalWidgetPos.x
draggedWidget.y = originalWidgetPos.y
draggedWidget.z = 0
if (dragStarted) {
// Hide drop indicator when mouse leaves, but keep ghost visible
dropIndicator.opacity = 0
pulseAnimation.running = false
}
}
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 {
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
Item {
Layout.fillWidth: true

View file

@ -16,10 +16,13 @@ ColumnLayout {
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
property int valueWarningThreshold: widgetData.warningThreshold
!== undefined ? widgetData.warningThreshold : widgetMetadata.warningThreshold
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
settings.warningThreshold = valueWarningThreshold
return settings
}
@ -28,4 +31,14 @@ ColumnLayout {
checked: root.valueAlwaysShowPercentage
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.Controls
import QtQuick.Layouts
import QtQuick.Window
import qs.Commons
import qs.Widgets
import qs.Services
@ -9,7 +10,6 @@ ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
@ -22,16 +22,189 @@ ColumnLayout {
return settings
}
// Icon setting
NTextInput {
id: iconInput
Layout.fillWidth: true
label: "Icon Name"
description: "Choose a name from the Material Icon set."
placeholderText: "Enter icon name (e.g., favorite, home, settings)"
description: "Select an icon from the library."
placeholderText: "Enter icon name (e.g., cat, gear, house, ...)"
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 {
id: leftClickExecInput
Layout.fillWidth: true

View file

@ -16,19 +16,24 @@ ColumnLayout {
// Local, editable state for checkboxes
property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage
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 valueShowMemoryAsPercent: widgetData.showMemoryAsPercent
!== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
property bool valueShowNetworkStats: widgetData.showNetworkStats
!== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats
property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showCpuUsage = valueShowCpuUsage
settings.showCpuTemp = valueShowCpuTemp
settings.showGpuTemp = valueShowGpuTemp
settings.showMemoryUsage = valueShowMemoryUsage
settings.showMemoryAsPercent = valueShowMemoryAsPercent
settings.showNetworkStats = valueShowNetworkStats
settings.showDiskUsage = valueShowDiskUsage
return settings
}
@ -48,6 +53,14 @@ ColumnLayout {
onToggled: checked => valueShowCpuTemp = checked
}
NToggle {
id: showGpuTemp
Layout.fillWidth: true
label: "GPU temperature"
checked: valueShowGpuTemp
onToggled: checked => valueShowGpuTemp = checked
}
NToggle {
id: showMemoryUsage
Layout.fillWidth: true
@ -59,7 +72,7 @@ ColumnLayout {
NToggle {
id: showMemoryAsPercent
Layout.fillWidth: true
label: "Show memory as percentage"
label: "Memory as percentage"
checked: valueShowMemoryAsPercent
onToggled: checked => valueShowMemoryAsPercent = checked
}
@ -71,4 +84,12 @@ ColumnLayout {
checked: valueShowNetworkStats
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 = [{
"id": SettingsPanel.Tab.General,
"label": "General",
"icon": "tune",
"icon": "settings-general",
"source": generalTab
}, {
"id": SettingsPanel.Tab.Bar,
"label": "Bar",
"icon": "web_asset",
"icon": "settings-bar",
"source": barTab
}, {
"id": SettingsPanel.Tab.Launcher,
"label": "Launcher",
"icon": "apps",
"icon": "settings-launcher",
"source": launcherTab
}, {
"id": SettingsPanel.Tab.Audio,
"label": "Audio",
"icon": "volume_up",
"icon": "settings-audio",
"source": audioTab
}, {
"id": SettingsPanel.Tab.Display,
"label": "Display",
"icon": "monitor",
"icon": "settings-display",
"source": displayTab
}, {
"id": SettingsPanel.Tab.Network,
"label": "Network",
"icon": "lan",
"icon": "settings-network",
"source": networkTab
}, {
"id": SettingsPanel.Tab.Brightness,
"label": "Brightness",
"icon": "brightness_6",
"icon": "settings-brightness",
"source": brightnessTab
}, {
"id": SettingsPanel.Tab.Weather,
"label": "Weather",
"icon": "partly_cloudy_day",
"icon": "settings-weather",
"source": weatherTab
}, {
"id": SettingsPanel.Tab.ColorScheme,
"label": "Color Scheme",
"icon": "palette",
"icon": "settings-color-scheme",
"source": colorSchemeTab
}, {
"id": SettingsPanel.Tab.Wallpaper,
"label": "Wallpaper",
"icon": "image",
"icon": "settings-wallpaper",
"source": wallpaperTab
}]
@ -177,7 +177,7 @@ NPanel {
newTabs.push({
"id": SettingsPanel.Tab.WallpaperSelector,
"label": "Wallpaper Selector",
"icon": "wallpaper_slideshow",
"icon": "settings-wallpaper-selector",
"source": wallpaperSelectorTab
})
}
@ -185,17 +185,17 @@ NPanel {
newTabs.push({
"id": SettingsPanel.Tab.ScreenRecorder,
"label": "Screen Recorder",
"icon": "videocam",
"icon": "settings-screen-recorder",
"source": screenRecorderTab
}, {
"id": SettingsPanel.Tab.Hooks,
"label": "Hooks",
"icon": "cable",
"icon": "settings-hooks",
"source": hooksTab
}, {
"id": SettingsPanel.Tab.About,
"label": "About",
"icon": "info",
"icon": "settings-about",
"source": aboutTab
})
@ -400,13 +400,13 @@ NPanel {
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling
spacing: Style.marginM * scaling
// Tab icon
NIcon {
text: modelData.icon
icon: modelData.icon
color: tabTextColor
font.pointSize: Style.fontSizeL * scaling
font.pointSize: Style.fontSizeXL * scaling
}
// Tab label
@ -416,6 +416,7 @@ NPanel {
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
}
@ -461,7 +462,14 @@ NPanel {
Layout.fillWidth: true
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 {
text: root.tabsModel[currentTabIndex]?.label || ""
font.pointSize: Style.fontSizeXL * scaling

View file

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

View file

@ -9,6 +9,22 @@ import qs.Modules.SettingsPanel.Bar
ColumnLayout {
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 {
spacing: Style.marginL * scaling
@ -116,6 +132,8 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
// Center Section
@ -128,6 +146,8 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
// Right Section
@ -140,6 +160,8 @@ ColumnLayout {
onRemoveWidget: (section, index) => _removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex)
onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings)
onDragPotentialStarted: root.handleDragStart()
onDragPotentialEnded: root.handleDragEnd()
}
}
}

View file

@ -194,6 +194,7 @@ ColumnLayout {
wlsunsetCheck.running = true
} else {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
NightLightService.apply()
ToastService.showNotice("Night Light", "Disabled")
}
@ -276,6 +277,7 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginXS * scaling
visible: Settings.data.nightLight.enabled && !Settings.data.nightLight.autoSchedule
&& !Settings.data.nightLight.forced
RowLayout {
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")
border.width: Math.max(1, Style.borderL * scaling)
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
// Mouse area for selection
@ -198,8 +199,8 @@ ColumnLayout {
Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting")
Settings.data.colorSchemes.predefinedScheme = schemePath
ColorSchemeService.applyScheme(schemePath)
Settings.data.colorSchemes.predefinedScheme = schemePath.split("/").pop().replace(".json", "")
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@ -281,7 +282,8 @@ ColumnLayout {
// Selection indicator (Checkmark)
Rectangle {
visible: !Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === schemePath)
&& (Settings.data.colorSchemes.predefinedScheme === schemePath.split("/").pop().replace(".json",
""))
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.marginS * scaling

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,8 @@
</a>
</p>
---
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!
@ -66,7 +68,6 @@ Features a modern modular architecture with a status bar, notification system, c
- `quickshell-git` - Core shell framework
- `ttf-roboto` - The default font used for most of the UI
- `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
- `brightnessctl` - For internal/laptop monitor brightness
- `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.
<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" />
</a>

View file

@ -62,6 +62,8 @@ Singleton {
function onMutedChanged() {
root._muted = (sink?.audio.muted ?? true)
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() {
root._inputMuted = (source?.audio.muted ?? true)
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
property var widgets: ({
"ActiveWindow": activeWindowComponent,
"ArchUpdater": archUpdaterComponent,
"Battery": batteryComponent,
"Bluetooth": bluetoothComponent,
"Brightness": brightnessComponent,
@ -60,7 +59,7 @@ Singleton {
},
"CustomButton": {
"allowUserSettings": true,
"icon": "favorite",
"icon": "heart",
"leftClickExec": "",
"rightClickExec": "",
"middleClickExec": ""
@ -82,9 +81,11 @@ Singleton {
"allowUserSettings": true,
"showCpuUsage": true,
"showCpuTemp": true,
"showGpuTemp": false,
"showMemoryUsage": true,
"showMemoryAsPercent": false,
"showNetworkStats": false
"showNetworkStats": false,
"showDiskUsage": false
},
"Workspace": {
"allowUserSettings": true,
@ -110,9 +111,6 @@ Singleton {
property Component activeWindowComponent: Component {
ActiveWindow {}
}
property Component archUpdaterComponent: Component {
ArchUpdater {}
}
property Component batteryComponent: Component {
Battery {}
}

View file

@ -2,6 +2,8 @@ pragma Singleton
import Quickshell
import Quickshell.Services.UPower
import qs.Commons
import qs.Services
Singleton {
id: root
@ -9,41 +11,21 @@ Singleton {
// Choose icon based on charge and charging state
function getIcon(percent, charging, isReady) {
if (!isReady) {
return "battery_error"
return "battery-exclamation"
}
if (charging) {
if (percent >= 95)
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"
return "battery-charging"
} else {
if (percent >= 95)
return "battery_full"
if (percent >= 85)
return "battery_6_bar"
if (percent >= 70)
return "battery_5_bar"
if (percent >= 55)
return "battery_4_bar"
if (percent >= 40)
return "battery_3_bar"
if (percent >= 90)
return "battery-4"
if (percent >= 50)
return "battery-3"
if (percent >= 25)
return "battery_2_bar"
if (percent >= 10)
return "battery_1_bar"
return "battery-2"
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) {
return devices.sort((a, b) => {
var aName = a.name || a.deviceName || ""
@ -51,36 +76,36 @@ Singleton {
function getDeviceIcon(device) {
if (!device) {
return "bluetooth"
return "bt-device-generic"
}
var name = (device.name || device.deviceName || "").toLowerCase()
var icon = (device.icon || "").toLowerCase()
if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod")
|| name.includes("headset") || name.includes("arctis")) {
return "headset"
return "bt-device-headphones"
}
if (icon.includes("mouse") || name.includes("mouse")) {
return "mouse"
return "bt-device-mouse"
}
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")
|| name.includes("samsung")) {
return "smartphone"
return "bt-device-phone"
}
if (icon.includes("watch") || name.includes("watch")) {
return "watch"
return "bt-device-watch"
}
if (icon.includes("speaker") || name.includes("speaker")) {
return "speaker"
return "bt-device-speaker"
}
if (icon.includes("display") || name.includes("tv")) {
return "tv"
return "bt-device-tv"
}
return "bluetooth"
return "bt-device-generic"
}
function canConnect(device) {

View file

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

View file

@ -23,6 +23,11 @@ Singleton {
// Re-apply current scheme to pick the right variant
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
}
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
var filePath = resolveSchemePath(nameOrPath)
schemeReader.path = ""
schemeReader.path = filePath
}
@ -64,6 +87,17 @@ Singleton {
schemes = files
scanning = false
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)
Logger.log("ColorScheme", "Applying color scheme:", path)
Logger.log("ColorScheme", "Applying color scheme:", getBasename(path))
} catch (e) {
Logger.error("ColorScheme", "Failed to parse scheme JSON:", e)
}

View file

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

View file

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

View file

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

View file

@ -30,6 +30,7 @@ Singleton {
FileView {
id: cacheFileView
path: root.cacheFile
printErrors: false
JsonAdapter {
id: cacheAdapter
@ -95,6 +96,7 @@ Singleton {
function setWifiEnabled(enabled) {
Settings.data.network.wifiEnabled = enabled
wifiStateEnableProcess.running = true
}
function scan() {
@ -201,14 +203,12 @@ Singleton {
// Helper functions
function signalIcon(signal) {
if (signal >= 80)
return "network_wifi"
if (signal >= 60)
return "network_wifi_3_bar"
if (signal >= 40)
return "network_wifi_2_bar"
return "wifi"
if (signal >= 50)
return "wifi-2"
if (signal >= 20)
return "network_wifi_1_bar"
return "signal_wifi_0_bar"
return "wifi-1"
return "wifi-0"
}
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 {
id: wifiStateProcess
running: false
@ -243,7 +245,7 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
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) {
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
Process {
id: profileCheckProcess

View file

@ -15,7 +15,7 @@ Singleton {
function apply() {
// If using LocationService, wait for it to be ready
if (params.autoSchedule && !LocationService.coordinatesReady) {
if (!params.forced && params.autoSchedule && !LocationService.coordinatesReady) {
return
}
@ -34,14 +34,25 @@ Singleton {
function buildCommand() {
var cmd = ["wlsunset"]
cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`)
if (params.autoSchedule) {
cmd.push("-l", `${LocationService.stableLatitude}`, "-L", `${LocationService.stableLongitude}`)
if (params.forced) {
// Force immediate full night temperature regardless of time
// 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 {
cmd.push("-S", params.manualSunrise)
cmd.push("-s", params.manualSunset)
cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`)
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
}
@ -50,6 +61,15 @@ Singleton {
target: Settings.data.nightLight
function onEnabledChanged() {
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() {
apply()

View file

@ -65,6 +65,7 @@ Singleton {
id: historyFileView
objectName: "notificationHistoryFileView"
path: historyFile
printErrors: false
watchChanges: true
onFileChanged: reload()
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 addNotification(notification) {
const resolvedImage = resolveNotificationImage(notification)
const resolvedAppName = resolveAppName(notification)
notificationModel.insert(0, {
"rawNotification": notification,
"summary": notification.summary,
"body": notification.body,
"appName": notification.appName,
"appName": resolvedAppName,
"desktopEntry": notification.desktopEntry,
"image": resolvedImage,
"appIcon": notification.appIcon,
"urgency": notification.urgency,
@ -164,7 +202,7 @@ Singleton {
// Resolve themed icon names to absolute paths
try {
const p = Icons.iconFromName(icon, "")
const p = AppIcons.iconFromName(icon, "")
return p || ""
} catch (e2) {
return ""
@ -174,12 +212,17 @@ Singleton {
}
}
// Add a simplified copy into persistent history
function addToHistory(notification) {
const resolvedAppName = resolveAppName(notification)
const resolvedImage = resolveNotificationImage(notification)
historyModel.insert(0, {
"summary": notification.summary,
"body": notification.body,
"appName": notification.appName,
"appName": resolvedAppName,
"desktopEntry": notification.desktopEntry || "",
"image": resolvedImage,
"appIcon": notification.appIcon || "",
"urgency": notification.urgency,
"timestamp": new Date()
})
@ -210,6 +253,9 @@ Singleton {
"summary": it.summary || "",
"body": it.body || "",
"appName": it.appName || "",
"desktopEntry": it.desktopEntry || "",
"image": it.image || "",
"appIcon": it.appIcon || "",
"urgency": it.urgency,
"timestamp": ts ? new Date(ts) : new Date()
})
@ -229,6 +275,9 @@ Singleton {
"summary": n.summary,
"body": n.body,
"appName": n.appName,
"desktopEntry": n.desktopEntry,
"image": n.image,
"appIcon": n.appIcon,
"urgency": n.urgency,
"timestamp"// Always persist in milliseconds
: (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 isPending: false
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
function toggleRecording() {
@ -21,6 +32,9 @@ Singleton {
// Start screen recording using Quickshell.execDetached
function startRecording() {
if (!isAvailable) {
return
}
if (isRecording || isPending) {
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 {
id: pendingTimer
interval: 2000 // Wait 2 seconds to see if process stays alive

View file

@ -12,6 +12,7 @@ Singleton {
// Public values
property real cpuUsage: 0
property real cpuTemp: 0
property real gpuTemp: 0
property real memGb: 0
property real memPercent: 0
property real diskPercent: 0
@ -35,6 +36,12 @@ Singleton {
readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
property string cpuTempSensorName: ""
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
property var intelTempValues: []
property int intelTempFilesChecked: 0
@ -66,6 +73,7 @@ Singleton {
dfProcess.running = true
updateCpuTemperature()
updateGpuTemperature()
}
}
@ -107,6 +115,7 @@ Singleton {
}
}
// --------------------------------------------
// --------------------------------------------
// CPU Temperature
// It's more complex.
@ -115,9 +124,10 @@ Singleton {
FileView {
id: cpuTempNameReader
property int currentIndex: 0
printErrors: false
function checkNext() {
if (currentIndex >= 10) {
if (currentIndex >= 16) {
// Check up to hwmon10
Logger.warn("No supported temperature sensor found")
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
function parseMemoryInfo(text) {
@ -321,10 +434,8 @@ Singleton {
// -------------------------------------------------------
// Helper function to format network speeds
function formatSpeed(bytesPerSecond) {
if (bytesPerSecond < 1024) {
return bytesPerSecond.toFixed(0) + "B/s"
} else if (bytesPerSecond < 1024 * 1024) {
return (bytesPerSecond / 1024).toFixed(0) + "KB/s"
if (bytesPerSecond < 1024 * 1024) {
return (bytesPerSecond / 1024).toFixed(1) + "KB/s"
} else if (bytesPerSecond < 1024 * 1024 * 1024) {
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB/s"
} 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 checkNextIntelTemp() {

View file

@ -185,6 +185,11 @@ Singleton {
// Process the message queue
function processQueue() {
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
return
}

View file

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

View file

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

View file

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

View file

@ -57,7 +57,7 @@ RowLayout {
NIcon {
visible: root.checked
anchors.centerIn: parent
text: "check"
icon: "check"
color: root.activeOnColor
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
Rectangle {
id: iconBadge
width: 28 * scaling * contentScale
width: iconText.implicitWidth + Style.marginXS * scaling
height: width
radius: width / 2
color: Color.mSurface
color: Color.mPrimary
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -6 * scaling * contentScale
anchors.topMargin: Style.marginXXS * scaling * contentScale
anchors.rightMargin: -2 * scaling
anchors.topMargin: -2 * scaling
NIcon {
id: iconText
anchors.centerIn: parent
text: root.icon
font.pointSize: Style.fontSizeLargeXL * scaling * contentScale
color: Color.mOnSurface
icon: root.icon
color: Color.mOnPrimary
font.pointSize: Style.fontSizeM * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}

View file

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

View file

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

View file

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

View file

@ -1,19 +1,27 @@
import QtQuick
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import QtQuick.Layouts
Text {
// Optional layout nudge for optical alignment when used inside Layouts
property real layoutTopMargin: 0
text: "question_mark"
font.family: "Material Symbols Rounded"
font.pointSize: Style.fontSizeL * scaling
font.variableAxes: {
"wght"// slightly bold to ensure all lines looks good
: (Font.Normal + Font.Bold) / 2.5
id: root
property string icon: Icons.defaultIcon
visible: (icon !== undefined) && (icon !== "")
text: {
if ((icon === undefined) || (icon === "")) {
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
verticalAlignment: Text.AlignVCenter
Layout.topMargin: layoutTopMargin
}

View file

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

View file

@ -9,9 +9,10 @@ Rectangle {
id: root
property string imagePath: ""
property string fallbackIcon: ""
property color borderColor: Color.transparent
property real borderWidth: 0
property string fallbackIcon: ""
property real fallbackIconSize: Style.fontSizeXXL * scaling
color: Color.transparent
radius: parent.width * 0.5
@ -45,18 +46,20 @@ Rectangle {
}
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
blending: true
}
// Fallback icon
NIcon {
anchors.centerIn: parent
text: fallbackIcon
font.pointSize: Style.fontSizeXXL * scaling
visible: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
z: 0
Loader {
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
sourceComponent: NIcon {
anchors.centerIn: parent
icon: fallbackIcon
font.pointSize: fallbackIconSize
z: 0
}
}
}

View file

@ -9,10 +9,11 @@ Rectangle {
id: root
property string imagePath: ""
property string fallbackIcon: ""
property color borderColor: Color.transparent
property real borderWidth: 0
property real imageRadius: width * 0.5
property string fallbackIcon: ""
property real fallbackIconSize: Style.fontSizeXXL * scaling
property real scaledRadius: imageRadius * Settings.data.general.radiusRatio
@ -56,7 +57,7 @@ Rectangle {
property real itemHeight: root.height
property real cornerRadius: root.radius
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
supportsAtlasTextures: false
@ -71,12 +72,14 @@ Rectangle {
}
// Fallback icon
NIcon {
anchors.centerIn: parent
text: fallbackIcon
font.pointSize: Style.fontSizeXXL * scaling
visible: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
z: 0
Loader {
active: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
sourceComponent: NIcon {
anchors.centerIn: parent
icon: fallbackIcon
font.pointSize: fallbackIconSize
z: 0
}
}
}

View file

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

View file

@ -40,8 +40,8 @@ Loader {
property int buttonWidth: 0
property int buttonHeight: 0
// Whether this panel should accept keyboard focus
property bool panelKeyboardFocus: false
property bool backgroundClickEnabled: true
// Animation properties
readonly property real originalScale: 0.7
@ -62,6 +62,24 @@ Loader {
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) {
// Don't toggle if screen is null or invalid
@ -110,6 +128,7 @@ Loader {
PanelService.willOpenPanel(root)
backgroundClickEnabled = true
active = true
root.opened()
}
@ -125,7 +144,8 @@ Loader {
function closeCompleted() {
root.closed()
active = false
useButtonPosition = false // Reset button position usage
useButtonPosition = false
backgroundClickEnabled = true
PanelService.closedPanel(root)
}
@ -179,6 +199,7 @@ Loader {
// Clicking outside of the rectangle to close
MouseArea {
anchors.fill: parent
enabled: root.backgroundClickEnabled
onClicked: root.close()
}
@ -208,7 +229,7 @@ Loader {
return Math.round(Math.max(minX, Math.min(targetX, maxX)))
} else if (!panelAnchorHorizontalCenter && panelAnchorLeft) {
return Math.round(marginS * scaling)
return Math.round(Style.marginS * scaling)
} else if (!panelAnchorHorizontalCenter && panelAnchorRight) {
return Math.round(panelWindow.width - panelWidth - (Style.marginS * scaling))
} else {

View file

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

View file

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

View file

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

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