Merge branch 'main' into shebang-fixes

This commit is contained in:
Lemmy 2025-08-20 20:43:09 -04:00 committed by GitHub
commit 1bd7ec27e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 3616 additions and 3550 deletions

View file

@ -5,7 +5,7 @@
"mSecondary": "#9ccfd8", "mSecondary": "#9ccfd8",
"mOnSecondary": "#191724", "mOnSecondary": "#191724",
"mTertiary": "#31748f", "mTertiary": "#31748f",
"mOnTertiary": "#e0def4", "mOnTertiary": "#191724",
"mError": "#eb6f92", "mError": "#eb6f92",
"mOnError": "#1f1d2e", "mOnError": "#1f1d2e",
"mSurface": "#191724", "mSurface": "#191724",

View file

@ -1,7 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env -S bash
# A Bash script to monitor system stats and output them in JSON format. # A Bash script to monitor system stats and output them in JSON format.
# This script is a conversion of ZigStat
# --- Configuration --- # --- Configuration ---
# Default sleep duration in seconds. Can be overridden by the first argument. # Default sleep duration in seconds. Can be overridden by the first argument.
@ -114,7 +113,7 @@ get_cpu_temp() {
local name local name
name=$(<"$dir/name") name=$(<"$dir/name")
# Check for supported sensor types. # Check for supported sensor types.
if [[ "$name" == "coretemp" || "$name" == "k10temp" ]]; then if [[ "$name" == "coretemp" || "$name" == "k10temp" || "$name" == "zenpower" ]]; then
TEMP_SENSOR_PATH=$dir TEMP_SENSOR_PATH=$dir
TEMP_SENSOR_TYPE=$name TEMP_SENSOR_TYPE=$name
break # Found it, no need to keep searching. break # Found it, no need to keep searching.
@ -166,6 +165,24 @@ get_cpu_temp() {
fi fi
done 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 if [[ -f "$tctl_input" ]]; then
# Read the temperature and convert from millidegrees to degrees. # Read the temperature and convert from millidegrees to degrees.
echo "$(($(<"$tctl_input") / 1000))" echo "$(($(<"$tctl_input") / 1000))"

View file

@ -30,9 +30,6 @@ Singleton {
// Flag to prevent unnecessary wallpaper calls during reloads // Flag to prevent unnecessary wallpaper calls during reloads
property bool isInitialLoad: true property bool isInitialLoad: true
// Needed to only have one NPanel loaded at a time. <--- VERY BROKEN
//property var openPanel: null
// Function to validate monitor configurations // Function to validate monitor configurations
function validateMonitorConfigurations() { function validateMonitorConfigurations() {
var availableScreenNames = [] var availableScreenNames = []
@ -82,16 +79,19 @@ Singleton {
reload() reload()
} }
onLoaded: function () { onLoaded: function () {
Logger.log("Settings", "OnLoaded")
Qt.callLater(function () { Qt.callLater(function () {
if (isInitialLoad) {
Logger.log("Settings", "OnLoaded")
// Only set wallpaper on initial load, not on reloads // Only set wallpaper on initial load, not on reloads
if (isInitialLoad && adapter.wallpaper.current !== "") { if (adapter.wallpaper.current !== "") {
Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current) Logger.log("Settings", "Set current wallpaper", adapter.wallpaper.current)
WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true) WallpaperService.setCurrentWallpaper(adapter.wallpaper.current, true)
} }
// Validate monitor configurations - if none of the configured monitors exist, clear the lists // Validate monitor configurations, only once
// if none of the configured monitors exist, clear the lists
validateMonitorConfigurations() validateMonitorConfigurations()
}
isInitialLoad = false isInitialLoad = false
}) })
@ -125,7 +125,7 @@ Singleton {
general: JsonObject { general: JsonObject {
property string avatarImage: defaultAvatar property string avatarImage: defaultAvatar
property bool dimDesktop: true property bool dimDesktop: false
property bool showScreenCorners: false property bool showScreenCorners: false
property real radiusRatio: 1.0 property real radiusRatio: 1.0
} }
@ -213,14 +213,25 @@ Singleton {
property JsonObject audio property JsonObject audio
audio: JsonObject { audio: JsonObject {
property bool showMiniplayerAlbumArt: false
property bool showMiniplayerCava: false
property string visualizerType: "linear" property string visualizerType: "linear"
property int volumeStep: 5
} }
// ui // ui
property JsonObject ui property JsonObject ui
ui: JsonObject { ui: JsonObject {
property string fontFamily: "Roboto" // Family for all text property string fontDefault: "Roboto" // Default font for all text
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
// Legacy compatibility
property string fontFamily: fontDefault // Keep for backward compatibility
// Idle inhibitor state
property bool idleInhibitorEnabled: false
} }
// Scaling (not stored inside JsonObject, or it crashes) // Scaling (not stored inside JsonObject, or it crashes)

152
Helpers/AdvancedMath.js Normal file
View file

@ -0,0 +1,152 @@
// AdvancedMath.js - Lightweight math library for Noctalia Calculator
// Provides advanced mathematical functions beyond basic arithmetic
// Helper function to convert degrees to radians
function toRadians(degrees) {
return degrees * (Math.PI / 180);
}
// Helper function to convert radians to degrees
function toDegrees(radians) {
return radians * (180 / Math.PI);
}
// Constants
var constants = {
PI: Math.PI,
E: Math.E,
LN2: Math.LN2,
LN10: Math.LN10,
LOG2E: Math.LOG2E,
LOG10E: Math.LOG10E,
SQRT1_2: Math.SQRT1_2,
SQRT2: Math.SQRT2
};
// Safe evaluation function that handles advanced math
function evaluate(expression) {
try {
// Replace mathematical constants
var processed = expression
.replace(/\bpi\b/gi, Math.PI)
.replace(/\be\b/gi, Math.E);
// Replace function calls with Math object equivalents
processed = processed
// Trigonometric functions
.replace(/\bsin\s*\(/g, 'Math.sin(')
.replace(/\bcos\s*\(/g, 'Math.cos(')
.replace(/\btan\s*\(/g, 'Math.tan(')
.replace(/\basin\s*\(/g, 'Math.asin(')
.replace(/\bacos\s*\(/g, 'Math.acos(')
.replace(/\batan\s*\(/g, 'Math.atan(')
.replace(/\batan2\s*\(/g, 'Math.atan2(')
// Hyperbolic functions
.replace(/\bsinh\s*\(/g, 'Math.sinh(')
.replace(/\bcosh\s*\(/g, 'Math.cosh(')
.replace(/\btanh\s*\(/g, 'Math.tanh(')
.replace(/\basinh\s*\(/g, 'Math.asinh(')
.replace(/\bacosh\s*\(/g, 'Math.acosh(')
.replace(/\batanh\s*\(/g, 'Math.atanh(')
// Logarithmic and exponential functions
.replace(/\blog\s*\(/g, 'Math.log10(')
.replace(/\bln\s*\(/g, 'Math.log(')
.replace(/\bexp\s*\(/g, 'Math.exp(')
.replace(/\bpow\s*\(/g, 'Math.pow(')
// Root functions
.replace(/\bsqrt\s*\(/g, 'Math.sqrt(')
.replace(/\bcbrt\s*\(/g, 'Math.cbrt(')
// Rounding and absolute
.replace(/\babs\s*\(/g, 'Math.abs(')
.replace(/\bfloor\s*\(/g, 'Math.floor(')
.replace(/\bceil\s*\(/g, 'Math.ceil(')
.replace(/\bround\s*\(/g, 'Math.round(')
.replace(/\btrunc\s*\(/g, 'Math.trunc(')
// Min/Max
.replace(/\bmin\s*\(/g, 'Math.min(')
.replace(/\bmax\s*\(/g, 'Math.max(')
// Random
.replace(/\brandom\s*\(\s*\)/g, 'Math.random()');
// Handle degree versions of trig functions
processed = processed
.replace(/\bsind\s*\(/g, '(function(x) { return Math.sin(' + (Math.PI / 180) + ' * x); })(')
.replace(/\bcosd\s*\(/g, '(function(x) { return Math.cos(' + (Math.PI / 180) + ' * x); })(')
.replace(/\btand\s*\(/g, '(function(x) { return Math.tan(' + (Math.PI / 180) + ' * x); })(');
// Sanitize expression (only allow safe characters)
if (!/^[0-9+\-*/().\s\w,]+$/.test(processed)) {
throw new Error("Invalid characters in expression");
}
// Evaluate the processed expression
var result = eval(processed);
if (!isFinite(result) || isNaN(result)) {
throw new Error("Invalid result");
}
return result;
} catch (error) {
throw new Error("Evaluation failed: " + error.message);
}
}
// Format result for display
function formatResult(result) {
if (Number.isInteger(result)) {
return result.toString();
}
// Handle very large or very small numbers
if (Math.abs(result) >= 1e15 || (Math.abs(result) < 1e-6 && result !== 0)) {
return result.toExponential(6);
}
// Normal decimal formatting
return parseFloat(result.toFixed(10)).toString();
}
// Get list of available functions for help
function getAvailableFunctions() {
return [
// Basic arithmetic: +, -, *, /, %, ^, ()
// Trigonometric functions
"sin(x), cos(x), tan(x) - trigonometric functions (radians)",
"sind(x), cosd(x), tand(x) - trigonometric functions (degrees)",
"asin(x), acos(x), atan(x) - inverse trigonometric",
"atan2(y, x) - two-argument arctangent",
// Hyperbolic functions
"sinh(x), cosh(x), tanh(x) - hyperbolic functions",
"asinh(x), acosh(x), atanh(x) - inverse hyperbolic",
// Logarithmic and exponential
"log(x) - base 10 logarithm",
"ln(x) - natural logarithm",
"exp(x) - e^x",
"pow(x, y) - x^y",
// Root functions
"sqrt(x) - square root",
"cbrt(x) - cube root",
// Rounding and absolute
"abs(x) - absolute value",
"floor(x), ceil(x), round(x), trunc(x)",
// Min/Max/Random
"min(a, b, ...), max(a, b, ...)",
"random() - random number 0-1",
// Constants
"pi, e - mathematical constants"
];
}

View file

@ -12,61 +12,22 @@ import qs.Widgets
import "../../Helpers/FuzzySort.js" as Fuzzysort import "../../Helpers/FuzzySort.js" as Fuzzysort
NLoader {
id: appLauncher
isLoaded: false
// Clipboard state is persisted in Services/ClipboardService.qml
content: Component {
NPanel { NPanel {
id: appLauncherPanel id: root
panelWidth: Math.min(700 * scaling, screen?.width * 0.75)
panelHeight: Math.min(550 * scaling, screen?.height * 0.8)
panelAnchorCentered: true
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand // Import modular components
Calculator {
// No local timer/processes; use persistent Clipboard service id: calculator
// Removed local clipboard processes; handled by Clipboard service
// Copy helpers via simple exec; avoid keeping processes alive locally
function copyImageBase64(mime, base64) {
Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`])
} }
function copyText(text) { ClipboardHistory {
Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) id: clipboardHistory
}
function updateClipboardHistory() {
ClipboardService.refresh()
}
function selectNext() {
if (filteredEntries.length > 0) {
selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1)
}
}
function selectPrev() {
if (filteredEntries.length > 0) {
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
function activateSelected() {
if (filteredEntries.length === 0)
return
var modelData = filteredEntries[selectedIndex]
if (modelData && modelData.execute) {
if (modelData.isCommand) {
modelData.execute()
return
} else {
modelData.execute()
}
appLauncherPanel.hide()
}
} }
// Properties
property var desktopEntries: DesktopEntries.applications.values property var desktopEntries: DesktopEntries.applications.values
property string searchText: "" property string searchText: ""
property int selectedIndex: 0 property int selectedIndex: 0
@ -74,9 +35,11 @@ NLoader {
// Refresh clipboard when user starts typing clipboard commands // Refresh clipboard when user starts typing clipboard commands
onSearchTextChanged: { onSearchTextChanged: {
if (searchText.startsWith(">clip")) { if (searchText.startsWith(">clip")) {
ClipboardService.refresh() clipboardHistory.refresh()
} }
} }
// Main filtering logic
property var filteredEntries: { property var filteredEntries: {
Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0) Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0)
if (!desktopEntries || desktopEntries.length === 0) { if (!desktopEntries || desktopEntries.length === 0) {
@ -103,11 +66,8 @@ NLoader {
"isCommand": true, "isCommand": true,
"name": ">calc", "name": ">calc",
"content": "Calculator - evaluate mathematical expressions", "content": "Calculator - evaluate mathematical expressions",
"icon": "tag", "icon": "calculate",
"execute": function () { "execute": executeCalcCommand
searchText = ">calc "
searchInput.cursorPosition = searchText.length
}
}) })
results.push({ results.push({
@ -115,10 +75,7 @@ NLoader {
"name": ">clip", "name": ">clip",
"content": "Clipboard history - browse and restore clipboard items", "content": "Clipboard history - browse and restore clipboard items",
"icon": "content_paste", "icon": "content_paste",
"execute": function () { "execute": executeClipCommand
searchText = ">clip "
searchInput.cursorPosition = searchText.length
}
}) })
return results return results
@ -126,154 +83,21 @@ NLoader {
// Handle clipboard history // Handle clipboard history
if (query.startsWith(">clip")) { if (query.startsWith(">clip")) {
const searchTerm = query.slice(5).trim() return clipboardHistory.processQuery(query)
ClipboardService.history.forEach(function (clip, index) {
let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip
if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) {
let entry
if (clip.type === 'image') {
entry = {
"isClipboard": true,
"name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(),
"content": "Image: " + clip.mimeType,
"icon": "image",
"type": 'image',
"data": clip.data,
"execute": function () {
const base64Data = clip.data.split(',')[1]
copyImageBase64(clip.mimeType, base64Data)
Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType])
}
}
} else {
const textContent = clip.content || clip
let displayContent = textContent
let previewContent = ""
displayContent = displayContent.replace(/\s+/g, ' ').trim()
if (displayContent.length > 50) {
previewContent = displayContent
displayContent = displayContent.split('\n')[0].substring(0, 50) + "..."
}
entry = {
"isClipboard": true,
"name": displayContent,
"content": previewContent || textContent,
"icon": "content_paste",
"execute": function () {
Quickshell.clipboardText = String(textContent)
copyText(String(textContent))
var preview = (textContent.length > 50) ? textContent.slice(0, 50) + "…" : textContent
Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview])
}
}
}
results.push(entry)
}
})
if (results.length === 0) {
results.push({
"isClipboard": true,
"name": "No clipboard history",
"content": "No matching clipboard entries found",
"icon": "content_paste_off"
})
}
return results
}
// Handle direct math expressions after ">"
if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) {
var mathExpr = query.slice(1).trim()
// Check if it looks like a math expression (contains numbers and math operators)
if (mathExpr && /[0-9+\-*/().]/.test(mathExpr)) {
try {
var sanitizedExpr = mathExpr.replace(/[^0-9+\-*/().\s]/g, '')
var result = eval(sanitizedExpr)
if (isFinite(result) && !isNaN(result)) {
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, '')
results.push({
"isCalculator": true,
"name": `${mathExpr} = ${displayResult}`,
"result": result,
"expr": mathExpr,
"icon": "tag",
"execute": function () {
Quickshell.clipboardText = displayResult
copyText(displayResult)
Quickshell.execDetached(
["notify-send", "Calculator", `${mathExpr} = ${displayResult} (copied to clipboard)`])
}
})
return results
}
} catch (error) {
// If math evaluation fails, fall through to regular search
}
}
} }
// Handle calculator // Handle calculator
if (query.startsWith(">calc")) { if (query.startsWith(">calc")) {
var expr = searchText.slice(5).trim() return calculator.processQuery(query, "calc")
if (expr && expr !== "") { }
try {
// Simple evaluation - only allow basic math operations
var sanitizedExpr = expr.replace(/[^0-9+\-*/().\s]/g, '')
var result = eval(sanitizedExpr)
if (isFinite(result) && !isNaN(result)) { // Handle direct math expressions after ">"
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) {
'') const mathResults = calculator.processQuery(query, "direct")
results.push({ if (mathResults.length > 0) {
"isCalculator": true, return mathResults
"name": `${expr} = ${displayResult}`,
"result": result,
"expr": expr,
"icon": "tag",
"execute": function () {
Quickshell.clipboardText = displayResult
copyText(displayResult)
Quickshell.execDetached(
["notify-send", "Calculator", `${expr} = ${displayResult} (copied to clipboard)`])
} }
}) // If math evaluation fails, fall through to regular search
} else {
results.push({
"isCalculator": true,
"name": "Invalid expression",
"content": "Please enter a valid mathematical expression",
"icon": "tag",
"execute": function () {}
})
}
} catch (error) {
results.push({
"isCalculator": true,
"name": "Invalid expression",
"content": "Please enter a valid mathematical expression",
"icon": "tag",
"execute": function () {}
})
}
} else {
// Show placeholder when just ">calc" is entered
results.push({
"isCalculator": true,
"name": "Calculator",
"content": "Enter a mathematical expression (e.g., 5+5, 2*3, 10/2)",
"icon": "tag",
"execute": function () {}
})
}
return results
} }
// Regular app search // Regular app search
@ -294,6 +118,46 @@ NLoader {
return results return results
} }
// Command execution functions
function executeCalcCommand() {
searchText = ">calc "
searchInput.cursorPosition = searchText.length
}
function executeClipCommand() {
searchText = ">clip "
searchInput.cursorPosition = searchText.length
}
// Navigation functions
function selectNext() {
if (filteredEntries.length > 0) {
selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1)
}
}
function selectPrev() {
if (filteredEntries.length > 0) {
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
function activateSelected() {
if (filteredEntries.length === 0)
return
var modelData = filteredEntries[selectedIndex]
if (modelData && modelData.execute) {
if (modelData.isCommand) {
modelData.execute()
return
} else {
modelData.execute()
}
root.close()
}
}
Component.onCompleted: { Component.onCompleted: {
Logger.log("AppLauncher", "Component completed") Logger.log("AppLauncher", "Component completed")
Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined') Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined')
@ -302,18 +166,11 @@ NLoader {
DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined') DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined')
} }
// Start clipboard refresh immediately on open // Start clipboard refresh immediately on open
updateClipboardHistory() clipboardHistory.refresh()
} }
// Main content container // Main content container
Rectangle { panelContent: Rectangle {
anchors.centerIn: parent
width: Math.min(700 * scaling, parent.width * 0.75)
height: Math.min(550 * scaling, parent.height * 0.8)
radius: Style.radiusL * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Style.borderS * scaling
// Subtle gradient background // Subtle gradient background
gradient: Gradient { gradient: Gradient {
@ -368,7 +225,8 @@ NLoader {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onTextChanged: { onTextChanged: {
searchText = text searchText = text
selectedIndex = 0 // Reset selection when search changes // Defer selectedIndex reset to avoid binding loops
Qt.callLater(() => selectedIndex = 0)
} }
selectedTextColor: Color.mOnSurface selectedTextColor: Color.mOnSurface
selectionColor: Color.mPrimary selectionColor: Color.mPrimary
@ -380,20 +238,16 @@ NLoader {
bottomPadding: 0 bottomPadding: 0
font.bold: true font.bold: true
Component.onCompleted: { Component.onCompleted: {
contentItem.cursorColor = Color.mOnSurface
contentItem.verticalAlignment = TextInput.AlignVCenter
// Focus the search bar by default // Focus the search bar by default
Qt.callLater(() => { Qt.callLater(() => {
searchInput.forceActiveFocus() searchInput.forceActiveFocus()
}) })
} }
onActiveFocusChanged: contentItem.cursorColor = Color.mOnSurface
Keys.onDownPressed: selectNext() Keys.onDownPressed: selectNext()
Keys.onUpPressed: selectPrev() Keys.onUpPressed: selectPrev()
Keys.onEnterPressed: activateSelected() Keys.onEnterPressed: activateSelected()
Keys.onReturnPressed: activateSelected() Keys.onReturnPressed: activateSelected()
Keys.onEscapePressed: appLauncherPanel.hide() Keys.onEscapePressed: root.close()
} }
} }
@ -486,8 +340,8 @@ NLoader {
anchors.margins: Style.marginXS * scaling anchors.margins: Style.marginXS * scaling
asynchronous: true asynchronous: true
source: modelData.isCalculator ? "" : modelData.isClipboard ? "" : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "") source: modelData.isCalculator ? "" : modelData.isClipboard ? "" : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "")
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded)
|| parent.iconLoaded) && modelData.type !== 'image' && modelData.type !== 'image'
} }
// Fallback icon container // Fallback icon container
@ -500,7 +354,7 @@ NLoader {
visible: !parent.iconLoaded visible: !parent.iconLoaded
} }
Text { NText {
anchors.centerIn: parent anchors.centerIn: parent
visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard
|| modelData.isCommand) || modelData.isCommand)
@ -584,5 +438,3 @@ NLoader {
} }
} }
} }
}
}

View file

@ -0,0 +1,151 @@
import QtQuick
import Quickshell
import qs.Commons
import "../../Helpers/AdvancedMath.js" as AdvancedMath
QtObject {
id: calculator
// Function to evaluate mathematical expressions
function evaluate(expression) {
if (!expression || expression.trim() === "") {
return {
"isValid": false,
"result": "",
"displayResult": "",
"error": "Empty expression"
}
}
try {
// Try advanced math first
if (typeof AdvancedMath !== 'undefined') {
const result = AdvancedMath.evaluate(expression.trim())
const displayResult = AdvancedMath.formatResult(result)
return {
"isValid": true,
"result": result,
"displayResult": displayResult,
"expression": expression,
"error": ""
}
} else {
// Fallback to basic evaluation
console.log("AdvancedMath not available, using basic eval")
// Basic preprocessing for common functions
var processed = expression.trim(
).replace(/\bpi\b/gi,
Math.PI).replace(/\be\b/gi,
Math.E).replace(/\bsqrt\s*\(/g,
'Math.sqrt(').replace(/\bsin\s*\(/g,
'Math.sin(').replace(/\bcos\s*\(/g,
'Math.cos(').replace(/\btan\s*\(/g, 'Math.tan(').replace(/\blog\s*\(/g, 'Math.log10(').replace(/\bln\s*\(/g, 'Math.log(').replace(/\bexp\s*\(/g, 'Math.exp(').replace(/\bpow\s*\(/g, 'Math.pow(').replace(/\babs\s*\(/g, 'Math.abs(')
// Sanitize and evaluate
if (!/^[0-9+\-*/().\s\w,]+$/.test(processed)) {
throw new Error("Invalid characters in expression")
}
const result = eval(processed)
if (!isFinite(result) || isNaN(result)) {
throw new Error("Invalid result")
}
const displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, '')
return {
"isValid": true,
"result": result,
"displayResult": displayResult,
"expression": expression,
"error": ""
}
}
} catch (error) {
return {
"isValid": false,
"result": "",
"displayResult": "",
"error": error.message || error.toString()
}
}
}
// Generate calculator entry for display
function createEntry(expression, searchContext = "") {
const evaluation = evaluate(expression)
if (!evaluation.isValid) {
return {
"isCalculator": true,
"name": "Invalid expression",
"content": evaluation.error,
"icon": "error",
"execute": function () {// Do nothing for invalid expressions
}
}
}
const displayName = searchContext
=== "calc" ? `${expression} = ${evaluation.displayResult}` : `${expression} = ${evaluation.displayResult}`
return {
"isCalculator": true,
"name": displayName,
"result": evaluation.result,
"expr": expression,
"displayResult": evaluation.displayResult,
"icon": "calculate",
"execute": function () {
Quickshell.clipboardText = evaluation.displayResult
// Also copy using shell command for better compatibility
Quickshell.execDetached(
["sh", "-lc", `printf %s ${evaluation.displayResult} | wl-copy -t text/plain;charset=utf-8`])
Quickshell.execDetached(
["notify-send", "Calculator", `${expression} = ${evaluation.displayResult} (copied to clipboard)`])
}
}
}
// Create placeholder entry for empty calculator mode
function createPlaceholderEntry() {
return {
"isCalculator": true,
"name": "Calculator",
"content": "Try: sqrt(16), sin(1), cos(0), pi*2, exp(1), pow(2,8), abs(-5)",
"icon": "calculate",
"execute": function () {// Do nothing for placeholder
}
}
}
// Process calculator queries
function processQuery(query, searchContext = "") {
const results = []
if (searchContext === "calc") {
// Handle ">calc" mode
const expr = query.slice(5).trim()
if (expr && expr !== "") {
results.push(createEntry(expr, "calc"))
} else {
results.push(createPlaceholderEntry())
}
} else if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) {
// Handle direct math expressions after ">"
const mathExpr = query.slice(1).trim()
const evaluation = evaluate(mathExpr)
if (evaluation.isValid) {
results.push(createEntry(mathExpr, "direct"))
}
// If invalid, don't add anything - let it fall through to regular search
}
return results
}
}

View file

@ -0,0 +1,157 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services
QtObject {
id: clipboardHistory
// Copy helpers for different content types
function copyImageBase64(mime, base64) {
Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`])
}
function copyText(text) {
// Use printf with proper quoting to handle special characters
Quickshell.execDetached(["sh", "-c", `printf '%s' ${JSON.stringify(text)} | wl-copy -t text/plain`])
}
// Create clipboard entry for display
function createClipboardEntry(clip, index) {
if (clip.type === 'image') {
return {
"isClipboard": true,
"name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(),
"content": "Image: " + clip.mimeType,
"icon": "image",
"type": 'image',
"data": clip.data,
"timestamp": clip.timestamp,
"index": index,
"execute": function () {
const dataParts = clip.data.split(',')
const base64Data = dataParts.length > 1 ? dataParts[1] : clip.data
copyImageBase64(clip.mimeType, base64Data)
Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType])
}
}
} else {
// Handle text content
const textContent = clip.content || clip
let displayContent = textContent
let previewContent = ""
// Normalize whitespace for display
displayContent = displayContent.replace(/\s+/g, ' ').trim()
// Create preview for long content
if (displayContent.length > 50) {
previewContent = displayContent
displayContent = displayContent.split('\n')[0].substring(0, 50) + "..."
}
return {
"isClipboard": true,
"name": displayContent,
"content": previewContent || textContent,
"icon": "content_paste",
"type": 'text',
"timestamp": clip.timestamp,
"index": index,
"textData": textContent,
"execute"// Store the text data for the execute function
: function () {
const text = this.textData || clip.content || clip
Quickshell.clipboardText = String(text)
copyText(String(text))
var preview = (text.length > 50) ? text.slice(0, 50) + "…" : text
Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview])
}
}
}
}
// Create empty state entry
function createEmptyEntry() {
return {
"isClipboard": true,
"name": "No clipboard history",
"content": "No matching clipboard entries found",
"icon": "content_paste_off",
"execute": function () {// Do nothing for empty state
}
}
}
// Process clipboard queries
function processQuery(query) {
const results = []
if (!query.startsWith(">clip")) {
return results
}
// Extract search term after ">clip "
const searchTerm = query.slice(5).trim()
// Note: Clipboard refresh should be handled externally to avoid binding loops
// Process each clipboard item
ClipboardService.history.forEach(function (clip, index) {
let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip
// Apply search filter if provided
if (!searchTerm || searchContent.toLowerCase().includes(searchTerm.toLowerCase())) {
const entry = createClipboardEntry(clip, index)
results.push(entry)
}
})
// Show empty state if no results
if (results.length === 0) {
results.push(createEmptyEntry())
}
return results
}
// Create command entry for clipboard mode (deprecated - use direct creation in parent)
function createCommandEntry() {
return {
"isCommand": true,
"name": ">clip",
"content": "Clipboard history - browse and restore clipboard items",
"icon": "content_paste",
"execute": function () {// This should be handled by the parent component
}
}
}
// Utility function to refresh clipboard
function refresh() {
ClipboardService.refresh()
}
// Get clipboard history count
function getHistoryCount() {
return ClipboardService.history ? ClipboardService.history.length : 0
}
// Get formatted timestamp for display
function formatTimestamp(timestamp) {
return new Date(timestamp).toLocaleTimeString()
}
// Get clipboard entry by index
function getEntryByIndex(index) {
if (ClipboardService.history && index >= 0 && index < ClipboardService.history.length) {
return ClipboardService.history[index]
}
return null
}
// Clear all clipboard history
function clearAll() {
ClipboardService.clearHistory()
}
}

View file

@ -4,7 +4,10 @@ import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Services import qs.Services
Variants { Loader {
active: !Settings.data.wallpaper.swww.enabled
sourceComponent: Variants {
model: Quickshell.screens model: Quickshell.screens
delegate: PanelWindow { delegate: PanelWindow {
@ -50,3 +53,4 @@ Variants {
} }
} }
} }
}

View file

@ -6,14 +6,12 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { Loader {
active: CompositorService.isNiri active: CompositorService.isNiri
Component.onCompleted: { Component.onCompleted: {
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
Logger.log("Overview", "Loading Overview component (Niri detected)") Logger.log("Overview", "Loading Overview component for Niri")
} else {
Logger.log("Overview", "Skipping Overview component (Niri not detected)")
} }
} }

View file

@ -6,10 +6,10 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { Loader {
isLoaded: Settings.data.general.showScreenCorners active: Settings.data.general.showScreenCorners
content: Variants { sourceComponent: Variants {
model: Quickshell.screens model: Quickshell.screens
PanelWindow { PanelWindow {

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@ -17,6 +18,8 @@ Variants {
readonly property real scaling: ScalingService.scale(screen) readonly property real scaling: ScalingService.scale(screen)
screen: modelData screen: modelData
WlrLayershell.namespace: "noctalia-bar"
implicitHeight: Style.barHeight * scaling implicitHeight: Style.barHeight * scaling
color: Color.transparent color: Color.transparent
@ -119,16 +122,6 @@ Variants {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
// NIconButton {
// id: demoPanelToggle
// icon: "experiment"
// tooltipText: "Open Demo Panel"
// sizeMultiplier: 0.8
// anchors.verticalCenter: parent.verticalCenter
// onClicked: {
// demoPanel.isLoaded = !demoPanel.isLoaded
// }
// }
SidePanelToggle {} SidePanelToggle {}
} }
} }

View file

@ -31,25 +31,10 @@ NIconButton {
} }
tooltipText: "Bluetooth Devices" tooltipText: "Bluetooth Devices"
onClicked: { onClicked: {
if (!bluetoothMenuLoader.active) { bluetoothPanel.toggle(screen)
bluetoothMenuLoader.isLoaded = true
}
if (bluetoothMenuLoader.item) {
if (bluetoothMenuLoader.item.visible) {
// Panel is visible, hide it with animation
if (bluetoothMenuLoader.item.hide) {
bluetoothMenuLoader.item.hide()
} else {
bluetoothMenuLoader.item.visible = false
}
} else {
// Panel is hidden, show it
bluetoothMenuLoader.item.visible = true
}
}
} }
BluetoothMenu { BluetoothPanel {
id: bluetoothMenuLoader id: bluetoothPanel
} }
} }

View file

@ -1,496 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
// Loader for Bluetooth menu
NLoader {
id: root
content: Component {
NPanel {
id: bluetoothPanel
function hide() {
bluetoothMenuRect.scaleValue = 0.8
bluetoothMenuRect.opacityValue = 0.0
hideTimer.start()
}
// Connect to NPanel's dismissed signal to handle external close events
Connections {
target: bluetoothPanel
ignoreUnknownSignals: true
function onDismissed() {
// Start hide animation
bluetoothMenuRect.scaleValue = 0.8
bluetoothMenuRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Also handle visibility changes from external sources
onVisibleChanged: {
if (visible && Settings.data.network.bluetoothEnabled) {
// Always refresh devices when menu opens to get fresh device objects
BluetoothService.adapter.discovering = true
} else if (bluetoothMenuRect.opacityValue > 0) {
// Start hide animation
bluetoothMenuRect.scaleValue = 0.8
bluetoothMenuRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Timer to hide panel after animation
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
bluetoothPanel.visible = false
bluetoothPanel.dismissed()
}
}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Rectangle {
id: bluetoothMenuRect
property var deviceData: null
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
width: 380 * scaling
height: 500 * scaling
anchors {
right: parent.right
rightMargin: Style.marginXS * scaling
top: Settings.data.bar.position === "top" ? parent.top : undefined
bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
topMargin: Settings.data.bar.position === "top" ? Style.marginXS * scaling : undefined
bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginXS * scaling : undefined
}
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
scale: scaleValue
opacity: opacityValue
// Prevent closing the window if clicking inside it
MouseArea {
anchors.fill: parent
}
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// HEADER
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
text: "bluetooth"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "Bluetooth"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh"
tooltipText: "Refresh Devices"
sizeMultiplier: 0.8
onClicked: {
if (BluetoothService.adapter) {
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
onClicked: {
bluetoothPanel.hide()
}
}
}
NDivider {}
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
// Available devices
Column {
id: column
width: parent.width
spacing: Style.marginM * scaling
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
RowLayout {
width: parent.width
spacing: Style.marginM * scaling
NText {
text: "Available Devices"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
}
}
Repeater {
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked && (dev.signalStrength === undefined
|| dev.signalStrength > 0)
})
return BluetoothService.sortDevices(filtered)
}
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 70
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Color.mTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
}
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
Row {
anchors.left: parent.left
anchors.leftMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Style.marginXXS * scaling
anchors.verticalCenter: parent.verticalCenter
// One device name
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fonttSizeMedium * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
}
Row {
spacing: Style.marginXS * scaling
Row {
spacing: Style.marginS * spacing
// One device signal strength - "Unknown" when not connected
NText {
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
}
NIcon {
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
}
}
}
}
Rectangle {
width: 80 * scaling
height: 28 * scaling
radius: Style.radiusM * scaling
anchors.right: parent.right
anchors.rightMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy ? 1 : 0.5
// On device connect button
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return "Connect"
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
// Fallback if nothing available
Column {
width: parent.width
spacing: Style.marginM * scaling
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return false
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.marginM * scaling
NIcon {
text: "sync"
font.pointSize: Style.fontSizeXLL * 1.5 * scaling
color: Color.mPrimary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
}
NText {
text: "Scanning for devices..."
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
}
}
NText {
text: "Make sure your device is in pairing mode"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
anchors.horizontalCenter: parent.horizontalCenter
}
}
NText {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
visible: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return true
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0 && !BluetoothService.adapter.discovering
}
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
}
// This item takes up all the remaining vertical space.
Item {
Layout.fillHeight: true
}
}
}
}
}
}

View file

@ -0,0 +1,398 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
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.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// HEADER
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
text: "bluetooth"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "Bluetooth"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh"
tooltipText: "Refresh Devices"
sizeMultiplier: 0.8
onClicked: {
if (BluetoothService.adapter) {
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
onClicked: {
root.close()
}
}
}
NDivider {
Layout.fillWidth: true
}
ScrollView {
id: scrollView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
// Available devices
Column {
id: column
width: parent.width
spacing: Style.marginM * scaling
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
RowLayout {
width: parent.width
spacing: Style.marginM * scaling
NText {
text: "Available Devices"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
}
}
Repeater {
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing && !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
})
return BluetoothService.sortDevices(filtered)
}
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 70
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Color.mTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
}
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
Row {
anchors.left: parent.left
anchors.leftMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Style.marginXXS * scaling
anchors.verticalCenter: parent.verticalCenter
// One device name
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fonttSizeMedium * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
}
Row {
spacing: Style.marginXS * scaling
Row {
spacing: Style.marginS * spacing
// One device signal strength - "Unknown" when not connected
NText {
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
}
NIcon {
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
}
}
}
}
Rectangle {
width: 80 * scaling
height: 28 * scaling
radius: Style.radiusM * scaling
anchors.right: parent.right
anchors.rightMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy ? 1 : 0.5
// On device connect button
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return "Connect"
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
// Fallback if nothing available
Column {
width: parent.width
spacing: Style.marginM * scaling
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return false
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.marginM * scaling
NIcon {
text: "sync"
font.pointSize: Style.fontSizeXLL * 1.5 * scaling
color: Color.mPrimary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
}
NText {
text: "Scanning for devices..."
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
}
}
NText {
text: "Make sure your device is in pairing mode"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
anchors.horizontalCenter: parent.horizontalCenter
}
}
NText {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
visible: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return true
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0 && !BluetoothService.adapter.discovering
}
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
}
// This item takes up all the remaining vertical space.
Item {
Layout.fillHeight: true
}
}
}
}

View file

@ -70,7 +70,7 @@ Item {
onClicked: { onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
settingsPanel.isLoaded = true settingsPanel.open(screen)
} }
} }
} }

View file

@ -24,7 +24,7 @@ Rectangle {
} }
onEntered: { onEntered: {
if (!calendarPanel.isLoaded) { if (!calendarPanel.active) {
tooltip.show() tooltip.show()
} }
} }
@ -33,7 +33,7 @@ Rectangle {
} }
onClicked: { onClicked: {
tooltip.hide() tooltip.hide()
calendarPanel.isLoaded = !calendarPanel.isLoaded calendarPanel.toggle(screen)
} }
} }
} }

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import qs.Modules.Audio
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@ -31,8 +32,6 @@ Row {
height: Math.round(Style.capsuleHeight * scaling) height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
border.color: Color.mOutline
border.width: Math.max(1, Math.round(Style.borderS * scaling))
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -42,19 +41,102 @@ Row {
anchors.leftMargin: Style.marginS * scaling anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" && MediaService.isPlaying
z: 0
sourceComponent: LinearSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: 20 * scaling
values: CavaService.values
fillColor: Color.mPrimary
opacity: 0.4
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" && MediaService.isPlaying
z: 0
sourceComponent: MirroredSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mPrimary
opacity: 0.4
}
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" && MediaService.isPlaying
z: 0
sourceComponent: WaveSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mPrimary
opacity: 0.4
}
}
}
Row { Row {
id: row id: row
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
z: 1 // Above the visualizer
// Window icon
NIcon { NIcon {
id: windowIcon id: windowIcon
text: MediaService.isPlaying ? "pause" : "play_arrow" text: MediaService.isPlaying ? "pause" : "play_arrow"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: getTitle() !== "" visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
}
Column {
anchors.verticalCenter: parent.verticalCenter
visible: Settings.data.audio.showMiniplayerAlbumArt
Rectangle {
width: 16 * scaling
height: 16 * scaling
radius: width * 0.5
color: Color.transparent
antialiasing: true
clip: true
NImageRounded {
id: trackArt
visible: MediaService.trackArtUrl.toString() !== ""
anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter
anchors.margins: scaling
imagePath: MediaService.trackArtUrl
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow"
borderWidth: 0
border.color: Color.transparent
imageRadius: width
antialiasing: true
}
// Fallback icon when no album art available
NIcon {
id: windowIconFallback
text: MediaService.isPlaying ? "pause" : "play_arrow"
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
visible: getTitle() !== "" && !trackArt.visible
}
}
} }
NText { NText {

View file

@ -20,21 +20,6 @@ NIconButton {
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
onClicked: { onClicked: {
if (!notificationHistoryPanel.active) { notificationHistoryPanel.toggle(screen)
notificationHistoryPanel.isLoaded = true
}
if (notificationHistoryPanel.item) {
if (notificationHistoryPanel.item.visible) {
// Panel is visible, hide it with animation
if (notificationHistoryPanel.item.hide) {
notificationHistoryPanel.item.hide()
} else {
notificationHistoryPanel.item.visible = false
}
} else {
// Panel is hidden, show it
notificationHistoryPanel.item.visible = true
}
}
} }
} }

View file

@ -14,23 +14,5 @@ NIconButton {
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onClicked: { onClicked: sidePanel.toggle(screen)
// Map this button's center to the screen and open the side panel below it
const localCenterX = width / 2
const localCenterY = height / 2
const globalPoint = mapToItem(null, localCenterX, localCenterY)
if (sidePanel.isLoaded) {
// Call hide() instead of directly setting isLoaded to false
if (sidePanel.item && sidePanel.item.hide) {
sidePanel.item.hide()
} else {
sidePanel.isLoaded = false
}
} else if (sidePanel.openAt) {
sidePanel.openAt(globalPoint.x, screen)
} else {
// Fallback: toggle if API unavailable
sidePanel.isLoaded = true
}
}
} }

View file

@ -76,37 +76,34 @@ Rectangle {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
// Close any open menu first // Close any open menu first
if (trayMenu && trayMenu.visible) { trayPanel.close()
trayMenu.hideMenu()
}
if (!modelData.onlyMenu) { if (!modelData.onlyMenu) {
modelData.activate() modelData.activate()
} }
} else if (mouse.button === Qt.MiddleButton) { } else if (mouse.button === Qt.MiddleButton) {
// Close any open menu first // Close any open menu first
if (trayMenu && trayMenu.visible) { trayPanel.close()
trayMenu.hideMenu()
}
modelData.secondaryActivate && modelData.secondaryActivate() modelData.secondaryActivate && modelData.secondaryActivate()
} else if (mouse.button === Qt.RightButton) { } else if (mouse.button === Qt.RightButton) {
trayTooltip.hide() trayTooltip.hide()
// If menu is already visible, close it
if (trayMenu && trayMenu.visible) { // Close the menu if it was visible
trayMenu.hideMenu() if (trayPanel && trayPanel.visible) {
trayPanel.close()
return return
} }
if (modelData.hasMenu && modelData.menu && trayMenu) { if (modelData.hasMenu && modelData.menu && trayMenu) {
trayPanel.open()
// Anchor the menu to the tray icon item (parent) and position it below the icon // Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.width / 2) const menuX = (width / 2) - (trayMenu.width / 2)
const menuY = (Style.barHeight * scaling) const menuY = (Style.barHeight * scaling)
trayMenu.menu = modelData.menu trayMenu.menu = modelData.menu
trayMenu.showAt(parent, menuX, menuY) trayMenu.showAt(parent, menuX, menuY)
trayPanel.show()
} else { } else {
Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set") Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
} }
} }
@ -125,89 +122,33 @@ Rectangle {
} }
} }
// Attached TrayMenu drop down PanelWindow {
// Wrapped in NPanel so we can detect click outside of the menu to close the TrayMenu
NPanel {
id: trayPanel id: trayPanel
showOverlay: false // no colors overlay even if activated in settings anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
visible: false
color: Color.transparent
screen: screen
// Override hide function to animate first function open() {
function hide() { visible = true
// Start hide animation
trayMenuRect.scaleValue = 0.8
trayMenuRect.opacityValue = 0.0
// Hide after animation completes // Register into the panel service
hideTimer.start() // so this will autoclose if we open another panel
PanelService.registerOpen(trayPanel)
} }
Connections { function close() {
target: trayPanel visible = false
ignoreUnknownSignals: true
function onDismissed() {
// Start hide animation
trayMenuRect.scaleValue = 0.8
trayMenuRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Also handle visibility changes from external sources
onVisibleChanged: {
if (!visible && trayMenuRect.opacityValue > 0) {
// Start hide animation
trayMenuRect.scaleValue = 0.8
trayMenuRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Timer to hide panel after animation
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
trayPanel.visible = false
trayMenu.hideMenu() trayMenu.hideMenu()
} }
}
Rectangle { // Clicking outside of the rectangle to close
id: trayMenuRect MouseArea {
color: Color.transparent
anchors.fill: parent anchors.fill: parent
onClicked: trayPanel.close()
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
scale: scaleValue
opacity: opacityValue
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
} }
TrayMenu { TrayMenu {
@ -215,4 +156,3 @@ Rectangle {
} }
} }
} }
}

View file

@ -65,7 +65,7 @@ Item {
} }
onClicked: { onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
settingsPanel.isLoaded = true settingsPanel.open(screen)
} }
} }
} }

View file

@ -34,27 +34,10 @@ NIconButton {
} }
tooltipText: "WiFi Networks" tooltipText: "WiFi Networks"
onClicked: { onClicked: {
if (!wifiMenuLoader.active) { wifiPanel.toggle(screen)
wifiMenuLoader.isLoaded = true
}
if (wifiMenuLoader.item) {
if (wifiMenuLoader.item.visible) {
// Panel is visible, hide it with animation
if (wifiMenuLoader.item.hide) {
wifiMenuLoader.item.hide()
} else {
wifiMenuLoader.item.visible = false
NetworkService.onMenuClosed()
}
} else {
// Panel is hidden, show it
wifiMenuLoader.item.visible = true
NetworkService.onMenuOpened()
}
}
} }
WiFiMenu { WiFiPanel {
id: wifiMenuLoader id: wifiPanel
} }
} }

View file

@ -1,435 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
// Loader for WiFi menu
NLoader {
id: root
content: Component {
NPanel {
id: wifiPanel
property string passwordPromptSsid: ""
property string passwordInput: ""
property bool showPasswordPrompt: false
function hide() {
wifiMenuRect.scaleValue = 0.8
wifiMenuRect.opacityValue = 0.0
hideTimer.start()
}
// Connect to NPanel's dismissed signal to handle external close events
Connections {
target: wifiPanel
ignoreUnknownSignals: true
function onDismissed() {
// Start hide animation
wifiMenuRect.scaleValue = 0.8
wifiMenuRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Also handle visibility changes from external sources
onVisibleChanged: {
if (visible && Settings.data.network.wifiEnabled) {
NetworkService.refreshNetworks()
} else if (wifiMenuRect.opacityValue > 0) {
// Start hide animation
wifiMenuRect.scaleValue = 0.8
wifiMenuRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Timer to hide panel after animation
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
wifiPanel.visible = false
wifiPanel.dismissed()
// NetworkService.onMenuClosed()
}
}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
// Timer to refresh networks when WiFi is enabled while menu is open
Timer {
id: wifiEnableRefreshTimer
interval: 3000 // Wait 3 seconds for WiFi to be fully ready
repeat: false
onTriggered: {
if (Settings.data.network.wifiEnabled && wifiPanel.visible) {
NetworkService.refreshNetworks()
}
}
}
Rectangle {
id: wifiMenuRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
width: 340 * scaling
height: 500 * scaling
anchors {
right: parent.right
rightMargin: Style.marginXS * scaling
top: Settings.data.bar.position === "top" ? parent.top : undefined
bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
topMargin: Settings.data.bar.position === "top" ? Style.marginXS * scaling : undefined
bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginXS * scaling : undefined
}
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
scale: scaleValue
opacity: opacityValue
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
text: "wifi"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "WiFi"
font.pointSize: Style.fontSizeL * scaling
font.bold: true
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: "refresh"
tooltipText: "Refresh Networks"
sizeMultiplier: 0.8
enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading
onClicked: {
NetworkService.refreshNetworks()
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
onClicked: {
wifiPanel.hide()
}
}
}
NDivider {}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
// Loading indicator
ColumnLayout {
anchors.centerIn: parent
visible: Settings.data.network.wifiEnabled && NetworkService.isLoading
spacing: Style.marginM * scaling
NBusyIndicator {
running: NetworkService.isLoading
color: Color.mPrimary
size: Style.baseWidgetSize * scaling
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Scanning for networks..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
// WiFi disabled message
ColumnLayout {
anchors.centerIn: parent
visible: !Settings.data.network.wifiEnabled
spacing: Style.marginM * scaling
NIcon {
text: "wifi_off"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "WiFi is disabled"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable WiFi to see available networks"
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
// Network list
ListView {
id: networkList
anchors.fill: parent
visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading
model: Object.values(NetworkService.networks)
spacing: Style.marginM * scaling
clip: true
delegate: Item {
width: parent ? parent.width : 0
height: modelData.ssid === passwordPromptSsid
&& showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling
radius: Style.radiusM * scaling
color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : Color.transparent)
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
NIcon {
text: NetworkService.signalIcon(modelData.signal)
font.pointSize: Style.fontSizeXXL * scaling
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXS * scaling
// SSID
NText {
text: modelData.ssid || "Unknown Network"
font.pointSize: Style.fontSizeNormal * scaling
elide: Text.ElideRight
Layout.fillWidth: true
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
}
// Security Protocol
NText {
text: modelData.security && modelData.security !== "--" ? modelData.security : "Open"
font.pointSize: Style.fontSizeXS * scaling
elide: Text.ElideRight
Layout.fillWidth: true
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
}
NText {
visible: NetworkService.connectStatusSsid === modelData.ssid
&& NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0
text: NetworkService.connectError
color: Color.mError
font.pointSize: Style.fontSizeXS * scaling
elide: Text.ElideRight
Layout.fillWidth: true
}
}
Item {
Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling
visible: NetworkService.connectStatusSsid === modelData.ssid
&& (NetworkService.connectStatus !== ""
|| NetworkService.connectingSsid === modelData.ssid)
NBusyIndicator {
visible: NetworkService.connectingSsid === modelData.ssid
running: NetworkService.connectingSsid === modelData.ssid
color: Color.mPrimary
anchors.centerIn: parent
size: Style.baseWidgetSize * 0.7 * scaling
}
}
NText {
visible: modelData.connected
text: "connected"
font.pointSize: Style.fontSizeXS * scaling
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
}
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (modelData.connected) {
NetworkService.disconnectNetwork(modelData.ssid)
} else if (NetworkService.isSecured(modelData.security) && !modelData.existing) {
passwordPromptSsid = modelData.ssid
showPasswordPrompt = true
passwordInput = "" // Clear previous input
Qt.callLater(function () {
passwordInputField.forceActiveFocus()
})
} else {
NetworkService.connectNetwork(modelData.ssid, modelData.security)
}
}
}
}
// Password prompt section
Rectangle {
id: passwordPromptSection
Layout.fillWidth: true
Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0
Layout.margins: Style.marginS * scaling
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.barHeight * scaling
Rectangle {
anchors.fill: parent
radius: Style.radiusXS * scaling
color: Color.transparent
border.color: passwordInputField.activeFocus ? Color.mPrimary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
TextInput {
id: passwordInputField
anchors.fill: parent
anchors.margins: Style.marginM * scaling
text: passwordInput
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
echoMode: TextInput.Password
onTextChanged: passwordInput = text
onAccepted: {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
}
MouseArea {
id: passwordInputMouseArea
anchors.fill: parent
onClicked: passwordInputField.forceActiveFocus()
}
}
}
}
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 2.5 * scaling
Layout.preferredHeight: Style.barHeight * scaling
radius: Style.radiusM * scaling
color: Color.mPrimary
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
anchors.centerIn: parent
text: "Connect"
color: Color.mSurface
font.pointSize: Style.fontSizeXS * scaling
}
MouseArea {
anchors.fill: parent
onClicked: {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
}
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.color = Qt.darker(Color.mPrimary, 1.1)
onExited: parent.color = Color.mPrimary
}
}
}
}
}
}
}
}
}
}
}
}
}

335
Modules/Bar/WiFiPanel.qml Normal file
View file

@ -0,0 +1,335 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
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
property string passwordPromptSsid: ""
property string passwordInput: ""
property bool showPasswordPrompt: false
onOpened: {
if (Settings.data.network.wifiEnabled && wifiPanel.visible) {
NetworkService.refreshNetworks()
}
}
panelContent: Rectangle {
color: Color.transparent
anchors.fill: parent
anchors.margins: Style.marginL * scaling
ColumnLayout {
anchors.fill: parent
// Header
RowLayout {
NIcon {
text: "wifi"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "WiFi"
font.pointSize: Style.fontSizeL * scaling
font.bold: true
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: "refresh"
tooltipText: "Refresh Networks"
sizeMultiplier: 0.8
enabled: Settings.data.network.wifiEnabled && !NetworkService.isLoading
onClicked: {
NetworkService.refreshNetworks()
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeMultiplier: 0.8
onClicked: {
root.close()
}
}
}
NDivider {
Layout.fillWidth: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
// Loading indicator
ColumnLayout {
anchors.centerIn: parent
visible: Settings.data.network.wifiEnabled && NetworkService.isLoading
spacing: Style.marginM * scaling
NBusyIndicator {
running: NetworkService.isLoading
color: Color.mPrimary
size: Style.baseWidgetSize * scaling
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Scanning for networks..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
// WiFi disabled message
ColumnLayout {
anchors.centerIn: parent
visible: !Settings.data.network.wifiEnabled
spacing: Style.marginM * scaling
NIcon {
text: "wifi_off"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "WiFi is disabled"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable WiFi to see available networks"
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
// Network list
ListView {
id: networkList
anchors.fill: parent
visible: Settings.data.network.wifiEnabled && !NetworkService.isLoading
model: Object.values(NetworkService.networks)
spacing: Style.marginM * scaling
clip: true
delegate: Item {
width: parent ? parent.width : 0
height: modelData.ssid === passwordPromptSsid
&& showPasswordPrompt ? 108 * scaling : Style.baseWidgetSize * 1.5 * scaling
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling
radius: Style.radiusS * scaling
color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : Color.transparent)
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
NIcon {
text: NetworkService.signalIcon(modelData.signal)
font.pointSize: Style.fontSizeXXL * scaling
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXS * scaling
// SSID
NText {
text: modelData.ssid || "Unknown Network"
font.pointSize: Style.fontSizeNormal * scaling
elide: Text.ElideRight
Layout.fillWidth: true
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
}
// Security Protocol
NText {
text: modelData.security && modelData.security !== "--" ? modelData.security : "Open"
font.pointSize: Style.fontSizeXS * scaling
elide: Text.ElideRight
Layout.fillWidth: true
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
}
NText {
visible: NetworkService.connectStatusSsid === modelData.ssid
&& NetworkService.connectStatus === "error" && NetworkService.connectError.length > 0
text: NetworkService.connectError
color: Color.mError
font.pointSize: Style.fontSizeXS * scaling
elide: Text.ElideRight
Layout.fillWidth: true
}
}
Item {
Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling
visible: NetworkService.connectStatusSsid === modelData.ssid
&& (NetworkService.connectStatus !== ""
|| NetworkService.connectingSsid === modelData.ssid)
NBusyIndicator {
visible: NetworkService.connectingSsid === modelData.ssid
running: NetworkService.connectingSsid === modelData.ssid
color: Color.mPrimary
anchors.centerIn: parent
size: Style.baseWidgetSize * 0.7 * scaling
}
}
NText {
visible: modelData.connected
text: "connected"
font.pointSize: Style.fontSizeXS * scaling
color: modelData.connected ? Color.mSurface : (networkMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface)
}
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (modelData.connected) {
NetworkService.disconnectNetwork(modelData.ssid)
} else if (NetworkService.isSecured(modelData.security) && !modelData.existing) {
passwordPromptSsid = modelData.ssid
showPasswordPrompt = true
passwordInput = "" // Clear previous input
Qt.callLater(function () {
passwordInputField.forceActiveFocus()
})
} else {
NetworkService.connectNetwork(modelData.ssid, modelData.security)
}
}
}
}
// Password prompt section
Rectangle {
id: passwordPromptSection
Layout.fillWidth: true
Layout.preferredHeight: modelData.ssid === passwordPromptSsid && showPasswordPrompt ? 60 : 0
Layout.margins: Style.marginS * scaling
visible: modelData.ssid === passwordPromptSsid && showPasswordPrompt
color: Color.mSurfaceVariant
radius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
Item {
Layout.fillWidth: true
Layout.preferredHeight: Style.barHeight * scaling
Rectangle {
anchors.fill: parent
radius: Style.radiusXS * scaling
color: Color.transparent
border.color: passwordInputField.activeFocus ? Color.mPrimary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
TextInput {
id: passwordInputField
anchors.fill: parent
anchors.margins: Style.marginM * scaling
text: passwordInput
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
echoMode: TextInput.Password
onTextChanged: passwordInput = text
onAccepted: {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
}
MouseArea {
id: passwordInputMouseArea
anchors.fill: parent
onClicked: passwordInputField.forceActiveFocus()
}
}
}
}
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 2.5 * scaling
Layout.preferredHeight: Style.barHeight * scaling
radius: Style.radiusM * scaling
color: Color.mPrimary
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
NText {
anchors.centerIn: parent
text: "Connect"
color: Color.mSurface
font.pointSize: Style.fontSizeXS * scaling
}
MouseArea {
anchors.fill: parent
onClicked: {
NetworkService.submitPassword(passwordPromptSsid, passwordInput)
showPasswordPrompt = false
}
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.color = Qt.darker(Color.mPrimary, 1.1)
onExited: parent.color = Color.mPrimary
}
}
}
}
}
}
}
}
}
}
}

View file

@ -7,107 +7,15 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { NPanel {
id: root id: root
content: Component { panelWidth: 340 * scaling
NPanel { panelHeight: 320 * scaling
id: calendarPanel panelAnchorRight: true
// Override hide function to animate first
function hide() {
// Start hide animation
calendarRect.scaleValue = 0.8
calendarRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
// Connect to NPanel's dismissed signal to handle external close events
Connections {
target: calendarPanel
function onDismissed() {
// Start hide animation
calendarRect.scaleValue = 0.8
calendarRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Also handle visibility changes from external sources
onVisibleChanged: {
if (!visible && calendarRect.opacityValue > 0) {
// Start hide animation
calendarRect.scaleValue = 0.8
calendarRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Timer to hide panel after animation
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
calendarPanel.visible = false
calendarPanel.dismissed()
}
}
Rectangle {
id: calendarRect
color: Color.mSurface
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderM * scaling)
width: 340 * scaling
height: 320 * scaling // Reduced height to eliminate bottom space
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Style.marginXS * scaling
anchors.rightMargin: Style.marginXS * scaling
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
scale: scaleValue
opacity: opacityValue
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
// Main Column // Main Column
ColumnLayout { panelContent: ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginM * scaling anchors.margins: Style.marginM * scaling
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
@ -231,6 +139,3 @@ NLoader {
} }
} }
} }
}
}
}

View file

@ -1,307 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NLoader {
id: root
content: Component {
NPanel {
id: demoPanel
property real sliderValue: 1.0
// Override hide function to animate first
function hide() {
// Start hide animation
bgRect.scaleValue = 0.8
bgRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
// Connect to NPanel's dismissed signal to handle external close events
Connections {
target: demoPanel
function onDismissed() {
// Start hide animation
bgRect.scaleValue = 0.8
bgRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Also handle visibility changes from external sources
onVisibleChanged: {
if (!visible && bgRect.opacityValue > 0) {
// Start hide animation
bgRect.scaleValue = 0.8
bgRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Timer to hide panel after animation
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
demoPanel.visible = false
demoPanel.dismissed()
}
}
// Ensure panel shows itself once created
Component.onCompleted: {
show()
}
Rectangle {
id: bgRect
color: Color.mSurfaceVariant
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
width: 500 * scaling
height: 900 * scaling
anchors.centerIn: parent
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
scale: scaleValue
opacity: opacityValue
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
NText {
text: "DemoPanel"
color: Color.mPrimary
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
Layout.alignment: Qt.AlignHCenter
}
ColumnLayout {
spacing: Style.marginM * scaling
// NSlider
ColumnLayout {
spacing: Style.marginL * scaling
NText {
text: "NSlider"
color: Color.mSecondary
font.weight: Style.fontWeightBold
}
NText {
text: `${Math.round(sliderValue * 100)}%`
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
spacing: Style.marginS * scaling
NSlider {
id: scaleSlider
from: 1.0
to: 2.0
stepSize: 0.01
value: sliderValue
onPressedChanged: {
sliderValue = value
}
Layout.fillWidth: true
}
NIconButton {
icon: "refresh"
tooltipText: "Reset Scaling"
fontPointSize: Style.fontSizeL * scaling
onClicked: {
sliderValue = 1.0
}
}
}
NDivider {
Layout.fillWidth: true
}
}
// NIconButton
ColumnLayout {
spacing: Style.marginL * scaling
NText {
text: "NIconButton"
color: Color.mSecondary
font.weight: Style.fontWeightBold
}
NIconButton {
id: myIconButton
icon: "celebration"
tooltipText: "A nice tooltip"
fontPointSize: Style.fontSizeL * scaling
}
NDivider {
Layout.fillWidth: true
}
}
// NToggle
ColumnLayout {
spacing: Style.marginM * scaling
NText {
text: "NToggle"
color: Color.mSecondary
font.weight: Style.fontWeightBold
}
NToggle {
label: "Label"
description: "Description"
onToggled: checked => {
Logger.log("DemoPanel", "NToggle:", checked)
}
}
NDivider {
Layout.fillWidth: true
}
}
// NComboBox
ColumnLayout {
spacing: Style.marginM * scaling
NText {
text: "NComboBox"
color: Color.mSecondary
font.weight: Style.fontWeightBold
}
NComboBox {
label: "Animal"
description: "What's your favorite?"
model: ListModel {
ListElement {
key: "cat"
name: "Cat"
}
ListElement {
key: "dog"
name: "Dog"
}
ListElement {
key: "bird"
name: "Bird"
}
ListElement {
key: "fish"
name: "Fish"
}
ListElement {
key: "turtle"
name: "Turtle"
}
ListElement {
key: "elephant"
name: "Elephant"
}
ListElement {
key: "tiger"
name: "Tiger"
}
}
currentKey: "dog"
onSelected: function (key) {
Logger.log("DemoPanel", "NComboBox: selected ", key)
}
}
NDivider {
Layout.fillWidth: true
}
}
// NTextInput
ColumnLayout {
spacing: Style.marginM * scaling
NText {
text: "NTextInput"
color: Color.mSecondary
font.weight: Style.fontWeightBold
}
NTextInput {
label: "Input label"
description: "A cool description"
text: "Type anything"
Layout.fillWidth: true
onEditingFinished: {
}
}
NDivider {
Layout.fillWidth: true
}
}
// NBusyIndicator
ColumnLayout {
spacing: Style.marginM * scaling
NText {
text: "NBusyIndicator"
color: Color.mSecondary
font.weight: Style.fontWeightBold
}
NBusyIndicator {}
NDivider {
Layout.fillWidth: true
}
}
}
}
}
}
}
}

View file

@ -9,9 +9,9 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { Loader {
isLoaded: (Settings.data.dock.monitors.length > 0) active: (Settings.data.dock.monitors.length > 0)
content: Component { sourceComponent: Component {
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens

View file

@ -1,5 +1,7 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Services
Item { Item {
id: root id: root
@ -8,7 +10,7 @@ Item {
target: "settings" target: "settings"
function toggle() { function toggle() {
settingsPanel.isLoaded = !settingsPanel.isLoaded settingsPanel.toggle(Quickshell.screens[0])
} }
} }
@ -16,7 +18,7 @@ Item {
target: "notifications" target: "notifications"
function toggleHistory() { function toggleHistory() {
notificationHistoryPanel.isLoaded = !notificationHistoryPanel.isLoaded notificationHistoryPanel.toggle(Quickshell.screens[0])
} }
function toggleDoNotDisturb() {// TODO function toggleDoNotDisturb() {// TODO
@ -26,7 +28,8 @@ Item {
IpcHandler { IpcHandler {
target: "idleInhibitor" target: "idleInhibitor"
function toggle() {// TODO function toggle() {
return IdleInhibitorService.manualToggle()
} }
} }
@ -34,7 +37,7 @@ Item {
target: "appLauncher" target: "appLauncher"
function toggle() { function toggle() {
appLauncherPanel.isLoaded = !appLauncherPanel.isLoaded appLauncherPanel.toggle(Quickshell.screens[0])
} }
} }
@ -42,7 +45,11 @@ Item {
target: "lockScreen" target: "lockScreen"
function toggle() { function toggle() {
lockScreen.isLoaded = !lockScreen.isLoaded // Only lock if not already locked (prevents the red screen issue)
// Note: No unlock via IPC for security reasons
if (!lockScreen.active) {
lockScreen.active = true
}
} }
} }

View file

@ -12,27 +12,43 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { Loader {
id: lockScreen id: lockScreen
active: false
// Log state changes to help debug lock screen issues
onActiveChanged: {
Logger.log("LockScreen", "State changed:", active)
}
// Allow a small grace period after unlocking so the compositor releases the lock surfaces // Allow a small grace period after unlocking so the compositor releases the lock surfaces
Timer { Timer {
id: unloadAfterUnlockTimer id: unloadAfterUnlockTimer
interval: 250 interval: 250
repeat: false repeat: false
onTriggered: lockScreen.isLoaded = false onTriggered: {
Logger.log("LockScreen", "Unload timer triggered - deactivating")
lockScreen.active = false
}
} }
function scheduleUnloadAfterUnlock() { function scheduleUnloadAfterUnlock() {
Logger.log("LockScreen", "Scheduling unload after unlock")
unloadAfterUnlockTimer.start() unloadAfterUnlockTimer.start()
} }
content: Component { sourceComponent: Component {
WlSessionLock { WlSessionLock {
id: lock id: lock
// Tie session lock to loader visibility // Tie session lock to loader visibility
locked: lockScreen.isLoaded locked: lockScreen.active
// Lockscreen is a different beast, needs a capital 'S' in 'Screen' to access the current screen // Lockscreen is a different beast, needs a capital 'S' in 'Screen' to access the current screen
// Also we use a different scaling algorithm based on the resolution, as the design is full screen // Also we use a different scaling algorithm based on the resolution, as the design is full screen
readonly property real scaling: ScalingService.dynamicScale(Screen) readonly property real scaling: {
var tt = ScalingService.dynamicScale(Screen)
console.log(tt)
return tt
}
property string errorMessage: "" property string errorMessage: ""
property bool authenticating: false property bool authenticating: false
@ -233,13 +249,13 @@ NLoader {
// Time display - Large and prominent with pulse animation // Time display - Large and prominent with pulse animation
Column { Column {
spacing: Style.marginS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Text { NText {
id: timeText id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm") text: Qt.formatDateTime(new Date(), "HH:mm")
font.family: "Inter" font.family: Settings.data.ui.fontBillboard
font.pointSize: Style.fontSizeXXXL * 6 * scaling font.pointSize: Style.fontSizeXXXL * 6 * scaling
font.weight: Font.Bold font.weight: Font.Bold
font.letterSpacing: -2 * scaling font.letterSpacing: -2 * scaling
@ -261,10 +277,10 @@ NLoader {
} }
} }
Text { NText {
id: dateText id: dateText
text: Qt.formatDateTime(new Date(), "dddd, MMMM d") text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
font.family: "Inter" font.family: Settings.data.ui.fontBillboard
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
font.weight: Font.Light font.weight: Font.Light
color: Color.mOnSurface color: Color.mOnSurface
@ -404,10 +420,10 @@ NLoader {
anchors.margins: Style.marginM * scaling anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Text { NText {
text: "SECURE TERMINAL" text: "SECURE TERMINAL"
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Font.Bold font.weight: Font.Bold
Layout.fillWidth: true Layout.fillWidth: true
@ -424,10 +440,10 @@ NLoader {
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
} }
Text { NText {
text: Math.round(batteryIndicator.percent) + "%" text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
font.weight: Font.Bold font.weight: Font.Bold
} }
@ -450,19 +466,19 @@ NLoader {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Text { NText {
text: "root@noctalia:~$" text: "root@noctalia:~$"
color: Color.mPrimary color: Color.mPrimary
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Font.Bold font.weight: Font.Bold
} }
Text { NText {
id: welcomeText id: welcomeText
text: "" text: ""
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
property int currentIndex: 0 property int currentIndex: 0
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!" property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
@ -488,18 +504,18 @@ NLoader {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
Text { NText {
text: "root@noctalia:~$" text: "root@noctalia:~$"
color: Color.mPrimary color: Color.mPrimary
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Font.Bold font.weight: Font.Bold
} }
Text { NText {
text: "sudo unlock-session" text: "sudo unlock-session"
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
} }
@ -509,7 +525,7 @@ NLoader {
width: 0 width: 0
height: 0 height: 0
visible: false visible: false
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface color: Color.mOnSurface
echoMode: TextInput.Password echoMode: TextInput.Password
@ -535,11 +551,11 @@ NLoader {
} }
// Visual password display with integrated cursor // Visual password display with integrated cursor
Text { NText {
id: asterisksText id: asterisksText
text: "*".repeat(passwordInput.text.length) text: "*".repeat(passwordInput.text.length)
color: Color.mOnSurface color: Color.mOnSurface
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
visible: passwordInput.activeFocus visible: passwordInput.activeFocus
@ -585,7 +601,7 @@ NLoader {
} }
// Status messages // Status messages
Text { NText {
text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "") text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "")
color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent) color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent)
font.family: "DejaVu Sans Mono" font.family: "DejaVu Sans Mono"
@ -618,11 +634,11 @@ NLoader {
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -12 * scaling Layout.bottomMargin: -12 * scaling
Text { NText {
anchors.centerIn: parent anchors.centerIn: parent
text: lock.authenticating ? "EXECUTING" : "EXECUTE" text: lock.authenticating ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
font.family: "DejaVu Sans Mono" font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
font.weight: Font.Bold font.weight: Font.Bold
} }

View file

@ -47,12 +47,28 @@ Variants {
// Connect to animation signal from service // Connect to animation signal from service
Component.onCompleted: { Component.onCompleted: {
NotificationService.animateAndRemove.connect(function (notification, index) { NotificationService.animateAndRemove.connect(function (notification, index) {
// Find the delegate and trigger its animation // Prefer lookup by identity to avoid index mismatches
if (notificationStack.children && notificationStack.children[index]) { var delegate = null
let delegate = notificationStack.children[index] if (notificationStack.children && notificationStack.children.length > 0) {
for (var i = 0; i < notificationStack.children.length; i++) {
var child = notificationStack.children[i]
if (child && child.model && child.model.rawNotification === notification) {
delegate = child
break
}
}
}
// Fallback to index if identity lookup failed
if (!delegate && notificationStack.children && notificationStack.children[index]) {
delegate = notificationStack.children[index]
}
if (delegate && delegate.animateOut) { if (delegate && delegate.animateOut) {
delegate.animateOut() delegate.animateOut()
} } else {
// As a last resort, force-remove without animation to avoid stuck popups
NotificationService.forceRemoveNotification(notification)
} }
}) })
} }

View file

@ -8,103 +8,17 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
// Loader for Notification History panel // Notification History panel
NLoader { NPanel {
id: root id: root
content: Component { panelWidth: 380 * scaling
NPanel { panelHeight: 500 * scaling
id: notificationPanel panelAnchorRight: true
// Override hide function to animate first panelContent: Rectangle {
function hide() {
// Start hide animation
notificationRect.scaleValue = 0.8
notificationRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
Connections {
target: notificationPanel
ignoreUnknownSignals: true
function onDismissed() {
// Start hide animation
notificationRect.scaleValue = 0.8
notificationRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Also handle visibility changes from external sources
onVisibleChanged: {
if (!visible && notificationRect.opacityValue > 0) {
// Start hide animation
notificationRect.scaleValue = 0.8
notificationRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Timer to hide panel after animation
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
notificationPanel.visible = false
notificationPanel.dismissed()
}
}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Rectangle {
id: notificationRect id: notificationRect
color: Color.mSurface color: Color.transparent
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
width: 400 * scaling
height: 500 * scaling
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Style.marginXS * scaling
anchors.rightMargin: Style.marginXS * scaling
clip: true
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
scale: scaleValue
opacity: opacityValue
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@ -141,12 +55,14 @@ NLoader {
tooltipText: "Close" tooltipText: "Close"
sizeMultiplier: 0.8 sizeMultiplier: 0.8
onClicked: { onClicked: {
notificationPanel.hide() root.close()
} }
} }
} }
NDivider {} NDivider {
Layout.fillWidth: true
}
// Empty state when no notifications // Empty state when no notifications
Item { Item {
@ -258,7 +174,6 @@ NLoader {
anchors.fill: parent anchors.fill: parent
anchors.rightMargin: Style.marginL * 3 * scaling anchors.rightMargin: Style.marginL * 3 * scaling
hoverEnabled: true hoverEnabled: true
// Remove the onClicked handler since we now have a dedicated delete button
} }
} }
@ -272,5 +187,3 @@ NLoader {
} }
} }
} }
}
}

View file

@ -0,0 +1,345 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
panelWidth: 440 * scaling
panelHeight: 380 * scaling
panelAnchorCentered: true
// Timer properties
property int timerDuration: 9000 // 9 seconds
property string pendingAction: ""
property bool timerActive: false
property int timeRemaining: 0
// Cancel timer when panel is closing
onClosed: {
cancelTimer()
}
// Timer management
function startTimer(action) {
if (timerActive && pendingAction === action) {
// Second click - execute immediately
executeAction(action)
return
}
pendingAction = action
timeRemaining = timerDuration
timerActive = true
countdownTimer.start()
}
function cancelTimer() {
timerActive = false
pendingAction = ""
timeRemaining = 0
countdownTimer.stop()
}
function executeAction(action) {
// Stop timer but don't reset other properties yet
countdownTimer.stop()
switch (action) {
case "lock":
// Access lockScreen directly like IPCManager does
if (!lockScreen.active) {
lockScreen.active = true
}
break
case "suspend":
CompositorService.suspend()
break
case "reboot":
CompositorService.reboot()
break
case "logout":
CompositorService.logout()
break
case "shutdown":
CompositorService.shutdown()
break
}
// Reset timer state and close panel
cancelTimer()
root.close()
}
// Countdown timer
Timer {
id: countdownTimer
interval: 100
repeat: true
onTriggered: {
timeRemaining -= interval
if (timeRemaining <= 0) {
executeAction(pendingAction)
}
}
}
panelContent: Rectangle {
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.topMargin: Style.marginL * scaling
anchors.leftMargin: Style.marginL * scaling
anchors.rightMargin: Style.marginL * scaling
anchors.bottomMargin: Style.marginM * scaling
spacing: Style.marginS * scaling
// Header with title and close button
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling
NText {
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(
timeRemaining / 1000)} seconds...` : "Power Options"
font.weight: Style.fontWeightBold
font.pointSize: Style.fontSizeL * scaling
color: timerActive ? Color.mPrimary : Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
verticalAlignment: Text.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NIconButton {
icon: timerActive ? "back_hand" : "close"
tooltipText: timerActive ? "Cancel Timer" : "Close"
Layout.alignment: Qt.AlignVCenter
colorBg: timerActive ? Color.applyOpacity(Color.mError, "20") : Color.transparent
colorFg: timerActive ? Color.mError : Color.mOnSurface
onClicked: {
if (timerActive) {
cancelTimer()
} else {
cancelTimer()
root.close()
}
}
}
}
// Power options
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Lock Screen
PowerButton {
Layout.fillWidth: true
icon: "lock_outline"
title: "Lock"
subtitle: "Lock your session"
onClicked: startTimer("lock")
pending: timerActive && pendingAction === "lock"
}
// Suspend
PowerButton {
Layout.fillWidth: true
icon: "bedtime"
title: "Suspend"
subtitle: "Put the system to sleep"
onClicked: startTimer("suspend")
pending: timerActive && pendingAction === "suspend"
}
// Reboot
PowerButton {
Layout.fillWidth: true
icon: "refresh"
title: "Reboot"
subtitle: "Restart the system"
onClicked: startTimer("reboot")
pending: timerActive && pendingAction === "reboot"
}
// Logout
PowerButton {
Layout.fillWidth: true
icon: "exit_to_app"
title: "Logout"
subtitle: "End your session"
onClicked: startTimer("logout")
pending: timerActive && pendingAction === "logout"
}
// Shutdown
PowerButton {
Layout.fillWidth: true
icon: "power_settings_new"
title: "Shutdown"
subtitle: "Turn off the system"
onClicked: startTimer("shutdown")
pending: timerActive && pendingAction === "shutdown"
isShutdown: true
}
}
}
}
// Custom power button component
component PowerButton: Rectangle {
id: buttonRoot
property string icon: ""
property string title: ""
property string subtitle: ""
property bool pending: false
property bool isShutdown: false
signal clicked
height: Style.baseWidgetSize * 1.6 * scaling
radius: Style.radiusS * scaling
color: {
if (pending)
return Color.applyOpacity(Color.mPrimary, "20")
if (mouseArea.containsMouse)
return Color.mTertiary
return Color.transparent
}
border.width: pending ? Math.max(Style.borderM * scaling) : 0
border.color: pending ? Color.mPrimary : Color.mOutline
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Item {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
// Icon on the left
NIcon {
id: iconElement
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: buttonRoot.icon
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
return Color.mOnTertiary
return Color.mOnSurface
}
font.pointSize: Style.fontSizeXXXL * scaling
width: Style.baseWidgetSize * 0.6 * scaling
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
// Text content in the middle
Column {
anchors.left: iconElement.right
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginXL * scaling
anchors.rightMargin: pendingIndicator.visible ? Style.marginM * scaling : 0
spacing: 0
NText {
text: buttonRoot.title
font.weight: Style.fontWeightMedium
font.pointSize: Style.fontSizeM * scaling
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
return Color.mOnTertiary
return Color.mOnSurface
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
NText {
text: {
if (buttonRoot.pending) {
return "Click again to execute immediately"
}
return buttonRoot.subtitle
}
font.pointSize: Style.fontSizeXS * scaling
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
return Color.mOnTertiary
return Color.mOnSurfaceVariant
}
opacity: Style.opacityHeavy
wrapMode: Text.WordWrap
}
}
// Pending indicator on the right
Rectangle {
id: pendingIndicator
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 24 * scaling
height: 24 * scaling
radius: width * 0.5
color: Color.mPrimary
visible: buttonRoot.pending
NText {
anchors.centerIn: parent
text: Math.ceil(timeRemaining / 1000)
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnPrimary
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: buttonRoot.clicked()
}
}
}

View file

@ -8,9 +8,13 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader { NPanel {
id: root id: root
panelWidth: Math.max(screen?.width * 0.5, 1280) * scaling
panelHeight: Math.max(screen?.height * 0.5, 720) * scaling
panelAnchorCentered: true
// Tabs enumeration, order is NOT relevant // Tabs enumeration, order is NOT relevant
enum Tab { enum Tab {
About, About,
@ -28,43 +32,8 @@ NLoader {
} }
property int requestedTab: SettingsPanel.Tab.General property int requestedTab: SettingsPanel.Tab.General
content: Component {
NPanel {
id: panel
property int currentTabIndex: 0 property int currentTabIndex: 0
// Override hide function to animate first
function hide() {
// Start hide animation
bgRect.scaleValue = 0.8
bgRect.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
// Connect to NPanel's dismissed signal to handle external close events
Connections {
target: panel
function onDismissed() {
hide()
}
}
// Timer to hide panel after animation
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
panel.visible = false
panel.dismissed()
}
}
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Component { Component {
id: generalTab id: generalTab
Tabs.GeneralTab {} Tabs.GeneralTab {}
@ -177,72 +146,29 @@ NLoader {
"source": aboutTab "source": aboutTab
}] }]
Component.onCompleted: { // When the panel opens, choose the appropriate tab
var initialIndex = 0 onOpened: {
var initialIndex = SettingsPanel.Tab.General
if (root.requestedTab !== null) { if (root.requestedTab !== null) {
for (var i = 0; i < panel.tabsModel.length; i++) { for (var i = 0; i < root.tabsModel.length; i++) {
if (panel.tabsModel[i].id === root.requestedTab) { if (root.tabsModel[i].id === root.requestedTab) {
initialIndex = i initialIndex = i
break break
} }
} }
} }
// Now that the UI is settled, set the current tab index. // Now that the UI is settled, set the current tab index.
panel.currentTabIndex = initialIndex root.currentTabIndex = initialIndex
show()
} }
onVisibleChanged: { panelContent: Rectangle {
if (!visible && (bgRect.opacityValue > 0)) {
hide()
}
}
Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
layer.enabled: true
width: Math.max(screen.width * 0.5, 1280) * scaling
height: Math.max(screen.height * 0.5, 720) * scaling
anchors.centerIn: parent
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
scale: scaleValue
opacity: opacityValue
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
MouseArea {
anchors.fill: parent anchors.fill: parent
} anchors.margins: Style.marginL * scaling
color: Color.transparent
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginL * scaling spacing: Style.marginM * scaling
spacing: Style.marginL * scaling
Rectangle { Rectangle {
id: sidebar id: sidebar
@ -260,7 +186,7 @@ NLoader {
Repeater { Repeater {
id: sections id: sections
model: panel.tabsModel model: root.tabsModel
delegate: Rectangle { delegate: Rectangle {
id: tabItem id: tabItem
width: parent.width width: parent.width
@ -328,7 +254,7 @@ NLoader {
// Tab label on the main right side // Tab label on the main right side
NText { NText {
text: panel.tabsModel[currentTabIndex].label text: root.tabsModel[currentTabIndex].label
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mPrimary color: Color.mPrimary
@ -338,7 +264,7 @@ NLoader {
icon: "close" icon: "close"
tooltipText: "Close" tooltipText: "Close"
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
onClicked: panel.hide() onClicked: root.close()
} }
} }
@ -352,16 +278,16 @@ NLoader {
clip: true clip: true
Repeater { Repeater {
model: panel.tabsModel model: root.tabsModel
onItemAdded: function (index, item) { onItemAdded: function (index, item) {
item.sourceComponent = panel.tabsModel[index].source item.sourceComponent = root.tabsModel[index].source
} }
delegate: Loader { delegate: Loader {
// All loaders will occupy the same space, stacked on top of each other. // All loaders will occupy the same space, stacked on top of each other.
anchors.fill: parent anchors.fill: parent
visible: index === panel.currentTabIndex visible: index === root.currentTabIndex
// The loader is only active (and uses memory) when its page is visible. // The loader is only active (and uses memory) when its page is visible.
active: visible active: visible
} }
@ -372,5 +298,3 @@ NLoader {
} }
} }
} }
}
}

View file

@ -120,6 +120,27 @@ ColumnLayout {
} }
} }
} }
// Volume Step Size
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NSpinBox {
Layout.fillWidth: true
label: "Volume Step Size"
description: "Adjust the step size for volume changes (scroll wheel, keyboard shortcuts)."
minimum: 1
maximum: 25
value: Settings.data.audio.volumeStep
stepSize: 1
suffix: "%"
onValueChanged: {
Settings.data.audio.volumeStep = value
}
}
}
} }
NDivider { NDivider {
@ -199,6 +220,47 @@ ColumnLayout {
} }
} }
// Divider
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginM * scaling
}
// AudioService Visualizer Category
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NText {
text: "Bar Media Player"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Miniplayer section
NToggle {
label: "Show Album Art In Bar Media Player"
description: "Show the album art of the currently playing song next to the title."
checked: Settings.data.audio.showMiniplayerAlbumArt
onToggled: checked => {
Settings.data.audio.showMiniplayerAlbumArt = checked
}
}
NToggle {
label: "Show Audio Visualizer In Bar Media Player"
description: "Shows an audio visualizer in the background of the miniplayer."
checked: Settings.data.audio.showMiniplayerCava
onToggled: checked => {
Settings.data.audio.showMiniplayerCava = checked
}
}
}
// Divider // Divider
NDivider { NDivider {
Layout.fillWidth: true Layout.fillWidth: true

View file

@ -49,38 +49,21 @@ Item {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
Layout.fillWidth: true Layout.fillWidth: true
NLabel { NSpinBox {
Layout.fillWidth: true
label: "Brightness Step Size" label: "Brightness Step Size"
description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)." description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)."
} minimum: 1
maximum: 50
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NSlider {
Layout.fillWidth: true
from: 1
to: 50
value: Settings.data.brightness.brightnessStep value: Settings.data.brightness.brightnessStep
stepSize: 1 stepSize: 1
onPressedChanged: { suffix: "%"
if (!pressed) { onValueChanged: {
Settings.data.brightness.brightnessStep = value Settings.data.brightness.brightnessStep = value
} }
} }
} }
NText {
text: Settings.data.brightness.brightnessStep + "%"
Layout.alignment: Qt.AlignVCenter
color: Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
}
NDivider { NDivider {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling Layout.topMargin: Style.marginL * scaling

View file

@ -157,6 +157,59 @@ ColumnLayout {
} }
} }
} }
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * 2 * scaling
Layout.bottomMargin: Style.marginL * scaling
}
NText {
text: "Fonts"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.bottomMargin: Style.marginS * scaling
}
// Font configuration section
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NTextInput {
label: "Default Font"
description: "Main font used throughout the interface."
text: Settings.data.ui.fontDefault
placeholderText: "Roboto"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.ui.fontDefault = text
}
}
NTextInput {
label: "Fixed Width Font"
description: "Monospace font used for terminal and code display."
text: Settings.data.ui.fontFixed
placeholderText: "DejaVu Sans Mono"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.ui.fontFixed = text
}
}
NTextInput {
label: "Billboard Font"
description: "Large font used for clocks and prominent displays."
text: Settings.data.ui.fontBillboard
placeholderText: "Inter"
Layout.fillWidth: true
onEditingFinished: {
Settings.data.ui.fontBillboard = text
}
}
}
} }
} }
} }

View file

@ -16,12 +16,13 @@ NBox {
// PowerProfiles service // PowerProfiles service
property var powerProfiles: PowerProfiles property var powerProfiles: PowerProfiles
readonly property bool hasPP: powerProfiles.hasPerformanceProfile readonly property bool hasPP: powerProfiles.hasPerformanceProfile
property real spacing: 0
RowLayout { RowLayout {
id: powerRow id: powerRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginS * scaling
spacing: sidePanel.cardSpacing spacing: spacing
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }

View file

@ -61,7 +61,7 @@ NBox {
tooltipText: "Open Settings" tooltipText: "Open Settings"
onClicked: { onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.General settingsPanel.requestedTab = SettingsPanel.Tab.General
settingsPanel.isLoaded = !settingsPanel.isLoaded settingsPanel.open(screen)
} }
} }
@ -70,16 +70,20 @@ NBox {
icon: "power_settings_new" icon: "power_settings_new"
tooltipText: "Power Menu" tooltipText: "Power Menu"
onClicked: { onClicked: {
powerMenu.show() powerPanel.open(screen)
} sidePanel.close()
}
} }
} }
PowerMenu { NIconButton {
id: powerMenu id: closeButton
anchors.top: powerButton.bottom icon: "close"
anchors.right: powerButton.right tooltipText: "Close Side Panel"
onClicked: {
sidePanel.close()
}
}
}
} }
// ---------------------------------- // ----------------------------------

View file

@ -9,6 +9,9 @@ import qs.Widgets
// Utilities: record & wallpaper // Utilities: record & wallpaper
NBox { NBox {
property real spacing: 0
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: 1 Layout.preferredWidth: 1
implicitHeight: utilRow.implicitHeight + Style.marginM * 2 * scaling implicitHeight: utilRow.implicitHeight + Style.marginM * 2 * scaling
@ -16,7 +19,7 @@ NBox {
id: utilRow id: utilRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginS * scaling
spacing: sidePanel.cardSpacing spacing: spacing
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }
@ -31,13 +34,24 @@ NBox {
} }
} }
// Idle Inhibitor
NIconButton {
icon: "coffee"
tooltipText: IdleInhibitorService.isInhibited ? "Disable Keep Awake" : "Enable Keep Awake"
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : Color.mSurfaceVariant
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mPrimary
onClicked: {
IdleInhibitorService.manualToggle()
}
}
// Wallpaper // Wallpaper
NIconButton { NIconButton {
icon: "image" icon: "image"
tooltipText: "Open Wallpaper Selector" tooltipText: "Open Wallpaper Selector"
onClicked: { onClicked: {
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
settingsPanel.isLoaded = true settingsPanel.open(screen)
} }
} }

View file

@ -1,376 +0,0 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.LockScreen
NPanel {
id: powerMenu
visible: false
property var entriesCount: 5
property var entryHeight: Style.baseWidgetSize * scaling
// Anchors will be set by the parent component
function show() {
visible = true
}
function hide() {
visible = false
}
Rectangle {
width: 160 * scaling
height: (entryHeight * entriesCount) + (Style.marginS * entriesCount * scaling)
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
color: Color.mSurface
visible: true
z: 9999
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: Style.marginL * scaling
anchors.topMargin: 86 * scaling
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
onClicked: {
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * scaling
// --------------
// Lock
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: entryHeight
radius: Style.radiusS * scaling
color: lockButtonArea.containsMouse ? Color.mTertiary : Color.transparent
Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
Row {
id: lockRow
spacing: Style.marginS * scaling
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
NIcon {
text: "lock_outline"
color: lockButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
NText {
text: "Lock Screen"
color: lockButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
}
}
MouseArea {
id: lockButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Logger.log("PowerMenu", "Lock screen requested")
// Lock the screen
lockScreen.isLoaded = true
powerMenu.visible = false
}
}
}
// --------------
// Suspend
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: entryHeight
radius: Style.radiusS * scaling
color: suspendButtonArea.containsMouse ? Color.mTertiary : Color.transparent
Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
Row {
id: suspendRow
spacing: Style.marginS * scaling
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
NIcon {
text: "bedtime"
color: suspendButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
NText {
text: "Suspend"
color: suspendButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
}
}
MouseArea {
id: suspendButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
suspend()
powerMenu.visible = false
}
}
}
// --------------
// Reboot
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: entryHeight
radius: Style.radiusS * scaling
color: rebootButtonArea.containsMouse ? Color.mTertiary : Color.transparent
Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
Row {
id: rebootRow
spacing: Style.marginS * scaling
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
NIcon {
text: "refresh"
color: rebootButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
NText {
text: "Reboot"
color: rebootButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
}
}
MouseArea {
id: rebootButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
reboot()
powerMenu.visible = false
}
}
}
// --------------
// Logout
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: entryHeight
radius: Style.radiusS * scaling
color: logoutButtonArea.containsMouse ? Color.mTertiary : Color.transparent
Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
Row {
id: logoutRow
spacing: Style.marginS * scaling
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
NIcon {
text: "exit_to_app"
color: logoutButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
NText {
text: "Logout"
color: logoutButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
}
}
MouseArea {
id: logoutButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
logout()
powerMenu.visible = false
}
}
}
// --------------
// Shutdown
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: entryHeight
radius: Style.radiusS * scaling
color: shutdownButtonArea.containsMouse ? Color.mTertiary : Color.transparent
Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
Row {
id: shutdownRow
spacing: Style.marginS * scaling
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
NIcon {
text: "power_settings_new"
color: shutdownButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
NText {
text: "Shutdown"
color: shutdownButtonArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface
font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1 * scaling
}
}
}
MouseArea {
id: shutdownButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
shutdown()
powerMenu.visible = false
}
}
}
}
}
// ----------------------------------
// System functions
function logout() {
CompositorService.logout()
}
function suspend() {
suspendProcess.running = true
}
function shutdown() {
shutdownProcess.running = true
}
function reboot() {
rebootProcess.running = true
}
Process {
id: shutdownProcess
command: ["shutdown", "-h", "now"]
running: false
}
Process {
id: rebootProcess
command: ["reboot"]
running: false
}
Process {
id: suspendProcess
command: ["systemctl", "suspend"]
running: false
}
Process {
id: logoutProcess
command: ["loginctl", "terminate-user", Quickshell.env("USER")]
running: false
}
// LockScreen instance
LockScreen {
id: lockScreen
}
}

View file

@ -7,150 +7,22 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
NLoader {
id: root
// X coordinate on screen (in pixels) where the panel should align its center.
// Set via openAt(x) from the bar button.
property real anchorX: 0
// Target screen to open on
property var targetScreen: null
function openAt(x, screen) {
anchorX = x
targetScreen = screen
isLoaded = true
// If the panel is already instantiated, update immediately
if (item) {
if (item.anchorX !== undefined)
item.anchorX = anchorX
if (item.screen !== undefined)
item.screen = targetScreen
}
}
content: Component {
NPanel { NPanel {
id: sidePanel id: panel
// Single source of truth for spacing between cards (both axes) panelWidth: 460 * scaling
property real cardSpacing: Style.marginL * scaling panelHeight: 700 * scaling
// X coordinate from the bar to align this panel under panelAnchorRight: true
property real anchorX: root.anchorX
// Ensure this panel attaches to the intended screen
screen: root.targetScreen
// Override hide function to animate first panelContent: Item {
function hide() {
// Start hide animation
panelBackground.scaleValue = 0.8
panelBackground.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
// Connect to NPanel's dismissed signal to handle external close events
Connections {
target: sidePanel
function onDismissed() {
// Start hide animation
panelBackground.scaleValue = 0.8
panelBackground.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Also handle visibility changes from external sources
onVisibleChanged: {
if (!visible && panelBackground.opacityValue > 0) {
// Start hide animation
panelBackground.scaleValue = 0.8
panelBackground.opacityValue = 0.0
// Hide after animation completes
hideTimer.start()
}
}
// Ensure panel shows itself once created
Component.onCompleted: show()
// Inline helpers moved to dedicated widgets: NCard and NCircleStat
Rectangle {
id: panelBackground
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
layer.enabled: true
width: 460 * scaling
property real innerMargin: sidePanel.cardSpacing
// Height scales to content plus vertical padding
height: content.implicitHeight + innerMargin * 2
// Place the panel relative to the bar based on its position
y: Settings.data.bar.position === "top" ? Style.marginS * scaling : undefined
anchors {
bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling + Style.marginS * scaling : undefined
}
// Center horizontally under the anchorX, clamped to the screen bounds
x: Math.max(Style.marginS * scaling, Math.min(parent.width - width - Style.marginS * scaling,
Math.round(anchorX - width / 2)))
// Animation properties
property real scaleValue: 0.8
property real opacityValue: 0.0
scale: scaleValue
opacity: opacityValue
// Animate in when component is completed
Component.onCompleted: {
scaleValue = 1.0
opacityValue = 1.0
}
// Timer to hide panel after animation
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
sidePanel.visible = false
sidePanel.dismissed()
}
}
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.OutExpo
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutQuad
}
}
// Content wrapper to ensure childrenRect drives implicit height
Item {
id: content id: content
property real cardSpacing: Style.marginL * scaling
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.margins: panelBackground.innerMargin anchors.margins: content.cardSpacing
implicitHeight: layout.implicitHeight implicitHeight: layout.implicitHeight
// Layout content (not vertically anchored so implicitHeight is valid) // Layout content (not vertically anchored so implicitHeight is valid)
@ -160,16 +32,14 @@ NLoader {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
spacing: sidePanel.cardSpacing spacing: content.cardSpacing
// Cards (consistent inter-card spacing via ColumnLayout spacing) // Cards (consistent inter-card spacing via ColumnLayout spacing)
ProfileCard { ProfileCard {// Layout.topMargin: 0
Layout.topMargin: 0 // Layout.bottomMargin: 0
Layout.bottomMargin: 0
} }
WeatherCard { WeatherCard {// Layout.topMargin: 0
Layout.topMargin: 0 // Layout.bottomMargin: 0
Layout.bottomMargin: 0
} }
// Middle section: media + stats column // Middle section: media + stats column
@ -177,7 +47,7 @@ NLoader {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 0 Layout.topMargin: 0
Layout.bottomMargin: 0 Layout.bottomMargin: 0
spacing: sidePanel.cardSpacing spacing: content.cardSpacing
// Media card // Media card
MediaCard { MediaCard {
@ -197,15 +67,16 @@ NLoader {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 0 Layout.topMargin: 0
Layout.bottomMargin: 0 Layout.bottomMargin: 0
spacing: sidePanel.cardSpacing spacing: content.cardSpacing
// Power Profiles switcher // Power Profiles switcher
PowerProfilesCard {} PowerProfilesCard {
spacing: content.cardSpacing
}
// Utilities buttons // Utilities buttons
UtilitiesCard {} UtilitiesCard {
} spacing: content.cardSpacing
}
} }
} }
} }

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Commons
Singleton { Singleton {
id: root id: root
@ -34,7 +35,7 @@ Singleton {
readonly property alias muted: root._muted readonly property alias muted: root._muted
property bool _muted: !!sink?.audio?.muted property bool _muted: !!sink?.audio?.muted
readonly property real stepVolume: 0.05 readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0
PwObjectTracker { PwObjectTracker {
objects: [...root.sinks, ...root.sources] objects: [...root.sinks, ...root.sources]

View file

@ -27,6 +27,15 @@ Singleton {
return methods return methods
} }
// Global helpers for IPC and shortcuts
function increaseBrightness(): void {
monitors.forEach(m => m.increaseBrightness())
}
function decreaseBrightness(): void {
monitors.forEach(m => m.decreaseBrightness())
}
function getDetectedDisplays(): list<var> { function getDetectedDisplays(): list<var> {
return detectedDisplays return detectedDisplays
} }

View file

@ -9,7 +9,7 @@ Singleton {
id: root id: root
property var values: Array(barsCount).fill(0) property var values: Array(barsCount).fill(0)
property int barsCount: 32 property int barsCount: 24
property var config: ({ property var config: ({
"general": { "general": {
@ -37,7 +37,7 @@ Singleton {
Process { Process {
id: process id: process
stdinEnabled: true stdinEnabled: true
running: (Settings.data.audio.visualizerType !== "none") && PanelService.sidePanel.isLoaded running: (Settings.data.audio.visualizerType !== "none") && (PanelService.sidePanel.active || Settings.data.audio.showMiniplayerCava)
command: ["cava", "-p", "/dev/stdin"] command: ["cava", "-p", "/dev/stdin"]
onExited: { onExited: {
stdinEnabled = true stdinEnabled = true

View file

@ -11,12 +11,40 @@ Singleton {
property var history: [] property var history: []
property bool initialized: false property bool initialized: false
property int maxHistory: 50 // Limit clipboard history entries
// Internal state // Internal state
property bool _enabled: true property bool _enabled: true
// Cached history file path
property string historyFile: Quickshell.env("NOCTALIA_CLIPBOARD_HISTORY_FILE")
|| (Settings.cacheDir + "clipboard.json")
// Persisted storage for clipboard history
property FileView historyFileView: FileView {
id: historyFileView
objectName: "clipboardHistoryFileView"
path: historyFile
watchChanges: false // We don't need to watch changes for clipboard
onAdapterUpdated: writeAdapter()
Component.onCompleted: reload()
onLoaded: loadFromHistory()
onLoadFailed: function (error) {
// Create file on first use
if (error.toString().includes("No such file") || error === 2) {
writeAdapter()
}
}
JsonAdapter {
id: historyAdapter
property var history: []
property double timestamp: 0
}
}
Timer { Timer {
interval: 1000 interval: 2000
repeat: true repeat: true
running: root._enabled running: root._enabled
onTriggered: root.refresh() onTriggered: root.refresh()
@ -32,14 +60,17 @@ Singleton {
if (exitCode === 0) { if (exitCode === 0) {
currentTypes = String(stdout.text).trim().split('\n').filter(t => t) currentTypes = String(stdout.text).trim().split('\n').filter(t => t)
// Always check for text first
textProcess.command = ["wl-paste", "-n", "--type", "text/plain"]
textProcess.isLoading = true
textProcess.running = true
// Also check for images if available
const imageType = currentTypes.find(t => t.startsWith('image/')) const imageType = currentTypes.find(t => t.startsWith('image/'))
if (imageType) { if (imageType) {
imageProcess.mimeType = imageType imageProcess.mimeType = imageType
imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`]
imageProcess.running = true imageProcess.running = true
} else {
textProcess.command = ["wl-paste", "-n", "--type", "text/plain"]
textProcess.running = true
} }
} else { } else {
typeProcess.isLoading = false typeProcess.isLoading = false
@ -65,18 +96,33 @@ Singleton {
"timestamp": new Date().getTime() "timestamp": new Date().getTime()
} }
// Check if this exact image already exists
const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) const exists = root.history.find(item => item.type === 'image' && item.data === entry.data)
if (!exists) { if (!exists) {
root.history = [entry, ...root.history].slice(0, 20) // Normalize existing history and add the new image
const normalizedHistory = root.history.map(item => {
if (typeof item === 'string') {
return {
"type": 'text',
"content": item,
"timestamp": new Date().getTime(
) - 1000 // Make it slightly older
}
}
return item
})
root.history = [entry, ...normalizedHistory].slice(0, maxHistory)
saveHistory()
} }
} }
} }
// Always mark as initialized when done
if (!textProcess.isLoading) { if (!textProcess.isLoading) {
root.initialized = true root.initialized = true
}
typeProcess.isLoading = false typeProcess.isLoading = false
} }
}
stdout: StdioCollector {} stdout: StdioCollector {}
} }
@ -87,15 +133,18 @@ Singleton {
property bool isLoading: false property bool isLoading: false
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
textProcess.isLoading = false
if (exitCode === 0) { if (exitCode === 0) {
const content = String(stdout.text).trim() const content = String(stdout.text).trim()
if (content) { if (content && content.length > 0) {
const entry = { const entry = {
"type": 'text', "type": 'text',
"content": content, "content": content,
"timestamp": new Date().getTime() "timestamp": new Date().getTime()
} }
// Check if this exact text content already exists
const exists = root.history.find(item => { const exists = root.history.find(item => {
if (item.type === 'text') { if (item.type === 'text') {
return item.content === content return item.content === content
@ -104,36 +153,76 @@ Singleton {
}) })
if (!exists) { if (!exists) {
const newHistory = root.history.map(item => { // Normalize existing history entries
const normalizedHistory = root.history.map(item => {
if (typeof item === 'string') { if (typeof item === 'string') {
return { return {
"type": 'text', "type": 'text',
"content": item, "content": item,
"timestamp": new Date().getTime() "timestamp": new Date().getTime(
) - 1000 // Make it slightly older
} }
} }
return item return item
}) })
root.history = [entry, ...newHistory].slice(0, 20) root.history = [entry, ...normalizedHistory].slice(0, maxHistory)
saveHistory()
} }
} }
} else {
textProcess.isLoading = false
} }
// Mark as initialized and clean up loading states
root.initialized = true root.initialized = true
if (!imageProcess.running) {
typeProcess.isLoading = false typeProcess.isLoading = false
} }
}
stdout: StdioCollector {} stdout: StdioCollector {}
} }
function refresh() { function refresh() {
if (!typeProcess.isLoading && !textProcess.isLoading) { if (!typeProcess.isLoading && !textProcess.isLoading && !imageProcess.running) {
typeProcess.isLoading = true typeProcess.isLoading = true
typeProcess.command = ["wl-paste", "-l"] typeProcess.command = ["wl-paste", "-l"]
typeProcess.running = true typeProcess.running = true
} }
} }
function loadFromHistory() {
// Populate in-memory history from cached file
try {
const items = historyAdapter.history || []
root.history = items.slice(0, maxHistory) // Apply limit when loading
Logger.log("Clipboard", "Loaded", root.history.length, "entries from cache")
} catch (e) {
Logger.error("Clipboard", "Failed to load history:", e)
root.history = []
}
}
function saveHistory() {
try {
// Ensure we don't exceed the maximum history limit
const limitedHistory = root.history.slice(0, maxHistory)
historyAdapter.history = limitedHistory
historyAdapter.timestamp = Time.timestamp
// Ensure cache directory exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir])
Qt.callLater(function () {
historyFileView.writeAdapter()
})
} catch (e) {
Logger.error("Clipboard", "Failed to save history:", e)
}
}
function clearHistory() {
root.history = []
saveHistory()
}
} }

View file

@ -396,6 +396,25 @@ Singleton {
} }
} }
// Get current workspace
function getCurrentWorkspace() {
for (var i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i)
if (ws.isFocused) {
return ws
}
}
return null
}
// Get focused window
function getFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
return windows[focusedWindowIndex]
}
return null
}
// Generic logout/shutdown commands // Generic logout/shutdown commands
function logout() { function logout() {
if (isHyprland) { if (isHyprland) {
@ -415,22 +434,15 @@ Singleton {
} }
} }
// Get current workspace function shutdown() {
function getCurrentWorkspace() { Quickshell.execDetached(["shutdown", "-h", "now"])
for (var i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i)
if (ws.isFocused) {
return ws
}
}
return null
} }
// Get focused window function reboot() {
function getFocusedWindow() { Quickshell.execDetached(["reboot"])
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
return windows[focusedWindowIndex]
} }
return null
function suspend() {
Quickshell.execDetached(["systemctl", "suspend"])
} }
} }

View file

@ -0,0 +1,183 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
Singleton {
id: root
property bool isInhibited: false
property string reason: "User requested"
property var activeInhibitors: []
// Different inhibitor strategies
property string strategy: "systemd" // "systemd", "wayland", or "auto"
Component.onCompleted: {
Logger.log("IdleInhibitor", "Service started")
detectStrategy()
// Restore previous state from settings
if (Settings.data.ui.idleInhibitorEnabled) {
addInhibitor("manual", "Restored from previous session")
Logger.log("IdleInhibitor", "Restored previous manual inhibition state")
}
}
// Auto-detect the best strategy
function detectStrategy() {
if (strategy === "auto") {
// Check if systemd-inhibit is available
try {
var systemdResult = Quickshell.execDetached(["which", "systemd-inhibit"])
strategy = "systemd"
Logger.log("IdleInhibitor", "Using systemd-inhibit strategy")
return
} catch (e) {
// systemd-inhibit not found, try Wayland tools
}
try {
var waylandResult = Quickshell.execDetached(["which", "wayhibitor"])
strategy = "wayland"
Logger.log("IdleInhibitor", "Using wayhibitor strategy")
return
} catch (e) {
// wayhibitor not found
}
Logger.warn("IdleInhibitor", "No suitable inhibitor found - will try systemd as fallback")
strategy = "systemd" // Fallback to systemd even if not detected
}
}
// Add an inhibitor
function addInhibitor(id, reason = "Application request") {
if (activeInhibitors.includes(id)) {
Logger.warn("IdleInhibitor", "Inhibitor already active:", id)
return false
}
activeInhibitors.push(id)
updateInhibition(reason)
Logger.log("IdleInhibitor", "Added inhibitor:", id)
return true
}
// Remove an inhibitor
function removeInhibitor(id) {
const index = activeInhibitors.indexOf(id)
if (index === -1) {
Logger.warn("IdleInhibitor", "Inhibitor not found:", id)
return false
}
activeInhibitors.splice(index, 1)
updateInhibition()
Logger.log("IdleInhibitor", "Removed inhibitor:", id)
return true
}
// Update the actual system inhibition
function updateInhibition(newReason = reason) {
const shouldInhibit = activeInhibitors.length > 0
if (shouldInhibit === isInhibited) {
return
// No change needed
}
if (shouldInhibit) {
startInhibition(newReason)
} else {
stopInhibition()
}
}
// Start system inhibition
function startInhibition(newReason) {
reason = newReason
if (strategy === "systemd") {
startSystemdInhibition()
} else if (strategy === "wayland") {
startWaylandInhibition()
} else {
Logger.warn("IdleInhibitor", "No inhibition strategy available")
return
}
isInhibited = true
Logger.log("IdleInhibitor", "Started inhibition:", reason)
}
// Stop system inhibition
function stopInhibition() {
if (!isInhibited)
return
if (inhibitorProcess.running) {
inhibitorProcess.signal(15) // SIGTERM
}
isInhibited = false
Logger.log("IdleInhibitor", "Stopped inhibition")
}
// Systemd inhibition using systemd-inhibit
function startSystemdInhibition() {
inhibitorProcess.command = ["systemd-inhibit", "--what=idle:sleep:handle-lid-switch", "--why="
+ reason, "--mode=block", "sleep", "infinity"]
inhibitorProcess.running = true
}
// Wayland inhibition using wayhibitor or similar
function startWaylandInhibition() {
inhibitorProcess.command = ["wayhibitor"]
inhibitorProcess.running = true
}
// Process for maintaining the inhibition
Process {
id: inhibitorProcess
running: false
onExited: function (exitCode, exitStatus) {
if (isInhibited) {
Logger.warn("IdleInhibitor", "Inhibitor process exited unexpectedly:", exitCode)
isInhibited = false
}
}
onStarted: function () {
Logger.log("IdleInhibitor", "Inhibitor process started successfully")
}
}
// Manual toggle for user control
function manualToggle() {
if (activeInhibitors.includes("manual")) {
removeInhibitor("manual")
Settings.data.ui.idleInhibitorEnabled = false
ToastService.showNotice("Keep Awake", "Disabled", false, 3000)
Logger.log("IdleInhibitor", "Manual inhibition disabled and saved to settings")
return false
} else {
addInhibitor("manual", "Manually activated by user")
Settings.data.ui.idleInhibitorEnabled = true
ToastService.showNotice("Keep Awake", "Enabled", false, 3000)
Logger.log("IdleInhibitor", "Manual inhibition enabled and saved to settings")
return true
}
}
// Clean up on shutdown
Component.onDestruction: {
stopInhibition()
}
}

View file

@ -5,8 +5,16 @@ import Quickshell
Singleton { Singleton {
id: root id: root
// A ref. to the sidePanel, so it's accessible from other services
property var sidePanel: null
// Currently opened panel // Currently opened panel
property var openedPanel: null property var openedPanel: null
property var sidePanel: null function registerOpen(panel) {
if (openedPanel && openedPanel != panel) {
openedPanel.close()
}
openedPanel = panel
}
} }

View file

@ -9,8 +9,16 @@ Singleton {
// ------------------------------------------- // -------------------------------------------
// Manual scaling via Settings // Manual scaling via Settings
function scale(aScreen) { function scale(aScreen) {
try {
if (aScreen !== undefined && aScreen.name !== undefined) {
return scaleByName(aScreen.name) return scaleByName(aScreen.name)
} }
} catch (e) {
//Logger.warn(e)
}
return 1.0
}
function scaleByName(aScreenName) { function scaleByName(aScreenName) {
try { try {

View file

@ -1,6 +1,7 @@
import QtQuick import QtQuick
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets
// Compact circular statistic display used in the SidePanel // Compact circular statistic display used in the SidePanel
Rectangle { Rectangle {
@ -73,7 +74,7 @@ Rectangle {
} }
// Percent centered in the circle // Percent centered in the circle
Text { NText {
id: valueLabel id: valueLabel
anchors.centerIn: parent anchors.centerIn: parent
text: `${root.value}${root.suffix}` text: `${root.value}${root.suffix}`

View file

@ -1,42 +0,0 @@
import QtQuick
// Example usage:
// NLoader {
// content: Component {
// NPanel {
Loader {
id: loader
// Boolean control to load/unload the item
property bool isLoaded: false
// Provide the component to be loaded.
property Component content
active: isLoaded
asynchronous: true
sourceComponent: content
// onLoaded: {
// Logger.log("NLoader", "OnLoaded:", item.toString());
// }
onActiveChanged: {
if (active && item && item.show) {
item.show()
}
}
onItemChanged: {
if (active && item && item.show) {
item.show()
}
}
Connections {
target: loader.item
ignoreUnknownSignals: true
function onDismissed() {
loader.isLoaded = false
}
}
}

View file

@ -4,111 +4,179 @@ import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Services import qs.Services
PanelWindow { Loader {
id: root id: root
active: false
asynchronous: true
readonly property real scaling: ScalingService.scale(screen) readonly property real scaling: ScalingService.scale(screen)
property ShellScreen screen
property bool showOverlay: Settings.data.general.dimDesktop property Component panelContent: null
property int topMargin: Settings.data.bar.position === "top" ? Style.barHeight * scaling : 0 property int panelWidth: 1500
property int bottomMargin: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling : 0 property int panelHeight: 400
// Show dimming if this panel is opened OR if we're in a transition (to prevent flickering) property bool panelAnchorCentered: false
property color overlayColor: (showOverlay && (PanelService.openedPanel === root property bool panelAnchorLeft: false
|| isTransitioning)) ? Color.applyOpacity(Color.mShadow, property bool panelAnchorRight: false
"AA") : Color.transparent
property bool isTransitioning: false
signal dismissed
function hide() { // Animation properties
// Clear the panel service when hiding readonly property real originalScale: 0.7
if (PanelService.openedPanel === root) { readonly property real originalOpacity: 0.0
PanelService.openedPanel = null property real scaleValue: originalScale
} property real opacityValue: originalOpacity
isTransitioning = false
visible = false
root.dismissed()
}
function show() { property alias isClosing: hideTimer.running
// Ensure only one panel is visible at a time using PanelService as ephemeral storage
try {
if (PanelService.openedPanel && PanelService.openedPanel !== root && PanelService.openedPanel.hide) {
// Mark both panels as transitioning to prevent dimming flicker
isTransitioning = true
PanelService.openedPanel.isTransitioning = true
PanelService.openedPanel.hide()
// Small delay to ensure smooth transition
showTimer.start()
return
}
// No previous panel, show immediately
PanelService.openedPanel = root
visible = true
} catch (e) {
// ignore signal opened
signal closed
// -----------------------------------------
function toggle(aScreen) {
if (!active || isClosing) {
open(aScreen)
} else {
close()
} }
} }
implicitWidth: screen.width // -----------------------------------------
implicitHeight: screen.height function open(aScreen) {
color: visible ? overlayColor : Color.transparent if (aScreen !== null) {
visible: false screen = aScreen
}
// Special case if currently closing/animating
if (isClosing) {
hideTimer.stop() // in case we were closing
scaleValue = 1.0
opacityValue = 1.0
}
PanelService.registerOpen(root)
active = true
root.opened()
}
// -----------------------------------------
function close() {
scaleValue = originalScale
opacityValue = originalOpacity
hideTimer.start()
}
// -----------------------------------------
function closeCompleted() {
root.closed()
active = false
}
// -----------------------------------------
// Timer to disable the loader after the close animation is completed
Timer {
id: hideTimer
interval: Style.animationSlow
repeat: false
onTriggered: {
closeCompleted()
}
}
// -----------------------------------------
sourceComponent: Component {
PanelWindow {
id: panelWindow
visible: true
// Dim desktop if required
color: (root.active && !root.isClosing && Settings.data.general.dimDesktop) ? Color.applyOpacity(
Color.mShadow,
"BB") : Color.transparent
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "noctalia-panel"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
}
}
anchors.top: true anchors.top: true
anchors.left: true anchors.left: true
anchors.right: true anchors.right: true
anchors.bottom: true anchors.bottom: true
margins.top: topMargin margins.top: Settings.data.bar.position === "top" ? Style.barHeight * scaling : 0
margins.bottom: bottomMargin margins.bottom: Settings.data.bar.position === "bottom" ? Style.barHeight * scaling : 0
// Clicking outside of the rectangle to close
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: root.hide() onClicked: root.close()
} }
Behavior on color { Rectangle {
ColorAnimation { id: panelBackground
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
layer.enabled: true
width: panelWidth
height: panelHeight
anchors {
centerIn: panelAnchorCentered ? parent : null
left: !panelAnchorCentered && panelAnchorLeft ? parent.left : parent.center
right: !panelAnchorCentered && panelAnchorRight ? parent.right : parent.center
top: !panelAnchorCentered && (Settings.data.bar.position === "top") ? parent.top : undefined
bottom: !panelAnchorCentered && (Settings.data.bar.position === "bottom") ? parent.bottom : undefined
// margins
topMargin: !panelAnchorCentered
&& (Settings.data.bar.position === "top") ? Style.marginS * scaling : undefined
bottomMargin: !panelAnchorCentered
&& (Settings.data.bar.position === "bottom") ? Style.marginS * scaling : undefined
rightMargin: !panelAnchorCentered && panelAnchorRight ? Style.marginS * scaling : undefined
}
scale: root.scaleValue
opacity: root.opacityValue
// Animate in when component is completed
Component.onCompleted: {
root.scaleValue = 1.0
root.opacityValue = 1.0
}
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
}
// Animation behaviors
Behavior on scale {
NumberAnimation {
duration: Style.animationSlow duration: Style.animationSlow
easing.type: Easing.InOutCubic easing.type: Easing.OutExpo
} }
} }
Timer { Behavior on opacity {
id: showTimer NumberAnimation {
interval: 50 // Small delay to ensure smooth transition duration: Style.animationNormal
repeat: false easing.type: Easing.OutQuad
onTriggered: {
PanelService.openedPanel = root
isTransitioning = false
visible = true
} }
} }
Component.onDestruction: { Loader {
try { anchors.fill: parent
if (visible && Settings.openPanel === root) sourceComponent: root.panelContent
Settings.openPanel = null }
} catch (e) { }
}
}
onVisibleChanged: {
try {
if (!visible) {
// Clear panel service when panel becomes invisible
if (PanelService.openedPanel === root) {
PanelService.openedPanel = null
}
if (Settings.openPanel === root) {
Settings.openPanel = null
}
isTransitioning = false
}
} catch (e) {
} }
} }
} }

203
Widgets/NSpinBox.qml Normal file
View file

@ -0,0 +1,203 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
RowLayout {
id: root
// Public properties
property alias value: spinBox.value
property alias from: spinBox.from
property alias to: spinBox.to
property alias stepSize: spinBox.stepSize
property string suffix: ""
property string prefix: ""
property string label: ""
property string description: ""
property bool enabled: true
property bool hovering: false
property int baseSize: Style.baseWidgetSize
// Convenience properties for common naming
property alias minimum: spinBox.from
property alias maximum: spinBox.to
signal entered
signal exited
Layout.fillWidth: true
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: label
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
visible: label !== ""
}
NText {
text: description
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: description !== ""
}
}
// Value
Rectangle {
id: spinBoxContainer
implicitWidth: 100 * scaling // Wider for better proportions
implicitHeight: (root.baseSize - 4) * scaling // Slightly shorter than toggle
radius: height * 0.5 // Fully rounded like toggle
color: Color.mSurfaceVariant
border.color: root.hovering ? Color.mPrimary : Color.mOutline
border.width: 1
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
// Mouse area for scroll wheel and hover
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
onEntered: {
root.hovering = true
root.entered()
}
onExited: {
root.hovering = false
root.exited()
}
onWheel: function (wheel) {
if (wheel.angleDelta.y > 0 && spinBox.value < spinBox.to) {
spinBox.increase()
} else if (wheel.angleDelta.y < 0 && spinBox.value > spinBox.from) {
spinBox.decrease()
}
}
}
// Decrease button (left)
Rectangle {
id: decreaseButton
width: parent.height * 0.8 // Make it circular
height: parent.height * 0.8
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: parent.height * 0.1
radius: width * 0.5 // Perfect circle
color: decreaseArea.containsMouse ? Color.mPrimary : "transparent"
opacity: root.enabled && spinBox.value > spinBox.from ? 1.0 : 0.3
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
NIcon {
anchors.centerIn: parent
text: "remove"
font.pointSize: Style.fontSizeS * scaling
color: decreaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
}
MouseArea {
id: decreaseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled && spinBox.value > spinBox.from
onClicked: spinBox.decrease()
}
}
// Increase button (right)
Rectangle {
id: increaseButton
width: parent.height * 0.8 // Make it circular
height: parent.height * 0.8
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: parent.height * 0.1
radius: width * 0.5 // Perfect circle
color: increaseArea.containsMouse ? Color.mPrimary : "transparent"
opacity: root.enabled && spinBox.value < spinBox.to ? 1.0 : 0.3
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
NIcon {
anchors.centerIn: parent
text: "add"
font.pointSize: Style.fontSizeS * scaling
color: increaseArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
}
MouseArea {
id: increaseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled && spinBox.value < spinBox.to
onClicked: spinBox.increase()
}
}
// Center value display
SpinBox {
id: spinBox
anchors.left: decreaseButton.right
anchors.right: increaseButton.left
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 4 * scaling
height: parent.height
background: Item {}
up.indicator: Item {}
down.indicator: Item {}
font.pointSize: Style.fontSizeM * scaling
font.family: Settings.data.ui.fontDefault
from: 0
to: 100
stepSize: 1
editable: false // Only use buttons/scroll
enabled: root.enabled
contentItem: Item {
anchors.fill: parent
NText {
anchors.centerIn: parent
text: root.prefix + spinBox.value + root.suffix
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}

View file

@ -6,7 +6,7 @@ import qs.Widgets
Text { Text {
id: root id: root
font.family: Settings.data.ui.fontFamily font.family: Settings.data.ui.fontDefault
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
color: Color.mOnSurface color: Color.mOnSurface

View file

@ -17,14 +17,15 @@ import qs.Modules.AppLauncher
import qs.Modules.Background import qs.Modules.Background
import qs.Modules.Bar import qs.Modules.Bar
import qs.Modules.Calendar import qs.Modules.Calendar
import qs.Modules.DemoPanel
import qs.Modules.Dock import qs.Modules.Dock
import qs.Modules.IPC import qs.Modules.IPC
import qs.Modules.LockScreen import qs.Modules.LockScreen
import qs.Modules.Notification import qs.Modules.Notification
import qs.Modules.SettingsPanel import qs.Modules.SettingsPanel
import qs.Modules.PowerPanel
import qs.Modules.SidePanel import qs.Modules.SidePanel
import qs.Modules.Toast import qs.Modules.Toast
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@ -41,10 +42,6 @@ ShellRoot {
id: appLauncherPanel id: appLauncherPanel
} }
DemoPanel {
id: demoPanel
}
SidePanel { SidePanel {
id: sidePanel id: sidePanel
} }
@ -69,6 +66,10 @@ ShellRoot {
id: lockScreen id: lockScreen
} }
PowerPanel {
id: powerPanel
}
ToastManager {} ToastManager {}
IPCManager {} IPCManager {}