Merge branch 'dev' into npanel-refactor
This commit is contained in:
commit
524135800e
21 changed files with 1234 additions and 322 deletions
|
|
@ -214,13 +214,22 @@ Singleton {
|
|||
|
||||
audio: JsonObject {
|
||||
property string visualizerType: "linear"
|
||||
property int volumeStep: 5
|
||||
}
|
||||
|
||||
// ui
|
||||
property JsonObject ui
|
||||
|
||||
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)
|
||||
|
|
|
|||
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"
|
||||
];
|
||||
}
|
||||
|
|
@ -15,30 +15,125 @@ import "../../Helpers/FuzzySort.js" as Fuzzysort
|
|||
NLoader {
|
||||
id: appLauncher
|
||||
isLoaded: false
|
||||
// Clipboard state is persisted in Services/ClipboardService.qml
|
||||
|
||||
content: Component {
|
||||
NPanel {
|
||||
id: appLauncherPanel
|
||||
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||
|
||||
// No local timer/processes; use persistent Clipboard service
|
||||
|
||||
// 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}'`])
|
||||
// Import modular components
|
||||
Calculator {
|
||||
id: calculator
|
||||
}
|
||||
|
||||
function copyText(text) {
|
||||
Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`])
|
||||
ClipboardHistory {
|
||||
id: clipboardHistory
|
||||
}
|
||||
|
||||
function updateClipboardHistory() {
|
||||
ClipboardService.refresh()
|
||||
// Properties
|
||||
property var desktopEntries: DesktopEntries.applications.values
|
||||
property string searchText: ""
|
||||
property int selectedIndex: 0
|
||||
|
||||
// Refresh clipboard when user starts typing clipboard commands
|
||||
onSearchTextChanged: {
|
||||
if (searchText.startsWith(">clip")) {
|
||||
clipboardHistory.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Main filtering logic
|
||||
property var filteredEntries: {
|
||||
Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0)
|
||||
if (!desktopEntries || desktopEntries.length === 0) {
|
||||
Logger.log("AppLauncher", "No desktop entries available")
|
||||
return []
|
||||
}
|
||||
|
||||
// Filter out entries that shouldn't be displayed
|
||||
var visibleEntries = desktopEntries.filter(entry => {
|
||||
if (!entry || entry.noDisplay) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
Logger.log("AppLauncher", "Visible entries:", visibleEntries.length)
|
||||
|
||||
var query = searchText ? searchText.toLowerCase() : ""
|
||||
var results = []
|
||||
|
||||
// Handle special commands
|
||||
if (query === ">") {
|
||||
results.push({
|
||||
"isCommand": true,
|
||||
"name": ">calc",
|
||||
"content": "Calculator - evaluate mathematical expressions",
|
||||
"icon": "calculate",
|
||||
"execute": executeCalcCommand
|
||||
})
|
||||
|
||||
results.push({
|
||||
"isCommand": true,
|
||||
"name": ">clip",
|
||||
"content": "Clipboard history - browse and restore clipboard items",
|
||||
"icon": "content_paste",
|
||||
"execute": executeClipCommand
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Handle clipboard history
|
||||
if (query.startsWith(">clip")) {
|
||||
return clipboardHistory.processQuery(query)
|
||||
}
|
||||
|
||||
// Handle calculator
|
||||
if (query.startsWith(">calc")) {
|
||||
return calculator.processQuery(query, "calc")
|
||||
}
|
||||
|
||||
// Handle direct math expressions after ">"
|
||||
if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) {
|
||||
const mathResults = calculator.processQuery(query, "direct")
|
||||
if (mathResults.length > 0) {
|
||||
return mathResults
|
||||
}
|
||||
// If math evaluation fails, fall through to regular search
|
||||
}
|
||||
|
||||
// Regular app search
|
||||
if (!query) {
|
||||
results = results.concat(visibleEntries.sort(function (a, b) {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
}))
|
||||
} else {
|
||||
var fuzzyResults = Fuzzysort.go(query, visibleEntries, {
|
||||
"keys": ["name", "comment", "genericName"]
|
||||
})
|
||||
results = results.concat(fuzzyResults.map(function (r) {
|
||||
return r.obj
|
||||
}))
|
||||
}
|
||||
|
||||
Logger.log("AppLauncher", "Filtered entries:", results.length)
|
||||
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)
|
||||
|
|
@ -67,233 +162,6 @@ NLoader {
|
|||
}
|
||||
}
|
||||
|
||||
property var desktopEntries: DesktopEntries.applications.values
|
||||
property string searchText: ""
|
||||
property int selectedIndex: 0
|
||||
|
||||
// Refresh clipboard when user starts typing clipboard commands
|
||||
onSearchTextChanged: {
|
||||
if (searchText.startsWith(">clip")) {
|
||||
ClipboardService.refresh()
|
||||
}
|
||||
}
|
||||
property var filteredEntries: {
|
||||
Logger.log("AppLauncher", "Total desktop entries:", desktopEntries ? desktopEntries.length : 0)
|
||||
if (!desktopEntries || desktopEntries.length === 0) {
|
||||
Logger.log("AppLauncher", "No desktop entries available")
|
||||
return []
|
||||
}
|
||||
|
||||
// Filter out entries that shouldn't be displayed
|
||||
var visibleEntries = desktopEntries.filter(entry => {
|
||||
if (!entry || entry.noDisplay) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
Logger.log("AppLauncher", "Visible entries:", visibleEntries.length)
|
||||
|
||||
var query = searchText ? searchText.toLowerCase() : ""
|
||||
var results = []
|
||||
|
||||
// Handle special commands
|
||||
if (query === ">") {
|
||||
results.push({
|
||||
"isCommand": true,
|
||||
"name": ">calc",
|
||||
"content": "Calculator - evaluate mathematical expressions",
|
||||
"icon": "tag",
|
||||
"execute": function () {
|
||||
searchText = ">calc "
|
||||
searchInput.cursorPosition = searchText.length
|
||||
}
|
||||
})
|
||||
|
||||
results.push({
|
||||
"isCommand": true,
|
||||
"name": ">clip",
|
||||
"content": "Clipboard history - browse and restore clipboard items",
|
||||
"icon": "content_paste",
|
||||
"execute": function () {
|
||||
searchText = ">clip "
|
||||
searchInput.cursorPosition = searchText.length
|
||||
}
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// Handle clipboard history
|
||||
if (query.startsWith(">clip")) {
|
||||
const searchTerm = query.slice(5).trim()
|
||||
|
||||
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
|
||||
if (query.startsWith(">calc")) {
|
||||
var expr = searchText.slice(5).trim()
|
||||
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)) {
|
||||
var displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/,
|
||||
'')
|
||||
results.push({
|
||||
"isCalculator": true,
|
||||
"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)`])
|
||||
}
|
||||
})
|
||||
} 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
|
||||
if (!query) {
|
||||
results = results.concat(visibleEntries.sort(function (a, b) {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
}))
|
||||
} else {
|
||||
var fuzzyResults = Fuzzysort.go(query, visibleEntries, {
|
||||
"keys": ["name", "comment", "genericName"]
|
||||
})
|
||||
results = results.concat(fuzzyResults.map(function (r) {
|
||||
return r.obj
|
||||
}))
|
||||
}
|
||||
|
||||
Logger.log("AppLauncher", "Filtered entries:", results.length)
|
||||
return results
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.log("AppLauncher", "Component completed")
|
||||
Logger.log("AppLauncher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined')
|
||||
|
|
@ -302,7 +170,7 @@ NLoader {
|
|||
DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined')
|
||||
}
|
||||
// Start clipboard refresh immediately on open
|
||||
updateClipboardHistory()
|
||||
clipboardHistory.refresh()
|
||||
}
|
||||
|
||||
// Main content container
|
||||
|
|
@ -368,7 +236,8 @@ NLoader {
|
|||
anchors.verticalCenter: parent.verticalCenter
|
||||
onTextChanged: {
|
||||
searchText = text
|
||||
selectedIndex = 0 // Reset selection when search changes
|
||||
// Defer selectedIndex reset to avoid binding loops
|
||||
Qt.callLater(() => selectedIndex = 0)
|
||||
}
|
||||
selectedTextColor: Color.mOnSurface
|
||||
selectionColor: Color.mPrimary
|
||||
|
|
@ -500,7 +369,7 @@ NLoader {
|
|||
visible: !parent.iconLoaded
|
||||
}
|
||||
|
||||
Text {
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard
|
||||
|| modelData.isCommand)
|
||||
|
|
|
|||
161
Modules/AppLauncher/Calculator.qml
Normal file
161
Modules/AppLauncher/Calculator.qml
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
158
Modules/AppLauncher/ClipboardHistory.qml
Normal file
158
Modules/AppLauncher/ClipboardHistory.qml
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
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, // Store the text data for the execute function
|
||||
"execute": 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import QtQuick
|
|||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
|
@ -17,6 +18,8 @@ Variants {
|
|||
readonly property real scaling: ScalingService.scale(screen)
|
||||
screen: modelData
|
||||
|
||||
WlrLayershell.namespace: "noctalia-bar"
|
||||
|
||||
implicitHeight: Style.barHeight * scaling
|
||||
color: Color.transparent
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ Item {
|
|||
IpcHandler {
|
||||
target: "idleInhibitor"
|
||||
|
||||
function toggle() {// TODO
|
||||
function toggle() {
|
||||
return IdleInhibitorService.manualToggle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +44,11 @@ Item {
|
|||
target: "lockScreen"
|
||||
|
||||
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.isLoaded) {
|
||||
lockScreen.isLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,14 +14,24 @@ import qs.Widgets
|
|||
|
||||
NLoader {
|
||||
id: lockScreen
|
||||
|
||||
// Log state changes to help debug lock screen issues
|
||||
onIsLoadedChanged: {
|
||||
Logger.log("LockScreen", "State changed - isLoaded:", isLoaded)
|
||||
}
|
||||
|
||||
// Allow a small grace period after unlocking so the compositor releases the lock surfaces
|
||||
Timer {
|
||||
id: unloadAfterUnlockTimer
|
||||
interval: 250
|
||||
repeat: false
|
||||
onTriggered: lockScreen.isLoaded = false
|
||||
onTriggered: {
|
||||
Logger.log("LockScreen", "Unload timer triggered - setting isLoaded to false")
|
||||
lockScreen.isLoaded = false
|
||||
}
|
||||
}
|
||||
function scheduleUnloadAfterUnlock() {
|
||||
Logger.log("LockScreen", "Scheduling unload after unlock")
|
||||
unloadAfterUnlockTimer.start()
|
||||
}
|
||||
content: Component {
|
||||
|
|
@ -233,13 +243,13 @@ NLoader {
|
|||
|
||||
// Time display - Large and prominent with pulse animation
|
||||
Column {
|
||||
spacing: Style.marginS * scaling
|
||||
spacing: Style.marginXS * scaling
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Text {
|
||||
NText {
|
||||
id: timeText
|
||||
text: Qt.formatDateTime(new Date(), "HH:mm")
|
||||
font.family: "Inter"
|
||||
font.family: Settings.data.ui.fontBillboard
|
||||
font.pointSize: Style.fontSizeXXXL * 6 * scaling
|
||||
font.weight: Font.Bold
|
||||
font.letterSpacing: -2 * scaling
|
||||
|
|
@ -261,10 +271,10 @@ NLoader {
|
|||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
NText {
|
||||
id: dateText
|
||||
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
|
||||
font.family: "Inter"
|
||||
font.family: Settings.data.ui.fontBillboard
|
||||
font.pointSize: Style.fontSizeXXL * scaling
|
||||
font.weight: Font.Light
|
||||
color: Color.mOnSurface
|
||||
|
|
@ -404,10 +414,10 @@ NLoader {
|
|||
anchors.margins: Style.marginM * scaling
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Text {
|
||||
NText {
|
||||
text: "SECURE TERMINAL"
|
||||
color: Color.mOnSurface
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Font.Bold
|
||||
Layout.fillWidth: true
|
||||
|
|
@ -424,10 +434,10 @@ NLoader {
|
|||
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
|
||||
}
|
||||
|
||||
Text {
|
||||
NText {
|
||||
text: Math.round(batteryIndicator.percent) + "%"
|
||||
color: Color.mOnSurface
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
|
@ -450,19 +460,19 @@ NLoader {
|
|||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Text {
|
||||
NText {
|
||||
text: "root@noctalia:~$"
|
||||
color: Color.mPrimary
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
Text {
|
||||
NText {
|
||||
id: welcomeText
|
||||
text: ""
|
||||
color: Color.mOnSurface
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
property int currentIndex: 0
|
||||
property string fullText: "Welcome back, " + Quickshell.env("USER") + "!"
|
||||
|
|
@ -488,18 +498,18 @@ NLoader {
|
|||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
Text {
|
||||
NText {
|
||||
text: "root@noctalia:~$"
|
||||
color: Color.mPrimary
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
Text {
|
||||
NText {
|
||||
text: "sudo unlock-session"
|
||||
color: Color.mOnSurface
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
}
|
||||
|
||||
|
|
@ -509,7 +519,7 @@ NLoader {
|
|||
width: 0
|
||||
height: 0
|
||||
visible: false
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
color: Color.mOnSurface
|
||||
echoMode: TextInput.Password
|
||||
|
|
@ -535,11 +545,11 @@ NLoader {
|
|||
}
|
||||
|
||||
// Visual password display with integrated cursor
|
||||
Text {
|
||||
NText {
|
||||
id: asterisksText
|
||||
text: "*".repeat(passwordInput.text.length)
|
||||
color: Color.mOnSurface
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeL * scaling
|
||||
visible: passwordInput.activeFocus
|
||||
|
||||
|
|
@ -585,7 +595,7 @@ NLoader {
|
|||
}
|
||||
|
||||
// Status messages
|
||||
Text {
|
||||
NText {
|
||||
text: lock.authenticating ? "Authenticating..." : (lock.errorMessage !== "" ? "Authentication failed." : "")
|
||||
color: lock.authenticating ? Color.mPrimary : (lock.errorMessage !== "" ? Color.mError : Color.transparent)
|
||||
font.family: "DejaVu Sans Mono"
|
||||
|
|
@ -618,11 +628,11 @@ NLoader {
|
|||
Layout.alignment: Qt.AlignRight
|
||||
Layout.bottomMargin: -12 * scaling
|
||||
|
||||
Text {
|
||||
NText {
|
||||
anchors.centerIn: parent
|
||||
text: lock.authenticating ? "EXECUTING" : "EXECUTE"
|
||||
color: executeButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary
|
||||
font.family: "DejaVu Sans Mono"
|
||||
font.family: Settings.data.ui.fontFixed
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -49,34 +49,17 @@ Item {
|
|||
spacing: Style.marginS * scaling
|
||||
Layout.fillWidth: true
|
||||
|
||||
NLabel {
|
||||
NSpinBox {
|
||||
Layout.fillWidth: true
|
||||
label: "Brightness Step Size"
|
||||
description: "Adjust the step size for brightness changes (scroll wheel, keyboard shortcuts)."
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM * scaling
|
||||
|
||||
NSlider {
|
||||
Layout.fillWidth: true
|
||||
from: 1
|
||||
to: 50
|
||||
value: Settings.data.brightness.brightnessStep
|
||||
stepSize: 1
|
||||
onPressedChanged: {
|
||||
if (!pressed) {
|
||||
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
|
||||
minimum: 1
|
||||
maximum: 50
|
||||
value: Settings.data.brightness.brightnessStep
|
||||
stepSize: 1
|
||||
suffix: "%"
|
||||
onValueChanged: {
|
||||
Settings.data.brightness.brightnessStep = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,17 @@ NBox {
|
|||
}
|
||||
}
|
||||
|
||||
// Idle Inhibitor
|
||||
NIconButton {
|
||||
icon: IdleInhibitorService.isInhibited ? "coffee" : "bedtime"
|
||||
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
|
||||
NIconButton {
|
||||
icon: "image"
|
||||
|
|
|
|||
|
|
@ -250,4 +250,4 @@ Thank you to everyone who supports me and this project 💜!
|
|||
|
||||
## License
|
||||
|
||||
This project is licensed under the terms of the [MIT License](./LICENSE).
|
||||
This project is licensed under the terms of the [MIT License](./LICENSE).
|
||||
|
|
@ -4,6 +4,7 @@ import QtQuick
|
|||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
|
@ -34,7 +35,7 @@ Singleton {
|
|||
readonly property alias muted: root._muted
|
||||
property bool _muted: !!sink?.audio?.muted
|
||||
|
||||
readonly property real stepVolume: 0.05
|
||||
readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [...root.sinks, ...root.sources]
|
||||
|
|
|
|||
|
|
@ -11,12 +11,40 @@ Singleton {
|
|||
|
||||
property var history: []
|
||||
property bool initialized: false
|
||||
property int maxHistory: 50 // Limit clipboard history entries
|
||||
|
||||
// Internal state
|
||||
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 {
|
||||
interval: 1000
|
||||
interval: 2000
|
||||
repeat: true
|
||||
running: root._enabled
|
||||
onTriggered: root.refresh()
|
||||
|
|
@ -32,14 +60,17 @@ Singleton {
|
|||
if (exitCode === 0) {
|
||||
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/'))
|
||||
if (imageType) {
|
||||
imageProcess.mimeType = imageType
|
||||
imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`]
|
||||
imageProcess.running = true
|
||||
} else {
|
||||
textProcess.command = ["wl-paste", "-n", "--type", "text/plain"]
|
||||
textProcess.running = true
|
||||
}
|
||||
} else {
|
||||
typeProcess.isLoading = false
|
||||
|
|
@ -65,17 +96,31 @@ Singleton {
|
|||
"timestamp": new Date().getTime()
|
||||
}
|
||||
|
||||
// Check if this exact image already exists
|
||||
const exists = root.history.find(item => item.type === 'image' && item.data === entry.data)
|
||||
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) {
|
||||
root.initialized = true
|
||||
typeProcess.isLoading = false
|
||||
}
|
||||
typeProcess.isLoading = false
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
|
|
@ -87,15 +132,18 @@ Singleton {
|
|||
property bool isLoading: false
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
textProcess.isLoading = false
|
||||
|
||||
if (exitCode === 0) {
|
||||
const content = String(stdout.text).trim()
|
||||
if (content) {
|
||||
if (content && content.length > 0) {
|
||||
const entry = {
|
||||
"type": 'text',
|
||||
"content": content,
|
||||
"timestamp": new Date().getTime()
|
||||
}
|
||||
|
||||
// Check if this exact text content already exists
|
||||
const exists = root.history.find(item => {
|
||||
if (item.type === 'text') {
|
||||
return item.content === content
|
||||
|
|
@ -104,36 +152,75 @@ Singleton {
|
|||
})
|
||||
|
||||
if (!exists) {
|
||||
const newHistory = root.history.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
"type": 'text',
|
||||
"content": item,
|
||||
"timestamp": new Date().getTime()
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
// Normalize existing history entries
|
||||
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, ...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
|
||||
typeProcess.isLoading = false
|
||||
if (!imageProcess.running) {
|
||||
typeProcess.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!typeProcess.isLoading && !textProcess.isLoading) {
|
||||
if (!typeProcess.isLoading && !textProcess.isLoading && !imageProcess.running) {
|
||||
typeProcess.isLoading = true
|
||||
typeProcess.command = ["wl-paste", "-l"]
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
184
Services/IdleInhibitorService.qml
Normal file
184
Services/IdleInhibitorService.qml
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
// Compact circular statistic display used in the SidePanel
|
||||
Rectangle {
|
||||
|
|
@ -73,7 +74,7 @@ Rectangle {
|
|||
}
|
||||
|
||||
// Percent centered in the circle
|
||||
Text {
|
||||
NText {
|
||||
id: valueLabel
|
||||
anchors.centerIn: parent
|
||||
text: `${root.value}${root.suffix}`
|
||||
|
|
|
|||
|
|
@ -47,4 +47,4 @@ Image {
|
|||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
id: root
|
||||
|
||||
font.family: Settings.data.ui.fontFamily
|
||||
font.family: Settings.data.ui.fontDefault
|
||||
font.pointSize: Style.fontSizeM * scaling
|
||||
font.weight: Style.fontWeightMedium
|
||||
color: Color.mOnSurface
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import qs.Modules.Notification
|
|||
import qs.Modules.SettingsPanel
|
||||
import qs.Modules.SidePanel
|
||||
import qs.Modules.Toast
|
||||
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue