Merge branch 'main' into shebang-fixes
This commit is contained in:
commit
1bd7ec27e8
56 changed files with 3616 additions and 3550 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -172,6 +171,24 @@ get_cpu_temp() {
|
||||||
else
|
else
|
||||||
echo 0 # Fallback
|
echo 0 # Fallback
|
||||||
fi
|
fi
|
||||||
|
elif [[ "$TEMP_SENSOR_TYPE" == "zenpower" ]]; then
|
||||||
|
# For zenpower, read the first available temp sensor
|
||||||
|
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
|
||||||
|
if [[ -f "$temp_file" ]]; then
|
||||||
|
local temp_value
|
||||||
|
temp_value=$(cat "$temp_file" | tr -d '\n\r') # Remove any newlines
|
||||||
|
echo "$((temp_value / 1000))"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo 0
|
||||||
|
|
||||||
|
if [[ -f "$tctl_input" ]]; then
|
||||||
|
# Read the temperature and convert from millidegrees to degrees.
|
||||||
|
echo "$(($(<"$tctl_input") / 1000))"
|
||||||
|
else
|
||||||
|
echo 0 # Fallback
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo 0 # Should not happen if cache logic is correct.
|
echo 0 # Should not happen if cache logic is correct.
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -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
152
Helpers/AdvancedMath.js
Normal 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"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -12,61 +12,22 @@ import qs.Widgets
|
||||||
|
|
||||||
import "../../Helpers/FuzzySort.js" as Fuzzysort
|
import "../../Helpers/FuzzySort.js" as Fuzzysort
|
||||||
|
|
||||||
NLoader {
|
NPanel {
|
||||||
id: appLauncher
|
id: root
|
||||||
isLoaded: false
|
panelWidth: Math.min(700 * scaling, screen?.width * 0.75)
|
||||||
// Clipboard state is persisted in Services/ClipboardService.qml
|
panelHeight: Math.min(550 * scaling, screen?.height * 0.8)
|
||||||
content: Component {
|
panelAnchorCentered: true
|
||||||
NPanel {
|
|
||||||
id: appLauncherPanel
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -583,6 +437,4 @@ NLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
151
Modules/AppLauncher/Calculator.qml
Normal file
151
Modules/AppLauncher/Calculator.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
157
Modules/AppLauncher/ClipboardHistory.qml
Normal file
157
Modules/AppLauncher/ClipboardHistory.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -49,4 +52,5 @@ Variants {
|
||||||
mipmap: false
|
mipmap: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
398
Modules/Bar/BluetoothPanel.qml
Normal file
398
Modules/Bar/BluetoothPanel.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -70,7 +70,7 @@ Item {
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
|
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
|
||||||
settingsPanel.isLoaded = true
|
settingsPanel.open(screen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,94 +122,37 @@ 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 {
|
||||||
id: trayMenu
|
id: trayMenu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ Item {
|
||||||
}
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
|
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
|
||||||
settingsPanel.isLoaded = true
|
settingsPanel.open(screen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
335
Modules/Bar/WiFiPanel.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -230,7 +138,4 @@ NLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,6 +186,4 @@ NLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
345
Modules/PowerPanel/PowerPanel.qml
Normal file
345
Modules/PowerPanel/PowerPanel.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -371,6 +297,4 @@ NLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,150 +7,22 @@ import qs.Commons
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
NLoader {
|
NPanel {
|
||||||
id: root
|
id: panel
|
||||||
|
|
||||||
// X coordinate on screen (in pixels) where the panel should align its center.
|
panelWidth: 460 * scaling
|
||||||
// Set via openAt(x) from the bar button.
|
panelHeight: 700 * scaling
|
||||||
property real anchorX: 0
|
panelAnchorRight: true
|
||||||
// Target screen to open on
|
|
||||||
property var targetScreen: null
|
|
||||||
|
|
||||||
function openAt(x, screen) {
|
panelContent: Item {
|
||||||
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 {
|
|
||||||
id: sidePanel
|
|
||||||
|
|
||||||
// Single source of truth for spacing between cards (both axes)
|
|
||||||
property real cardSpacing: Style.marginL * scaling
|
|
||||||
// X coordinate from the bar to align this panel under
|
|
||||||
property real anchorX: root.anchorX
|
|
||||||
// Ensure this panel attaches to the intended screen
|
|
||||||
screen: root.targetScreen
|
|
||||||
|
|
||||||
// Override hide function to animate first
|
|
||||||
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
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
183
Services/IdleInhibitorService.qml
Normal file
183
Services/IdleInhibitorService.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}`
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
203
Widgets/NSpinBox.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
11
shell.qml
11
shell.qml
|
|
@ -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 {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue