1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-14 09:42:10 -04:00

replace qmlformat with a better tool

still not perfect, but well - what can ya do
This commit is contained in:
bbedward
2025-08-08 15:55:37 -04:00
parent 8dc6e2805d
commit 4d408c65f2
137 changed files with 30315 additions and 29625 deletions

View File

@@ -1,28 +1,26 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
Singleton { Singleton {
id: root id: root
readonly property int durShort: 200 readonly property int durShort: 200
readonly property int durMed: 450 readonly property int durMed: 450
readonly property int durLong: 600 readonly property int durLong: 600
readonly property int slidePx: 80 readonly property int slidePx: 80
readonly property var emphasized: [ readonly property var emphasized: [0.05, 0.00, 0.133333, 0.06, 0.166667, 0.40, 0.208333, 0.82, 0.25, 1.00, 1.00, 1.00]
0.05, 0.00, 0.133333, 0.06, 0.166667, 0.40,
0.208333, 0.82, 0.25, 1.00, 1.00, 1.00
]
readonly property var emphasizedDecel: [ 0.05, 0.70, 0.10, 1.00, 1.00, 1.00 ] readonly property var emphasizedDecel: [0.05, 0.70, 0.10, 1.00, 1.00, 1.00]
readonly property var emphasizedAccel: [ 0.30, 0.00, 0.80, 0.15, 1.00, 1.00 ] readonly property var emphasizedAccel: [0.30, 0.00, 0.80, 0.15, 1.00, 1.00]
readonly property var standard: [ 0.20, 0.00, 0.00, 1.00, 1.00, 1.00 ] readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardDecel: [ 0.00, 0.00, 0.00, 1.00, 1.00, 1.00 ] readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardAccel: [ 0.30, 0.00, 1.00, 1.00, 1.00, 1.00 ] readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtCore import QtCore
import QtQuick import QtQuick
@@ -8,118 +9,123 @@ import Quickshell.Io
Singleton { Singleton {
id: root id: root
property var appUsageRanking: {} property var appUsageRanking: {
Component.onCompleted: { }
loadSettings();
Component.onCompleted: {
loadSettings()
}
function loadSettings() {
parseSettings(settingsFile.text())
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content)
appUsageRanking = settings.appUsageRanking || {}
}
} catch (e) {
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
"appUsageRanking": appUsageRanking
}, null, 2))
}
function addAppUsage(app) {
if (!app)
return
var appId = app.id || (app.execString || app.exec || "")
if (!appId)
return
var currentRanking = Object.assign({}, appUsageRanking)
if (currentRanking[appId]) {
currentRanking[appId].usageCount = (currentRanking[appId].usageCount
|| 1) + 1
currentRanking[appId].lastUsed = Date.now()
currentRanking[appId].icon = app.icon || currentRanking[appId].icon
|| "application-x-executable"
currentRanking[appId].name = app.name || currentRanking[appId].name || ""
} else {
currentRanking[appId] = {
"name": app.name || "",
"exec": app.execString || app.exec || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"usageCount": 1,
"lastUsed": Date.now()
}
} }
function loadSettings() { appUsageRanking = currentRanking
parseSettings(settingsFile.text()); saveSettings()
}
function getAppUsageRanking() {
return appUsageRanking
}
function getRankedApps() {
var apps = []
for (var appId in appUsageRanking) {
var appData = appUsageRanking[appId]
apps.push({
"id": appId,
"name": appData.name,
"exec": appData.exec,
"icon": appData.icon,
"comment": appData.comment,
"usageCount": appData.usageCount,
"lastUsed": appData.lastUsed
})
} }
function parseSettings(content) { return apps.sort(function (a, b) {
try { if (a.usageCount !== b.usageCount)
if (content && content.trim()) { return b.usageCount - a.usageCount
var settings = JSON.parse(content); return a.name.localeCompare(b.name)
appUsageRanking = settings.appUsageRanking || {}; })
} }
} catch (e) {
} function cleanupAppUsageRanking(availableAppIds) {
var currentRanking = Object.assign({}, appUsageRanking)
var hasChanges = false
for (var appId in currentRanking) {
if (availableAppIds.indexOf(appId) === -1) {
delete currentRanking[appId]
hasChanges = true
}
} }
function saveSettings() { if (hasChanges) {
settingsFile.setText(JSON.stringify({ appUsageRanking = currentRanking
"appUsageRanking": appUsageRanking saveSettings()
}, null, 2));
} }
}
function addAppUsage(app) { FileView {
if (!app) id: settingsFile
return;
var appId = app.id || (app.execString || app.exec || ""); path: StandardPaths.writableLocation(
if (!appId) StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json"
return; blockLoading: true
blockWrites: true
var currentRanking = Object.assign({}, appUsageRanking); watchChanges: true
onLoaded: {
if (currentRanking[appId]) { parseSettings(settingsFile.text())
currentRanking[appId].usageCount = (currentRanking[appId].usageCount || 1) + 1;
currentRanking[appId].lastUsed = Date.now();
currentRanking[appId].icon = app.icon || currentRanking[appId].icon || "application-x-executable";
currentRanking[appId].name = app.name || currentRanking[appId].name || "";
} else {
currentRanking[appId] = {
"name": app.name || "",
"exec": app.execString || app.exec || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"usageCount": 1,
"lastUsed": Date.now()
};
}
appUsageRanking = currentRanking;
saveSettings();
}
function getAppUsageRanking() {
return appUsageRanking;
}
function getRankedApps() {
var apps = [];
for (var appId in appUsageRanking) {
var appData = appUsageRanking[appId];
apps.push({
id: appId,
name: appData.name,
exec: appData.exec,
icon: appData.icon,
comment: appData.comment,
usageCount: appData.usageCount,
lastUsed: appData.lastUsed
});
}
return apps.sort(function(a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount;
return a.name.localeCompare(b.name);
});
}
function cleanupAppUsageRanking(availableAppIds) {
var currentRanking = Object.assign({}, appUsageRanking);
var hasChanges = false;
for (var appId in currentRanking) {
if (availableAppIds.indexOf(appId) === -1) {
delete currentRanking[appId];
hasChanges = true;
}
}
if (hasChanges) {
appUsageRanking = currentRanking;
saveSettings();
}
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
parseSettings(settingsFile.text());
}
onLoadFailed: (error) => {
}
} }
onLoadFailed: error => {}
}
} }

View File

@@ -1,65 +1,67 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
Singleton { Singleton {
id: root id: root
readonly property Rounding rounding: Rounding {} readonly property Rounding rounding: Rounding {}
readonly property Spacing spacing: Spacing {} readonly property Spacing spacing: Spacing {}
readonly property FontSize fontSize: FontSize {} readonly property FontSize fontSize: FontSize {}
readonly property Anim anim: Anim {} readonly property Anim anim: Anim {}
component Rounding: QtObject { component Rounding: QtObject {
readonly property int small: 8 readonly property int small: 8
readonly property int normal: 12 readonly property int normal: 12
readonly property int large: 16 readonly property int large: 16
readonly property int extraLarge: 24 readonly property int extraLarge: 24
readonly property int full: 1000 readonly property int full: 1000
} }
component Spacing: QtObject { component Spacing: QtObject {
readonly property int small: 4 readonly property int small: 4
readonly property int normal: 8 readonly property int normal: 8
readonly property int large: 12 readonly property int large: 12
readonly property int extraLarge: 16 readonly property int extraLarge: 16
readonly property int huge: 24 readonly property int huge: 24
} }
component FontSize: QtObject { component FontSize: QtObject {
readonly property int small: 12 readonly property int small: 12
readonly property int normal: 14 readonly property int normal: 14
readonly property int large: 16 readonly property int large: 16
readonly property int extraLarge: 20 readonly property int extraLarge: 20
readonly property int huge: 24 readonly property int huge: 24
} }
component AnimCurves: QtObject { component AnimCurves: QtObject {
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1] readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1] readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1] readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
} readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
}
component AnimDurations: QtObject { component AnimDurations: QtObject {
readonly property int quick: 150 readonly property int quick: 150
readonly property int normal: 300 readonly property int normal: 300
readonly property int slow: 500 readonly property int slow: 500
readonly property int extraSlow: 1000 readonly property int extraSlow: 1000
readonly property int expressiveFastSpatial: 350 readonly property int expressiveFastSpatial: 350
readonly property int expressiveDefaultSpatial: 500 readonly property int expressiveDefaultSpatial: 500
readonly property int expressiveEffects: 200 readonly property int expressiveEffects: 200
} }
component Anim: QtObject { component Anim: QtObject {
readonly property AnimCurves curves: AnimCurves {} readonly property AnimCurves curves: AnimCurves {}
readonly property AnimDurations durations: AnimDurations {} readonly property AnimDurations durations: AnimDurations {}
} }
} }

View File

@@ -3,39 +3,43 @@ import qs.Common
pragma Singleton pragma Singleton
Singleton { Singleton {
id: root id: root
// Clear all image cache // Clear all image cache
function clearImageCache() { function clearImageCache() {
Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)]); Quickshell.execDetached(["rm", "-rf", Paths.stringify(Paths.imagecache)])
Paths.mkdir(Paths.imagecache); Paths.mkdir(Paths.imagecache)
} }
// Clear cache older than specified minutes // Clear cache older than specified minutes
function clearOldCache(ageInMinutes) { function clearOldCache(ageInMinutes) {
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]); Quickshell.execDetached(
} ["find", Paths.stringify(
Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"])
}
// Clear cache for specific size // Clear cache for specific size
function clearCacheForSize(size) { function clearCacheForSize(size) {
Quickshell.execDetached(["find", Paths.stringify(Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]); Quickshell.execDetached(
} ["find", Paths.stringify(
Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"])
// Get cache size in MB }
function getCacheSize(callback) {
var process = Qt.createQmlObject(`
import Quickshell.Io
Process {
command: ["du", "-sm", "${Paths.stringify(Paths.imagecache)}"]
running: true
stdout: StdioCollector {
onStreamFinished: {
var sizeMB = parseInt(text.split("\\t")[0]) || 0
callback(sizeMB)
}
}
}
`, root);
}
// Get cache size in MB
function getCacheSize(callback) {
var process = Qt.createQmlObject(`
import Quickshell.Io
Process {
command: ["du", "-sm", "${Paths.stringify(
Paths.imagecache)}"]
running: true
stdout: StdioCollector {
onStreamFinished: {
var sizeMB = parseInt(text.split("\\t")[0]) || 0
callback(sizeMB)
}
}
}
`, root)
}
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import Qt.labs.platform import Qt.labs.platform
import QtQuick import QtQuick
@@ -9,347 +10,376 @@ import qs.Services
import qs.Common import qs.Common
Singleton { Singleton {
id: root id: root
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation) readonly property string _homeUrl: StandardPaths.writableLocation(
readonly property string homeDir: _homeUrl.startsWith("file://") ? _homeUrl.substring(7) : _homeUrl StandardPaths.HomeLocation)
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation) readonly property string homeDir: _homeUrl.startsWith(
readonly property string configDir: _configUrl.startsWith("file://") ? _configUrl.substring(7) : _configUrl "file://") ? _homeUrl.substring(
readonly property string shellDir: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Common/", "") 7) : _homeUrl
readonly property string wallpaperPath: SessionData.wallpaperPath readonly property string _configUrl: StandardPaths.writableLocation(
property bool matugenAvailable: false StandardPaths.ConfigLocation)
property bool gtkThemingEnabled: false readonly property string configDir: _configUrl.startsWith(
property bool qtThemingEnabled: false "file://") ? _configUrl.substring(
property bool systemThemeGenerationInProgress: false 7) : _configUrl
property string matugenJson: "" readonly property string shellDir: Qt.resolvedUrl(".").toString().replace(
property var matugenColors: ({}) "file://", "").replace("/Common/", "")
property bool extractionRequested: false readonly property string wallpaperPath: SessionData.wallpaperPath
property int colorUpdateTrigger: 0 property bool matugenAvailable: false
property string lastWallpaperTimestamp: "" property bool gtkThemingEnabled: false
property color primary: getMatugenColor("primary", "#42a5f5") property bool qtThemingEnabled: false
property color secondary: getMatugenColor("secondary", "#8ab4f8") property bool systemThemeGenerationInProgress: false
property color tertiary: getMatugenColor("tertiary", "#bb86fc") property string matugenJson: ""
property color tertiaryContainer: getMatugenColor("tertiary_container", "#3700b3") property var matugenColors: ({})
property color error: getMatugenColor("error", "#cf6679") property bool extractionRequested: false
property color inversePrimary: getMatugenColor("inverse_primary", "#6200ea") property int colorUpdateTrigger: 0
property color bg: getMatugenColor("background", "#1a1c1e") property string lastWallpaperTimestamp: ""
property color surface: getMatugenColor("surface", "#1a1c1e") property color primary: getMatugenColor("primary", "#42a5f5")
property color surfaceContainer: getMatugenColor("surface_container", "#1e2023") property color secondary: getMatugenColor("secondary", "#8ab4f8")
property color surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f") property color tertiary: getMatugenColor("tertiary", "#bb86fc")
property color surfaceVariant: getMatugenColor("surface_variant", "#44464f") property color tertiaryContainer: getMatugenColor("tertiary_container",
property color surfaceText: getMatugenColor("on_background", "#e3e8ef") "#3700b3")
property color primaryText: getMatugenColor("on_primary", "#ffffff") property color error: getMatugenColor("error", "#cf6679")
property color surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5") property color inversePrimary: getMatugenColor("inverse_primary", "#6200ea")
property color primaryContainer: getMatugenColor("primary_container", "#1976d2") property color bg: getMatugenColor("background", "#1a1c1e")
property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8") property color surface: getMatugenColor("surface", "#1a1c1e")
property color outline: getMatugenColor("outline", "#8e918f") property color surfaceContainer: getMatugenColor("surface_container",
property color accentHi: primary "#1e2023")
property color accentLo: secondary property color surfaceContainerHigh: getMatugenColor(
"surface_container_high", "#292b2f")
property color surfaceVariant: getMatugenColor("surface_variant", "#44464f")
property color surfaceText: getMatugenColor("on_background", "#e3e8ef")
property color primaryText: getMatugenColor("on_primary", "#ffffff")
property color surfaceVariantText: getMatugenColor("on_surface_variant",
"#c4c7c5")
property color primaryContainer: getMatugenColor("primary_container",
"#1976d2")
property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8")
property color outline: getMatugenColor("outline", "#8e918f")
property color accentHi: primary
property color accentLo: secondary
signal colorsUpdated signal colorsUpdated
function onLightModeChanged() { function onLightModeChanged() {
if (matugenColors && Object.keys(matugenColors).length > 0) { if (matugenColors && Object.keys(matugenColors).length > 0) {
colorUpdateTrigger++; colorUpdateTrigger++
colorsUpdated(); colorsUpdated()
if (typeof Theme !== "undefined" && Theme.isDynamicTheme) { if (typeof Theme !== "undefined" && Theme.isDynamicTheme) {
generateSystemThemes(); generateSystemThemes()
} }
}
}
function extractColors() {
extractionRequested = true
if (matugenAvailable)
fileChecker.running = true
else
matugenCheck.running = true
}
function getMatugenColor(path, fallback) {
colorUpdateTrigger
const colorMode = (typeof SessionData !== "undefined"
&& SessionData.isLightMode) ? "light" : "dark"
let cur = matugenColors && matugenColors.colors
&& matugenColors.colors[colorMode]
for (const part of path.split(".")) {
if (!cur || typeof cur !== "object" || !(part in cur))
return fallback
cur = cur[part]
}
return cur || fallback
}
function isColorDark(c) {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
}
Component.onCompleted: {
matugenCheck.running = true
checkGtkThemingAvailability()
checkQtThemingAvailability()
if (typeof SessionData !== "undefined")
SessionData.isLightModeChanged.connect(root.onLightModeChanged)
}
Process {
id: matugenCheck
command: ["which", "matugen"]
onExited: code => {
matugenAvailable = (code === 0)
if (!matugenAvailable) {
ToastService.wallpaperErrorStatus = "matugen_missing"
ToastService.showWarning("matugen not found - dynamic theming disabled")
return
}
if (extractionRequested) {
fileChecker.running = true
}
}
}
Process {
id: fileChecker
command: ["test", "-r", wallpaperPath]
onExited: code => {
if (code === 0) {
matugenProcess.running = true
} else {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed")
}
}
}
Process {
id: matugenProcess
command: ["matugen", "-v", "image", wallpaperPath, "--json", "hex"]
stdout: StdioCollector {
id: matugenCollector
onStreamFinished: {
const out = matugenCollector.text
if (!out.length) {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper Processing Failed")
return
} }
} try {
root.matugenJson = out
function extractColors() { root.matugenColors = JSON.parse(out)
extractionRequested = true; root.colorsUpdated()
if (matugenAvailable) generateAppConfigs()
fileChecker.running = true; ToastService.clearWallpaperError()
else } catch (e) {
matugenCheck.running = true; ToastService.wallpaperErrorStatus = "error"
} ToastService.showError("Wallpaper Processing Failed")
function getMatugenColor(path, fallback) {
colorUpdateTrigger;
const colorMode = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "light" : "dark";
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode];
for (const part of path.split(".")) {
if (!cur || typeof cur !== "object" || !(part in cur))
return fallback;
cur = cur[part];
} }
return cur || fallback; }
} }
function isColorDark(c) { stderr: StdioCollector {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5; id: matugenErr
}
}
function generateAppConfigs() {
if (!matugenColors || !matugenColors.colors) {
return
} }
Component.onCompleted: { generateNiriConfig()
matugenCheck.running = true; generateGhosttyConfig()
checkGtkThemingAvailability();
checkQtThemingAvailability(); if (gtkThemingEnabled && typeof SettingsData !== "undefined"
if (typeof SessionData !== "undefined") && SettingsData.gtkThemingEnabled) {
SessionData.isLightModeChanged.connect(root.onLightModeChanged); generateSystemThemes()
} else if (qtThemingEnabled && typeof SettingsData !== "undefined"
&& SettingsData.qtThemingEnabled) {
generateSystemThemes()
} }
}
Process { function generateNiriConfig() {
id: matugenCheck var dark = matugenColors.colors.dark
if (!dark)
return
command: ["which", "matugen"] var bg = dark.background || "#1a1c1e"
onExited: code => { var primary = dark.primary || "#42a5f5"
matugenAvailable = (code === 0); var secondary = dark.secondary || "#8ab4f8"
if (!matugenAvailable) { var inverse = dark.inverse_primary || "#6200ea"
ToastService.wallpaperErrorStatus = "matugen_missing";
ToastService.showWarning("matugen not found - dynamic theming disabled");
return;
}
if (extractionRequested) {
fileChecker.running = true;
}
}
}
Process { var content = `layout {
id: fileChecker
command: ["test", "-r", wallpaperPath]
onExited: code => {
if (code === 0) {
matugenProcess.running = true;
} else {
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper processing failed");
}
}
}
Process {
id: matugenProcess
command: ["matugen", "-v", "image", wallpaperPath, "--json", "hex"]
stdout: StdioCollector {
id: matugenCollector
onStreamFinished: {
const out = matugenCollector.text;
if (!out.length) {
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper Processing Failed");
return;
}
try {
root.matugenJson = out;
root.matugenColors = JSON.parse(out);
root.colorsUpdated();
generateAppConfigs();
ToastService.clearWallpaperError();
} catch (e) {
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper Processing Failed");
}
}
}
stderr: StdioCollector {
id: matugenErr
}
}
function generateAppConfigs() {
if (!matugenColors || !matugenColors.colors) {
return;
}
generateNiriConfig();
generateGhosttyConfig();
if (gtkThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) {
generateSystemThemes();
} else if (qtThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) {
generateSystemThemes();
}
}
function generateNiriConfig() {
var dark = matugenColors.colors.dark;
if (!dark)
return;
var bg = dark.background || "#1a1c1e";
var primary = dark.primary || "#42a5f5";
var secondary = dark.secondary || "#8ab4f8";
var inverse = dark.inverse_primary || "#6200ea";
var content = `layout {
border { border {
active-color "${primary}" active-color "${primary}"
inactive-color "${secondary}" inactive-color "${secondary}"
} }
focus-ring { focus-ring {
active-color "${inverse}" active-color "${inverse}"
} }
background-color "${bg}" background-color "${bg}"
}`; }`
Quickshell.execDetached(["bash", "-c", `echo '${content}' > niri-colors.generated.kdl`]); Quickshell.execDetached(
["bash", "-c", `echo '${content}' > niri-colors.generated.kdl`])
}
function generateGhosttyConfig() {
var dark = matugenColors.colors.dark
var light = matugenColors.colors.light
if (!dark || !light)
return
var bg = dark.background || "#1a1c1e"
var fg = dark.on_background || "#e3e8ef"
var primary = dark.primary || "#42a5f5"
var secondary = dark.secondary || "#8ab4f8"
var tertiary = dark.tertiary || "#bb86fc"
var tertiary_ctr = dark.tertiary_container || "#3700b3"
var error = dark.error || "#cf6679"
var inverse = dark.inverse_primary || "#6200ea"
var bg_b = light.background || "#fef7ff"
var fg_b = light.on_background || "#1d1b20"
var primary_b = light.primary || "#1976d2"
var secondary_b = light.secondary || "#1565c0"
var tertiary_b = light.tertiary || "#7b1fa2"
var tertiary_ctr_b = light.tertiary_container || "#e1bee7"
var error_b = light.error || "#b00020"
var inverse_b = light.inverse_primary || "#bb86fc"
var content = `background = ${bg}
foreground = ${fg}
cursor-color = ${inverse}
selection-background = ${secondary}
selection-foreground = #ffffff
palette = 0=${bg}
palette = 1=${error}
palette = 2=${tertiary}
palette = 3=${secondary}
palette = 4=${primary}
palette = 5=${tertiary_ctr}
palette = 6=${inverse}
palette = 7=${fg}
palette = 8=${bg_b}
palette = 9=${error_b}
palette = 10=${tertiary_b}
palette = 11=${secondary_b}
palette = 12=${primary_b}
palette = 13=${tertiary_ctr_b}
palette = 14=${inverse_b}
palette = 15=${fg_b}`
var ghosttyConfigDir = configDir + "/ghostty"
var ghosttyConfigPath = ghosttyConfigDir + "/config-dankcolors"
Quickshell.execDetached(
["bash", "-c", `mkdir -p '${ghosttyConfigDir}' && echo '${content}' > '${ghosttyConfigPath}'`])
}
function checkGtkThemingAvailability() {
gtkAvailabilityChecker.running = true
}
function checkQtThemingAvailability() {
qtAvailabilityChecker.running = true
}
function generateSystemThemes() {
if (systemThemeGenerationInProgress) {
return
} }
function generateGhosttyConfig() { if (!matugenAvailable) {
var dark = matugenColors.colors.dark; return
var light = matugenColors.colors.light;
if (!dark || !light)
return;
var bg = dark.background || "#1a1c1e";
var fg = dark.on_background || "#e3e8ef";
var primary = dark.primary || "#42a5f5";
var secondary = dark.secondary || "#8ab4f8";
var tertiary = dark.tertiary || "#bb86fc";
var tertiary_ctr = dark.tertiary_container || "#3700b3";
var error = dark.error || "#cf6679";
var inverse = dark.inverse_primary || "#6200ea";
var bg_b = light.background || "#fef7ff";
var fg_b = light.on_background || "#1d1b20";
var primary_b = light.primary || "#1976d2";
var secondary_b = light.secondary || "#1565c0";
var tertiary_b = light.tertiary || "#7b1fa2";
var tertiary_ctr_b = light.tertiary_container || "#e1bee7";
var error_b = light.error || "#b00020";
var inverse_b = light.inverse_primary || "#bb86fc";
var content = `background = ${bg}
foreground = ${fg}
cursor-color = ${inverse}
selection-background = ${secondary}
selection-foreground = #ffffff
palette = 0=${bg}
palette = 1=${error}
palette = 2=${tertiary}
palette = 3=${secondary}
palette = 4=${primary}
palette = 5=${tertiary_ctr}
palette = 6=${inverse}
palette = 7=${fg}
palette = 8=${bg_b}
palette = 9=${error_b}
palette = 10=${tertiary_b}
palette = 11=${secondary_b}
palette = 12=${primary_b}
palette = 13=${tertiary_ctr_b}
palette = 14=${inverse_b}
palette = 15=${fg_b}`;
var ghosttyConfigDir = configDir + "/ghostty";
var ghosttyConfigPath = ghosttyConfigDir + "/config-dankcolors";
Quickshell.execDetached(["bash", "-c", `mkdir -p '${ghosttyConfigDir}' && echo '${content}' > '${ghosttyConfigPath}'`]);
} }
function checkGtkThemingAvailability() { if (!wallpaperPath || wallpaperPath === "") {
gtkAvailabilityChecker.running = true; return
} }
function checkQtThemingAvailability() { const isLight = (typeof SessionData !== "undefined"
qtAvailabilityChecker.running = true; && SessionData.isLightMode) ? "true" : "false"
const iconTheme = (typeof SettingsData !== "undefined"
&& SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
const gtkTheming = (typeof SettingsData !== "undefined"
&& SettingsData.gtkThemingEnabled) ? "true" : "false"
const qtTheming = (typeof SettingsData !== "undefined"
&& SettingsData.qtThemingEnabled) ? "true" : "false"
systemThemeGenerationInProgress = true
systemThemeGenerator.command = [shellDir + "/generate-themes.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme, gtkTheming, qtTheming]
systemThemeGenerator.running = true
}
function restoreSystemThemes() {
const shellDir = root.shellDir
if (!shellDir) {
return
} }
function generateSystemThemes() { const isLight = (typeof SessionData !== "undefined"
if (systemThemeGenerationInProgress) { && SessionData.isLightMode) ? "true" : "false"
return; const iconTheme = (typeof SettingsData !== "undefined"
} && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"
const gtkTheming = (typeof SettingsData !== "undefined"
&& SettingsData.gtkThemingEnabled) ? "true" : "false"
const qtTheming = (typeof SettingsData !== "undefined"
&& SettingsData.qtThemingEnabled) ? "true" : "false"
if (!matugenAvailable) { systemThemeRestoreProcess.command = [shellDir + "/generate-themes.sh", "", shellDir, configDir, "restore", isLight, iconTheme, gtkTheming, qtTheming]
return; systemThemeRestoreProcess.running = true
} }
if (!wallpaperPath || wallpaperPath === "") { Process {
return; id: gtkAvailabilityChecker
} command: ["bash", "-c", "command -v gsettings >/dev/null && [ -d "
+ configDir + "/gtk-3.0 -o -d " + configDir + "/gtk-4.0 ]"]
running: false
onExited: exitCode => {
gtkThemingEnabled = (exitCode === 0)
}
}
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"; Process {
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"; id: qtAvailabilityChecker
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false"; command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"]
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false"; running: false
onExited: exitCode => {
qtThemingEnabled = (exitCode === 0)
}
}
systemThemeGenerationInProgress = true; Process {
systemThemeGenerator.command = [shellDir + "/generate-themes.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme, gtkTheming, qtTheming]; id: systemThemeGenerator
systemThemeGenerator.running = true; running: false
stdout: StdioCollector {
id: systemThemeStdout
} }
function restoreSystemThemes() { stderr: StdioCollector {
const shellDir = root.shellDir; id: systemThemeStderr
if (!shellDir) {
return;
}
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false";
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false";
systemThemeRestoreProcess.command = [shellDir + "/generate-themes.sh", "", shellDir, configDir, "restore", isLight, iconTheme, gtkTheming, qtTheming];
systemThemeRestoreProcess.running = true;
} }
Process { onExited: exitCode => {
id: gtkAvailabilityChecker systemThemeGenerationInProgress = false
command: ["bash", "-c", "command -v gsettings >/dev/null && [ -d " + configDir + "/gtk-3.0 -o -d " + configDir + "/gtk-4.0 ]"]
running: false if (exitCode !== 0) {
onExited: exitCode => { ToastService.showError(
gtkThemingEnabled = (exitCode === 0); "Failed to generate system themes: " + systemThemeStderr.text)
} }
}
}
Process {
id: systemThemeRestoreProcess
running: false
stdout: StdioCollector {
id: restoreThemeStdout
} }
Process { stderr: StdioCollector {
id: qtAvailabilityChecker id: restoreThemeStderr
command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"]
running: false
onExited: exitCode => {
qtThemingEnabled = (exitCode === 0);
}
} }
Process { onExited: exitCode => {
id: systemThemeGenerator if (exitCode === 0) {
running: false ToastService.showInfo("System themes restored to default")
} else {
stdout: StdioCollector { ToastService.showWarning(
id: systemThemeStdout "Failed to restore system themes: " + restoreThemeStderr.text)
} }
stderr: StdioCollector {
id: systemThemeStderr
}
onExited: exitCode => {
systemThemeGenerationInProgress = false;
if (exitCode !== 0) {
ToastService.showError("Failed to generate system themes: " + systemThemeStderr.text);
}
}
}
Process {
id: systemThemeRestoreProcess
running: false
stdout: StdioCollector {
id: restoreThemeStdout
}
stderr: StdioCollector {
id: restoreThemeStderr
}
onExited: exitCode => {
if (exitCode === 0) {
ToastService.showInfo("System themes restored to default");
} else {
ToastService.showWarning("Failed to restore system themes: " + restoreThemeStderr.text);
}
}
} }
}
} }

View File

@@ -4,39 +4,45 @@ import Quickshell
import QtCore import QtCore
Singleton { Singleton {
id: root id: root
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] readonly property url home: StandardPaths.standardLocations(
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] StandardPaths.HomeLocation)[0]
readonly property url pictures: StandardPaths.standardLocations(
StandardPaths.PicturesLocation)[0]
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell` readonly property url data: `${StandardPaths.standardLocations(
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell` StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
readonly property url cache: `${StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell` readonly property url state: `${StandardPaths.standardLocations(
readonly property url config: `${StandardPaths.standardLocations(StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell` StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
readonly property url cache: `${StandardPaths.standardLocations(
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
readonly property url config: `${StandardPaths.standardLocations(
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell`
readonly property url imagecache: `${cache}/imagecache` readonly property url imagecache: `${cache}/imagecache`
function stringify(path: url): string { function stringify(path: url): string {
return path.toString().replace(/%20/g, " "); return path.toString().replace(/%20/g, " ")
} }
function expandTilde(path: string): string { function expandTilde(path: string): string {
return strip(path.replace("~", stringify(root.home))); return strip(path.replace("~", stringify(root.home)))
} }
function shortenHome(path: string): string { function shortenHome(path: string): string {
return path.replace(strip(root.home), "~"); return path.replace(strip(root.home), "~")
} }
function strip(path: url): string { function strip(path: url): string {
return stringify(path).replace("file://", ""); return stringify(path).replace("file://", "")
} }
function mkdir(path: url): void { function mkdir(path: url): void {
Quickshell.execDetached(["mkdir", "-p", strip(path)]); Quickshell.execDetached(["mkdir", "-p", strip(path)])
} }
function copy(from: url, to: url): void { function copy(from: url, to: url): void {
Quickshell.execDetached(["cp", strip(from), strip(to)]); Quickshell.execDetached(["cp", strip(from), strip(to)])
} }
} }

View File

@@ -2,8 +2,8 @@ import QtQuick
import Quickshell import Quickshell
QtObject { QtObject {
required property Singleton service required property Singleton service
Component.onCompleted: service.refCount++ Component.onCompleted: service.refCount++
Component.onDestruction: service.refCount-- Component.onDestruction: service.refCount--
} }

View File

@@ -1,5 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
pragma ComponentBehavior
import QtCore import QtCore
import QtQuick import QtQuick
@@ -8,169 +9,176 @@ import Quickshell.Io
Singleton { Singleton {
id: root id: root
property bool isLightMode: false property bool isLightMode: false
property string wallpaperPath: "" property string wallpaperPath: ""
property string wallpaperLastPath: "" property string wallpaperLastPath: ""
property string profileLastPath: "" property string profileLastPath: ""
property bool doNotDisturb: false property bool doNotDisturb: false
property var pinnedApps: [] property var pinnedApps: []
Component.onCompleted: { Component.onCompleted: {
loadSettings(); loadSettings()
}
function loadSettings() {
parseSettings(settingsFile.text())
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content)
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : ""
wallpaperLastPath = settings.wallpaperLastPath
!== undefined ? settings.wallpaperLastPath : ""
profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : ""
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
}
} catch (e) {
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
"isLightMode": isLightMode,
"wallpaperPath": wallpaperPath,
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"doNotDisturb": doNotDisturb,
"pinnedApps": pinnedApps
}, null, 2))
}
function setLightMode(lightMode) {
isLightMode = lightMode
saveSettings()
}
function setDoNotDisturb(enabled) {
doNotDisturb = enabled
saveSettings()
}
function setWallpaperPath(path) {
wallpaperPath = path
saveSettings()
}
function setWallpaper(imagePath) {
wallpaperPath = imagePath
saveSettings()
if (typeof Colors !== "undefined" && typeof SettingsData !== "undefined"
&& SettingsData.wallpaperDynamicTheming) {
Colors.extractColors()
}
}
function setWallpaperLastPath(path) {
wallpaperLastPath = path
saveSettings()
}
function setProfileLastPath(path) {
profileLastPath = path
saveSettings()
}
function setPinnedApps(apps) {
pinnedApps = apps
saveSettings()
}
function addPinnedApp(appId) {
if (!appId)
return
var currentPinned = [...pinnedApps]
if (currentPinned.indexOf(appId) === -1) {
currentPinned.push(appId)
setPinnedApps(currentPinned)
}
}
function removePinnedApp(appId) {
if (!appId)
return
var currentPinned = pinnedApps.filter(id => id !== appId)
setPinnedApps(currentPinned)
}
function isPinnedApp(appId) {
return appId && pinnedApps.indexOf(appId) !== -1
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(
StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
parseSettings(settingsFile.text())
}
onLoadFailed: error => {}
}
IpcHandler {
target: "wallpaper"
function get(): string {
return root.wallpaperPath || ""
} }
function loadSettings() { function set(path: string): string {
parseSettings(settingsFile.text()); if (!path) {
return "ERROR: No path provided"
}
var absolutePath = path.startsWith(
"/") ? path : StandardPaths.writableLocation(
StandardPaths.HomeLocation) + "/" + path
try {
root.setWallpaper(absolutePath)
return "SUCCESS: Wallpaper set to " + absolutePath
} catch (e) {
return "ERROR: Failed to set wallpaper: " + e.toString()
}
} }
function parseSettings(content) { function clear(): string {
try { root.setWallpaper("")
if (content && content.trim()) { return "SUCCESS: Wallpaper cleared"
var settings = JSON.parse(content); }
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false; }
wallpaperPath = settings.wallpaperPath !== undefined ? settings.wallpaperPath : "";
wallpaperLastPath = settings.wallpaperLastPath !== undefined ? settings.wallpaperLastPath : ""; IpcHandler {
profileLastPath = settings.profileLastPath !== undefined ? settings.profileLastPath : ""; target: "theme"
doNotDisturb = settings.doNotDisturb !== undefined ? settings.doNotDisturb : false;
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []; function toggle(): string {
} root.setLightMode(!root.isLightMode)
} catch (e) { return root.isLightMode ? "light" : "dark"
}
} }
function saveSettings() { function light(): string {
settingsFile.setText(JSON.stringify({ root.setLightMode(true)
"isLightMode": isLightMode, return "light"
"wallpaperPath": wallpaperPath,
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"doNotDisturb": doNotDisturb,
"pinnedApps": pinnedApps
}, null, 2));
} }
function setLightMode(lightMode) { function dark(): string {
isLightMode = lightMode; root.setLightMode(false)
saveSettings(); return "dark"
} }
function setDoNotDisturb(enabled) { function getMode(): string {
doNotDisturb = enabled; return root.isLightMode ? "light" : "dark"
saveSettings();
}
function setWallpaperPath(path) {
wallpaperPath = path;
saveSettings();
}
function setWallpaper(imagePath) {
wallpaperPath = imagePath;
saveSettings();
if (typeof Colors !== "undefined" && typeof SettingsData !== "undefined" && SettingsData.wallpaperDynamicTheming) {
Colors.extractColors();
}
}
function setWallpaperLastPath(path) {
wallpaperLastPath = path;
saveSettings();
}
function setProfileLastPath(path) {
profileLastPath = path;
saveSettings();
}
function setPinnedApps(apps) {
pinnedApps = apps;
saveSettings();
}
function addPinnedApp(appId) {
if (!appId) return;
var currentPinned = [...pinnedApps];
if (currentPinned.indexOf(appId) === -1) {
currentPinned.push(appId);
setPinnedApps(currentPinned);
}
}
function removePinnedApp(appId) {
if (!appId) return;
var currentPinned = pinnedApps.filter(id => id !== appId);
setPinnedApps(currentPinned);
}
function isPinnedApp(appId) {
return appId && pinnedApps.indexOf(appId) !== -1;
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell/session.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
parseSettings(settingsFile.text());
}
onLoadFailed: (error) => {
}
}
IpcHandler {
target: "wallpaper"
function get(): string {
return root.wallpaperPath || ""
}
function set(path: string): string {
if (!path) {
return "ERROR: No path provided"
}
var absolutePath = path.startsWith("/") ? path : StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/" + path
try {
root.setWallpaper(absolutePath)
return "SUCCESS: Wallpaper set to " + absolutePath
} catch (e) {
return "ERROR: Failed to set wallpaper: " + e.toString()
}
}
function clear(): string {
root.setWallpaper("")
return "SUCCESS: Wallpaper cleared"
}
}
IpcHandler {
target: "theme"
function toggle(): string {
root.setLightMode(!root.isLightMode)
return root.isLightMode ? "light" : "dark"
}
function light(): string {
root.setLightMode(true)
return "light"
}
function dark(): string {
root.setLightMode(false)
return "dark"
}
function getMode(): string {
return root.isLightMode ? "light" : "dark"
}
} }
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,208 +5,204 @@ import Quickshell.Wayland
import qs.Common import qs.Common
PanelWindow { PanelWindow {
id: root id: root
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
property real width: 400 property real width: 400
property real height: 300 property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920 readonly property real screenWidth: screen ? screen.width : 1920
readonly property real screenHeight: screen ? screen.height : 1080 readonly property real screenHeight: screen ? screen.height : 1080
property bool showBackground: true property bool showBackground: true
property real backgroundOpacity: 0.5 property real backgroundOpacity: 0.5
property string positioning: "center" property string positioning: "center"
property point customPosition: Qt.point(0, 0) property point customPosition: Qt.point(0, 0)
property string keyboardFocus: "ondemand" property string keyboardFocus: "ondemand"
property bool closeOnEscapeKey: true property bool closeOnEscapeKey: true
property bool closeOnBackgroundClick: true property bool closeOnBackgroundClick: true
property string animationType: "scale" property string animationType: "scale"
property int animationDuration: Theme.mediumDuration property int animationDuration: Theme.mediumDuration
property var animationEasing: Theme.emphasizedEasing property var animationEasing: Theme.emphasizedEasing
property color backgroundColor: Theme.surfaceContainer property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium property color borderColor: Theme.outlineMedium
property real borderWidth: 1 property real borderWidth: 1
property real cornerRadius: Theme.cornerRadiusLarge property real cornerRadius: Theme.cornerRadiusLarge
property bool enableShadow: false property bool enableShadow: false
signal opened() signal opened
signal dialogClosed() signal dialogClosed
signal backgroundClicked() signal backgroundClicked
function open() { function open() {
visible = true; visible = true
}
function close() {
visible = false
}
function toggle() {
visible = !visible
}
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
switch (root.keyboardFocus) {
case "exclusive":
return WlrKeyboardFocus.Exclusive
case "none":
return WlrKeyboardFocus.None
default:
return WlrKeyboardFocus.OnDemand
}
}
onVisibleChanged: {
if (root.visible) {
opened()
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide()
Qt.inputMethod.reset()
}
dialogClosed()
}
}
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground ? (root.visible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick
onClicked: mouse => {
var localPos = mapToItem(contentContainer, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > contentContainer.width
|| localPos.y < 0
|| localPos.y > contentContainer.height)
root.backgroundClicked()
}
} }
function close() { Behavior on opacity {
visible = false; NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
}
Rectangle {
id: contentContainer
width: root.width
height: root.height
anchors.centerIn: positioning === "center" ? parent : undefined
x: {
if (positioning === "top-right")
return Math.max(Theme.spacingL,
root.screenWidth - width - Theme.spacingL)
else if (positioning === "custom")
return root.customPosition.x
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
}
y: {
if (positioning === "top-right")
return Theme.barHeight + Theme.spacingXS
else if (positioning === "custom")
return root.customPosition.y
return 0 // Will be overridden by anchors.centerIn when positioning === "center"
}
color: root.backgroundColor
radius: root.cornerRadius
border.color: root.borderColor
border.width: root.borderWidth
layer.enabled: root.enableShadow
opacity: root.visible ? 1 : 0
scale: {
if (root.animationType === "scale")
return root.visible ? 1 : 0.9
return 1
}
transform: root.animationType === "slide" ? slideTransform : null
Translate {
id: slideTransform
x: root.visible ? 0 : 15
y: root.visible ? 0 : -30
} }
function toggle() { Loader {
visible = !visible; id: contentLoader
anchors.fill: parent
active: true
asynchronous: false
} }
color: "transparent" Behavior on opacity {
WlrLayershell.layer: WlrLayershell.Overlay NumberAnimation {
WlrLayershell.exclusiveZone: -1 duration: root.animationDuration
WlrLayershell.keyboardFocus: { easing.type: root.animationEasing
switch (root.keyboardFocus) { }
case "exclusive":
return WlrKeyboardFocus.Exclusive;
case "none":
return WlrKeyboardFocus.None;
default:
return WlrKeyboardFocus.OnDemand;
}
} }
Behavior on scale {
enabled: root.animationType === "scale"
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1
shadowColor: Theme.shadowStrong
shadowOpacity: 0.3
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.visible // Only active when the modal is visible
focus: root.visible
Keys.onEscapePressed: event => {
if (root.closeOnEscapeKey) {
root.visible = false
event.accepted = true
}
}
onVisibleChanged: { onVisibleChanged: {
if (root.visible) { if (visible) {
opened(); Qt.callLater(function () {
} else { focusScope.forceActiveFocus()
if (Qt.inputMethod) { })
Qt.inputMethod.hide(); }
Qt.inputMethod.reset();
}
dialogClosed();
}
} }
}
anchors { // Expose the focusScope for external access
top: true property alias modalFocusScope: focusScope
left: true
right: true
bottom: true
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground ? (root.visible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick
onClicked: (mouse) => {
var localPos = mapToItem(contentContainer, mouse.x, mouse.y);
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height)
root.backgroundClicked();
}
}
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
}
Rectangle {
id: contentContainer
width: root.width
height: root.height
anchors.centerIn: positioning === "center" ? parent : undefined
x: {
if (positioning === "top-right")
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL);
else if (positioning === "custom")
return root.customPosition.x;
return 0; // Will be overridden by anchors.centerIn when positioning === "center"
}
y: {
if (positioning === "top-right")
return Theme.barHeight + Theme.spacingXS;
else if (positioning === "custom")
return root.customPosition.y;
return 0; // Will be overridden by anchors.centerIn when positioning === "center"
}
color: root.backgroundColor
radius: root.cornerRadius
border.color: root.borderColor
border.width: root.borderWidth
layer.enabled: root.enableShadow
opacity: root.visible ? 1 : 0
scale: {
if (root.animationType === "scale")
return root.visible ? 1 : 0.9;
return 1;
}
transform: root.animationType === "slide" ? slideTransform : null
Translate {
id: slideTransform
x: root.visible ? 0 : 15
y: root.visible ? 0 : -30
}
Loader {
id: contentLoader
anchors.fill: parent
active: true
asynchronous: false
}
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
Behavior on scale {
enabled: root.animationType === "scale"
NumberAnimation {
duration: root.animationDuration
easing.type: root.animationEasing
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1
shadowColor: Theme.shadowStrong
shadowOpacity: 0.3
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.visible // Only active when the modal is visible
focus: root.visible
Keys.onEscapePressed: (event) => {
if (root.closeOnEscapeKey) {
root.visible = false;
event.accepted = true;
}
}
onVisibleChanged: {
if (visible) {
Qt.callLater(function() {
focusScope.forceActiveFocus();
});
}
}
}
// Expose the focusScope for external access
property alias modalFocusScope: focusScope
} }

View File

@@ -1,4 +1,4 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -7,289 +7,300 @@ import Qt.labs.folderlistmodel
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
DankModal { DankModal {
id: fileBrowserModal id: fileBrowserModal
signal fileSelected(string path) signal fileSelected(string path)
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation) property string homeDir: StandardPaths.writableLocation(
property string currentPath: "" StandardPaths.HomeLocation)
property var fileExtensions: ["*.*"] property string currentPath: ""
property string browserTitle: "Select File" property var fileExtensions: ["*.*"]
property string browserIcon: "folder_open" property string browserTitle: "Select File"
property string browserType: "generic" // "wallpaper" or "profile" for last path memory property string browserIcon: "folder_open"
property string browserType: "generic" // "wallpaper" or "profile" for last path memory
FolderListModel { FolderListModel {
id: folderModel id: folderModel
showDirsFirst: true showDirsFirst: true
showDotAndDotDot: false showDotAndDotDot: false
showHidden: false showHidden: false
nameFilters: fileExtensions nameFilters: fileExtensions
showFiles: true showFiles: true
showDirs: true showDirs: true
folder: currentPath ? "file://" + currentPath : "file://" + homeDir folder: currentPath ? "file://" + currentPath : "file://" + homeDir
}
function isImageFile(fileName) {
if (!fileName)
return false
var ext = fileName.toLowerCase().split('.').pop()
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
}
function getLastPath() {
var lastPath = ""
if (browserType === "wallpaper") {
lastPath = SessionData.wallpaperLastPath
} else if (browserType === "profile") {
lastPath = SessionData.profileLastPath
} }
function isImageFile(fileName) { if (lastPath && lastPath !== "") {
if (!fileName) return false return lastPath
var ext = fileName.toLowerCase().split('.').pop() }
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext) return homeDir
}
function saveLastPath(path) {
if (browserType === "wallpaper") {
SessionData.setWallpaperLastPath(path)
} else if (browserType === "profile") {
SessionData.setProfileLastPath(path)
}
}
Component.onCompleted: {
currentPath = getLastPath()
}
width: 800
height: 600
keyboardFocus: "ondemand"
enableShadow: true
visible: false
onBackgroundClicked: visible = false
onVisibleChanged: {
if (visible) {
var startPath = getLastPath()
currentPath = startPath
}
}
onCurrentPathChanged: {
}
function navigateUp() {
var path = currentPath
if (path === homeDir) {
return
} }
function getLastPath() { var lastSlash = path.lastIndexOf('/')
var lastPath = ""; if (lastSlash > 0) {
if (browserType === "wallpaper") { var newPath = path.substring(0, lastSlash)
lastPath = SessionData.wallpaperLastPath; if (newPath.length < homeDir.length) {
} else if (browserType === "profile") { currentPath = homeDir
lastPath = SessionData.profileLastPath; saveLastPath(homeDir)
} else {
currentPath = newPath
saveLastPath(newPath)
}
}
}
function navigateTo(path) {
currentPath = path
saveLastPath(path) // Save the path when navigating
}
content: Component {
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Item {
width: parent.width
height: 40
Row {
spacing: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: browserIcon
size: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: browserTitle
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
} }
if (lastPath && lastPath !== "") { DankActionButton {
return lastPath; circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: fileBrowserModal.visible = false
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
} }
return homeDir; }
}
function saveLastPath(path) { Row {
if (browserType === "wallpaper") { width: parent.width
SessionData.setWallpaperLastPath(path); spacing: Theme.spacingS
} else if (browserType === "profile") {
SessionData.setProfileLastPath(path);
}
}
Component.onCompleted: { StyledRect {
currentPath = getLastPath(); width: 32
} height: 32
radius: Theme.cornerRadius
color: mouseArea.containsMouse
&& currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
opacity: currentPath !== homeDir ? 1.0 : 0.0
width: 800 DankIcon {
height: 600 anchors.centerIn: parent
keyboardFocus: "ondemand" name: "arrow_back"
enableShadow: true size: Theme.iconSizeSmall
visible: false color: Theme.surfaceText
}
onBackgroundClicked: visible = false MouseArea {
id: mouseArea
onVisibleChanged: {
if (visible) {
var startPath = getLastPath();
currentPath = startPath;
}
}
onCurrentPathChanged: {
}
function navigateUp() {
var path = currentPath;
if (path === homeDir) {
return;
}
var lastSlash = path.lastIndexOf('/');
if (lastSlash > 0) {
var newPath = path.substring(0, lastSlash);
if (newPath.length < homeDir.length) {
currentPath = homeDir;
saveLastPath(homeDir);
} else {
currentPath = newPath;
saveLastPath(newPath);
}
}
}
function navigateTo(path) {
currentPath = path;
saveLastPath(path); // Save the path when navigating
}
content: Component {
Column {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM hoverEnabled: currentPath !== homeDir
spacing: Theme.spacingS cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: currentPath !== homeDir
onClicked: navigateUp()
}
}
StyledText {
text: fileBrowserModal.currentPath.replace("file://", "")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width - 40 - Theme.spacingS
elide: Text.ElideMiddle
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
wrapMode: Text.NoWrap
}
}
GridView {
id: fileGrid
width: parent.width
height: parent.height - 80
clip: true
cellWidth: 150
cellHeight: 130
cacheBuffer: 260
model: folderModel
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
interactive: true
flickDeceleration: 1500 // Touch only in Qt 6.9+ // Lower = more momentum, longer scrolling
maximumFlickVelocity: 2000 // Touch only in Qt 6.9+ // Higher = faster maximum scroll speed
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
delegate: StyledRect {
id: delegateRoot
required property bool fileIsDir
required property string filePath
required property string fileName
required property url fileURL
width: 140
height: 120
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
border.color: Theme.outline
border.width: mouseArea.containsMouse ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Item { Item {
width: parent.width width: 80
height: 40 height: 60
anchors.horizontalCenter: parent.horizontalCenter
Row { CachingImage {
spacing: Theme.spacingM anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter imagePath: !delegateRoot.fileIsDir ? delegateRoot.filePath : ""
fillMode: Image.PreserveAspectCrop
visible: !delegateRoot.fileIsDir && isImageFile(
delegateRoot.fileName)
maxCacheSize: 80
}
DankIcon { DankIcon {
name: browserIcon anchors.centerIn: parent
size: Theme.iconSizeLarge name: "description"
color: Theme.primary size: Theme.iconSizeLarge
anchors.verticalCenter: parent.verticalCenter color: Theme.primary
} visible: !delegateRoot.fileIsDir && !isImageFile(
delegateRoot.fileName)
}
StyledText { DankIcon {
text: browserTitle anchors.centerIn: parent
font.pixelSize: Theme.fontSizeXLarge name: "folder"
color: Theme.surfaceText size: Theme.iconSizeLarge
font.weight: Font.Medium color: Theme.primary
anchors.verticalCenter: parent.verticalCenter visible: delegateRoot.fileIsDir
} }
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: fileBrowserModal.visible = false
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
} }
Row { StyledText {
width: parent.width text: delegateRoot.fileName || ""
spacing: Theme.spacingS font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
StyledRect { width: 120
width: 32 elide: Text.ElideMiddle
height: 32 horizontalAlignment: Text.AlignHCenter
radius: Theme.cornerRadius anchors.horizontalCenter: parent.horizontalCenter
color: mouseArea.containsMouse && currentPath !== homeDir ? Theme.surfaceVariant : "transparent" maximumLineCount: 2
opacity: currentPath !== homeDir ? 1.0 : 0.0 wrapMode: Text.WordWrap
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: currentPath !== homeDir
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: currentPath !== homeDir
onClicked: navigateUp()
}
}
StyledText {
text: fileBrowserModal.currentPath.replace("file://", "")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width - 40 - Theme.spacingS
elide: Text.ElideMiddle
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
wrapMode: Text.NoWrap
}
} }
}
GridView { MouseArea {
id: fileGrid id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
width: parent.width onClicked: {
height: parent.height - 80 if (delegateRoot.fileIsDir) {
clip: true navigateTo(delegateRoot.filePath)
cellWidth: 150 } else {
cellHeight: 130 fileSelected(delegateRoot.filePath)
cacheBuffer: 260 }
}
model: folderModel }
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AlwaysOff }
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
interactive: true
flickDeceleration: 1500 // Touch only in Qt 6.9+ // Lower = more momentum, longer scrolling
maximumFlickVelocity: 2000 // Touch only in Qt 6.9+ // Higher = faster maximum scroll speed
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
delegate: StyledRect {
id: delegateRoot
required property bool fileIsDir
required property string filePath
required property string fileName
required property url fileURL
width: 140
height: 120
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
border.color: Theme.outline
border.width: mouseArea.containsMouse ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Item {
width: 80
height: 60
anchors.horizontalCenter: parent.horizontalCenter
CachingImage {
anchors.fill: parent
imagePath: !delegateRoot.fileIsDir ? delegateRoot.filePath : ""
fillMode: Image.PreserveAspectCrop
visible: !delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)
maxCacheSize: 80
}
DankIcon {
anchors.centerIn: parent
name: "description"
size: Theme.iconSizeLarge
color: Theme.primary
visible: !delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)
}
DankIcon {
anchors.centerIn: parent
name: "folder"
size: Theme.iconSizeLarge
color: Theme.primary
visible: delegateRoot.fileIsDir
}
}
StyledText {
text: delegateRoot.fileName || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: 120
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (delegateRoot.fileIsDir) {
navigateTo(delegateRoot.filePath);
} else {
fileSelected(delegateRoot.filePath);
}
}
}
}
}
} }
}
} }
}
} }

View File

@@ -8,198 +8,193 @@ import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
id: root id: root
property bool networkInfoModalVisible: false property bool networkInfoModalVisible: false
property string networkSSID: "" property string networkSSID: ""
property var networkData: null property var networkData: null
property string networkDetails: "" property string networkDetails: ""
function showNetworkInfo(ssid, data) { function showNetworkInfo(ssid, data) {
networkSSID = ssid; networkSSID = ssid
networkData = data; networkData = data
networkInfoModalVisible = true; networkInfoModalVisible = true
NetworkService.fetchNetworkInfo(ssid); NetworkService.fetchNetworkInfo(ssid)
}
function hideDialog() {
networkInfoModalVisible = false
networkSSID = ""
networkData = null
networkDetails = ""
}
visible: networkInfoModalVisible
width: 600
height: 500
enableShadow: true
onBackgroundClicked: {
hideDialog()
}
onVisibleChanged: {
if (!visible) {
networkSSID = ""
networkData = null
networkDetails = ""
} }
}
function hideDialog() { content: Component {
networkInfoModalVisible = false; Item {
networkSSID = ""; anchors.fill: parent
networkData = null;
networkDetails = "";
}
visible: networkInfoModalVisible Column {
width: 600 anchors.fill: parent
height: 500 anchors.margins: Theme.spacingL
enableShadow: true spacing: Theme.spacingL
onBackgroundClicked: {
hideDialog();
}
onVisibleChanged: {
if (!visible) {
networkSSID = "";
networkData = null;
networkDetails = "";
}
}
content: Component { Row {
Item { width: parent.width
anchors.fill: parent
Column { Column {
anchors.fill: parent width: parent.width - 40
anchors.margins: Theme.spacingL spacing: Theme.spacingXS
spacing: Theme.spacingL
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: "Network Information"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Details for \"" + networkSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
root.hideDialog();
}
}
}
Flickable {
width: parent.width
height: parent.height - 140
clip: true
contentWidth: width
contentHeight: detailsRect.height
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(0, Math.min(parent.contentHeight - parent.height, newY))
parent.contentY = newY
event.accepted = true
}
}
Rectangle {
id: detailsRect
width: parent.width
height: Math.max(parent.parent.height, detailsText.contentHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: Theme.outlineStrong
border.width: 1
StyledText {
id: detailsText
anchors.fill: parent
anchors.margins: Theme.spacingM
text: NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || "No information available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
wrapMode: Text.WordWrap
lineHeight: 1.5
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
}
Item {
width: parent.width
height: 40
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
StyledText {
id: closeText
anchors.centerIn: parent
text: "Close"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hideDialog();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
StyledText {
text: "Network Information"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
} }
StyledText {
text: "Details for \"" + networkSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
root.hideDialog()
}
}
} }
} Flickable {
width: parent.width
height: parent.height - 140
clip: true
contentWidth: width
contentHeight: detailsRect.height
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
let delta = event.pixelDelta.y
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(0, Math.min(
parent.contentHeight - parent.height,
newY))
parent.contentY = newY
event.accepted = true
}
}
Rectangle {
id: detailsRect
width: parent.width
height: Math.max(parent.parent.height,
detailsText.contentHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: Theme.outlineStrong
border.width: 1
StyledText {
id: detailsText
anchors.fill: parent
anchors.margins: Theme.spacingM
text: NetworkService.networkInfoDetails.replace(/\\n/g, '\n')
|| "No information available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
wrapMode: Text.WordWrap
lineHeight: 1.5
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
}
Item {
width: parent.width
height: 40
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.darker(Theme.primary,
1.1) : Theme.primary
StyledText {
id: closeText
anchors.centerIn: parent
text: "Close"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.hideDialog()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
} }

View File

@@ -7,158 +7,154 @@ import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
id: root id: root
property bool powerConfirmVisible: false property bool powerConfirmVisible: false
property string powerConfirmAction: "" property string powerConfirmAction: ""
property string powerConfirmTitle: "" property string powerConfirmTitle: ""
property string powerConfirmMessage: "" property string powerConfirmMessage: ""
function executePowerAction(action) { function executePowerAction(action) {
switch (action) { switch (action) {
case "logout": case "logout":
NiriService.quit(); NiriService.quit()
break; break
case "suspend": case "suspend":
Quickshell.execDetached(["systemctl", "suspend"]); Quickshell.execDetached(["systemctl", "suspend"])
break; break
case "reboot": case "reboot":
Quickshell.execDetached(["systemctl", "reboot"]); Quickshell.execDetached(["systemctl", "reboot"])
break; break
case "poweroff": case "poweroff":
Quickshell.execDetached(["systemctl", "poweroff"]); Quickshell.execDetached(["systemctl", "poweroff"])
break; break
}
}
visible: powerConfirmVisible
width: 350
height: 160
keyboardFocus: "ondemand"
enableShadow: false
onBackgroundClicked: {
powerConfirmVisible = false
}
content: Component {
Item {
anchors.fill: parent
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
StyledText {
text: powerConfirmTitle
font.pixelSize: Theme.fontSizeLarge
color: {
switch (powerConfirmAction) {
case "poweroff":
return Theme.error
case "reboot":
return Theme.warning
default:
return Theme.surfaceText
}
}
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
} }
}
visible: powerConfirmVisible StyledText {
width: 350 text: powerConfirmMessage
height: 160 font.pixelSize: Theme.fontSizeMedium
keyboardFocus: "ondemand" color: Theme.surfaceText
enableShadow: false width: parent.width
onBackgroundClicked: { horizontalAlignment: Text.AlignHCenter
powerConfirmVisible = false; wrapMode: Text.WordWrap
} }
content: Component {
Item { Item {
anchors.fill: parent height: Theme.spacingS
}
Column { Row {
anchors.centerIn: parent anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - Theme.spacingM * 2 spacing: Theme.spacingM
spacing: Theme.spacingM
StyledText { Rectangle {
text: powerConfirmTitle width: 120
font.pixelSize: Theme.fontSizeLarge height: 40
color: { radius: Theme.cornerRadius
switch (powerConfirmAction) { color: cancelButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha
case "poweroff":
return Theme.error;
case "reboot":
return Theme.warning;
default:
return Theme.surfaceText;
}
}
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: powerConfirmMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
Item {
height: Theme.spacingS
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: cancelButton.containsMouse ? Theme.surfaceTextPressed : Theme.surfaceVariantAlpha
StyledText {
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false;
}
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
let baseColor;
switch (powerConfirmAction) {
case "poweroff":
baseColor = Theme.error;
break;
case "reboot":
baseColor = Theme.warning;
break;
default:
baseColor = Theme.primary;
break;
}
return confirmButton.containsMouse ? Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) : baseColor;
}
StyledText {
text: "Confirm"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false;
executePowerAction(powerConfirmAction);
}
}
}
}
StyledText {
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
} }
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false
}
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
let baseColor
switch (powerConfirmAction) {
case "poweroff":
baseColor = Theme.error
break
case "reboot":
baseColor = Theme.warning
break
default:
baseColor = Theme.primary
break
}
return confirmButton.containsMouse ? Qt.rgba(baseColor.r,
baseColor.g,
baseColor.b,
0.9) : baseColor
}
StyledText {
text: "Confirm"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false
executePowerAction(powerConfirmAction)
}
}
}
} }
}
} }
}
} }

View File

@@ -12,306 +12,277 @@ import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
id: processListModal id: processListModal
property int currentTab: 0 property int currentTab: 0
property var tabNames: ["Processes", "Performance", "System"] property var tabNames: ["Processes", "Performance", "System"]
function show() { function show() {
processListModal.visible = true; processListModal.visible = true
UserInfoService.getUptime(); UserInfoService.getUptime()
}
function hide() {
processListModal.visible = false
if (processContextMenu.visible)
processContextMenu.close()
}
function toggle() {
if (processListModal.visible)
hide()
else
show()
}
width: 900
height: 680
visible: false
keyboardFocus: "exclusive"
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadiusXLarge
enableShadow: true
onBackgroundClicked: hide()
Ref {
service: SysMonitorService
}
Component {
id: processesTabComponent
ProcessesTab {
contextMenu: processContextMenu
} }
}
function hide() { Component {
processListModal.visible = false; id: performanceTabComponent
if (processContextMenu.visible)
processContextMenu.close();
} PerformanceTab {}
}
function toggle() { Component {
if (processListModal.visible) id: systemTabComponent
hide();
else
show();
}
width: 900 SystemTab {}
height: 680 }
visible: false
keyboardFocus: "exclusive"
backgroundColor: Theme.popupBackground()
cornerRadius: Theme.cornerRadiusXLarge
enableShadow: true
onBackgroundClicked: hide()
Ref { ProcessContextMenu {
service: SysMonitorService id: processContextMenu
} }
Component { content: Component {
id: processesTabComponent Item {
anchors.fill: parent
focus: true
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
processListModal.hide()
event.accepted = true
} else if (event.key === Qt.Key_1) {
currentTab = 0
event.accepted = true
} else if (event.key === Qt.Key_2) {
currentTab = 1
event.accepted = true
} else if (event.key === Qt.Key_3) {
currentTab = 2
event.accepted = true
}
}
ProcessesTab { ColumnLayout {
contextMenu: processContextMenu anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
RowLayout {
Layout.fillWidth: true
height: 40
StyledText {
text: "System Monitor"
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: processListModal.hide()
Layout.alignment: Qt.AlignVCenter
}
} }
} Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.surfaceSelected
radius: Theme.cornerRadiusLarge
border.color: Theme.outlineLight
border.width: 1
Component { Row {
id: performanceTabComponent
PerformanceTab {
}
}
Component {
id: systemTabComponent
SystemTab {
}
}
ProcessContextMenu {
id: processContextMenu
}
content: Component {
Item {
anchors.fill: parent anchors.fill: parent
focus: true anchors.margins: 4
Keys.onPressed: function(event) { spacing: 2
if (event.key === Qt.Key_Escape) {
processListModal.hide(); Repeater {
event.accepted = true; model: tabNames
} else if (event.key === Qt.Key_1) {
currentTab = 0; Rectangle {
event.accepted = true; width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
} else if (event.key === Qt.Key_2) { height: 44
currentTab = 1; radius: Theme.cornerRadiusLarge
event.accepted = true; color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
} else if (event.key === Qt.Key_3) { border.color: currentTab === index ? Theme.primary : "transparent"
currentTab = 2; border.width: currentTab === index ? 1 : 0
event.accepted = true;
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
switch (index) {
case 0:
return "list_alt"
case 1:
return "analytics"
case 2:
return "settings"
default:
return "tab"
}
}
size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
} }
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
currentTab = index
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
} }
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
RowLayout {
Layout.fillWidth: true
height: 40
StyledText {
text: "System Monitor"
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: processListModal.hide()
Layout.alignment: Qt.AlignVCenter
}
}
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.surfaceSelected
radius: Theme.cornerRadiusLarge
border.color: Theme.outlineLight
border.width: 1
Row {
anchors.fill: parent
anchors.margins: 4
spacing: 2
Repeater {
model: tabNames
Rectangle {
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
height: 44
radius: Theme.cornerRadiusLarge
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
switch (index) {
case 0:
return "list_alt";
case 1:
return "analytics";
case 2:
return "settings";
default:
return "tab";
}
}
size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
currentTab = index;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadiusLarge
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
Loader {
id: processesTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: currentTab === 0
visible: currentTab === 0
opacity: currentTab === 0 ? 1 : 0
sourceComponent: processesTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: performanceTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: currentTab === 1
visible: currentTab === 1
opacity: currentTab === 1 ? 1 : 0
sourceComponent: performanceTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: systemTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: currentTab === 2
visible: currentTab === 2
opacity: currentTab === 2 ? 1 : 0
sourceComponent: systemTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
} }
} Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadiusLarge
color: Theme.surfaceLight
border.color: Theme.outlineLight
border.width: 1
Loader {
id: processesTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: currentTab === 0
visible: currentTab === 0
opacity: currentTab === 0 ? 1 : 0
sourceComponent: processesTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: performanceTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: currentTab === 1
visible: currentTab === 1
opacity: currentTab === 1 ? 1 : 0
sourceComponent: performanceTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: systemTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: currentTab === 2
visible: currentTab === 2
opacity: currentTab === 2 ? 1 : 0
sourceComponent: systemTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
}
} }

View File

@@ -1,4 +1,4 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior
import Quickshell.Io import Quickshell.Io
import QtQuick import QtQuick
@@ -8,183 +8,190 @@ import qs.Modules.Settings
import qs.Widgets import qs.Widgets
DankModal { DankModal {
id: settingsModal id: settingsModal
signal closingModal() signal closingModal
function show() { function show() {
settingsModal.visible = true; settingsModal.visible = true
}
function hide() {
settingsModal.visible = false
}
function toggle() {
if (settingsModal.visible)
hide()
else
show()
}
width: 750
height: 750
visible: false
keyboardFocus: "ondemand"
onBackgroundClicked: hide()
content: Component {
Item {
anchors.fill: parent
focus: true
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
settingsModal.hide()
event.accepted = true
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "settings"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Settings"
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 175 // Spacer to push close button to the right
height: 1
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: settingsModal.hide()
}
}
Column {
width: parent.width
height: parent.height - 50
spacing: 0
DankTabBar {
id: settingsTabBar
width: parent.width
model: [{
"text": "Personalization",
"icon": "person"
}, {
"text": "Time & Weather",
"icon": "schedule"
}, {
"text": "Widgets",
"icon": "widgets"
}, {
"text": "Launcher",
"icon": "apps"
}, {
"text": "Appearance",
"icon": "palette"
}]
}
Item {
width: parent.width
height: parent.height - settingsTabBar.height
Rectangle {
anchors.fill: parent
anchors.margins: Theme.spacingL
color: "transparent"
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 0
visible: active
asynchronous: false
sourceComponent: Component {
PersonalizationTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 1
visible: active
asynchronous: true
sourceComponent: Component {
TimeWeatherTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 2
visible: active
asynchronous: true
sourceComponent: Component {
WidgetsTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 3
visible: active
asynchronous: true
sourceComponent: Component {
LauncherTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 4
visible: active
asynchronous: false
sourceComponent: Component {
AppearanceTab {}
}
}
}
}
}
}
}
}
IpcHandler {
function open() {
settingsModal.show()
return "SETTINGS_OPEN_SUCCESS"
} }
function hide() { function close() {
settingsModal.visible = false; settingsModal.hide()
return "SETTINGS_CLOSE_SUCCESS"
} }
function toggle() { function toggle() {
if (settingsModal.visible) settingsModal.toggle()
hide(); return "SETTINGS_TOGGLE_SUCCESS"
else
show();
} }
width: 750 target: "settings"
height: 750 }
visible: false
keyboardFocus: "ondemand"
onBackgroundClicked: hide()
content: Component {
Item {
anchors.fill: parent
focus: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
settingsModal.hide();
event.accepted = true;
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "settings"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Settings"
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 175 // Spacer to push close button to the right
height: 1
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: settingsModal.hide()
}
}
Column {
width: parent.width
height: parent.height - 50
spacing: 0
DankTabBar {
id: settingsTabBar
width: parent.width
model: [
{ text: "Personalization", icon: "person" },
{ text: "Time & Weather", icon: "schedule" },
{ text: "Widgets", icon: "widgets" },
{ text: "Launcher", icon: "apps" },
{ text: "Appearance", icon: "palette" }
]
}
Item {
width: parent.width
height: parent.height - settingsTabBar.height
Rectangle {
anchors.fill: parent
anchors.margins: Theme.spacingL
color: "transparent"
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 0
visible: active
asynchronous: false
sourceComponent: Component {
PersonalizationTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 1
visible: active
asynchronous: true
sourceComponent: Component {
TimeWeatherTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 2
visible: active
asynchronous: true
sourceComponent: Component {
WidgetsTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 3
visible: active
asynchronous: true
sourceComponent: Component {
LauncherTab {}
}
}
Loader {
anchors.fill: parent
active: settingsTabBar.currentIndex === 4
visible: active
asynchronous: false
sourceComponent: Component {
AppearanceTab {}
}
}
}
}
}
}
}
}
IpcHandler {
function open() {
settingsModal.show();
return "SETTINGS_OPEN_SUCCESS";
}
function close() {
settingsModal.hide();
return "SETTINGS_CLOSE_SUCCESS";
}
function toggle() {
settingsModal.toggle();
return "SETTINGS_TOGGLE_SUCCESS";
}
target: "settings"
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -5,277 +5,266 @@ import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
id: root id: root
property bool wifiPasswordModalVisible: false property bool wifiPasswordModalVisible: false
property string wifiPasswordSSID: "" property string wifiPasswordSSID: ""
property string wifiPasswordInput: "" property string wifiPasswordInput: ""
visible: wifiPasswordModalVisible visible: wifiPasswordModalVisible
width: 420 width: 420
height: 230 height: 230
keyboardFocus: "exclusive" keyboardFocus: "exclusive"
onVisibleChanged: { onVisibleChanged: {
if (!visible) if (!visible)
wifiPasswordInput = ""; wifiPasswordInput = ""
}
onBackgroundClicked: {
wifiPasswordModalVisible = false
wifiPasswordInput = ""
}
} Connections {
onBackgroundClicked: { function onPasswordDialogShouldReopenChanged() {
wifiPasswordModalVisible = false; if (NetworkService.passwordDialogShouldReopen
wifiPasswordInput = ""; && NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID
wifiPasswordInput = ""
wifiPasswordModalVisible = true
NetworkService.passwordDialogShouldReopen = false
}
} }
Connections { target: NetworkService
function onPasswordDialogShouldReopenChanged() { }
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID; content: Component {
wifiPasswordInput = ""; FocusScope {
wifiPasswordModalVisible = true; anchors.fill: parent
NetworkService.passwordDialogShouldReopen = false; focus: true
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
spacing: Theme.spacingM
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: "Connect to Wi-Fi"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
} }
StyledText {
text: "Enter password for \"" + wifiPasswordSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
wifiPasswordModalVisible = false
wifiPasswordInput = ""
}
}
} }
target: NetworkService Rectangle {
} width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordInput.activeFocus ? 2 : 1
DankTextField {
id: passwordInput
content: Component {
FocusScope {
anchors.fill: parent anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: "Enter password"
backgroundColor: "transparent"
focus: true focus: true
onTextEdited: {
Column { wifiPasswordInput = text
anchors.centerIn: parent }
width: parent.width - Theme.spacingM * 2 onAccepted: {
spacing: Theme.spacingM NetworkService.connectToWifiWithPassword(wifiPasswordSSID,
passwordInput.text)
Row { wifiPasswordModalVisible = false
width: parent.width wifiPasswordInput = ""
passwordInput.text = ""
Column {
width: parent.width - 40
spacing: Theme.spacingXS
StyledText {
text: "Connect to Wi-Fi"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "Enter password for \"" + wifiPasswordSSID + "\""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: {
wifiPasswordModalVisible = false;
wifiPasswordInput = "";
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceHover
border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
border.width: passwordInput.activeFocus ? 2 : 1
DankTextField {
id: passwordInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: "Enter password"
backgroundColor: "transparent"
focus: true
onTextEdited: {
wifiPasswordInput = text;
}
onAccepted: {
NetworkService.connectToWifiWithPassword(wifiPasswordSSID, passwordInput.text);
wifiPasswordModalVisible = false;
wifiPasswordInput = "";
passwordInput.text = "";
}
Timer {
id: focusTimer
interval: 50
onTriggered: passwordInput.forceActiveFocus()
}
Component.onCompleted: {
focusTimer.start();
}
Connections {
function onOpened() {
focusTimer.start();
}
function onVisibleChanged() {
if (root.visible) {
focusTimer.start();
}
}
target: root
}
}
}
Row {
spacing: Theme.spacingS
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
}
}
}
StyledText {
text: "Show password"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
StyledText {
id: cancelText
anchors.centerIn: parent
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiPasswordModalVisible = false;
wifiPasswordInput = "";
}
}
}
Rectangle {
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: passwordInput.text.length > 0
opacity: enabled ? 1 : 0.5
StyledText {
id: connectText
anchors.centerIn: parent
text: "Connect"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
NetworkService.connectToWifiWithPassword(wifiPasswordSSID, passwordInput.text);
wifiPasswordModalVisible = false;
wifiPasswordInput = "";
passwordInput.text = "";
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
} }
} Timer {
} id: focusTimer
interval: 50
onTriggered: passwordInput.forceActiveFocus()
}
Component.onCompleted: {
focusTimer.start()
}
Connections {
function onOpened() {
focusTimer.start()
}
function onVisibleChanged() {
if (root.visible) {
focusTimer.start()
}
}
target: root
}
}
}
Row {
spacing: Theme.spacingS
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
}
}
}
StyledText {
text: "Show password"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
border.color: Theme.surfaceVariantAlpha
border.width: 1
StyledText {
id: cancelText
anchors.centerIn: parent
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiPasswordModalVisible = false
wifiPasswordInput = ""
}
}
}
Rectangle {
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary,
1.1) : Theme.primary
enabled: passwordInput.text.length > 0
opacity: enabled ? 1 : 0.5
StyledText {
id: connectText
anchors.centerIn: parent
text: "Connect"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
NetworkService.connectToWifiWithPassword(wifiPasswordSSID,
passwordInput.text)
wifiPasswordModalVisible = false
wifiPasswordInput = ""
passwordInput.text = ""
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -6,180 +6,185 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
property string searchQuery: "" property string searchQuery: ""
property string selectedCategory: "All" property string selectedCategory: "All"
property string viewMode: "list" // "list" or "grid" property string viewMode: "list" // "list" or "grid"
property int selectedIndex: 0 property int selectedIndex: 0
property int maxResults: 50 property int maxResults: 50
property int gridColumns: 4 property int gridColumns: 4
property bool debounceSearch: true property bool debounceSearch: true
property int debounceInterval: 50 property int debounceInterval: 50
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property var categories: { property var categories: {
var allCategories = AppSearchService.getAllCategories().filter((cat) => { var allCategories = AppSearchService.getAllCategories().filter(cat => {
return cat !== "Education" && cat !== "Science"; return cat !== "Education"
}); && cat !== "Science"
var result = ["All"]; })
return result.concat(allCategories.filter((cat) => { var result = ["All"]
return cat !== "All"; return result.concat(allCategories.filter(cat => {
})); return cat !== "All"
} }))
property var categoryIcons: categories.map((category) => { }
return AppSearchService.getCategoryIcon(category); property var categoryIcons: categories.map(category => {
}) return AppSearchService.getCategoryIcon(
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {} category)
property alias model: filteredModel })
property var _watchApplications: AppSearchService.applications property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
property alias model: filteredModel
property var _watchApplications: AppSearchService.applications
signal appLaunched(var app) signal appLaunched(var app)
signal categorySelected(string category) signal categorySelected(string category)
signal viewModeSelected(string mode) signal viewModeSelected(string mode)
function updateFilteredModel() { function updateFilteredModel() {
filteredModel.clear(); filteredModel.clear()
selectedIndex = 0; selectedIndex = 0
keyboardNavigationActive = false; keyboardNavigationActive = false
var apps = []; var apps = []
if (searchQuery.length === 0) { if (searchQuery.length === 0) {
if (selectedCategory === "All") { if (selectedCategory === "All") {
apps = AppSearchService.applications || []; apps = AppSearchService.applications || []
} else { } else {
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory); var categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
apps = categoryApps.slice(0, maxResults); apps = categoryApps.slice(0, maxResults)
} }
} else {
if (selectedCategory === "All") {
apps = AppSearchService.searchApplications(searchQuery)
} else {
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
if (categoryApps.length > 0) {
var allSearchResults = AppSearchService.searchApplications(
searchQuery)
var categoryNames = new Set(categoryApps.map(app => {
return app.name
}))
apps = allSearchResults.filter(searchApp => {
return categoryNames.has(
searchApp.name)
}).slice(0, maxResults)
} else { } else {
if (selectedCategory === "All") { apps = []
apps = AppSearchService.searchApplications(searchQuery);
} else {
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
if (categoryApps.length > 0) {
var allSearchResults = AppSearchService.searchApplications(searchQuery);
var categoryNames = new Set(categoryApps.map((app) => {
return app.name;
}));
apps = allSearchResults.filter((searchApp) => {
return categoryNames.has(searchApp.name);
}).slice(0, maxResults);
} else {
apps = [];
}
}
} }
if (searchQuery.length === 0) }
apps = apps.sort(function(a, b) {
var aId = a.id || (a.execString || a.exec || "");
var bId = b.id || (b.execString || b.exec || "");
var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0;
var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0;
if (aUsage !== bUsage)
return bUsage - aUsage;
return (a.name || "").localeCompare(b.name || "");
});
apps.forEach((app) => {
if (app)
filteredModel.append({
"name": app.name || "",
"exec": app.execString || "",
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"desktopEntry": app
});
});
} }
if (searchQuery.length === 0)
apps = apps.sort(function (a, b) {
var aId = a.id || (a.execString || a.exec || "")
var bId = b.id || (b.execString || b.exec || "")
var aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
var bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
if (aUsage !== bUsage)
return bUsage - aUsage
function selectNext() { return (a.name || "").localeCompare(b.name || "")
if (filteredModel.count > 0) { })
keyboardNavigationActive = true;
if (viewMode === "grid") { apps.forEach(app => {
var newIndex = Math.min(selectedIndex + gridColumns, filteredModel.count - 1); if (app)
selectedIndex = newIndex; filteredModel.append({
} else { "name": app.name || "",
selectedIndex = (selectedIndex + 1) % filteredModel.count; "exec": app.execString || "",
} "icon": app.icon
} || "application-x-executable",
"comment": app.comment || "",
"categories": app.categories || [],
"desktopEntry": app
})
})
}
function selectNext() {
if (filteredModel.count > 0) {
keyboardNavigationActive = true
if (viewMode === "grid") {
var newIndex = Math.min(selectedIndex + gridColumns,
filteredModel.count - 1)
selectedIndex = newIndex
} else {
selectedIndex = (selectedIndex + 1) % filteredModel.count
}
} }
}
function selectPrevious() { function selectPrevious() {
if (filteredModel.count > 0) { if (filteredModel.count > 0) {
keyboardNavigationActive = true; keyboardNavigationActive = true
if (viewMode === "grid") { if (viewMode === "grid") {
var newIndex = Math.max(selectedIndex - gridColumns, 0); var newIndex = Math.max(selectedIndex - gridColumns, 0)
selectedIndex = newIndex; selectedIndex = newIndex
} else { } else {
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredModel.count - 1; selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredModel.count - 1
} }
}
} }
}
function selectNextInRow() { function selectNextInRow() {
if (filteredModel.count > 0 && viewMode === "grid") { if (filteredModel.count > 0 && viewMode === "grid") {
keyboardNavigationActive = true; keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1); selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
}
} }
}
function selectPreviousInRow() { function selectPreviousInRow() {
if (filteredModel.count > 0 && viewMode === "grid") { if (filteredModel.count > 0 && viewMode === "grid") {
keyboardNavigationActive = true; keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, 0); selectedIndex = Math.max(selectedIndex - 1, 0)
}
} }
}
function launchSelected() { function launchSelected() {
if (filteredModel.count > 0 && selectedIndex >= 0 && selectedIndex < filteredModel.count) { if (filteredModel.count > 0 && selectedIndex >= 0
var selectedApp = filteredModel.get(selectedIndex); && selectedIndex < filteredModel.count) {
launchApp(selectedApp); var selectedApp = filteredModel.get(selectedIndex)
} launchApp(selectedApp)
} }
}
function launchApp(appData) { function launchApp(appData) {
if (!appData) if (!appData)
return ; return
appData.desktopEntry.execute(); appData.desktopEntry.execute()
appLaunched(appData); appLaunched(appData)
AppUsageHistoryData.addAppUsage(appData.desktopEntry); AppUsageHistoryData.addAppUsage(appData.desktopEntry)
} }
function setCategory(category) { function setCategory(category) {
selectedCategory = category; selectedCategory = category
categorySelected(category); categorySelected(category)
} }
function setViewMode(mode) { function setViewMode(mode) {
viewMode = mode; viewMode = mode
viewModeSelected(mode); viewModeSelected(mode)
} }
onSearchQueryChanged: { onSearchQueryChanged: {
if (debounceSearch) if (debounceSearch)
searchDebounceTimer.restart(); searchDebounceTimer.restart()
else else
updateFilteredModel(); updateFilteredModel()
} }
onSelectedCategoryChanged: updateFilteredModel() onSelectedCategoryChanged: updateFilteredModel()
onAppUsageRankingChanged: updateFilteredModel() onAppUsageRankingChanged: updateFilteredModel()
on_WatchApplicationsChanged: updateFilteredModel() on_WatchApplicationsChanged: updateFilteredModel()
Component.onCompleted: { Component.onCompleted: {
updateFilteredModel(); updateFilteredModel()
} }
ListModel { ListModel {
id: filteredModel id: filteredModel
} }
Timer { Timer {
id: searchDebounceTimer id: searchDebounceTimer
interval: root.debounceInterval
repeat: false
onTriggered: updateFilteredModel()
}
interval: root.debounceInterval
repeat: false
onTriggered: updateFilteredModel()
}
} }

View File

@@ -4,147 +4,151 @@ import qs.Common
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
property var categories: [] property var categories: []
property string selectedCategory: "All" property string selectedCategory: "All"
property bool compact: false // For different layout styles property bool compact: false // For different layout styles
signal categorySelected(string category) signal categorySelected(string category)
height: compact ? 36 : (72 + Theme.spacingS) // Single row vs two rows height: compact ? 36 : (72 + Theme.spacingS) // Single row vs two rows
Row {
visible: compact
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories.slice(0, Math.min(categories.length,
8)) // Limit for space
Rectangle {
height: 36
width: (parent.width - (Math.min(categories.length,
8) - 1) * Theme.spacingS) / Math.min(
categories.length, 8)
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
categorySelected(modelData)
}
}
}
}
}
Column {
visible: !compact
width: parent.width
spacing: Theme.spacingS
Row { Row {
visible: compact property var topRowCategories: ["All", "Development", "Graphics", "Games"]
width: parent.width
spacing: Theme.spacingS
Repeater { width: parent.width
model: categories.slice(0, Math.min(categories.length, 8)) // Limit for space spacing: Theme.spacingS
Rectangle { Repeater {
height: 36 model: parent.topRowCategories.filter(cat => {
width: (parent.width - (Math.min(categories.length, 8) - 1) * Theme.spacingS) / Math.min(categories.length, 8) return categories.includes(cat)
radius: Theme.cornerRadiusLarge })
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
StyledText { Rectangle {
anchors.centerIn: parent height: 36
text: modelData width: (parent.width - (parent.topRowCategories.length - 1)
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText * Theme.spacingS) / parent.topRowCategories.length
font.pixelSize: Theme.fontSizeMedium radius: Theme.cornerRadiusLarge
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal color: selectedCategory === modelData ? Theme.primary : "transparent"
elide: Text.ElideRight border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(
} Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
MouseArea { StyledText {
anchors.fill: parent anchors.centerIn: parent
hoverEnabled: true text: modelData
cursorShape: Qt.PointingHandCursor color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
onClicked: { font.pixelSize: Theme.fontSizeMedium
selectedCategory = modelData; font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
categorySelected(modelData); elide: Text.ElideRight
} }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
categorySelected(modelData)
} }
}
} }
}
} }
Column { Row {
visible: !compact property var bottomRowCategories: ["Internet", "Media", "Office", "Settings", "System"]
width: parent.width
spacing: Theme.spacingS
Row { width: parent.width
property var topRowCategories: ["All", "Development", "Graphics", "Games"] spacing: Theme.spacingS
width: parent.width Repeater {
spacing: Theme.spacingS model: parent.bottomRowCategories.filter(cat => {
return categories.includes(
cat)
})
Repeater { Rectangle {
model: parent.topRowCategories.filter((cat) => { height: 36
return categories.includes(cat); width: (parent.width - (parent.bottomRowCategories.length - 1)
}) * Theme.spacingS) / parent.bottomRowCategories.length
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
Rectangle { StyledText {
height: 36 anchors.centerIn: parent
width: (parent.width - (parent.topRowCategories.length - 1) * Theme.spacingS) / parent.topRowCategories.length text: modelData
radius: Theme.cornerRadiusLarge color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
color: selectedCategory === modelData ? Theme.primary : "transparent" font.pixelSize: Theme.fontSizeMedium
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
StyledText { }
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
categorySelected(modelData);
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData
categorySelected(modelData)
} }
}
} }
}
Row {
property var bottomRowCategories: ["Internet", "Media", "Office", "Settings", "System"]
width: parent.width
spacing: Theme.spacingS
Repeater {
model: parent.bottomRowCategories.filter((cat) => {
return categories.includes(cat);
})
Rectangle {
height: 36
width: (parent.width - (parent.bottomRowCategories.length - 1) * Theme.spacingS) / parent.bottomRowCategories.length
radius: Theme.cornerRadiusLarge
color: selectedCategory === modelData ? Theme.primary : "transparent"
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedCategory = modelData;
categorySelected(modelData);
}
}
}
}
}
} }
}
} }

View File

@@ -8,202 +8,204 @@ import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
property var modelData property var modelData
property bool brightnessPopupVisible: false property bool brightnessPopupVisible: false
property var brightnessDebounceTimer property var brightnessDebounceTimer
brightnessDebounceTimer: Timer { brightnessDebounceTimer: Timer {
property int pendingValue: 0 property int pendingValue: 0
interval: BrightnessService.ddcAvailable ? 500 : 50 interval: BrightnessService.ddcAvailable ? 500 : 50
repeat: false repeat: false
onTriggered: { onTriggered: {
BrightnessService.setBrightnessInternal(pendingValue); BrightnessService.setBrightnessInternal(pendingValue)
} }
}
function show() {
root.brightnessPopupVisible = true
hideTimer.restart()
}
function resetHideTimer() {
if (root.brightnessPopupVisible)
hideTimer.restart()
}
screen: modelData
visible: brightnessPopupVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Timer {
id: hideTimer
interval: 3000
repeat: false
onTriggered: {
if (!brightnessPopup.containsMouse)
root.brightnessPopupVisible = false
else
hideTimer.restart()
}
}
Connections {
function onBrightnessChanged() {
root.show()
} }
function show() { target: BrightnessService
root.brightnessPopupVisible = true; }
hideTimer.restart();
}
function resetHideTimer() { Rectangle {
if (root.brightnessPopupVisible) id: brightnessPopup
hideTimer.restart();
}
screen: modelData property bool containsMouse: popupMouseArea.containsMouse
visible: brightnessPopupVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors { width: Math.min(260, Screen.width - Theme.spacingM * 2)
top: true height: brightnessContent.height + Theme.spacingS * 2
left: true anchors.horizontalCenter: parent.horizontalCenter
right: true anchors.bottom: parent.bottom
bottom: true anchors.bottomMargin: Theme.spacingM
} color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
opacity: root.brightnessPopupVisible ? 1 : 0
scale: root.brightnessPopupVisible ? 1 : 0.9
layer.enabled: true
Timer { Column {
id: hideTimer id: brightnessContent
interval: 3000 anchors.centerIn: parent
repeat: false width: parent.width - Theme.spacingS * 2
onTriggered: { spacing: Theme.spacingXS
if (!brightnessPopup.containsMouse)
root.brightnessPopupVisible = false;
else
hideTimer.restart();
}
}
Connections { Item {
function onBrightnessChanged() { property int gap: Theme.spacingS
root.show();
}
target: BrightnessService width: parent.width
} height: 40
Rectangle { Rectangle {
id: brightnessPopup width: Theme.iconSize
height: Theme.iconSize
property bool containsMouse: popupMouseArea.containsMouse radius: Theme.iconSize / 2
color: "transparent"
width: Math.min(260, Screen.width - Theme.spacingM * 2) x: parent.gap
height: brightnessContent.height + Theme.spacingS * 2 anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: root.brightnessPopupVisible ? 1 : 0
scale: root.brightnessPopupVisible ? 1 : 0.9
layer.enabled: true
Column {
id: brightnessContent
DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2 name: BrightnessService.brightnessLevel
spacing: Theme.spacingXS < 30 ? "brightness_low" : BrightnessService.brightnessLevel
< 70 ? "brightness_medium" : "brightness_high"
size: Theme.iconSize
color: Theme.primary
}
}
Item { DankSlider {
property int gap: Theme.spacingS id: brightnessSlider
width: parent.width width: parent.width - Theme.iconSize - parent.gap * 3
height: 40 height: 40
x: parent.gap * 2 + Theme.iconSize
Rectangle { anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize minimum: 1
height: Theme.iconSize maximum: 100
radius: Theme.iconSize / 2 enabled: BrightnessService.brightnessAvailable
color: "transparent" showValue: true
x: parent.gap unit: "%"
anchors.verticalCenter: parent.verticalCenter Component.onCompleted: {
if (BrightnessService.brightnessAvailable)
DankIcon { value = BrightnessService.brightnessLevel
anchors.centerIn: parent }
name: BrightnessService.brightnessLevel < 30 ? "brightness_low" : onSliderValueChanged: function (newValue) {
BrightnessService.brightnessLevel < 70 ? "brightness_medium" : "brightness_high" if (BrightnessService.brightnessAvailable) {
size: Theme.iconSize brightnessDebounceTimer.pendingValue = newValue
color: Theme.primary brightnessDebounceTimer.restart()
} root.resetHideTimer()
}
DankSlider {
id: brightnessSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: 1
maximum: 100
enabled: BrightnessService.brightnessAvailable
showValue: true
unit: "%"
Component.onCompleted: {
if (BrightnessService.brightnessAvailable)
value = BrightnessService.brightnessLevel;
}
onSliderValueChanged: function(newValue) {
if (BrightnessService.brightnessAvailable) {
brightnessDebounceTimer.pendingValue = newValue;
brightnessDebounceTimer.restart();
root.resetHideTimer();
}
}
onSliderDragFinished: function(finalValue) {
if (BrightnessService.brightnessAvailable) {
brightnessDebounceTimer.stop();
BrightnessService.setBrightnessInternal(finalValue);
}
}
Connections {
function onBrightnessChanged() {
brightnessSlider.value = BrightnessService.brightnessLevel;
}
target: BrightnessService
}
}
} }
} }
onSliderDragFinished: function (finalValue) {
MouseArea { if (BrightnessService.brightnessAvailable) {
id: popupMouseArea brightnessDebounceTimer.stop()
BrightnessService.setBrightnessInternal(finalValue)
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
z: -1
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
transform: Translate {
y: root.brightnessPopupVisible ? 0 : 20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
} }
} }
Behavior on scale { Connections {
NumberAnimation { function onBrightnessChanged() {
duration: Theme.mediumDuration brightnessSlider.value = BrightnessService.brightnessLevel
easing.type: Theme.emphasizedEasing
} }
}
Behavior on transform { target: BrightnessService
PropertyAnimation { }
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
} }
}
} }
mask: Region { MouseArea {
item: brightnessPopup id: popupMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
z: -1
} }
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
transform: Translate {
y: root.brightnessPopupVisible ? 0 : 20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on transform {
PropertyAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
mask: Region {
item: brightnessPopup
}
} }

View File

@@ -6,264 +6,267 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: calendarGrid id: calendarGrid
property date displayDate: new Date() property date displayDate: new Date()
property date selectedDate: new Date() property date selectedDate: new Date()
function loadEventsForMonth() { function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) if (!CalendarService || !CalendarService.khalAvailable)
return ; return
let firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1); let firstDay = new Date(displayDate.getFullYear(),
let dayOfWeek = firstDay.getDay(); displayDate.getMonth(), 1)
let startDate = new Date(firstDay); let dayOfWeek = firstDay.getDay()
startDate.setDate(startDate.getDate() - dayOfWeek - 7); // Extra week padding let startDate = new Date(firstDay)
let lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0); startDate.setDate(startDate.getDate() - dayOfWeek - 7) // Extra week padding
let endDate = new Date(lastDay); let lastDay = new Date(displayDate.getFullYear(),
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7); // Extra week padding displayDate.getMonth() + 1, 0)
CalendarService.loadEvents(startDate, endDate); let endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay(
)) + 7) // Extra week padding
CalendarService.loadEvents(startDate, endDate)
}
spacing: Theme.spacingM
onDisplayDateChanged: {
loadEventsForMonth()
}
Component.onCompleted: {
loadEventsForMonth()
}
Connections {
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable)
loadEventsForMonth()
} }
spacing: Theme.spacingM target: CalendarService
onDisplayDateChanged: { enabled: CalendarService !== null
loadEventsForMonth(); }
}
Component.onCompleted: {
loadEventsForMonth();
}
Connections { Row {
function onKhalAvailableChanged() { width: parent.width
if (CalendarService && CalendarService.khalAvailable) height: 40
loadEventsForMonth();
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_left"
size: Theme.iconSize
color: Theme.primary
}
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() - 1)
displayDate = newDate
} }
}
target: CalendarService
enabled: CalendarService !== null
} }
Row { StyledText {
width: parent.width width: parent.width - 80
height: 40 height: 40
text: Qt.formatDate(displayDate, "MMMM yyyy")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle { Rectangle {
width: 40 width: 40
height: 40 height: 40
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "chevron_left" name: "chevron_right"
size: Theme.iconSize size: Theme.iconSize
color: Theme.primary color: Theme.primary
} }
MouseArea { MouseArea {
id: prevMonthArea id: nextMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate);
newDate.setMonth(newDate.getMonth() - 1);
displayDate = newDate;
}
}
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() + 1)
displayDate = newDate
} }
}
}
}
Row {
width: parent.width
height: 32
Repeater {
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Rectangle {
width: parent.width / 7
height: 32
color: "transparent"
StyledText { StyledText {
width: parent.width - 80 anchors.centerIn: parent
height: 40 text: modelData
text: Qt.formatDate(displayDate, "MMMM yyyy") font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.fontSizeLarge color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
color: Theme.surfaceText Theme.surfaceText.b, 0.6)
font.weight: Font.Medium font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
} }
}
}
}
Grid {
property date firstDay: {
let date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
let dayOfWeek = date.getDay()
date.setDate(date.getDate() - dayOfWeek)
return date
}
width: parent.width
height: 200 // Fixed height for calendar
columns: 7
rows: 6
Repeater {
model: 42
Rectangle {
property date dayDate: {
let date = new Date(parent.firstDay)
date.setDate(date.getDate() + index)
return date
}
property bool isCurrentMonth: dayDate.getMonth(
) === displayDate.getMonth()
property bool isToday: dayDate.toDateString(
) === new Date().toDateString()
property bool isSelected: dayDate.toDateString(
) === selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
clip: true
Rectangle { Rectangle {
width: 40 anchors.centerIn: parent
height: 40 width: parent.width - 4
radius: Theme.cornerRadius height: parent.height - 4
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: isSelected ? Theme.primary : isToday ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadiusSmall
clip: true
DankIcon { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
name: "chevron_right" text: dayDate.getDate()
size: Theme.iconSize font.pixelSize: Theme.fontSizeMedium
color: Theme.primary color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
}
Rectangle {
id: eventIndicator
anchors.fill: parent
radius: parent.radius
visible: CalendarService && CalendarService.khalAvailable
&& CalendarService.hasEventsForDate(dayDate)
opacity: {
if (isSelected)
return 0.9
else if (isToday)
return 0.8
else
return 0.6
} }
MouseArea { gradient: Gradient {
id: nextMonthArea GradientStop {
position: 0.89
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate);
newDate.setMonth(newDate.getMonth() + 1);
displayDate = newDate;
}
}
}
}
Row {
width: parent.width
height: 32
Repeater {
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Rectangle {
width: parent.width / 7
height: 32
color: "transparent" color: "transparent"
}
StyledText { GradientStop {
anchors.centerIn: parent position: 0.9
text: modelData color: {
font.pixelSize: Theme.fontSizeSmall if (isSelected)
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) return Qt.lighter(Theme.primary, 1.3)
font.weight: Font.Medium else if (isToday)
return Theme.primary
else
return Theme.primary
} }
}
GradientStop {
position: 1
color: {
if (isSelected)
return Qt.lighter(Theme.primary, 1.3)
else if (isToday)
return Theme.primary
else
return Theme.primary
}
}
} }
} Behavior on opacity {
NumberAnimation {
} duration: Theme.shortDuration
easing.type: Theme.standardEasing
Grid { }
property date firstDay: {
let date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
let dayOfWeek = date.getDay();
date.setDate(date.getDate() - dayOfWeek);
return date;
}
width: parent.width
height: 200 // Fixed height for calendar
columns: 7
rows: 6
Repeater {
model: 42
Rectangle {
property date dayDate: {
let date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
property bool isCurrentMonth: dayDate.getMonth() === displayDate.getMonth()
property bool isToday: dayDate.toDateString() === new Date().toDateString()
property bool isSelected: dayDate.toDateString() === selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
clip: true
Rectangle {
anchors.centerIn: parent
width: parent.width - 4
height: parent.height - 4
color: isSelected ? Theme.primary : isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadiusSmall
clip: true
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeMedium
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
}
Rectangle {
id: eventIndicator
anchors.fill: parent
radius: parent.radius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
opacity: {
if (isSelected)
return 0.9;
else if (isToday)
return 0.8;
else
return 0.6;
}
gradient: Gradient {
GradientStop {
position: 0.89
color: "transparent"
}
GradientStop {
position: 0.9
color: {
if (isSelected)
return Qt.lighter(Theme.primary, 1.3);
else if (isToday)
return Theme.primary;
else
return Theme.primary;
}
}
GradientStop {
position: 1
color: {
if (isSelected)
return Qt.lighter(Theme.primary, 1.3);
else if (isToday)
return Theme.primary;
else
return Theme.primary;
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedDate = dayDate;
}
}
} }
}
} }
} MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedDate = dayDate
}
}
}
}
}
} }

View File

@@ -10,304 +10,303 @@ import qs.Modules.CentcomCenter
import qs.Services import qs.Services
PanelWindow { PanelWindow {
id: root id: root
readonly property bool hasActiveMedia: MprisController.activePlayer !== null readonly property bool hasActiveMedia: MprisController.activePlayer !== null
property bool calendarVisible: false property bool calendarVisible: false
property bool internalVisible: false property bool internalVisible: false
property real triggerX: (Screen.width - 480) / 2 property real triggerX: (Screen.width - 480) / 2
property real triggerY: Theme.barHeight + 4 property real triggerY: Theme.barHeight + 4
property real triggerWidth: 80 property real triggerWidth: 80
property string triggerSection: "center" property string triggerSection: "center"
property var triggerScreen: null property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) { function setTriggerPosition(x, y, width, section, screen) {
triggerX = x; triggerX = x
triggerY = y; triggerY = y
triggerWidth = width; triggerWidth = width
triggerSection = section; triggerSection = section
triggerScreen = screen; triggerScreen = screen
}
visible: internalVisible
screen: triggerScreen
onCalendarVisibleChanged: {
if (calendarVisible) {
internalVisible = true
Qt.callLater(() => {
internalVisible = true
calendarGrid.loadEventsForMonth()
})
} else {
internalVisible = false
}
}
onVisibleChanged: {
if (visible && calendarGrid)
calendarGrid.loadEventsForMonth()
}
implicitWidth: 480
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: calendarVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: mainContainer
readonly property real targetWidth: Math.min(
(root.screen ? root.screen.width : Screen.width) * 0.9,
600)
function calculateWidth() {
let baseWidth = 320
if (leftWidgets.hasAnyWidgets)
return Math.min(parent.width * 0.9, 600)
return Math.min(parent.width * 0.7, 400)
} }
visible: internalVisible function calculateHeight() {
screen: triggerScreen let contentHeight = Theme.spacingM * 2
onCalendarVisibleChanged: { // margins
if (calendarVisible) { let widgetHeight = 160
internalVisible = true; widgetHeight += 140 + Theme.spacingM
Qt.callLater(() => { let calendarHeight = 300
internalVisible = true; let mainRowHeight = Math.max(widgetHeight, calendarHeight)
calendarGrid.loadEventsForMonth(); contentHeight += mainRowHeight + Theme.spacingM
}); if (CalendarService && CalendarService.khalAvailable) {
} else { let hasEvents = events.selectedDateEvents
internalVisible = false; && events.selectedDateEvents.length > 0
} let eventsHeight = hasEvents ? Math.min(
300,
80 + events.selectedDateEvents.length * 60) : 120
contentHeight += eventsHeight
} else {
contentHeight -= Theme.spacingM
}
return Math.min(contentHeight, parent.height * 0.9)
} }
onVisibleChanged: {
if (visible && calendarGrid)
calendarGrid.loadEventsForMonth();
readonly property real calculatedX: {
var screenWidth = root.screen ? root.screen.width : Screen.width
if (root.triggerSection === "center") {
return (screenWidth - targetWidth) / 2
}
var centerX = root.triggerX + (root.triggerWidth / 2) - (targetWidth / 2)
if (centerX >= Theme.spacingM
&& centerX + targetWidth <= screenWidth - Theme.spacingM) {
return centerX
}
if (centerX < Theme.spacingM) {
return Theme.spacingM
}
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
return screenWidth - targetWidth - Theme.spacingM
}
return centerX
} }
implicitWidth: 480
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: calendarVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent"
anchors { width: targetWidth
top: true height: calculateHeight()
left: true color: Theme.surfaceContainer
right: true radius: Theme.cornerRadiusLarge
bottom: true border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
opacity: calendarVisible ? 1 : 0
scale: calendarVisible ? 1 : 0.9
x: calculatedX
y: root.triggerY
onOpacityChanged: {
if (opacity === 1)
Qt.callLater(() => {
height = calculateHeight()
})
}
Connections {
function onEventsByDateChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight()
}
function onKhalAvailableChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight()
}
target: CalendarService
enabled: CalendarService !== null
}
Connections {
function onSelectedDateEventsChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight()
}
target: events
enabled: events !== null
} }
Rectangle { Rectangle {
id: mainContainer anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
Theme.surfaceTint.b, 0.04)
radius: parent.radius
readonly property real targetWidth: Math.min((root.screen ? root.screen.width : Screen.width) * 0.9, 600) SequentialAnimation on opacity {
running: calendarVisible
loops: Animation.Infinite
function calculateWidth() { NumberAnimation {
let baseWidth = 320; to: 0.08
if (leftWidgets.hasAnyWidgets) duration: Theme.extraLongDuration
return Math.min(parent.width * 0.9, 600); easing.type: Theme.standardEasing
return Math.min(parent.width * 0.7, 400);
} }
function calculateHeight() { NumberAnimation {
let contentHeight = Theme.spacingM * 2; // margins to: 0.02
let widgetHeight = 160; duration: Theme.extraLongDuration
widgetHeight += 140 + Theme.spacingM; easing.type: Theme.standardEasing
let calendarHeight = 300;
let mainRowHeight = Math.max(widgetHeight, calendarHeight);
contentHeight += mainRowHeight + Theme.spacingM;
if (CalendarService && CalendarService.khalAvailable) {
let hasEvents = events.selectedDateEvents && events.selectedDateEvents.length > 0;
let eventsHeight = hasEvents ? Math.min(300, 80 + events.selectedDateEvents.length * 60) : 120;
contentHeight += eventsHeight;
} else {
contentHeight -= Theme.spacingM;
}
return Math.min(contentHeight, parent.height * 0.9);
} }
}
}
readonly property real calculatedX: { Column {
var screenWidth = root.screen ? root.screen.width : Screen.width; anchors.fill: parent
if (root.triggerSection === "center") { anchors.margins: Theme.spacingM
return (screenWidth - targetWidth) / 2; spacing: Theme.spacingM
} focus: true
Keys.onPressed: function (event) {
var centerX = root.triggerX + (root.triggerWidth / 2) - (targetWidth / 2); if (event.key === Qt.Key_Escape) {
calendarVisible = false
if (centerX >= Theme.spacingM && centerX + targetWidth <= screenWidth - Theme.spacingM) { event.accepted = true
return centerX; } else {
} // Don't handle other keys - let them bubble up to modals
event.accepted = false
if (centerX < Theme.spacingM) {
return Theme.spacingM;
}
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
return screenWidth - targetWidth - Theme.spacingM;
}
return centerX;
} }
}
width: targetWidth Row {
height: calculateHeight() width: parent.width
color: Theme.surfaceContainer height: {
radius: Theme.cornerRadiusLarge let widgetHeight = 160
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) // Media widget
border.width: 1 widgetHeight += 140 + Theme.spacingM // Weather/SystemInfo widget with spacing
layer.enabled: true let calendarHeight = 300
opacity: calendarVisible ? 1 : 0 // Calendar
scale: calendarVisible ? 1 : 0.9 return Math.max(widgetHeight, calendarHeight)
x: calculatedX
y: root.triggerY
onOpacityChanged: {
if (opacity === 1)
Qt.callLater(() => {
height = calculateHeight();
});
} }
spacing: Theme.spacingM
Connections { Column {
function onEventsByDateChanged() { id: leftWidgets
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight();
} property bool hasAnyWidgets: true
function onKhalAvailableChanged() { width: hasAnyWidgets ? parent.width * 0.42 : 0 // Slightly narrower for better proportions
if (mainContainer.opacity === 1) height: childrenRect.height
mainContainer.height = mainContainer.calculateHeight(); spacing: Theme.spacingM
visible: hasAnyWidgets
anchors.top: parent.top
} MediaPlayer {
width: parent.width
height: 160
}
target: CalendarService Weather {
enabled: CalendarService !== null width: parent.width
} height: 140
visible: SettingsData.weatherEnabled
}
Connections { SystemInfo {
function onSelectedDateEventsChanged() { width: parent.width
if (mainContainer.opacity === 1) height: 140
mainContainer.height = mainContainer.calculateHeight(); visible: !SettingsData.weatherEnabled
}
}
target: events
enabled: events !== null
} }
Rectangle { Rectangle {
width: leftWidgets.hasAnyWidgets ? parent.width - leftWidgets.width
- Theme.spacingM : parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
CalendarGrid {
id: calendarGrid
anchors.fill: parent anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04) anchors.margins: Theme.spacingS
radius: parent.radius }
SequentialAnimation on opacity {
running: calendarVisible
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
} }
}
Column { Events {
anchors.fill: parent id: events
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
focus: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
calendarVisible = false;
event.accepted = true;
} else {
// Don't handle other keys - let them bubble up to modals
event.accepted = false;
}
}
Row {
width: parent.width
height: {
let widgetHeight = 160; // Media widget
widgetHeight += 140 + Theme.spacingM; // Weather/SystemInfo widget with spacing
let calendarHeight = 300; // Calendar
return Math.max(widgetHeight, calendarHeight);
}
spacing: Theme.spacingM
Column {
id: leftWidgets
property bool hasAnyWidgets: true
width: hasAnyWidgets ? parent.width * 0.42 : 0 // Slightly narrower for better proportions
height: childrenRect.height
spacing: Theme.spacingM
visible: hasAnyWidgets
anchors.top: parent.top
MediaPlayer {
width: parent.width
height: 160
}
Weather {
width: parent.width
height: 140
visible: SettingsData.weatherEnabled
}
SystemInfo {
width: parent.width
height: 140
visible: !SettingsData.weatherEnabled
}
}
Rectangle {
width: leftWidgets.hasAnyWidgets ? parent.width - leftWidgets.width - Theme.spacingM : parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
CalendarGrid {
id: calendarGrid
anchors.fill: parent
anchors.margins: Theme.spacingS
}
}
}
Events {
id: events
width: parent.width
selectedDate: calendarGrid.selectedDate
}
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
width: parent.width
selectedDate: calendarGrid.selectedDate
}
} }
MouseArea { Behavior on opacity {
anchors.fill: parent NumberAnimation {
z: -1 duration: Anims.durMed
enabled: calendarVisible easing.type: Easing.BezierSpline
onClicked: function(mouse) { easing.bezierCurve: Anims.emphasized
var localPos = mapToItem(mainContainer, mouse.x, mouse.y); }
if (localPos.x < 0 || localPos.x > mainContainer.width || localPos.y < 0 || localPos.y > mainContainer.height)
calendarVisible = false;
}
} }
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
}
MouseArea {
anchors.fill: parent
z: -1
enabled: calendarVisible
onClicked: function (mouse) {
var localPos = mapToItem(mainContainer, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > mainContainer.width || localPos.y < 0
|| localPos.y > mainContainer.height)
calendarVisible = false
}
}
} }

View File

@@ -6,330 +6,340 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: events id: events
property date selectedDate: new Date() property date selectedDate: new Date()
property var selectedDateEvents: [] property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0 property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
property bool shouldShow: CalendarService && CalendarService.khalAvailable property bool shouldShow: CalendarService && CalendarService.khalAvailable
function updateSelectedDateEvents() { function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) { if (CalendarService && CalendarService.khalAvailable) {
let events = CalendarService.getEventsForDate(selectedDate); let events = CalendarService.getEventsForDate(selectedDate)
selectedDateEvents = events; selectedDateEvents = events
} else { } else {
selectedDateEvents = []; selectedDateEvents = []
} }
}
onSelectedDateEventsChanged: {
eventsList.model = selectedDateEvents
}
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(
300,
80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.12)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
visible: shouldShow
layer.enabled: true
Component.onCompleted: {
updateSelectedDateEvents()
}
onSelectedDateChanged: {
updateSelectedDateEvents()
}
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents()
} }
onSelectedDateEventsChanged: { function onKhalAvailableChanged() {
eventsList.model = selectedDateEvents; updateSelectedDateEvents()
}
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
visible: shouldShow
layer.enabled: true
Component.onCompleted: {
updateSelectedDateEvents();
}
onSelectedDateChanged: {
updateSelectedDateEvents();
} }
Connections { target: CalendarService
function onEventsByDateChanged() { enabled: CalendarService !== null
updateSelectedDateEvents(); }
}
function onKhalAvailableChanged() { Row {
updateSelectedDateEvents(); id: headerRow
}
target: CalendarService anchors.top: parent.top
enabled: CalendarService !== null anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
DankIcon {
name: "event"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
} }
Row { StyledText {
id: headerRow text: hasEvents ? (Qt.formatDate(
selectedDate,
"MMM d") + " • " + (selectedDateEvents.length
=== 1 ? "1 event" : selectedDateEvents.length
+ " events")) : Qt.formatDate(
selectedDate, "MMM d")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: !hasEvents
DankIcon {
name: "event_busy"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No events"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
}
DankListView {
id: eventsList
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingM
visible: opacity > 0
opacity: hasEvents ? 1 : 0
clip: true
spacing: Theme.spacingS
boundsBehavior: Flickable.StopAtBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
property real momentum: 0
onWheel: event => {
if (event.pixelDelta.y !== 0) {
// Touchpad with pixel delta
momentum = event.pixelDelta.y * 1.8
} else {
// Mouse wheel with angle delta
momentum = (event.angleDelta.y / 120) * (60 * 2.5) // ~2.5 items per wheel step
}
let newY = parent.contentY - momentum
newY = Math.max(0,
Math.min(parent.contentHeight - parent.height,
newY))
parent.contentY = newY
momentum *= 0.92 // Decay for smooth momentum
event.accepted = true
}
}
ScrollBar.vertical: ScrollBar {
policy: eventsList.contentHeight
> eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
delegate: Rectangle {
width: eventsList.width
height: eventContent.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: {
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.06)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.06)
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.15)
return "transparent"
}
border.width: 1
Rectangle {
width: 4
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: Theme.primary
opacity: 0.8
}
Column {
id: eventContent
anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Theme.spacingL anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS anchors.leftMargin: Theme.spacingL + 4
anchors.rightMargin: Theme.spacingM
DankIcon { spacing: 6
name: "event"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText { StyledText {
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d") width: parent.width
font.pixelSize: Theme.fontSizeMedium text: modelData.title
color: Theme.surfaceText font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Item {
width: parent.width
height: Math.max(timeRow.height, locationRow.height)
Row {
id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
}
} DankIcon {
name: "schedule"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
Column { StyledText {
anchors.centerIn: parent text: {
spacing: Theme.spacingXS if (modelData.allDay) {
visible: !hasEvents return "All day"
DankIcon {
name: "event_busy"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No events"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
}
DankListView {
id: eventsList
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingM
visible: opacity > 0
opacity: hasEvents ? 1 : 0
clip: true
spacing: Theme.spacingS
boundsBehavior: Flickable.StopAtBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
property real momentum: 0
onWheel: (event) => {
if (event.pixelDelta.y !== 0) {
// Touchpad with pixel delta
momentum = event.pixelDelta.y * 1.8
} else { } else {
// Mouse wheel with angle delta let timeFormat = SettingsData.use24HourClock ? "H:mm" : "h:mm AP"
momentum = (event.angleDelta.y / 120) * (60 * 2.5) // ~2.5 items per wheel step let startTime = Qt.formatTime(modelData.start, timeFormat)
} if (modelData.start.toDateString(
) !== modelData.end.toDateString()
|| modelData.start.getTime() !== modelData.end.getTime())
return startTime + " " + Qt.formatTime(modelData.end,
timeFormat)
let newY = parent.contentY - momentum return startTime
newY = Math.max(0, Math.min(parent.contentHeight - parent.height, newY)) }
parent.contentY = newY }
momentum *= 0.92 // Decay for smooth momentum font.pixelSize: Theme.fontSizeSmall
event.accepted = true color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
} }
}
Row {
id: locationRow
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: modelData.location !== ""
DankIcon {
name: "location_on"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.location
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
width: Math.min(implicitWidth, 200)
}
}
} }
}
ScrollBar.vertical: ScrollBar { MouseArea {
policy: eventsList.contentHeight > eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false)
console.warn("Failed to open URL: " + modelData.url)
}
} }
}
Behavior on opacity { Behavior on color {
NumberAnimation { ColorAnimation {
duration: Theme.mediumDuration duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.standardEasing
}
} }
}
delegate: Rectangle { Behavior on border.color {
width: eventsList.width ColorAnimation {
height: eventContent.implicitHeight + Theme.spacingM duration: Theme.shortDuration
radius: Theme.cornerRadius easing.type: Theme.standardEasing
color: {
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06);
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
return "transparent";
}
border.width: 1
Rectangle {
width: 4
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: Theme.primary
opacity: 0.8
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL + 4
anchors.rightMargin: Theme.spacingM
spacing: 6
StyledText {
width: parent.width
text: modelData.title
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Item {
width: parent.width
height: Math.max(timeRow.height, locationRow.height)
Row {
id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "schedule"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (modelData.allDay) {
return "All day";
} else {
let timeFormat = SettingsData.use24HourClock ? "H:mm" : "h:mm AP";
let startTime = Qt.formatTime(modelData.start, timeFormat);
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime())
return startTime + " " + Qt.formatTime(modelData.end, timeFormat);
return startTime;
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: locationRow
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: modelData.location !== ""
DankIcon {
name: "location_on"
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.location
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
width: Math.min(implicitWidth, 200)
}
}
}
}
MouseArea {
id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false)
console.warn("Failed to open URL: " + modelData.url);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
}
} }
}
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowEnabled: true shadowEnabled: true
shadowHorizontalOffset: 0 shadowHorizontalOffset: 0
shadowVerticalOffset: 2 shadowVerticalOffset: 2
shadowBlur: 0.25 shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1) shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1 shadowOpacity: 0.1
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
} }
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
} }

View File

@@ -8,435 +8,445 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: mediaPlayer id: mediaPlayer
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
property string lastValidTitle: "" property string lastValidTitle: ""
property string lastValidArtist: "" property string lastValidArtist: ""
property string lastValidAlbum: "" property string lastValidAlbum: ""
property string lastValidArtUrl: "" property string lastValidArtUrl: ""
property real currentPosition: 0 property real currentPosition: 0
function ratio() { function ratio() {
return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0; return activePlayer
&& activePlayer.length > 0 ? currentPosition / activePlayer.length : 0
}
onActivePlayerChanged: {
if (!activePlayer)
updateTimer.start()
else
updateTimer.stop()
}
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Timer {
id: updateTimer
interval: 2000
running: {
return (!activePlayer)
|| (activePlayer
&& activePlayer.playbackState === MprisPlaybackState.Playing
&& activePlayer.length > 0 && !progressMouseArea.isSeeking)
}
repeat: true
onTriggered: {
if (!activePlayer) {
lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
stop() // Stop after clearing cache
} else if (activePlayer.playbackState === MprisPlaybackState.Playing
&& !progressMouseArea.isSeeking) {
currentPosition = activePlayer.position
}
}
}
Connections {
function onPositionChanged() {
if (!progressMouseArea.isSeeking)
currentPosition = activePlayer.position
} }
onActivePlayerChanged: { function onPostTrackChanged() {
if (!activePlayer) currentPosition = activePlayer && activePlayer.position || 0
updateTimer.start();
else
updateTimer.stop();
} }
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Timer { function onTrackTitleChanged() {
id: updateTimer currentPosition = activePlayer && activePlayer.position || 0
}
interval: 2000 target: activePlayer
running: { }
return (!activePlayer) || (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.length > 0 && !progressMouseArea.isSeeking);
} Item {
repeat: true anchors.fill: parent
onTriggered: { anchors.margins: Theme.spacingS
if (!activePlayer) {
lastValidTitle = ""; Column {
lastValidArtist = ""; anchors.centerIn: parent
lastValidAlbum = ""; spacing: Theme.spacingS
lastValidArtUrl = ""; visible: (!activePlayer && !lastValidTitle)
stop(); // Stop after clearing cache || (activePlayer && activePlayer.trackTitle === ""
} else if (activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking) { && lastValidTitle === "")
currentPosition = activePlayer.position;
DankIcon {
name: "music_note"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "No Media Playing"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingS
visible: activePlayer && activePlayer.trackTitle !== ""
|| lastValidTitle !== ""
Row {
width: parent.width
height: 60
spacing: Theme.spacingM
Rectangle {
width: 60
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
id: albumArt
anchors.fill: parent
source: activePlayer && activePlayer.trackArtUrl
|| lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer && activePlayer.trackArtUrl)
lastValidArtUrl = activePlayer.trackArtUrl
}
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
} }
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready
color: "transparent"
DankIcon {
anchors.centerIn: parent
name: "album"
size: 28
color: Theme.surfaceVariantText
}
}
}
} }
}
Connections {
function onPositionChanged() {
if (!progressMouseArea.isSeeking)
currentPosition = activePlayer.position;
}
function onPostTrackChanged() {
currentPosition = activePlayer && activePlayer.position || 0;
}
function onTrackTitleChanged() {
currentPosition = activePlayer && activePlayer.position || 0;
}
target: activePlayer
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
Column { Column {
anchors.centerIn: parent width: parent.width - 60 - Theme.spacingM
spacing: Theme.spacingS height: parent.height
visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "") spacing: Theme.spacingXS
StyledText {
text: activePlayer && activePlayer.trackTitle || lastValidTitle
|| "Unknown Track"
onTextChanged: {
if (activePlayer && activePlayer.trackTitle)
lastValidTitle = activePlayer.trackTitle
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: activePlayer && activePlayer.trackArtist || lastValidArtist
|| "Unknown Artist"
onTextChanged: {
if (activePlayer && activePlayer.trackArtist)
lastValidArtist = activePlayer.trackArtist
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: activePlayer && activePlayer.trackAlbum
|| lastValidAlbum || ""
onTextChanged: {
if (activePlayer && activePlayer.trackAlbum)
lastValidAlbum = activePlayer.trackAlbum
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
visible: text.length > 0
}
}
}
Item {
id: progressBarContainer
width: parent.width
height: 24
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
visible: activePlayer !== null
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: parent.width * ratio()
Behavior on width {
NumberAnimation {
duration: 100
}
}
}
Rectangle {
id: progressHandle
width: 12
height: 12
radius: 6
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width,
progressFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.length > 0
scale: progressMouseArea.containsMouse
|| progressMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
id: progressMouseArea
property bool isSeeking: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0
&& activePlayer.canSeek
preventStealing: true
onPressed: function (mouse) {
isSeeking = true
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(
1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
}
}
onReleased: {
isSeeking = false
}
onPositionChanged: function (mouse) {
if (pressed && isSeeking && activePlayer
&& activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(
1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
}
}
onClicked: function (mouse) {
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(
1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
}
}
}
MouseArea {
id: progressGlobalMouseArea
x: 0
y: 0
width: mediaPlayer.width
height: mediaPlayer.height
enabled: progressMouseArea.isSeeking
visible: false
preventStealing: true
onPositionChanged: function (mouse) {
if (progressMouseArea.isSeeking && activePlayer
&& activePlayer.length > 0) {
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y)
let ratio = Math.max(
0, Math.min(1, globalPos.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
}
}
onReleased: {
progressMouseArea.isSeeking = false
}
}
}
Item {
width: parent.width
height: 32
visible: activePlayer !== null
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
height: parent.height
Rectangle {
width: 28
height: 28
radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.12) : "transparent"
DankIcon { DankIcon {
name: "music_note" anchors.centerIn: parent
size: Theme.iconSize + 8 name: "skip_previous"
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) size: 16
anchors.horizontalCenter: parent.horizontalCenter color: Theme.surfaceText
} }
StyledText { MouseArea {
text: "No Media Playing" id: prevBtnArea
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) anchors.fill: parent
anchors.horizontalCenter: parent.horizontalCenter hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer)
return
if (currentPosition > 8 && activePlayer.canSeek) {
activePlayer.position = 0
currentPosition = 0
} else {
activePlayer.previous()
}
}
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: Theme.primary
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState
=== MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 20
color: Theme.background
} }
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
} }
}
Column {
anchors.fill: parent
spacing: Theme.spacingS
visible: activePlayer && activePlayer.trackTitle !== "" || lastValidTitle !== ""
Row {
width: parent.width
height: 60
spacing: Theme.spacingM
Rectangle {
width: 60
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
id: albumArt
anchors.fill: parent
source: activePlayer && activePlayer.trackArtUrl || lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer && activePlayer.trackArtUrl)
lastValidArtUrl = activePlayer.trackArtUrl;
}
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
}
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready
color: "transparent"
DankIcon {
anchors.centerIn: parent
name: "album"
size: 28
color: Theme.surfaceVariantText
}
}
}
}
Column {
width: parent.width - 60 - Theme.spacingM
height: parent.height
spacing: Theme.spacingXS
StyledText {
text: activePlayer && activePlayer.trackTitle || lastValidTitle || "Unknown Track"
onTextChanged: {
if (activePlayer && activePlayer.trackTitle)
lastValidTitle = activePlayer.trackTitle;
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: activePlayer && activePlayer.trackArtist || lastValidArtist || "Unknown Artist"
onTextChanged: {
if (activePlayer && activePlayer.trackArtist)
lastValidArtist = activePlayer.trackArtist;
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
}
StyledText {
text: activePlayer && activePlayer.trackAlbum || lastValidAlbum || ""
onTextChanged: {
if (activePlayer && activePlayer.trackAlbum)
lastValidAlbum = activePlayer.trackAlbum;
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
visible: text.length > 0
}
}
}
Item {
id: progressBarContainer
width: parent.width
height: 24
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
visible: activePlayer !== null
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: parent.width * ratio()
Behavior on width {
NumberAnimation {
duration: 100
}
}
}
Rectangle {
id: progressHandle
width: 12
height: 12
radius: 6
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.length > 0
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
id: progressMouseArea
property bool isSeeking: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
preventStealing: true
onPressed: function(mouse) {
isSeeking = true;
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
isSeeking = false;
}
onPositionChanged: function(mouse) {
if (pressed && isSeeking && activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onClicked: function(mouse) {
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
}
MouseArea {
id: progressGlobalMouseArea
x: 0
y: 0
width: mediaPlayer.width
height: mediaPlayer.height
enabled: progressMouseArea.isSeeking
visible: false
preventStealing: true
onPositionChanged: function(mouse) {
if (progressMouseArea.isSeeking && activePlayer && activePlayer.length > 0) {
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
progressMouseArea.isSeeking = false;
}
}
}
Item {
width: parent.width
height: 32
visible: activePlayer !== null
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
height: parent.height
Rectangle {
width: 28
height: 28
radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer)
return ;
if (currentPosition > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
currentPosition = 0;
} else {
activePlayer.previous();
}
}
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: Theme.primary
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 20
color: Theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 16
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
} }
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
} }

View File

@@ -4,133 +4,132 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) Theme.surfaceContainer.b, 0.4)
border.width: 1 border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Ref { Ref {
service: SysMonitorService service: SysMonitorService
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingM
SystemLogo {
width: 48
height: 48
}
Column {
width: parent.width - 48 - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: SysMonitorService.hostname
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: SysMonitorService.distribution + " • " + SysMonitorService.architecture
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
} }
Column { Column {
anchors.fill: parent width: parent.width
anchors.margins: Theme.spacingM spacing: Theme.spacingXS
spacing: Theme.spacingS
Row { StyledText {
width: parent.width text: "Uptime " + formatUptime(UserInfoService.uptime)
spacing: Theme.spacingM font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
SystemLogo { StyledText {
width: 48 text: "Load: " + SysMonitorService.loadAverage
height: 48 font.pixelSize: Theme.fontSizeSmall
} color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Column { StyledText {
width: parent.width - 48 - Theme.spacingM text: SysMonitorService.processCount + " proc, "
anchors.verticalCenter: parent.verticalCenter + SysMonitorService.threadCount + " threads"
spacing: Theme.spacingXS font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
}
}
}
StyledText { function formatUptime(uptime) {
text: SysMonitorService.hostname if (!uptime)
font.pixelSize: Theme.fontSizeLarge return "0m"
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText { // Parse the uptime string - handle formats like "1 week, 4 days, 3:45" or "4 days, 3:45" or "3:45"
text: SysMonitorService.distribution + " • " + SysMonitorService.architecture var uptimeStr = uptime.toString().trim()
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
}
} // Check for weeks and days - need to add them together
var weekMatch = uptimeStr.match(/(\d+)\s+weeks?/)
} var dayMatch = uptimeStr.match(/(\d+)\s+days?/)
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
Column {
width: parent.width
spacing: Theme.spacingXS
StyledText {
text: "Uptime " + formatUptime(UserInfoService.uptime)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: "Load: " + SysMonitorService.loadAverage
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: SysMonitorService.processCount + " proc, " + SysMonitorService.threadCount + " threads"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
}
}
if (weekMatch) {
var weeks = parseInt(weekMatch[1])
var totalDays = weeks * 7
if (dayMatch) {
var days = parseInt(dayMatch[1])
totalDays += days
}
return totalDays + "d"
} else if (dayMatch) {
var days = parseInt(dayMatch[1])
return days + "d"
} }
function formatUptime(uptime) { // If it's just hours:minutes, show the largest unit
if (!uptime) return "0m" var timeMatch = uptimeStr.match(/(\d+):(\d+)/)
if (timeMatch) {
// Parse the uptime string - handle formats like "1 week, 4 days, 3:45" or "4 days, 3:45" or "3:45" var hours = parseInt(timeMatch[1])
var uptimeStr = uptime.toString().trim() var minutes = parseInt(timeMatch[2])
if (hours > 0) {
// Check for weeks and days - need to add them together return hours + "h"
var weekMatch = uptimeStr.match(/(\d+)\s+weeks?/) } else {
var dayMatch = uptimeStr.match(/(\d+)\s+days?/) return minutes + "m"
}
if (weekMatch) {
var weeks = parseInt(weekMatch[1])
var totalDays = weeks * 7
if (dayMatch) {
var days = parseInt(dayMatch[1])
totalDays += days
}
return totalDays + "d"
} else if (dayMatch) {
var days = parseInt(dayMatch[1])
return days + "d"
}
// If it's just hours:minutes, show the largest unit
var timeMatch = uptimeStr.match(/(\d+):(\d+)/)
if (timeMatch) {
var hours = parseInt(timeMatch[1])
var minutes = parseInt(timeMatch[2])
if (hours > 0) {
return hours + "h"
} else {
return minutes + "m"
}
}
// Fallback - return as is but truncated
return uptimeStr.length > 8 ? uptimeStr.substring(0, 8) + "…" : uptimeStr
} }
// Fallback - return as is but truncated
return uptimeStr.length > 8 ? uptimeStr.substring(0, 8) + "…" : uptimeStr
}
} }

View File

@@ -6,191 +6,185 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: weather id: weather
width: parent.width width: parent.width
height: parent.height height: parent.height
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) Theme.surfaceContainer.b, 0.4)
border.width: 1 border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
layer.enabled: true border.width: 1
layer.enabled: true
Ref { Ref {
service: WeatherService service: WeatherService
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !WeatherService.weather.available
|| WeatherService.weather.temp === 0
DankIcon {
name: "cloud_off"
size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
} }
Column { StyledText {
text: "No Weather Data"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
visible: WeatherService.weather.available
&& WeatherService.weather.temp !== 0
Item {
width: parent.width
height: 60
Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingL
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
DankIcon { DankIcon {
name: "cloud_off" name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize + 8 size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5) color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp)
+ "°" + (SettingsData.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Light
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (WeatherService.weather.available)
SettingsData.setTemperatureUnit(!SettingsData.useFahrenheit)
}
enabled: WeatherService.weather.available
}
}
StyledText {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Grid {
columns: 2
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingXS
DankIcon {
name: "humidity_low"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
StyledText { StyledText {
text: "No Weather Data" text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "air"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
} StyledText {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column { Row {
anchors.fill: parent spacing: Theme.spacingXS
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
Item {
width: parent.width
height: 60
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize + 8
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + (SettingsData.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Light
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (WeatherService.weather.available)
SettingsData.setTemperatureUnit(!SettingsData.useFahrenheit);
}
enabled: WeatherService.weather.available
}
}
StyledText {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
DankIcon {
name: "wb_twilight"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
Grid { StyledText {
columns: 2 text: WeatherService.weather.sunrise || "--"
spacing: Theme.spacingM font.pixelSize: Theme.fontSizeSmall
anchors.horizontalCenter: parent.horizontalCenter color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row { Row {
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon {
name: "humidity_low"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "air"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "wb_twilight"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.sunrise || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
DankIcon {
name: "bedtime"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: WeatherService.weather.sunset || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankIcon {
name: "bedtime"
size: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
StyledText {
text: WeatherService.weather.sunset || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
} }
}
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowEnabled: true shadowEnabled: true
shadowHorizontalOffset: 0 shadowHorizontalOffset: 0
shadowVerticalOffset: 2 shadowVerticalOffset: 2
shadowBlur: 0.5 shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1) shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1 shadowOpacity: 0.1
} }
} }

View File

@@ -9,142 +9,140 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(AudioService.sink) : "" property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(
AudioService.sink) : ""
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Output Device"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: parent.width width: parent.width
spacing: Theme.spacingM height: 35
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.3)
border.width: 1
visible: AudioService.sink !== null
StyledText { Row {
text: "Output Device" anchors.left: parent.left
font.pixelSize: Theme.fontSizeLarge anchors.leftMargin: Theme.spacingM
color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "check_circle"
size: Theme.iconSize - 4
color: Theme.primary
}
StyledText {
text: "Current: " + (root.currentSinkDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
}
}
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return []
let sinks = []
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node || node.isStream)
continue
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink)
sinks.push(node)
}
return sinks
} }
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 35 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) color: deviceArea.containsMouse ? Qt.rgba(
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) Theme.primary.r, Theme.primary.g,
border.width: 1 Theme.primary.b, 0.08) : (modelData === AudioService.sink ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
visible: AudioService.sink !== null border.color: modelData === AudioService.sink ? Theme.primary : "transparent"
border.width: 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS spacing: Theme.spacingM
DankIcon {
name: "check_circle"
size: Theme.iconSize - 4
color: Theme.primary
}
StyledText {
text: "Current: " + (root.currentSinkDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
}
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset"
else if (modelData.name.includes("hdmi"))
return "tv"
else if (modelData.name.includes("usb"))
return "headset"
else
return "speaker"
}
size: Theme.iconSize
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
}
StyledText {
text: {
if (AudioService.subtitle(modelData.name)
&& AudioService.subtitle(modelData.name) !== "")
return AudioService.subtitle(
modelData.name) + (modelData === AudioService.sink ? " • Selected" : "")
else
return modelData === AudioService.sink ? "Selected" : ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: deviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSink = modelData
}
}
} }
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return [];
let sinks = [];
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i];
if (!node || node.isStream)
continue;
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink)
sinks.push(node);
}
return sinks;
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.sink ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData === AudioService.sink ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset";
else if (modelData.name.includes("hdmi"))
return "tv";
else if (modelData.name.includes("usb"))
return "headset";
else
return "speaker";
}
size: Theme.iconSize
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
}
StyledText {
text: {
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
return AudioService.subtitle(modelData.name) + (modelData === AudioService.sink ? " • Selected" : "");
else
return modelData === AudioService.sink ? "Selected" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: deviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSink = modelData;
}
}
}
}
} }

View File

@@ -9,140 +9,139 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(AudioService.source) : "" property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(
AudioService.source) : ""
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Input Device"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: parent.width width: parent.width
spacing: Theme.spacingM height: 35
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.3)
border.width: 1
visible: AudioService.source !== null
StyledText { Row {
text: "Input Device" anchors.left: parent.left
font.pixelSize: Theme.fontSizeLarge anchors.leftMargin: Theme.spacingM
color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "check_circle"
size: Theme.iconSize - 4
color: Theme.primary
}
StyledText {
text: "Current: " + (root.currentSourceDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
}
}
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return []
let sources = []
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node || node.isStream)
continue
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource
&& !node.name.includes(".monitor"))
sources.push(node)
}
return sources
} }
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 35 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) color: sourceArea.containsMouse ? Qt.rgba(
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) Theme.primary.r, Theme.primary.g,
border.width: 1 Theme.primary.b, 0.08) : (modelData === AudioService.source ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
visible: AudioService.source !== null border.color: modelData === AudioService.source ? Theme.primary : "transparent"
border.width: 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS spacing: Theme.spacingM
DankIcon {
name: "check_circle"
size: Theme.iconSize - 4
color: Theme.primary
}
StyledText {
text: "Current: " + (root.currentSourceDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
}
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset_mic"
else if (modelData.name.includes("usb"))
return "headset_mic"
else
return "mic"
}
size: Theme.iconSize
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
}
StyledText {
text: {
if (AudioService.subtitle(modelData.name)
&& AudioService.subtitle(modelData.name) !== "")
return AudioService.subtitle(
modelData.name) + (modelData === AudioService.source ? " • Selected" : "")
else
return modelData === AudioService.source ? "Selected" : ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: sourceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSource = modelData
}
}
} }
}
Repeater {
model: {
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values)
return [];
let sources = [];
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i];
if (!node || node.isStream)
continue;
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.name.includes(".monitor"))
sources.push(node);
}
return sources;
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: sourceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.source ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData === AudioService.source ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset_mic";
else if (modelData.name.includes("usb"))
return "headset_mic";
else
return "mic";
}
size: Theme.iconSize
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
}
StyledText {
text: {
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
return AudioService.subtitle(modelData.name) + (modelData === AudioService.source ? " • Selected" : "");
else
return modelData === AudioService.source ? "Selected" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: sourceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData)
Pipewire.preferredDefaultAudioSource = modelData;
}
}
}
}
} }

View File

@@ -8,216 +8,222 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property real micLevel: Math.min(100, (AudioService.source && AudioService.source.audio && AudioService.source.audio.volume * 100) || 0) property real micLevel: Math.min(
property bool micMuted: (AudioService.source && AudioService.source.audio && AudioService.source.audio.muted) || false 100,
(AudioService.source && AudioService.source.audio
&& AudioService.source.audio.volume * 100) || 0)
property bool micMuted: (AudioService.source && AudioService.source.audio
&& AudioService.source.audio.muted) || false
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Microphone Level"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
StyledText { DankIcon {
text: "Microphone Level" name: root.micMuted ? "mic_off" : "mic"
font.pixelSize: Theme.fontSizeLarge size: Theme.iconSize
color: Theme.surfaceText color: root.micMuted ? Theme.error : Theme.surfaceText
font.weight: Font.Medium anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (AudioService.source && AudioService.source.audio)
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
} }
Row { Item {
id: micSliderContainer
width: parent.width - 80
height: 32
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: micSliderTrack
width: parent.width width: parent.width
spacing: Theme.spacingM height: 8
radius: 4
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
DankIcon { Rectangle {
name: root.micMuted ? "mic_off" : "mic" id: micSliderFill
size: Theme.iconSize
color: root.micMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
MouseArea { width: parent.width * (root.micLevel / 100)
anchors.fill: parent height: parent.height
hoverEnabled: true radius: parent.radius
cursorShape: Qt.PointingHandCursor color: Theme.primary
onClicked: {
if (AudioService.source && AudioService.source.audio)
AudioService.source.audio.muted = !AudioService.source.audio.muted;
} Behavior on width {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
} }
}
} }
Item { Rectangle {
id: micSliderContainer id: micHandle
width: parent.width - 80 width: 18
height: 32 height: 18
anchors.verticalCenter: parent.verticalCenter radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width,
micSliderFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
Rectangle { Rectangle {
id: micSliderTrack id: micTooltip
width: parent.width width: tooltipText.contentWidth + Theme.spacingS * 2
height: 8 height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: 4 radius: Theme.cornerRadiusSmall
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) color: Theme.surfaceContainer
anchors.verticalCenter: parent.verticalCenter border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: (micMouseArea.containsMouse && !root.micMuted)
|| micMouseArea.isDragging
opacity: visible ? 1 : 0
Rectangle { StyledText {
id: micSliderFill id: tooltipText
width: parent.width * (root.micLevel / 100)
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
Rectangle {
id: micHandle
width: 18
height: 18
radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
Rectangle {
id: micTooltip
width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadiusSmall
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: (micMouseArea.containsMouse && !root.micMuted) || micMouseArea.isDragging
opacity: visible ? 1 : 0
StyledText {
id: tooltipText
text: Math.round(root.micLevel) + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
text: Math.round(root.micLevel) + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
} }
MouseArea { Behavior on opacity {
id: micMouseArea NumberAnimation {
duration: Theme.shortDuration
property bool isDragging: false easing.type: Theme.standardEasing
}
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
onPressed: (mouse) => {
isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newMicLevel / 100;
}
}
onReleased: {
isDragging = false;
}
onPositionChanged: (mouse) => {
if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.max(0, Math.min(100, Math.round(ratio * 100)));
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newMicLevel / 100;
}
}
}
onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newMicLevel / 100;
}
}
} }
}
MouseArea { Behavior on scale {
id: micGlobalMouseArea NumberAnimation {
duration: Anims.durShort
x: 0 easing.type: Easing.BezierSpline
y: 0 easing.bezierCurve: Anims.standard
width: root.parent ? root.parent.width : 0
height: root.parent ? root.parent.height : 0
enabled: micMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (micMouseArea.isDragging) {
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width));
let newMicLevel = Math.max(0, Math.min(100, Math.round(ratio * 100)));
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newMicLevel / 100;
}
}
}
onReleased: {
micMouseArea.isDragging = false;
}
} }
}
} }
}
DankIcon { MouseArea {
name: "mic" id: micMouseArea
size: Theme.iconSize
color: Theme.surfaceText property bool isDragging: false
anchors.verticalCenter: parent.verticalCenter
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
onPressed: mouse => {
isDragging = true
let ratio = Math.max(0, Math.min(
1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100
}
}
onReleased: {
isDragging = false
} }
onPositionChanged: mouse => {
if (pressed && isDragging) {
let ratio = Math.max(
0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.max(
0, Math.min(100, Math.round(ratio * 100)))
if (AudioService.source
&& AudioService.source.audio) {
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100
}
}
}
onClicked: mouse => {
let ratio = Math.max(0, Math.min(
1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100
}
}
}
MouseArea {
id: micGlobalMouseArea
x: 0
y: 0
width: root.parent ? root.parent.width : 0
height: root.parent ? root.parent.height : 0
enabled: micMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: mouse => {
if (micMouseArea.isDragging) {
let globalPos = mapToItem(micSliderTrack,
mouse.x, mouse.y)
let ratio = Math.max(
0,
Math.min(1,
globalPos.x / micSliderTrack.width))
let newMicLevel = Math.max(
0, Math.min(100, Math.round(ratio * 100)))
if (AudioService.source
&& AudioService.source.audio) {
AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100
}
}
}
onReleased: {
micMouseArea.isDragging = false
}
}
} }
DankIcon {
name: "mic"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
} }

View File

@@ -4,60 +4,63 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property real volumeLevel: Math.min(100, (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0) property real volumeLevel: Math.min(
property bool volumeMuted: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted) || false 100,
(AudioService.sink && AudioService.sink.audio
&& AudioService.sink.audio.volume * 100) || 0)
property bool volumeMuted: (AudioService.sink && AudioService.sink.audio
&& AudioService.sink.audio.muted) || false
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Volume"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
id: volumeSlider
width: parent.width width: parent.width
spacing: Theme.spacingM minimum: 0
maximum: 100
leftIcon: root.volumeMuted ? "volume_off" : "volume_down"
rightIcon: "volume_up"
enabled: !root.volumeMuted
showValue: true
unit: "%"
StyledText { Connections {
text: "Volume" target: AudioService.sink
font.pixelSize: Theme.fontSizeLarge && AudioService.sink.audio ? AudioService.sink.audio : null
color: Theme.surfaceText function onVolumeChanged() {
font.weight: Font.Medium volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100)
}
} }
DankSlider { Component.onCompleted: {
id: volumeSlider if (AudioService.sink && AudioService.sink.audio) {
value = Math.round(AudioService.sink.audio.volume * 100)
}
width: parent.width let leftIconItem = volumeSlider.children[0].children[0]
minimum: 0 if (leftIconItem) {
maximum: 100 let mouseArea = Qt.createQmlObject(
leftIcon: root.volumeMuted ? "volume_off" : "volume_down" 'import QtQuick; import qs.Services; MouseArea { anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: { if (AudioService.sink && AudioService.sink.audio) AudioService.sink.audio.muted = !AudioService.sink.audio.muted; } }',
rightIcon: "volume_up" leftIconItem, "dynamicMouseArea")
enabled: !root.volumeMuted }
showValue: true
unit: "%"
Connections {
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
function onVolumeChanged() {
volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100);
}
}
Component.onCompleted: {
if (AudioService.sink && AudioService.sink.audio) {
value = Math.round(AudioService.sink.audio.volume * 100);
}
let leftIconItem = volumeSlider.children[0].children[0];
if (leftIconItem) {
let mouseArea = Qt.createQmlObject(
'import QtQuick; import qs.Services; MouseArea { anchors.fill: parent; hoverEnabled: true; cursorShape: Qt.PointingHandCursor; onClicked: { if (AudioService.sink && AudioService.sink.audio) AudioService.sink.audio.muted = !AudioService.sink.audio.muted; } }',
leftIconItem,
"dynamicMouseArea"
);
}
}
onSliderValueChanged: (newValue) => {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newValue / 100;
}
}
} }
onSliderValueChanged: newValue => {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newValue / 100
}
}
}
} }

View File

@@ -10,111 +10,111 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: audioTab id: audioTab
property int audioSubTab: 0 property int audioSubTab: 0
Column { Column {
anchors.fill: parent anchors.fill: parent
spacing: Theme.spacingM spacing: Theme.spacingM
DankTabBar { DankTabBar {
width: parent.width width: parent.width
tabHeight: 40 tabHeight: 40
currentIndex: audioTab.audioSubTab currentIndex: audioTab.audioSubTab
showIcons: false showIcons: false
model: [{ model: [{
"text": "Output" "text": "Output"
}, { }, {
"text": "Input" "text": "Input"
}] }]
onTabClicked: function(index) { onTabClicked: function (index) {
audioTab.audioSubTab = index; audioTab.audioSubTab = index
} }
}
// Output Tab - DankFlickable
DankFlickable {
width: parent.width
height: parent.height - 48
visible: audioTab.audioSubTab === 0
clip: true
contentHeight: outputColumn.height
contentWidth: width
Column {
id: outputColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: volumeComponent
}
Loader {
width: parent.width
sourceComponent: outputDevicesComponent
}
}
}
// Input Tab - DankFlickable
DankFlickable {
width: parent.width
height: parent.height - 48
visible: audioTab.audioSubTab === 1
clip: true
contentHeight: inputColumn.height
contentWidth: width
Column {
id: inputColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: microphoneComponent
}
Loader {
width: parent.width
sourceComponent: inputDevicesComponent
}
}
}
} }
// Volume Control Component // Output Tab - DankFlickable
Component { DankFlickable {
id: volumeComponent width: parent.width
VolumeControl { height: parent.height - 48
width: parent.width visible: audioTab.audioSubTab === 0
clip: true
contentHeight: outputColumn.height
contentWidth: width
Column {
id: outputColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: volumeComponent
} }
Loader {
width: parent.width
sourceComponent: outputDevicesComponent
}
}
} }
// Microphone Control Component // Input Tab - DankFlickable
Component { DankFlickable {
id: microphoneComponent width: parent.width
MicrophoneControl { height: parent.height - 48
width: parent.width visible: audioTab.audioSubTab === 1
} clip: true
} contentHeight: inputColumn.height
contentWidth: width
// Output Devices Component Column {
Component { id: inputColumn
id: outputDevicesComponent width: parent.width
AudioDevicesList { spacing: Theme.spacingL
width: parent.width
}
}
// Input Devices Component Loader {
Component { width: parent.width
id: inputDevicesComponent sourceComponent: microphoneComponent
AudioInputDevicesList {
width: parent.width
} }
Loader {
width: parent.width
sourceComponent: inputDevicesComponent
}
}
} }
}
// Volume Control Component
Component {
id: volumeComponent
VolumeControl {
width: parent.width
}
}
// Microphone Control Component
Component {
id: microphoneComponent
MicrophoneControl {
width: parent.width
}
}
// Output Devices Component
Component {
id: outputDevicesComponent
AudioDevicesList {
width: parent.width
}
}
// Input Devices Component
Component {
id: inputDevicesComponent
AudioInputDevicesList {
width: parent.width
}
}
} }

View File

@@ -9,398 +9,427 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Row {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Row { StyledText {
width: parent.width text: "Available Devices"
spacing: Theme.spacingM font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
StyledText { font.weight: Font.Medium
text: "Available Devices" anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Theme.fontSizeLarge }
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - scanButton.width - parent.spacing - 150 // Spacer to push button right
height: 1
}
Rectangle {
id: scanButton
width: Math.max(100, scanText.contentWidth + Theme.spacingL * 2)
height: 32
radius: Theme.cornerRadius
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Theme.primary
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
size: Theme.iconSize - 6
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: scanText
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Stop Scanning" : "Scan"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
}
}
}
Item {
width: parent.width - scanButton.width - parent.spacing - 150 // Spacer to push button right
height: 1
} }
Rectangle { Rectangle {
width: parent.width id: scanButton
height: noteColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2)
border.width: 1
Column { width: Math.max(100, scanText.contentWidth + Theme.spacingL * 2)
id: noteColumn height: 32
radius: Theme.cornerRadius
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b,
0.12) : Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08)
border.color: Theme.primary
border.width: 1
anchors.fill: parent Row {
anchors.margins: Theme.spacingM anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "info"
size: Theme.iconSize - 2
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Pairing Limitation"
font.pixelSize: Theme.fontSizeMedium
color: Theme.warning
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "Quickshell does not support pairing devices that require pin or confirmation."
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
wrapMode: Text.WordWrap
width: parent.width
}
}
}
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: Theme.cornerRadius
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
if (modelData.blocked)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: {
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
}
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize
color: {
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: {
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Theme.surfaceText;
}
font.weight: modelData.pairing ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingS
StyledText {
text: {
if (modelData.pairing)
return "Pairing...";
if (modelData.blocked)
return "Blocked";
return BluetoothService.getSignalStrength(modelData);
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
}
}
DankIcon {
name: BluetoothService.getSignalIcon(modelData)
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
}
StyledText {
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
}
}
}
}
}
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadiusSmall
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: {
if (!canConnect && !isBusy)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
if (actionButtonArea.containsMouse && !isBusy)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
return "transparent";
}
border.color: canConnect || isBusy ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
opacity: canConnect || isBusy ? 1 : 0.5
StyledText {
anchors.centerIn: parent
text: {
if (modelData.pairing)
return "Pairing...";
if (modelData.blocked)
return "Blocked";
return "Connect";
}
font.pixelSize: Theme.fontSizeSmall
color: canConnect || isBusy ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Medium
}
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
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: Theme.spacingM
DankIcon {
name: "sync"
size: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
}
StyledText {
text: "Scanning for devices..."
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
DankIcon {
name: BluetoothService.adapter
&& BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
size: Theme.iconSize - 6
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
} }
StyledText { StyledText {
text: "Make sure your device is in pairing mode" id: scanText
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) text: BluetoothService.adapter
anchors.horizontalCenter: parent.horizontalCenter && BluetoothService.adapter.discovering ? "Stop Scanning" : "Scan"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
}
}
Rectangle {
width: parent.width
height: noteColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.2)
border.width: 1
Column {
id: noteColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "info"
size: Theme.iconSize - 2
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
} }
StyledText {
text: "Pairing Limitation"
font.pixelSize: Theme.fontSizeMedium
color: Theme.warning
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: "Quickshell does not support pairing devices that require pin or confirmation."
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
wrapMode: Text.WordWrap
width: parent.width
}
}
}
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: Theme.cornerRadius
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.08)
if (modelData.pairing
|| modelData.state === BluetoothDeviceState.Connecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.12)
if (modelData.blocked)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
}
border.color: {
if (modelData.pairing)
return Theme.warning
if (modelData.blocked)
return Theme.error
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize
color: {
if (modelData.pairing)
return Theme.warning
if (modelData.blocked)
return Theme.error
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: {
if (modelData.pairing)
return Theme.warning
if (modelData.blocked)
return Theme.error
return Theme.surfaceText
}
font.weight: modelData.pairing ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingS
StyledText {
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (modelData.pairing)
return Theme.warning
if (modelData.blocked)
return Theme.error
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
}
DankIcon {
name: BluetoothService.getSignalIcon(modelData)
size: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: modelData.signalStrength !== undefined
&& modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
}
StyledText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
visible: modelData.signalStrength !== undefined
&& modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
}
}
}
}
}
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadiusSmall
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: {
if (!canConnect && !isBusy)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
if (actionButtonArea.containsMouse && !isBusy)
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
return "transparent"
}
border.color: canConnect || isBusy ? Theme.primary : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
opacity: canConnect || isBusy ? 1 : 0.5
StyledText {
anchors.centerIn: parent
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return "Connect"
}
font.pixelSize: Theme.fontSizeSmall
color: canConnect || isBusy ? Theme.primary : Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
font.weight: Font.Medium
}
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90
hoverEnabled: true
cursorShape: canConnect
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
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: Theme.spacingM
DankIcon {
name: "sync"
size: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
}
StyledText {
text: "Scanning for devices..."
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
} }
StyledText { StyledText {
text: "No devices found. Put your device in pairing mode and click Start Scanning." text: "Make sure your device is in pairing mode"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
visible: { Theme.surfaceText.b, 0.7)
if (!BluetoothService.adapter || !Bluetooth.devices) anchors.horizontalCenter: parent.horizontalCenter
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
} }
}
StyledText {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
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
}
} }

View File

@@ -9,203 +9,199 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property var deviceData: null property var deviceData: null
property bool menuVisible: false property bool menuVisible: false
property var parentItem property var parentItem
function show(x, y) { function show(x, y) {
const menuWidth = 160; const menuWidth = 160
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2; const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x - menuWidth / 2; let finalX = x - menuWidth / 2
let finalY = y; let finalY = y
finalX = Math.max(0, Math.min(finalX, parentItem.width - menuWidth)); finalX = Math.max(0, Math.min(finalX, parentItem.width - menuWidth))
finalY = Math.max(0, Math.min(finalY, parentItem.height - menuHeight)); finalY = Math.max(0, Math.min(finalY, parentItem.height - menuHeight))
root.x = finalX; root.x = finalX
root.y = finalY; root.y = finalY
root.visible = true; root.visible = true
root.menuVisible = true; root.menuVisible = true
} }
function hide() { function hide() {
root.menuVisible = false; root.menuVisible = false
Qt.callLater(() => { Qt.callLater(() => {
root.visible = false; root.visible = false
}); })
} }
visible: false visible: false
width: 160 width: 160
height: menuColumn.implicitHeight + Theme.spacingS * 2 height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Theme.popupBackground() color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: 1
z: 1000 z: 1000
opacity: menuVisible ? 1 : 0 opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85 scale: menuVisible ? 1 : 0.85
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle { Rectangle {
anchors.fill: parent width: parent.width
anchors.topMargin: 4 height: 32
anchors.leftMargin: 2 radius: Theme.cornerRadiusSmall
anchors.rightMargin: -2 color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r,
anchors.bottomMargin: -4 Theme.primary.g,
radius: parent.radius Theme.primary.b,
color: Qt.rgba(0, 0, 0, 0.15) 0.12) : "transparent"
z: parent.z - 1
}
Column { Row {
id: menuColumn anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: root.deviceData
&& root.deviceData.connected ? "link_off" : "link"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.deviceData
&& root.deviceData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectArea
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS hoverEnabled: true
spacing: 1 cursorShape: Qt.PointingHandCursor
onClicked: {
Rectangle { if (root.deviceData) {
width: parent.width if (root.deviceData.connected)
height: 32 root.deviceData.disconnect()
radius: Theme.cornerRadiusSmall else
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" BluetoothService.connectDeviceWithTrust(root.deviceData)
}
Row { root.hide()
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: root.deviceData && root.deviceData.connected ? "link_off" : "link"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.deviceData && root.deviceData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.deviceData) {
if (root.deviceData.connected)
root.deviceData.disconnect();
else
BluetoothService.connectDeviceWithTrust(root.deviceData);
}
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
}
Rectangle { Behavior on color {
width: parent.width - Theme.spacingS * 2 ColorAnimation {
height: 5 duration: Theme.shortDuration
anchors.horizontalCenter: parent.horizontalCenter easing.type: Theme.standardEasing
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
} }
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "delete"
size: Theme.iconSize - 2
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Forget Device"
font.pixelSize: Theme.fontSizeSmall
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.deviceData)
root.deviceData.forget();
root.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
} }
Behavior on opacity { Rectangle {
NumberAnimation { width: parent.width - Theme.spacingS * 2
duration: Theme.mediumDuration height: 5
easing.type: Theme.emphasizedEasing anchors.horizontalCenter: parent.horizontalCenter
} color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
} }
Behavior on scale { Rectangle {
NumberAnimation { width: parent.width
duration: Theme.mediumDuration height: 32
easing.type: Theme.emphasizedEasing radius: Theme.cornerRadiusSmall
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b,
0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "delete"
size: Theme.iconSize - 2
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
} }
} StyledText {
text: "Forget Device"
font.pixelSize: Theme.fontSizeSmall
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.deviceData)
root.deviceData.forget()
root.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
} }

View File

@@ -9,60 +9,64 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
width: parent.width width: parent.width
height: 60 height: 60
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (BluetoothService.adapter && BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12)) color: bluetoothToggle.containsMouse ? Qt.rgba(
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : "transparent" Theme.primary.r, Theme.primary.g,
border.width: 2 Theme.primary.b, 0.12) : (BluetoothService.adapter
&& BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
border.color: BluetoothService.adapter
&& BluetoothService.adapter.enabled ? Theme.primary : "transparent"
border.width: 2
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingL anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon {
name: "bluetooth"
size: Theme.iconSizeLarge
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Bluetooth"
font.pixelSize: Theme.fontSizeLarge
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: BluetoothService.adapter && BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
DankIcon {
name: "bluetooth"
size: Theme.iconSizeLarge
color: BluetoothService.adapter
&& BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
MouseArea { Column {
id: bluetoothToggle spacing: 2
anchors.verticalCenter: parent.verticalCenter
anchors.fill: parent StyledText {
hoverEnabled: true text: "Bluetooth"
cursorShape: Qt.PointingHandCursor font.pixelSize: Theme.fontSizeLarge
onClicked: { color: BluetoothService.adapter
if (BluetoothService.adapter) && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled; font.weight: Font.Medium
}
} StyledText {
text: BluetoothService.adapter
&& BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
} }
}
MouseArea {
id: bluetoothToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
}
}
} }

View File

@@ -9,158 +9,173 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
function findBluetoothContextMenu() { function findBluetoothContextMenu() {
var p = parent; var p = parent
while (p) { while (p) {
if (p.bluetoothContextMenuWindow) if (p.bluetoothContextMenuWindow)
return p.bluetoothContextMenuWindow; return p.bluetoothContextMenuWindow
p = p.parent; p = p.parent
}
return null;
} }
return null
}
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled visible: BluetoothService.adapter && BluetoothService.adapter.enabled
StyledText { StyledText {
text: "Paired Devices" text: "Paired Devices"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
} }
Repeater { Repeater {
model: BluetoothService.adapter && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter((dev) => { model: BluetoothService.adapter
return dev && (dev.paired || dev.trusted); && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter(
}) : [] dev => {
return dev
&& (dev.paired
|| dev.trusted)
}) : []
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 60 height: 60
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) color: btDeviceArea.containsMouse ? Qt.rgba(
border.color: modelData.connected ? Theme.primary : "transparent" Theme.primary.r, Theme.primary.g,
border.width: 1 Theme.primary.b,
0.08) : (modelData.connected ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : Qt.rgba(
Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b,
0.08))
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: 1
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize
color: modelData.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
StyledText {
text: BluetoothDeviceState.toString(modelData.state)
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
StyledText {
text: {
if (modelData.batteryAvailable && modelData.battery > 0)
return "• " + Math.round(modelData.battery * 100) + "%";
var btBattery = BatteryService.bluetoothDevices.find((dev) => {
return dev.name === (modelData.name || modelData.deviceName) || dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || (modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase());
});
return btBattery ? "• " + btBattery.percentage + "%" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Rectangle {
id: btMenuButton
width: 32
height: 32
radius: Theme.cornerRadius
color: btMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "more_vert"
size: Theme.iconSize
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: btMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
var contextMenu = root.findBluetoothContextMenu();
if (contextMenu) {
contextMenu.deviceData = modelData;
let localPos = btMenuButtonArea.mapToItem(contextMenu.parentItem, btMenuButtonArea.width / 2, btMenuButtonArea.height);
contextMenu.show(localPos.x, localPos.y);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
MouseArea {
id: btDeviceArea
anchors.fill: parent
anchors.rightMargin: 40
hoverEnabled: true
enabled: !BluetoothService.isDeviceBusy(modelData)
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor
onClicked: {
if (modelData.connected)
modelData.disconnect();
else
BluetoothService.connectDeviceWithTrust(modelData);
}
}
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize
color: modelData.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
} Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
StyledText {
text: BluetoothDeviceState.toString(modelData.state)
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
StyledText {
text: {
if (modelData.batteryAvailable && modelData.battery > 0)
return "• " + Math.round(modelData.battery * 100) + "%"
var btBattery = BatteryService.bluetoothDevices.find(dev => {
return dev.name === (modelData.name || modelData.deviceName) || dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || (modelData.name || modelData.deviceName).toLowerCase(
).includes(
dev.name.toLowerCase(
))
})
return btBattery ? "• " + btBattery.percentage + "%" : ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Rectangle {
id: btMenuButton
width: 32
height: 32
radius: Theme.cornerRadius
color: btMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "more_vert"
size: Theme.iconSize
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: btMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
var contextMenu = root.findBluetoothContextMenu()
if (contextMenu) {
contextMenu.deviceData = modelData
let localPos = btMenuButtonArea.mapToItem(
contextMenu.parentItem, btMenuButtonArea.width / 2,
btMenuButtonArea.height)
contextMenu.show(localPos.x, localPos.y)
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
MouseArea {
id: btDeviceArea
anchors.fill: parent
anchors.rightMargin: 40
hoverEnabled: true
enabled: !BluetoothService.isDeviceBusy(modelData)
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor
onClicked: {
if (modelData.connected)
modelData.disconnect()
else
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
} }

View File

@@ -10,78 +10,79 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: bluetoothTab id: bluetoothTab
property alias bluetoothContextMenuWindow: bluetoothContextMenuWindow property alias bluetoothContextMenuWindow: bluetoothContextMenuWindow
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
contentHeight: mainColumn.height contentHeight: mainColumn.height
contentWidth: width contentWidth: width
Column { Column {
id: mainColumn id: mainColumn
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
Loader { Loader {
width: parent.width width: parent.width
sourceComponent: toggleComponent sourceComponent: toggleComponent
} }
Loader { Loader {
width: parent.width width: parent.width
sourceComponent: pairedComponent sourceComponent: pairedComponent
} }
Loader { Loader {
width: parent.width width: parent.width
sourceComponent: availableComponent sourceComponent: availableComponent
} }
}
} }
}
BluetoothContextMenu { BluetoothContextMenu {
id: bluetoothContextMenuWindow id: bluetoothContextMenuWindow
parentItem: bluetoothTab parentItem: bluetoothTab
}
MouseArea {
anchors.fill: parent
visible: bluetoothContextMenuWindow.visible
onClicked: {
bluetoothContextMenuWindow.hide()
} }
MouseArea { MouseArea {
anchors.fill: parent x: bluetoothContextMenuWindow.x
visible: bluetoothContextMenuWindow.visible y: bluetoothContextMenuWindow.y
onClicked: { width: bluetoothContextMenuWindow.width
bluetoothContextMenuWindow.hide(); height: bluetoothContextMenuWindow.height
} onClicked: {
MouseArea { }
x: bluetoothContextMenuWindow.x
y: bluetoothContextMenuWindow.y
width: bluetoothContextMenuWindow.width
height: bluetoothContextMenuWindow.height
onClicked: {
}
}
} }
}
Component { Component {
id: toggleComponent id: toggleComponent
BluetoothToggle { BluetoothToggle {
width: parent.width width: parent.width
}
} }
}
Component { Component {
id: pairedComponent id: pairedComponent
PairedDevicesList { PairedDevicesList {
width: parent.width width: parent.width
}
} }
}
Component { Component {
id: availableComponent id: availableComponent
AvailableDevicesList { AvailableDevicesList {
width: parent.width width: parent.width
}
} }
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,213 +9,221 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: displayTab id: displayTab
property var brightnessDebounceTimer property var brightnessDebounceTimer
brightnessDebounceTimer: Timer { brightnessDebounceTimer: Timer {
property int pendingValue: 0 property int pendingValue: 0
interval: BrightnessService.ddcAvailable ? 500 : 50 interval: BrightnessService.ddcAvailable ? 500 : 50
repeat: false repeat: false
onTriggered: { onTriggered: {
BrightnessService.setBrightnessInternal(pendingValue); BrightnessService.setBrightnessInternal(pendingValue)
}
} }
}
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
contentHeight: mainColumn.height contentHeight: mainColumn.height
contentWidth: width contentWidth: width
Column { Column {
id: mainColumn id: mainColumn
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
Loader { Loader {
width: parent.width width: parent.width
sourceComponent: brightnessComponent sourceComponent: brightnessComponent
} }
Loader { Loader {
width: parent.width width: parent.width
sourceComponent: settingsComponent sourceComponent: settingsComponent
} }
}
} }
}
Process { Process {
id: nightModeEnableProcess id: nightModeEnableProcess
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"] command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
running: false running: false
onExited: (exitCode) => { onExited: exitCode => {
if (exitCode !== 0) { if (exitCode !== 0) {
SettingsData.setNightModeEnabled(false); SettingsData.setNightModeEnabled(false)
}
}
}
Process {
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
}
}
}
Component {
id: brightnessComponent
Column {
width: parent.width
spacing: Theme.spacingM
visible: BrightnessService.brightnessAvailable
StyledText {
text: "Brightness"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
value: BrightnessService.brightnessLevel
leftIcon: "brightness_low"
rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable
onSliderValueChanged: function(newValue) {
brightnessDebounceTimer.pendingValue = newValue;
brightnessDebounceTimer.restart();
} }
onSliderDragFinished: function(finalValue) { }
brightnessDebounceTimer.stop(); }
BrightnessService.setBrightnessInternal(finalValue);
Process {
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: exitCode => {
if (exitCode !== 0) {
} }
}
}
Component {
id: brightnessComponent
Column {
width: parent.width
spacing: Theme.spacingM
visible: BrightnessService.brightnessAvailable
StyledText {
text: "Brightness"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
value: BrightnessService.brightnessLevel
leftIcon: "brightness_low"
rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable
onSliderValueChanged: function (newValue) {
brightnessDebounceTimer.pendingValue = newValue
brightnessDebounceTimer.restart()
}
onSliderDragFinished: function (finalValue) {
brightnessDebounceTimer.stop()
BrightnessService.setBrightnessInternal(finalValue)
}
}
StyledText {
text: "using ddc - changes may take a moment to apply"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: BrightnessService.ddcAvailable
&& !BrightnessService.laptopBacklightAvailable
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Component {
id: settingsComponent
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: "Display Settings"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: SettingsData.nightModeEnabled ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: SettingsData.nightModeEnabled ? Theme.primary : "transparent"
border.width: SettingsData.nightModeEnabled ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: SettingsData.nightModeEnabled ? "nightlight" : "dark_mode"
size: Theme.iconSizeLarge
color: SettingsData.nightModeEnabled ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
} }
StyledText { StyledText {
text: "using ddc - changes may take a moment to apply" text: "Night Mode"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: SettingsData.nightModeEnabled ? Theme.primary : Theme.surfaceText
visible: BrightnessService.ddcAvailable && !BrightnessService.laptopBacklightAvailable font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
}
Component { MouseArea {
id: settingsComponent id: nightModeToggle
Column { anchors.fill: parent
width: parent.width hoverEnabled: true
spacing: Theme.spacingM cursorShape: Qt.PointingHandCursor
onClicked: {
if (SettingsData.nightModeEnabled) {
nightModeDisableProcess.running = true
SettingsData.setNightModeEnabled(false)
} else {
nightModeEnableProcess.running = true
SettingsData.setNightModeEnabled(true)
}
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Theme.isLightMode ? Qt.rgba(
Theme.primary.r, Theme.primary.g,
Theme.primary.b,
0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: Theme.isLightMode ? Theme.primary : "transparent"
border.width: Theme.isLightMode ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: Theme.isLightMode ? "light_mode" : "palette"
size: Theme.iconSizeLarge
color: Theme.isLightMode ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText { StyledText {
text: "Display Settings" text: Theme.isLightMode ? "Light Mode" : "Dark Mode"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.isLightMode ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
} }
}
Row { MouseArea {
width: parent.width id: lightModeToggle
spacing: Theme.spacingM anchors.fill: parent
hoverEnabled: true
Rectangle { cursorShape: Qt.PointingHandCursor
width: (parent.width - Theme.spacingM) / 2 onClicked: {
height: 80 Theme.toggleLightMode()
radius: Theme.cornerRadius
color: SettingsData.nightModeEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: SettingsData.nightModeEnabled ? Theme.primary : "transparent"
border.width: SettingsData.nightModeEnabled ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: SettingsData.nightModeEnabled ? "nightlight" : "dark_mode"
size: Theme.iconSizeLarge
color: SettingsData.nightModeEnabled ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: "Night Mode"
font.pixelSize: Theme.fontSizeMedium
color: SettingsData.nightModeEnabled ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: nightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (SettingsData.nightModeEnabled) {
nightModeDisableProcess.running = true;
SettingsData.setNightModeEnabled(false);
} else {
nightModeEnableProcess.running = true;
SettingsData.setNightModeEnabled(true);
}
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Theme.isLightMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: Theme.isLightMode ? Theme.primary : "transparent"
border.width: Theme.isLightMode ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: Theme.isLightMode ? "light_mode" : "palette"
size: Theme.iconSizeLarge
color: Theme.isLightMode ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: Theme.isLightMode ? "Light Mode" : "Dark Mode"
font.pixelSize: Theme.fontSizeMedium
color: Theme.isLightMode ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: lightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.toggleLightMode();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
} }
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
}
} }
}
} }

View File

@@ -8,121 +8,131 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: ethernetCard id: ethernetCard
width: parent.width width: parent.width
height: 80 height: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (ethernetPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet") if (ethernetPreferenceArea.containsMouse && NetworkService.ethernetConnected
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8); && NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "ethernet")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.8)
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5); return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
} Theme.surfaceContainer.b, 0.5)
border.color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) }
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1 border.color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b,
0.12)
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1
Column { Column {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.right: ethernetToggle.left
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Row {
spacing: Theme.spacingM
DankIcon {
name: "lan"
size: Theme.iconSize
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: ethernetToggle.left }
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Row { StyledText {
spacing: Theme.spacingM text: "Ethernet"
font.pixelSize: Theme.fontSizeMedium
DankIcon { color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
name: "lan" font.weight: Font.Medium
size: Theme.iconSize
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Ethernet"
font.pixelSize: Theme.fontSizeMedium
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
}
}
StyledText {
text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP || "Connected") : "Disconnected"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
leftPadding: Theme.iconSize + Theme.spacingM
elide: Text.ElideRight
}
}
DankIcon {
id: ethernetLoadingSpinner
name: "refresh"
size: Theme.iconSize - 4
color: Theme.primary
anchors.right: ethernetToggle.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference && NetworkService.targetPreference === "ethernet" elide: Text.ElideRight
z: 10 }
RotationAnimation {
target: ethernetLoadingSpinner
property: "rotation"
running: ethernetLoadingSpinner.visible
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
} }
DankToggle { StyledText {
id: ethernetToggle text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP
|| "Connected") : "Disconnected"
checked: NetworkService.ethernetConnected font.pixelSize: Theme.fontSizeSmall
enabled: true color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
anchors.right: parent.right Theme.surfaceText.b, 0.7)
anchors.rightMargin: Theme.spacingM leftPadding: Theme.iconSize + Theme.spacingM
anchors.verticalCenter: parent.verticalCenter elide: Text.ElideRight
onClicked: {
NetworkService.toggleNetworkConnection("ethernet");
}
} }
}
MouseArea { DankIcon {
id: ethernetPreferenceArea id: ethernetLoadingSpinner
anchors.fill: parent name: "refresh"
anchors.rightMargin: 60 // Exclude toggle area size: Theme.iconSize - 4
hoverEnabled: true color: Theme.primary
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet") ? Qt.PointingHandCursor : Qt.ArrowCursor anchors.right: ethernetToggle.left
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "ethernet" && !NetworkService.changingNetworkPreference anchors.rightMargin: Theme.spacingS
onClicked: { anchors.verticalCenter: parent.verticalCenter
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) { visible: NetworkService.changingPreference
&& NetworkService.targetPreference === "ethernet"
z: 10
if (NetworkService.networkStatus !== "ethernet") RotationAnimation {
NetworkService.setNetworkPreference("ethernet"); target: ethernetLoadingSpinner
else property: "rotation"
NetworkService.setNetworkPreference("auto"); running: ethernetLoadingSpinner.visible
} from: 0
} to: 360
duration: 1000
loops: Animation.Infinite
} }
}
Behavior on color { DankToggle {
ColorAnimation { id: ethernetToggle
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
checked: NetworkService.ethernetConnected
enabled: true
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
onClicked: {
NetworkService.toggleNetworkConnection("ethernet")
} }
}
MouseArea {
id: ethernetPreferenceArea
anchors.fill: parent
anchors.rightMargin: 60 // Exclude toggle area
hoverEnabled: true
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled
&& NetworkService.networkStatus
!== "ethernet") ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "ethernet"
&& !NetworkService.changingNetworkPreference
onClicked: {
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) {
if (NetworkService.networkStatus !== "ethernet")
NetworkService.setNetworkPreference("ethernet")
else
NetworkService.setNetworkPreference("auto")
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }

View File

@@ -8,174 +8,182 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: wifiCard id: wifiCard
property var refreshTimer property var refreshTimer
function getWiFiSignalIcon(signalStrength) { function getWiFiSignalIcon(signalStrength) {
switch (signalStrength) { switch (signalStrength) {
case "excellent": case "excellent":
return "wifi"; return "wifi"
case "good": case "good":
return "wifi_2_bar"; return "wifi_2_bar"
case "fair": case "fair":
return "wifi_1_bar"; return "wifi_1_bar"
case "poor": case "poor":
return "signal_wifi_0_bar"; return "signal_wifi_0_bar"
default: default:
return "wifi"; return "wifi"
}
}
width: parent.width
height: 80
radius: Theme.cornerRadius
color: {
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected
&& NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "wifi")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.8)
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5)
}
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b,
0.12)
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
visible: NetworkService.wifiAvailable
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
anchors.right: wifiToggle.left
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Row {
spacing: Theme.spacingM
DankIcon {
name: {
if (!NetworkService.wifiEnabled)
return "wifi_off"
else if (NetworkService.currentWifiSSID !== "")
return getWiFiSignalIcon(NetworkService.wifiSignalStrength)
else
return "wifi"
} }
} size: Theme.iconSize
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
width: parent.width
height: 80
radius: Theme.cornerRadius
color: {
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8);
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5);
}
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
visible: NetworkService.wifiAvailable
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: wifiToggle.left }
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingS
Row {
spacing: Theme.spacingM
DankIcon {
name: {
if (!NetworkService.wifiEnabled)
return "wifi_off";
else if (NetworkService.currentWifiSSID !== "")
return getWiFiSignalIcon(NetworkService.wifiSignalStrength);
else
return "wifi";
}
size: Theme.iconSize
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!NetworkService.wifiEnabled)
return "WiFi is off";
else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID || "Connected";
else
return "Not Connected";
}
font.pixelSize: Theme.fontSizeMedium
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
}
StyledText {
text: {
if (!NetworkService.wifiEnabled)
return "WiFi is off"
else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID)
return NetworkService.currentWifiSSID || "Connected"
else
return "Not Connected"
} }
font.pixelSize: Theme.fontSizeMedium
StyledText { color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
text: { font.weight: Font.Medium
if (!NetworkService.wifiEnabled)
return "Turn on WiFi to see networks";
else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID)
return NetworkService.wifiIP || "Connected";
else
return "Select a network below";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
leftPadding: Theme.iconSize + Theme.spacingM
elide: Text.ElideRight
}
}
DankIcon {
id: wifiLoadingSpinner
name: "refresh"
size: Theme.iconSize - 4
color: Theme.primary
anchors.right: wifiToggle.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference && NetworkService.targetPreference === "wifi" elide: Text.ElideRight
z: 10 }
RotationAnimation {
target: wifiLoadingSpinner
property: "rotation"
running: wifiLoadingSpinner.visible
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
} }
DankToggle { StyledText {
id: wifiToggle text: {
if (!NetworkService.wifiEnabled)
checked: NetworkService.wifiEnabled return "Turn on WiFi to see networks"
enabled: true else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID)
toggling: NetworkService.wifiToggling return NetworkService.wifiIP || "Connected"
anchors.right: parent.right else
anchors.rightMargin: Theme.spacingM return "Select a network below"
anchors.verticalCenter: parent.verticalCenter }
onClicked: { font.pixelSize: Theme.fontSizeSmall
if (NetworkService.wifiEnabled) { color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
NetworkService.currentWifiSSID = ""; Theme.surfaceText.b, 0.7)
NetworkService.wifiSignalStrength = "excellent"; leftPadding: Theme.iconSize + Theme.spacingM
NetworkService.wifiNetworks = []; elide: Text.ElideRight
NetworkService.savedWifiNetworks = [];
NetworkService.connectionStatus = "";
NetworkService.connectingSSID = "";
NetworkService.isScanning = false;
NetworkService.refreshNetworkStatus();
}
NetworkService.toggleWifiRadio();
if (refreshTimer)
refreshTimer.triggered = true;
}
} }
}
MouseArea { DankIcon {
id: wifiPreferenceArea id: wifiLoadingSpinner
anchors.fill: parent name: "refresh"
anchors.rightMargin: 60 // Exclude toggle area size: Theme.iconSize - 4
hoverEnabled: true color: Theme.primary
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi") ? Qt.PointingHandCursor : Qt.ArrowCursor anchors.right: wifiToggle.left
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled && NetworkService.networkStatus !== "wifi" && !NetworkService.changingNetworkPreference anchors.rightMargin: Theme.spacingS
onClicked: { anchors.verticalCenter: parent.verticalCenter
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) { visible: NetworkService.changingPreference
&& NetworkService.targetPreference === "wifi"
z: 10
if (NetworkService.networkStatus !== "wifi") RotationAnimation {
NetworkService.setNetworkPreference("wifi"); target: wifiLoadingSpinner
else property: "rotation"
NetworkService.setNetworkPreference("auto"); running: wifiLoadingSpinner.visible
} from: 0
} to: 360
duration: 1000
loops: Animation.Infinite
} }
}
Behavior on color { DankToggle {
ColorAnimation { id: wifiToggle
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
checked: NetworkService.wifiEnabled
enabled: true
toggling: NetworkService.wifiToggling
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
onClicked: {
if (NetworkService.wifiEnabled) {
NetworkService.currentWifiSSID = ""
NetworkService.wifiSignalStrength = "excellent"
NetworkService.wifiNetworks = []
NetworkService.savedWifiNetworks = []
NetworkService.connectionStatus = ""
NetworkService.connectingSSID = ""
NetworkService.isScanning = false
NetworkService.refreshNetworkStatus()
}
NetworkService.toggleWifiRadio()
if (refreshTimer)
refreshTimer.triggered = true
} }
}
MouseArea {
id: wifiPreferenceArea
anchors.fill: parent
anchors.rightMargin: 60 // Exclude toggle area
hoverEnabled: true
cursorShape: (NetworkService.ethernetConnected && NetworkService.wifiEnabled
&& NetworkService.networkStatus
!== "wifi") ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "wifi"
&& !NetworkService.changingNetworkPreference
onClicked: {
if (NetworkService.ethernetConnected && NetworkService.wifiEnabled) {
if (NetworkService.networkStatus !== "wifi")
NetworkService.setNetworkPreference("wifi")
else
NetworkService.setNetworkPreference("auto")
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }

View File

@@ -8,281 +8,291 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: wifiContextMenuWindow id: wifiContextMenuWindow
property var networkData: null property var networkData: null
property bool menuVisible: false property bool menuVisible: false
property var parentItem property var parentItem
property var wifiPasswordModalRef property var wifiPasswordModalRef
property var networkInfoModalRef property var networkInfoModalRef
function show(x, y) { function show(x, y) {
const menuWidth = 160; const menuWidth = 160
wifiContextMenuWindow.visible = true; wifiContextMenuWindow.visible = true
Qt.callLater(() => { Qt.callLater(() => {
const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2; const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x - menuWidth / 2; let finalX = x - menuWidth / 2
let finalY = y + 4; let finalY = y + 4
finalX = Math.max(Theme.spacingS, Math.min(finalX, parentItem.width - menuWidth - Theme.spacingS)); finalX = Math.max(
finalY = Math.max(Theme.spacingS, Math.min(finalY, parentItem.height - menuHeight - Theme.spacingS)); Theme.spacingS,
if (finalY + menuHeight > parentItem.height - Theme.spacingS) { Math.min(finalX,
finalY = y - menuHeight - 4; parentItem.width - menuWidth - Theme.spacingS))
finalY = Math.max(Theme.spacingS, finalY); finalY = Math.max(
Theme.spacingS,
Math.min(finalY,
parentItem.height - menuHeight - Theme.spacingS))
if (finalY + menuHeight > parentItem.height - Theme.spacingS) {
finalY = y - menuHeight - 4
finalY = Math.max(Theme.spacingS, finalY)
}
wifiContextMenuWindow.x = finalX
wifiContextMenuWindow.y = finalY
wifiContextMenuWindow.menuVisible = true
})
}
function hide() {
wifiContextMenuWindow.menuVisible = false
Qt.callLater(() => {
wifiContextMenuWindow.visible = false
})
}
visible: false
width: 160
height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Component.onCompleted: {
menuVisible = false
visible = false
}
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: wifiMenuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: wifiContextMenuWindow.networkData
&& wifiContextMenuWindow.networkData.connected ? "wifi_off" : "wifi"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: wifiContextMenuWindow.networkData
&& wifiContextMenuWindow.networkData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData) {
if (wifiContextMenuWindow.networkData.connected) {
NetworkService.disconnectWifi()
} else {
if (wifiContextMenuWindow.networkData.saved) {
NetworkService.connectToWifi(
wifiContextMenuWindow.networkData.ssid)
} else if (wifiContextMenuWindow.networkData.secured) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.wifiPasswordSSID = wifiContextMenuWindow.networkData.ssid
wifiPasswordModalRef.wifiPasswordInput = ""
wifiPasswordModalRef.wifiPasswordModalVisible = true
}
} else {
NetworkService.connectToWifi(
wifiContextMenuWindow.networkData.ssid)
}
} }
wifiContextMenuWindow.x = finalX; }
wifiContextMenuWindow.y = finalY; wifiContextMenuWindow.hide()
wifiContextMenuWindow.menuVisible = true; }
}); }
}
function hide() { Behavior on color {
wifiContextMenuWindow.menuVisible = false; ColorAnimation {
Qt.callLater(() => { duration: Theme.shortDuration
wifiContextMenuWindow.visible = false; easing.type: Theme.standardEasing
}); }
} }
visible: false
width: 160
height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Component.onCompleted: {
menuVisible = false;
visible = false;
} }
Rectangle { Rectangle {
anchors.fill: parent width: parent.width - Theme.spacingS * 2
anchors.topMargin: 4 height: 5
anchors.leftMargin: 2 anchors.horizontalCenter: parent.horizontalCenter
anchors.rightMargin: -2 color: "transparent"
anchors.bottomMargin: -4
radius: parent.radius Rectangle {
color: Qt.rgba(0, 0, 0, 0.15) anchors.centerIn: parent
z: parent.z - 1 width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
} }
Column { Rectangle {
id: wifiMenuColumn width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: forgetWifiArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : "transparent"
visible: wifiContextMenuWindow.networkData
&& (wifiContextMenuWindow.networkData.saved
|| wifiContextMenuWindow.networkData.connected)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "delete"
size: Theme.iconSize - 2
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Forget Network"
font.pixelSize: Theme.fontSizeSmall
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetWifiArea
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS hoverEnabled: true
spacing: 1 cursorShape: Qt.PointingHandCursor
onClicked: {
Rectangle { if (wifiContextMenuWindow.networkData)
width: parent.width NetworkService.forgetWifiNetwork(
height: 32 wifiContextMenuWindow.networkData.ssid)
radius: Theme.cornerRadiusSmall
color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "wifi_off" : "wifi"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: wifiContextMenuWindow.networkData && wifiContextMenuWindow.networkData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData) {
if (wifiContextMenuWindow.networkData.connected) {
NetworkService.disconnectWifi();
} else {
if (wifiContextMenuWindow.networkData.saved) {
NetworkService.connectToWifi(wifiContextMenuWindow.networkData.ssid);
} else if (wifiContextMenuWindow.networkData.secured) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.wifiPasswordSSID = wifiContextMenuWindow.networkData.ssid;
wifiPasswordModalRef.wifiPasswordInput = "";
wifiPasswordModalRef.wifiPasswordModalVisible = true;
}
} else {
NetworkService.connectToWifi(wifiContextMenuWindow.networkData.ssid);
}
}
}
wifiContextMenuWindow.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
wifiContextMenuWindow.hide()
} }
}
Rectangle { Behavior on color {
width: parent.width - Theme.spacingS * 2 ColorAnimation {
height: 5 duration: Theme.shortDuration
anchors.horizontalCenter: parent.horizontalCenter easing.type: Theme.standardEasing
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
} }
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: forgetWifiArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
visible: wifiContextMenuWindow.networkData && (wifiContextMenuWindow.networkData.saved || wifiContextMenuWindow.networkData.connected)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "delete"
size: Theme.iconSize - 2
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Forget Network"
font.pixelSize: Theme.fontSizeSmall
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData)
NetworkService.forgetWifiNetwork(wifiContextMenuWindow.networkData.ssid);
wifiContextMenuWindow.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: infoWifiArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "info"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Network Info"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: infoWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData && networkInfoModalRef)
networkInfoModalRef.showNetworkInfo(wifiContextMenuWindow.networkData.ssid, wifiContextMenuWindow.networkData);
wifiContextMenuWindow.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
} }
Behavior on opacity { Rectangle {
NumberAnimation { width: parent.width
duration: Theme.mediumDuration height: 32
easing.type: Theme.emphasizedEasing radius: Theme.cornerRadiusSmall
color: infoWifiArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "info"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
} }
} StyledText {
text: "Network Info"
Behavior on scale { font.pixelSize: Theme.fontSizeSmall
NumberAnimation { color: Theme.surfaceText
duration: Theme.mediumDuration font.weight: Font.Normal
easing.type: Theme.emphasizedEasing anchors.verticalCenter: parent.verticalCenter
} }
}
MouseArea {
id: infoWifiArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (wifiContextMenuWindow.networkData && networkInfoModalRef)
networkInfoModalRef.showNetworkInfo(
wifiContextMenuWindow.networkData.ssid,
wifiContextMenuWindow.networkData)
wifiContextMenuWindow.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
} }

View File

@@ -8,305 +8,316 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property var wifiContextMenuWindow property var wifiContextMenuWindow
property var sortedWifiNetworks property var sortedWifiNetworks
property var wifiPasswordModalRef property var wifiPasswordModalRef
function getWiFiSignalIcon(signalStrength) { function getWiFiSignalIcon(signalStrength) {
switch (signalStrength) { switch (signalStrength) {
case "excellent": case "excellent":
return "wifi"; return "wifi"
case "good": case "good":
return "wifi_2_bar"; return "wifi_2_bar"
case "fair": case "fair":
return "wifi_1_bar"; return "wifi_1_bar"
case "poor": case "poor":
return "signal_wifi_0_bar"; return "signal_wifi_0_bar"
default: default:
return "wifi"; return "wifi"
}
} }
}
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 100 anchors.topMargin: 100
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
visible: NetworkService.wifiEnabled visible: NetworkService.wifiEnabled
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS spacing: Theme.spacingS
Row { StyledText {
width: parent.width text: "Available Networks"
spacing: Theme.spacingS font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
StyledText { Item {
text: "Available Networks" width: parent.width - 170
font.pixelSize: Theme.fontSizeMedium height: 1
color: Theme.surfaceText }
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter Rectangle {
width: 28
height: 28
radius: 14
color: refreshAreaSpan.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : NetworkService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
DankIcon {
id: refreshIconSpan
anchors.centerIn: parent
name: "refresh"
size: Theme.iconSize - 6
color: refreshAreaSpan.containsMouse ? Theme.primary : Theme.surfaceText
rotation: NetworkService.isScanning ? refreshIconSpan.rotation : 0
RotationAnimation {
target: refreshIconSpan
property: "rotation"
running: NetworkService.isScanning
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
} }
Item { Behavior on rotation {
width: parent.width - 170 RotationAnimation {
height: 1 duration: 200
easing.type: Easing.OutQuad
}
} }
}
MouseArea {
id: refreshAreaSpan
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!NetworkService.isScanning) {
refreshIconSpan.rotation += 30
NetworkService.scanWifi()
}
}
}
}
}
Flickable {
width: parent.width
height: parent.height - 40
clip: true
contentWidth: width
contentHeight: spanningNetworksColumn.height
boundsBehavior: Flickable.DragAndOvershootBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500
maximumFlickVelocity: 2000
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
let delta = event.pixelDelta.y
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(0,
Math.min(parent.contentHeight - parent.height,
newY))
parent.contentY = newY
event.accepted = true
}
}
Column {
id: spanningNetworksColumn
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: NetworkService.wifiAvailable
&& NetworkService.wifiEnabled ? sortedWifiNetworks : []
Rectangle { Rectangle {
width: 28 width: spanningNetworksColumn.width
height: 28 height: 38
radius: 14 radius: Theme.cornerRadiusSmall
color: refreshAreaSpan.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : NetworkService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent" color: networkArea2.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: modelData.connected ? 1 : 0
Item {
anchors.fill: parent
anchors.margins: Theme.spacingXS
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
DankIcon { DankIcon {
id: refreshIconSpan id: signalIcon2
anchors.centerIn: parent
name: "refresh"
size: Theme.iconSize - 6
color: refreshAreaSpan.containsMouse ? Theme.primary : Theme.surfaceText
rotation: NetworkService.isScanning ? refreshIconSpan.rotation : 0
RotationAnimation {
target: refreshIconSpan
property: "rotation"
running: NetworkService.isScanning
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
Behavior on rotation {
RotationAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
name: getWiFiSignalIcon(modelData.signalStrength)
size: Theme.iconSize - 2
color: modelData.connected ? Theme.primary : Theme.surfaceText
} }
MouseArea { Column {
id: refreshAreaSpan anchors.left: signalIcon2.right
anchors.leftMargin: Theme.spacingXS
anchors.right: rightIcons2.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
spacing: 2
anchors.fill: parent StyledText {
hoverEnabled: true width: parent.width
cursorShape: Qt.PointingHandCursor text: modelData.ssid
onClicked: { font.pixelSize: Theme.fontSizeSmall
if (!NetworkService.isScanning) { color: modelData.connected ? Theme.primary : Theme.surfaceText
refreshIconSpan.rotation += 30; font.weight: modelData.connected ? Font.Medium : Font.Normal
NetworkService.scanWifi(); elide: Text.ElideRight
} }
StyledText {
width: parent.width
text: {
if (modelData.connected)
return "Connected"
if (NetworkService.connectionStatus === "connecting"
&& NetworkService.connectingSSID === modelData.ssid)
return "Connecting..."
if (NetworkService.connectionStatus === "invalid_password"
&& NetworkService.connectingSSID === modelData.ssid)
return "Invalid password"
if (modelData.saved)
return "Saved" + (modelData.secured ? " • Secured" : " • Open")
return modelData.secured ? "Secured" : "Open"
} }
font.pixelSize: Theme.fontSizeSmall - 1
color: {
if (NetworkService.connectionStatus === "connecting"
&& NetworkService.connectingSSID === modelData.ssid)
return Theme.primary
if (NetworkService.connectionStatus === "invalid_password"
&& NetworkService.connectingSSID === modelData.ssid)
return Theme.error
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
}
elide: Text.ElideRight
}
} }
Row {
id: rightIcons2
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankIcon {
name: "lock"
size: Theme.iconSize - 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
visible: modelData.secured
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: wifiMenuButton
width: 24
height: 24
radius: 12
color: wifiMenuButtonArea.containsMouse ? Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
DankIcon {
name: "more_vert"
size: Theme.iconSize - 8
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: wifiMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiContextMenuWindow.networkData = modelData
let buttonCenter = wifiMenuButtonArea.width / 2
let buttonBottom = wifiMenuButtonArea.height
let globalPos = wifiMenuButtonArea.mapToItem(
wifiContextMenuWindow.parentItem, buttonCenter,
buttonBottom)
Qt.callLater(() => {
wifiContextMenuWindow.show(globalPos.x,
globalPos.y)
})
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
MouseArea {
id: networkArea2
anchors.fill: parent
anchors.rightMargin: 32 // Exclude menu button area
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected)
return
if (modelData.saved) {
NetworkService.connectToWifi(modelData.ssid)
} else if (modelData.secured) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.wifiPasswordSSID = modelData.ssid
wifiPasswordModalRef.wifiPasswordInput = ""
wifiPasswordModalRef.wifiPasswordModalVisible = true
}
} else {
NetworkService.connectToWifi(modelData.ssid)
}
}
}
} }
}
} }
Flickable { ScrollBar.vertical: ScrollBar {
width: parent.width policy: ScrollBar.AsNeeded
height: parent.height - 40
clip: true
contentWidth: width
contentHeight: spanningNetworksColumn.height
boundsBehavior: Flickable.DragAndOvershootBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500
maximumFlickVelocity: 2000
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(0, Math.min(parent.contentHeight - parent.height, newY))
parent.contentY = newY
event.accepted = true
}
}
Column {
id: spanningNetworksColumn
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: NetworkService.wifiAvailable && NetworkService.wifiEnabled ? sortedWifiNetworks : []
Rectangle {
width: spanningNetworksColumn.width
height: 38
radius: Theme.cornerRadiusSmall
color: networkArea2.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: modelData.connected ? 1 : 0
Item {
anchors.fill: parent
anchors.margins: Theme.spacingXS
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
DankIcon {
id: signalIcon2
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
name: getWiFiSignalIcon(modelData.signalStrength)
size: Theme.iconSize - 2
color: modelData.connected ? Theme.primary : Theme.surfaceText
}
Column {
anchors.left: signalIcon2.right
anchors.leftMargin: Theme.spacingXS
anchors.right: rightIcons2.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
width: parent.width
text: modelData.ssid
font.pixelSize: Theme.fontSizeSmall
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: {
if (modelData.connected)
return "Connected";
if (NetworkService.connectionStatus === "connecting" && NetworkService.connectingSSID === modelData.ssid)
return "Connecting...";
if (NetworkService.connectionStatus === "invalid_password" && NetworkService.connectingSSID === modelData.ssid)
return "Invalid password";
if (modelData.saved)
return "Saved" + (modelData.secured ? " • Secured" : " • Open");
return modelData.secured ? "Secured" : "Open";
}
font.pixelSize: Theme.fontSizeSmall - 1
color: {
if (NetworkService.connectionStatus === "connecting" && NetworkService.connectingSSID === modelData.ssid)
return Theme.primary;
if (NetworkService.connectionStatus === "invalid_password" && NetworkService.connectingSSID === modelData.ssid)
return Theme.error;
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
}
elide: Text.ElideRight
}
}
Row {
id: rightIcons2
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
DankIcon {
name: "lock"
size: Theme.iconSize - 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
visible: modelData.secured
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: wifiMenuButton
width: 24
height: 24
radius: 12
color: wifiMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
DankIcon {
name: "more_vert"
size: Theme.iconSize - 8
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: wifiMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiContextMenuWindow.networkData = modelData;
let buttonCenter = wifiMenuButtonArea.width / 2;
let buttonBottom = wifiMenuButtonArea.height;
let globalPos = wifiMenuButtonArea.mapToItem(wifiContextMenuWindow.parentItem, buttonCenter, buttonBottom);
Qt.callLater(() => {
wifiContextMenuWindow.show(globalPos.x, globalPos.y);
});
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
MouseArea {
id: networkArea2
anchors.fill: parent
anchors.rightMargin: 32 // Exclude menu button area
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected)
return ;
if (modelData.saved) {
NetworkService.connectToWifi(modelData.ssid);
} else if (modelData.secured) {
if (wifiPasswordModalRef) {
wifiPasswordModalRef.wifiPasswordSSID = modelData.ssid;
wifiPasswordModalRef.wifiPasswordInput = "";
wifiPasswordModalRef.wifiPasswordModalVisible = true;
}
} else {
NetworkService.connectToWifi(modelData.ssid);
}
}
}
}
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
} }
}
} }

View File

@@ -9,322 +9,332 @@ import qs.Widgets
import qs.Modules.ControlCenter.Network import qs.Modules.ControlCenter.Network
Item { Item {
id: networkTab id: networkTab
property var wifiPasswordModalRef: wifiPasswordModal property var wifiPasswordModalRef: wifiPasswordModal
property var networkInfoModalRef: networkInfoModal property var networkInfoModalRef: networkInfoModal
property var sortedWifiNetworks: { property var sortedWifiNetworks: {
if (!NetworkService.wifiAvailable || !NetworkService.wifiEnabled) { if (!NetworkService.wifiAvailable || !NetworkService.wifiEnabled) {
return []; return []
}
var allNetworks = NetworkService.wifiNetworks;
var savedNetworks = NetworkService.savedWifiNetworks;
var currentSSID = NetworkService.currentWifiSSID;
var signalStrength = NetworkService.wifiSignalStrength;
var refreshTrigger = forceRefresh; // Force recalculation
var networks = [...allNetworks];
networks.forEach(function(network) {
network.connected = (network.ssid === currentSSID);
network.saved = savedNetworks.some(function(saved) {
return saved.ssid === network.ssid;
});
if (network.connected && signalStrength) {
network.signalStrength = signalStrength;
}
});
networks.sort(function(a, b) {
if (a.connected && !b.connected) return -1;
if (!a.connected && b.connected) return 1;
return b.signal - a.signal;
});
return networks;
} }
property int forceRefresh: 0 var allNetworks = NetworkService.wifiNetworks
var savedNetworks = NetworkService.savedWifiNetworks
var currentSSID = NetworkService.currentWifiSSID
var signalStrength = NetworkService.wifiSignalStrength
var refreshTrigger = forceRefresh
Connections { // Force recalculation
target: NetworkService var networks = [...allNetworks]
function onNetworksUpdated() {
forceRefresh++; networks.forEach(function (network) {
} network.connected = (network.ssid === currentSSID)
network.saved = savedNetworks.some(function (saved) {
return saved.ssid === network.ssid
})
if (network.connected && signalStrength) {
network.signalStrength = signalStrength
}
})
networks.sort(function (a, b) {
if (a.connected && !b.connected)
return -1
if (!a.connected && b.connected)
return 1
return b.signal - a.signal
})
return networks
}
property int forceRefresh: 0
Connections {
target: NetworkService
function onNetworksUpdated() {
forceRefresh++
} }
}
Component.onCompleted: { Component.onCompleted: {
NetworkService.addRef(); NetworkService.addRef()
NetworkService.autoRefreshEnabled = true; NetworkService.autoRefreshEnabled = true
if (NetworkService.wifiEnabled) if (NetworkService.wifiEnabled)
NetworkService.scanWifi(); NetworkService.scanWifi()
wifiMonitorTimer.start(); wifiMonitorTimer.start()
} }
Component.onDestruction: { Component.onDestruction: {
NetworkService.removeRef(); NetworkService.removeRef()
NetworkService.autoRefreshEnabled = false; NetworkService.autoRefreshEnabled = false
} }
Row { Row {
anchors.fill: parent anchors.fill: parent
spacing: Theme.spacingM spacing: Theme.spacingM
Column { Column {
width: (parent.width - Theme.spacingM) / 2 width: (parent.width - Theme.spacingM) / 2
height: parent.height height: parent.height
spacing: Theme.spacingS spacing: Theme.spacingS
Flickable { Flickable {
width: parent.width width: parent.width
height: parent.height - 30 height: parent.height - 30
clip: true clip: true
contentWidth: width contentWidth: width
contentHeight: wifiContent.height contentHeight: wifiContent.height
boundsBehavior: Flickable.DragAndOvershootBounds boundsBehavior: Flickable.DragAndOvershootBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now // Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500 flickDeceleration: 1500
maximumFlickVelocity: 2000 maximumFlickVelocity: 2000
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling // Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler { WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => { onWheel: event => {
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60 let delta = event.pixelDelta.y
let newY = parent.contentY - delta !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
newY = Math.max(0, Math.min(parent.contentHeight - parent.height, newY)) let newY = parent.contentY - delta
parent.contentY = newY newY = Math.max(
event.accepted = true 0, Math.min(parent.contentHeight - parent.height, newY))
} parent.contentY = newY
} event.accepted = true
}
Column {
id: wifiContent
width: parent.width
spacing: Theme.spacingM
WiFiCard {
refreshTimer: refreshTimer
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
} }
Column { Column {
width: (parent.width - Theme.spacingM) / 2 id: wifiContent
height: parent.height width: parent.width
spacing: Theme.spacingS spacing: Theme.spacingM
Flickable { WiFiCard {
width: parent.width refreshTimer: refreshTimer
height: parent.height - 30 }
clip: true
contentWidth: width
contentHeight: ethernetContent.height
boundsBehavior: Flickable.StopAtBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500
maximumFlickVelocity: 2000
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(0, Math.min(parent.contentHeight - parent.height, newY))
parent.contentY = newY
event.accepted = true
}
}
Column {
id: ethernetContent
width: parent.width
spacing: Theme.spacingM
EthernetCard {
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
} }
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
} }
Rectangle { Column {
anchors.top: parent.top width: (parent.width - Theme.spacingM) / 2
anchors.topMargin: 100 height: parent.height
anchors.left: parent.left spacing: Theme.spacingS
anchors.right: parent.right
anchors.bottom: parent.bottom Flickable {
color: "transparent" width: parent.width
visible: !NetworkService.wifiEnabled height: parent.height - 30
clip: true
contentWidth: width
contentHeight: ethernetContent.height
boundsBehavior: Flickable.StopAtBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500
maximumFlickVelocity: 2000
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
let delta = event.pixelDelta.y
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta
newY = Math.max(
0, Math.min(parent.contentHeight - parent.height, newY))
parent.contentY = newY
event.accepted = true
}
}
Column { Column {
anchors.centerIn: parent id: ethernetContent
spacing: Theme.spacingM width: parent.width
spacing: Theme.spacingM
DankIcon { EthernetCard {}
anchors.horizontalCenter: parent.horizontalCenter
name: "wifi_off"
size: 48
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "WiFi is turned off"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Turn on WiFi to see networks"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
}
} }
}
WiFiNetworksList { ScrollBar.vertical: ScrollBar {
wifiContextMenuWindow: wifiContextMenuWindow policy: ScrollBar.AsNeeded
sortedWifiNetworks: networkTab.sortedWifiNetworks
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
}
Timer {
id: refreshTimer
interval: 2000
running: visible && refreshTimer.triggered
property bool triggered: false
onTriggered: {
NetworkService.refreshNetworkStatus();
if (NetworkService.wifiEnabled && !NetworkService.isScanning) {
NetworkService.scanWifi();
}
triggered = false;
} }
}
} }
}
Connections { Rectangle {
target: NetworkService anchors.top: parent.top
function onWifiEnabledChanged() { anchors.topMargin: 100
if (NetworkService.wifiEnabled && visible) { anchors.left: parent.left
wifiScanDelayTimer.start(); anchors.right: parent.right
wifiMonitorTimer.start(); anchors.bottom: parent.bottom
} else { color: "transparent"
NetworkService.currentWifiSSID = ""; visible: !NetworkService.wifiEnabled
NetworkService.wifiSignalStrength = "excellent";
NetworkService.wifiNetworks = []; Column {
NetworkService.savedWifiNetworks = []; anchors.centerIn: parent
NetworkService.connectionStatus = ""; spacing: Theme.spacingM
NetworkService.connectingSSID = "";
NetworkService.isScanning = false; DankIcon {
NetworkService.refreshNetworkStatus(); anchors.horizontalCenter: parent.horizontalCenter
wifiMonitorTimer.stop(); name: "wifi_off"
} size: 48
} color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "WiFi is turned off"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Turn on WiFi to see networks"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.4)
}
} }
}
Timer { WiFiNetworksList {
id: wifiScanDelayTimer wifiContextMenuWindow: wifiContextMenuWindow
interval: 1500 sortedWifiNetworks: networkTab.sortedWifiNetworks
running: false wifiPasswordModalRef: networkTab.wifiPasswordModalRef
repeat: false }
onTriggered: {
if (NetworkService.wifiEnabled && visible) { Timer {
if (!NetworkService.isScanning) { id: refreshTimer
NetworkService.scanWifi(); interval: 2000
} else { running: visible && refreshTimer.triggered
wifiRetryTimer.start(); property bool triggered: false
} onTriggered: {
} NetworkService.refreshNetworkStatus()
} if (NetworkService.wifiEnabled && !NetworkService.isScanning) {
NetworkService.scanWifi()
}
triggered = false
} }
}
Timer { Connections {
id: wifiRetryTimer target: NetworkService
interval: 2000 function onWifiEnabledChanged() {
running: false if (NetworkService.wifiEnabled && visible) {
repeat: false wifiScanDelayTimer.start()
onTriggered: { wifiMonitorTimer.start()
if (NetworkService.wifiEnabled && visible && NetworkService.wifiNetworks.length === 0) { } else {
if (!NetworkService.isScanning) { NetworkService.currentWifiSSID = ""
NetworkService.scanWifi(); NetworkService.wifiSignalStrength = "excellent"
} NetworkService.wifiNetworks = []
} NetworkService.savedWifiNetworks = []
} NetworkService.connectionStatus = ""
NetworkService.connectingSSID = ""
NetworkService.isScanning = false
NetworkService.refreshNetworkStatus()
wifiMonitorTimer.stop()
}
} }
}
Timer { Timer {
id: wifiMonitorTimer id: wifiScanDelayTimer
interval: 8000 // Check every 8 seconds interval: 1500
running: false running: false
repeat: true repeat: false
onTriggered: { onTriggered: {
if (!visible || !NetworkService.wifiEnabled) { if (NetworkService.wifiEnabled && visible) {
running = false; if (!NetworkService.isScanning) {
return; NetworkService.scanWifi()
}
var shouldScan = false;
var reason = "";
if (NetworkService.networkStatus !== "wifi") {
shouldScan = true;
reason = "not connected to WiFi";
}
else if (NetworkService.wifiNetworks.length === 0) {
shouldScan = true;
reason = "no networks cached";
}
if (shouldScan && !NetworkService.isScanning) {
NetworkService.scanWifi();
}
}
}
onVisibleChanged: {
if (visible && NetworkService.wifiEnabled) {
wifiMonitorTimer.start();
} else { } else {
wifiMonitorTimer.stop(); wifiRetryTimer.start()
} }
}
} }
}
WiFiContextMenu { Timer {
id: wifiContextMenuWindow id: wifiRetryTimer
parentItem: networkTab interval: 2000
wifiPasswordModalRef: networkTab.wifiPasswordModalRef running: false
networkInfoModalRef: networkTab.networkInfoModalRef repeat: false
onTriggered: {
if (NetworkService.wifiEnabled && visible
&& NetworkService.wifiNetworks.length === 0) {
if (!NetworkService.isScanning) {
NetworkService.scanWifi()
}
}
}
}
Timer {
id: wifiMonitorTimer
interval: 8000 // Check every 8 seconds
running: false
repeat: true
onTriggered: {
if (!visible || !NetworkService.wifiEnabled) {
running = false
return
}
var shouldScan = false
var reason = ""
if (NetworkService.networkStatus !== "wifi") {
shouldScan = true
reason = "not connected to WiFi"
} else if (NetworkService.wifiNetworks.length === 0) {
shouldScan = true
reason = "no networks cached"
}
if (shouldScan && !NetworkService.isScanning) {
NetworkService.scanWifi()
}
}
}
onVisibleChanged: {
if (visible && NetworkService.wifiEnabled) {
wifiMonitorTimer.start()
} else {
wifiMonitorTimer.stop()
}
}
WiFiContextMenu {
id: wifiContextMenuWindow
parentItem: networkTab
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
networkInfoModalRef: networkTab.networkInfoModalRef
}
MouseArea {
anchors.fill: parent
visible: wifiContextMenuWindow.visible
onClicked: {
wifiContextMenuWindow.hide()
} }
MouseArea { MouseArea {
anchors.fill: parent x: wifiContextMenuWindow.x
visible: wifiContextMenuWindow.visible y: wifiContextMenuWindow.y
onClicked: { width: wifiContextMenuWindow.width
wifiContextMenuWindow.hide(); height: wifiContextMenuWindow.height
} onClicked: {
MouseArea { }
x: wifiContextMenuWindow.x
y: wifiContextMenuWindow.y
width: wifiContextMenuWindow.width
height: wifiContextMenuWindow.height
onClicked: {
}
}
} }
}
} }

View File

@@ -8,296 +8,307 @@ import qs.Common
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
property bool powerMenuVisible: false property bool powerMenuVisible: false
property bool powerConfirmVisible: false property bool powerConfirmVisible: false
property string powerConfirmAction: "" property string powerConfirmAction: ""
property string powerConfirmTitle: "" property string powerConfirmTitle: ""
property string powerConfirmMessage: "" property string powerConfirmMessage: ""
visible: powerMenuVisible visible: powerMenuVisible
implicitWidth: 400 implicitWidth: 400
implicitHeight: 320 implicitHeight: 320
WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent" color: "transparent"
anchors { anchors {
top: true top: true
left: true left: true
right: true right: true
bottom: true bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: {
powerMenuVisible = false
} }
}
Rectangle {
width: Math.min(320, parent.width - Theme.spacingL * 2)
height: 320 // Fixed height to prevent cropping
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
y: Theme.barHeight + Theme.spacingXS
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
MouseArea { MouseArea {
anchors.fill: parent
onClicked: { anchors.fill: parent
powerMenuVisible = false; onClicked: {
}
}
} }
Rectangle { Column {
width: Math.min(320, parent.width - Theme.spacingL * 2) anchors.fill: parent
height: 320 // Fixed height to prevent cropping anchors.margins: Theme.spacingL
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL) spacing: Theme.spacingM
y: Theme.barHeight + Theme.spacingXS
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
MouseArea { Row {
width: parent.width
anchors.fill: parent StyledText {
onClicked: { text: "Power Options"
} font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
} }
Column { Item {
anchors.fill: parent width: parent.width - 150
anchors.margins: Theme.spacingL height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
onClicked: {
powerMenuVisible = false
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
Row { DankIcon {
width: parent.width name: "logout"
size: Theme.iconSize
StyledText { color: Theme.surfaceText
text: "Power Options" anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
onClicked: {
powerMenuVisible = false;
}
}
} }
Column { StyledText {
width: parent.width text: "Log Out"
spacing: Theme.spacingS font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
Rectangle { font.weight: Font.Medium
width: parent.width anchors.verticalCenter: parent.verticalCenter
height: 50
radius: Theme.cornerRadius
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "logout"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Log Out"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false;
root.powerConfirmAction = "logout";
root.powerConfirmTitle = "Log Out";
root.powerConfirmMessage = "Are you sure you want to log out?";
root.powerConfirmVisible = true;
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Suspend"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false;
root.powerConfirmAction = "suspend";
root.powerConfirmTitle = "Suspend";
root.powerConfirmMessage = "Are you sure you want to suspend the system?";
root.powerConfirmVisible = true;
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Reboot"
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false;
root.powerConfirmAction = "reboot";
root.powerConfirmTitle = "Reboot";
root.powerConfirmMessage = "Are you sure you want to reboot the system?";
root.powerConfirmVisible = true;
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Power Off"
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false;
root.powerConfirmAction = "poweroff";
root.powerConfirmTitle = "Power Off";
root.powerConfirmMessage = "Are you sure you want to power off the system?";
root.powerConfirmVisible = true;
}
}
}
} }
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "logout"
root.powerConfirmTitle = "Log Out"
root.powerConfirmMessage = "Are you sure you want to log out?"
root.powerConfirmVisible = true
}
}
} }
Behavior on opacity { Rectangle {
NumberAnimation { width: parent.width
duration: Theme.mediumDuration height: 50
easing.type: Theme.emphasizedEasing radius: Theme.cornerRadius
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
StyledText {
text: "Suspend"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "suspend"
root.powerConfirmTitle = "Suspend"
root.powerConfirmMessage = "Are you sure you want to suspend the system?"
root.powerConfirmVisible = true
}
}
} }
Behavior on scale { Rectangle {
NumberAnimation { width: parent.width
duration: Theme.mediumDuration height: 50
easing.type: Theme.emphasizedEasing radius: Theme.cornerRadius
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
Theme.warning.g,
Theme.warning.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
StyledText {
text: "Reboot"
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "reboot"
root.powerConfirmTitle = "Reboot"
root.powerConfirmMessage = "Are you sure you want to reboot the system?"
root.powerConfirmVisible = true
}
}
} }
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Power Off"
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "poweroff"
root.powerConfirmTitle = "Power Off"
root.powerConfirmMessage = "Are you sure you want to power off the system?"
root.powerConfirmVisible = true
}
}
}
}
} }
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
} }

View File

@@ -8,181 +8,194 @@ import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: dock id: dock
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
property var modelData property var modelData
property var contextMenu property var contextMenu
property var windowsMenu property var windowsMenu
property bool autoHide: SettingsData.dockAutoHide property bool autoHide: SettingsData.dockAutoHide
property real backgroundTransparency: SettingsData.dockTransparency property real backgroundTransparency: SettingsData.dockTransparency
property bool contextMenuOpen: (contextMenu && contextMenu.visible
&& contextMenu.screen === modelData)
|| (windowsMenu && windowsMenu.visible
&& windowsMenu.screen === modelData)
property bool windowIsFullscreen: {
if (!NiriService.focusedWindowId || !NiriService.niriAvailable)
return false
var focusedWindow = NiriService.windows.find(
w => w.id === NiriService.focusedWindowId)
if (!focusedWindow)
return false
var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
return fullscreenApps.some(app => focusedWindow.app_id
&& focusedWindow.app_id.toLowerCase(
).includes(app))
}
property bool reveal: (!autoHide || dockMouseArea.containsMouse
|| dockApps.requestDockShow || contextMenuOpen)
&& !windowIsFullscreen
property bool contextMenuOpen: (contextMenu && contextMenu.visible && contextMenu.screen === modelData) || (windowsMenu && windowsMenu.visible && windowsMenu.screen === modelData) Connections {
property bool windowIsFullscreen: { target: SettingsData
if (!NiriService.focusedWindowId || !NiriService.niriAvailable) return false function onDockTransparencyChanged() {
var focusedWindow = NiriService.windows.find(w => w.id === NiriService.focusedWindowId) dock.backgroundTransparency = SettingsData.dockTransparency
if (!focusedWindow) return false
var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
return fullscreenApps.some(app => focusedWindow.app_id && focusedWindow.app_id.toLowerCase().includes(app))
} }
property bool reveal: (!autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen) && !windowIsFullscreen }
Connections { screen: modelData
target: SettingsData visible: SettingsData.showDock
function onDockTransparencyChanged() { color: "transparent"
dock.backgroundTransparency = SettingsData.dockTransparency;
}
}
screen: modelData anchors {
visible: SettingsData.showDock bottom: true
color: "transparent" left: true
right: true
}
margins {
left: 0
right: 0
}
implicitHeight: 100
exclusiveZone: autoHide ? -1 : 65 - 16
mask: Region {
item: dockMouseArea
}
MouseArea {
id: dockMouseArea
height: dock.reveal ? 65 : 12
anchors { anchors {
bottom: true bottom: parent.bottom
left: true horizontalCenter: parent.horizontalCenter
right: true }
implicitWidth: dock.reveal ? dockBackground.width + 32 : (dockBackground.width + 32)
hoverEnabled: true
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
} }
margins { Item {
left: 0 id: dockContainer
right: 0 anchors.fill: parent
}
implicitHeight: 100 transform: Translate {
exclusiveZone: autoHide ? -1 : 65 - 16 id: dockSlide
y: dock.reveal ? 0 : 60
mask: Region { Behavior on y {
item: dockMouseArea NumberAnimation {
} duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea { Rectangle {
id: dockMouseArea id: dockBackground
height: dock.reveal ? 65 : 12 objectName: "dockBackground"
anchors { anchors {
bottom: parent.bottom top: parent.top
horizontalCenter: parent.horizontalCenter bottom: parent.bottom
} horizontalCenter: parent.horizontalCenter
implicitWidth: dock.reveal ? dockBackground.width + 32 : (dockBackground.width + 32)
hoverEnabled: true
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
} }
width: dockApps.implicitWidth + 12
height: parent.height - 8
Item { anchors.topMargin: 4
id: dockContainer anchors.bottomMargin: 1
anchors.fill: parent
transform: Translate { color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
id: dockSlide Theme.surfaceContainer.b, backgroundTransparency)
y: dock.reveal ? 0 : 60 radius: Theme.cornerRadiusXLarge
border.width: 1
border.color: Theme.outlineMedium
layer.enabled: true
Behavior on y { Rectangle {
NumberAnimation { anchors.fill: parent
duration: 200 color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
easing.type: Easing.OutCubic Theme.surfaceTint.b, 0.04)
} radius: parent.radius
}
}
Rectangle {
id: dockBackground
objectName: "dockBackground"
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: dockApps.implicitWidth + 12
height: parent.height - 8
anchors.topMargin: 4
anchors.bottomMargin: 1
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
radius: Theme.cornerRadiusXLarge
border.width: 1
border.color: Theme.outlineMedium
layer.enabled: true
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
}
DockApps {
id: dockApps
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 4
anchors.bottomMargin: 4
contextMenu: dock.contextMenu
windowsMenu: dock.windowsMenu
}
}
Rectangle {
id: appTooltip
property var hoveredButton: {
if (!dockApps.children[0]) return null
var row = dockApps.children[0]
var repeater = null
for (var i = 0; i < row.children.length; i++) {
var child = row.children[i]
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
repeater = child
break
}
}
if (!repeater || !repeater.itemAt) return null
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i)
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton
}
}
return null
}
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
visible: hoveredButton !== null && tooltipText !== ""
width: tooltipLabel.implicitWidth + 24
height: tooltipLabel.implicitHeight + 12
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 8
x: hoveredButton ? hoveredButton.mapToItem(dockContainer, hoveredButton.width/2, 0).x - width/2 : 0
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: appTooltip.tooltipText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
}
} }
DockApps {
id: dockApps
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 4
anchors.bottomMargin: 4
contextMenu: dock.contextMenu
windowsMenu: dock.windowsMenu
}
}
Rectangle {
id: appTooltip
property var hoveredButton: {
if (!dockApps.children[0])
return null
var row = dockApps.children[0]
var repeater = null
for (var i = 0; i < row.children.length; i++) {
var child = row.children[i]
if (child && typeof child.count !== "undefined"
&& typeof child.itemAt === "function") {
repeater = child
break
}
}
if (!repeater || !repeater.itemAt)
return null
for (var i = 0; i < repeater.count; i++) {
var item = repeater.itemAt(i)
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton
}
}
return null
}
property string tooltipText: hoveredButton ? hoveredButton.tooltipText : ""
visible: hoveredButton !== null && tooltipText !== ""
width: tooltipLabel.implicitWidth + 24
height: tooltipLabel.implicitHeight + 12
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 8
x: hoveredButton ? hoveredButton.mapToItem(dockContainer,
hoveredButton.width / 2,
0).x - width / 2 : 0
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: appTooltip.tooltipText
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
}
} }
}
} }

View File

@@ -7,283 +7,307 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
property var appData property var appData
property var contextMenu: null property var contextMenu: null
property var windowsMenu: null property var windowsMenu: null
property var dockApps: null property var dockApps: null
property int index: -1 property int index: -1
property bool longPressing: false property bool longPressing: false
property bool dragging: false property bool dragging: false
property point dragStartPos: Qt.point(0, 0) property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0) property point dragOffset: Qt.point(0, 0)
property int targetIndex: -1 property int targetIndex: -1
property int originalIndex: -1 property int originalIndex: -1
width: 40 width: 40
height: 40 height: 40
property bool isHovered: mouseArea.containsMouse && !dragging property bool isHovered: mouseArea.containsMouse && !dragging
transform: Translate { transform: Translate {
id: translateY id: translateY
y: 0 y: 0
} }
SequentialAnimation { SequentialAnimation {
id: bounceAnimation id: bounceAnimation
running: false running: false
NumberAnimation { NumberAnimation {
target: translateY target: translateY
property: "y" property: "y"
to: -10 to: -10
duration: Anims.durShort duration: Anims.durShort
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: translateY
property: "y"
to: -8
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
} }
NumberAnimation { NumberAnimation {
id: exitAnimation target: translateY
running: false property: "y"
target: translateY to: -8
property: "y" duration: Anims.durShort
to: 0 easing.type: Easing.BezierSpline
duration: Anims.durShort easing.bezierCurve: Anims.emphasizedDecel
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
} }
}
onIsHoveredChanged: { NumberAnimation {
if (isHovered) { id: exitAnimation
exitAnimation.stop() running: false
if (!bounceAnimation.running) { target: translateY
bounceAnimation.restart() property: "y"
} to: 0
} else { duration: Anims.durShort
bounceAnimation.stop() easing.type: Easing.BezierSpline
exitAnimation.restart() easing.bezierCurve: Anims.emphasizedDecel
} }
onIsHoveredChanged: {
if (isHovered) {
exitAnimation.stop()
if (!bounceAnimation.running) {
bounceAnimation.restart()
}
} else {
bounceAnimation.stop()
exitAnimation.restart()
} }
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2 border.width: 2
border.color: Theme.primary border.color: Theme.primary
visible: dragging visible: dragging
z: -1 z: -1
}
Timer {
id: longPressTimer
interval: 500
repeat: false
onTriggered: {
if (appData && appData.isPinned) {
longPressing = true
}
} }
}
Timer { MouseArea {
id: longPressTimer id: mouseArea
interval: 500 anchors.fill: parent
repeat: false anchors.bottomMargin: -20
onTriggered: { hoverEnabled: true
if (appData && appData.isPinned) { cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
longPressing = true acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
}
}
}
MouseArea { onPressed: mouse => {
id: mouseArea if (mouse.button === Qt.LeftButton && appData
anchors.fill: parent && appData.isPinned) {
anchors.bottomMargin: -20 dragStartPos = Qt.point(mouse.x, mouse.y)
hoverEnabled: true longPressTimer.start()
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor }
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton }
onReleased: mouse => {
onPressed: (mouse) => { longPressTimer.stop()
if (mouse.button === Qt.LeftButton && appData && appData.isPinned) { if (longPressing) {
dragStartPos = Qt.point(mouse.x, mouse.y) if (dragging && targetIndex >= 0
longPressTimer.start() && targetIndex !== originalIndex && dockApps) {
} dockApps.movePinnedApp(originalIndex, targetIndex)
}
onReleased: (mouse) => {
longPressTimer.stop()
if (longPressing) {
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps) {
dockApps.movePinnedApp(originalIndex, targetIndex)
}
longPressing = false
dragging = false
dragOffset = Qt.point(0, 0)
targetIndex = -1
originalIndex = -1
}
}
onPositionChanged: (mouse) => {
if (longPressing && !dragging) {
var distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2))
if (distance > 5) {
dragging = true
targetIndex = index
originalIndex = index
}
}
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y)
if (dockApps) {
var threshold = 40
var newTargetIndex = targetIndex
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) {
newTargetIndex = targetIndex + 1
} else if (dragOffset.x < -threshold && targetIndex > 0) {
newTargetIndex = targetIndex - 1
} }
if (newTargetIndex !== targetIndex) { longPressing = false
targetIndex = newTargetIndex dragging = false
dragStartPos = Qt.point(mouse.x, mouse.y) dragOffset = Qt.point(0, 0)
} targetIndex = -1
originalIndex = -1
}
} }
}
}
onClicked: (mouse) => { onPositionChanged: mouse => {
if (!appData || longPressing) return if (longPressing && !dragging) {
var distance = Math.sqrt(
Math.pow(mouse.x - dragStartPos.x,
2) + Math.pow(mouse.y - dragStartPos.y,
2))
if (distance > 5) {
dragging = true
targetIndex = index
originalIndex = index
}
}
if (mouse.button === Qt.LeftButton) { if (dragging) {
var windowCount = appData.windows ? appData.windows.count : 0 dragOffset = Qt.point(mouse.x - dragStartPos.x,
mouse.y - dragStartPos.y)
if (windowCount === 0) { if (dockApps) {
if (appData && appData.appId) { var threshold = 40
var desktopEntry = DesktopEntries.byId(appData.appId) var newTargetIndex = targetIndex
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({ if (dragOffset.x > threshold
id: appData.appId, && targetIndex < dockApps.pinnedAppCount - 1) {
name: desktopEntry.name || appData.appId, newTargetIndex = targetIndex + 1
icon: desktopEntry.icon || "", } else if (dragOffset.x < -threshold
exec: desktopEntry.exec || "", && targetIndex > 0) {
comment: desktopEntry.comment || "" newTargetIndex = targetIndex - 1
}) }
}
Quickshell.execDetached(["gtk-launch", appData.appId]) if (newTargetIndex !== targetIndex) {
} targetIndex = newTargetIndex
} else if (windowCount === 1) { dragStartPos = Qt.point(mouse.x, mouse.y)
var window = appData.windows.get(0) }
NiriService.focusWindow(window.id) }
} else { }
windowsMenu.showForButton(root, appData, 40) }
}
} else if (mouse.button === Qt.MiddleButton) { onClicked: mouse => {
if (appData && appData.appId) { if (!appData || longPressing)
var desktopEntry = DesktopEntries.byId(appData.appId) return
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({ if (mouse.button === Qt.LeftButton) {
id: appData.appId, var windowCount = appData.windows ? appData.windows.count : 0
name: desktopEntry.name || appData.appId,
icon: desktopEntry.icon || "", if (windowCount === 0) {
exec: desktopEntry.exec || "", if (appData && appData.appId) {
comment: desktopEntry.comment || "" var desktopEntry = DesktopEntries.byId(appData.appId)
}) if (desktopEntry) {
} AppUsageHistoryData.addAppUsage({
Quickshell.execDetached(["gtk-launch", appData.appId]) "id": appData.appId,
} "name": desktopEntry.name
} else if (mouse.button === Qt.RightButton) { || appData.appId,
if (contextMenu) { "icon": desktopEntry.icon
contextMenu.showForButton(root, appData, 40) || "",
} "exec": desktopEntry.exec
} || "",
} "comment": desktopEntry.comment
|| ""
})
}
Quickshell.execDetached(["gtk-launch", appData.appId])
}
} else if (windowCount === 1) {
var window = appData.windows.get(0)
NiriService.focusWindow(window.id)
} else {
windowsMenu.showForButton(root, appData, 40)
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name
|| appData.appId,
"icon": desktopEntry.icon
|| "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment
|| ""
})
}
Quickshell.execDetached(["gtk-launch", appData.appId])
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu) {
contextMenu.showForButton(root, appData, 40)
}
}
}
}
property bool showTooltip: mouseArea.containsMouse && !dragging
property string tooltipText: {
if (!appData || !appData.appId)
return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId
}
IconImage {
id: iconImg
width: 40
height: 40
anchors.centerIn: parent
source: {
if (!appData || !appData.appId)
return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(
desktopEntry.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme)
return iconPath
}
return ""
} }
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
implicitSize: 40
}
property bool showTooltip: mouseArea.containsMouse && !dragging Rectangle {
property string tooltipText: { width: 40
if (!appData || !appData.appId) return "" height: 40
anchors.centerIn: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadiusLarge
border.width: 1
border.color: Theme.primarySelected
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId)
return "?"
var desktopEntry = DesktopEntries.byId(appData.appId) var desktopEntry = DesktopEntries.byId(appData.appId)
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId if (desktopEntry && desktopEntry.name) {
} return desktopEntry.name.charAt(0).toUpperCase()
IconImage {
id: iconImg
width: 40
height: 40
anchors.centerIn: parent
source: {
if (!appData || !appData.appId) return ""
var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(desktopEntry.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme)
return iconPath
}
return ""
} }
smooth: true return appData.appId.charAt(0).toUpperCase()
mipmap: true }
asynchronous: true font.pixelSize: 14
visible: status === Image.Ready color: Theme.primary
implicitSize: 40 font.weight: Font.Bold
} }
}
Rectangle { Row {
width: 40 anchors.horizontalCenter: parent.horizontalCenter
height: 40 anchors.bottom: parent.bottom
anchors.centerIn: parent anchors.bottomMargin: -2
visible: !iconImg.visible spacing: 2
color: Theme.surfaceLight
radius: Theme.cornerRadiusLarge
border.width: 1
border.color: Theme.primarySelected
Text { Repeater {
anchors.centerIn: parent model: appData && appData.windows ? Math.min(appData.windows.count, 4) : 0
text: {
if (!appData || !appData.appId) return "?" Rectangle {
var desktopEntry = DesktopEntries.byId(appData.appId) width: appData && appData.windows && appData.windows.count <= 3 ? 5 : 3
if (desktopEntry && desktopEntry.name) { height: 2
return desktopEntry.name.charAt(0).toUpperCase() radius: 1
} color: {
return appData.appId.charAt(0).toUpperCase() if (!appData || !appData.windows || appData.windows.count === 0)
} return "transparent"
font.pixelSize: 14 var window = appData.windows.get(index)
color: Theme.primary return window
font.weight: Font.Bold && window.id == NiriService.focusedWindowId ? Theme.primary : Qt.rgba(
} Theme.surfaceText.r,
} Theme.surfaceText.g,
Theme.surfaceText.b,
0.6)
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: -2
spacing: 2
Repeater {
model: appData && appData.windows ? Math.min(appData.windows.count, 4) : 0
Rectangle {
width: appData && appData.windows && appData.windows.count <= 3 ? 5 : 3
height: 2
radius: 1
color: {
if (!appData || !appData.windows || appData.windows.count === 0) return "transparent"
var window = appData.windows.get(index)
return window && window.id == NiriService.focusedWindowId ? Theme.primary :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
}
} }
}
} }
}
} }

View File

@@ -6,164 +6,176 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
property var contextMenu: null property var contextMenu: null
property var windowsMenu: null property var windowsMenu: null
property bool requestDockShow: false property bool requestDockShow: false
property int pinnedAppCount: 0 property int pinnedAppCount: 0
implicitWidth: row.width implicitWidth: row.width
implicitHeight: row.height implicitHeight: row.height
function movePinnedApp(fromIndex, toIndex) { function movePinnedApp(fromIndex, toIndex) {
if (fromIndex === toIndex) return if (fromIndex === toIndex)
return
var currentPinned = [...(SessionData.pinnedApps || [])] var currentPinned = [...(SessionData.pinnedApps || [])]
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) return if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0
|| toIndex >= currentPinned.length)
return
var movedApp = currentPinned.splice(fromIndex, 1)[0] var movedApp = currentPinned.splice(fromIndex, 1)[0]
currentPinned.splice(toIndex, 0, movedApp) currentPinned.splice(toIndex, 0, movedApp)
SessionData.setPinnedApps(currentPinned) SessionData.setPinnedApps(currentPinned)
} }
Row { Row {
id: row id: row
spacing: 2 spacing: 2
anchors.centerIn: parent anchors.centerIn: parent
height: 40
Repeater {
id: repeater
model: ListModel {
id: dockModel
Component.onCompleted: updateModel()
function updateModel() {
clear()
var items = []
var runningApps = NiriService.getRunningAppIds()
var pinnedApps = [...(SessionData.pinnedApps || [])]
var addedApps = new Set()
pinnedApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
var windows = NiriService.getWindowsByAppId(
appId)
items.push({
"appId": appId,
"windows": windows,
"isPinned": true,
"isRunning": windows.length > 0
})
addedApps.add(lowerAppId)
}
})
root.pinnedAppCount = pinnedApps.length
var appUsageRanking = AppUsageHistoryData.appUsageRanking || {}
var unpinnedApps = []
var unpinnedAppsSet = new Set()
// First: Add ALL currently running apps that aren't pinned
runningApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
unpinnedApps.push(appId)
unpinnedAppsSet.add(lowerAppId)
}
})
// Then: Fill remaining slots up to 3 with recently used apps
var remainingSlots = Math.max(0, 3 - unpinnedApps.length)
if (remainingSlots > 0) {
// Sort recent apps by usage
var recentApps = []
for (var appId in appUsageRanking) {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId) && !unpinnedAppsSet.has(
lowerAppId)) {
recentApps.push({
"appId": appId,
"lastUsed": appUsageRanking[appId].lastUsed
|| 0
})
}
}
recentApps.sort((a, b) => b.lastUsed - a.lastUsed)
var recentToAdd = Math.min(remainingSlots, recentApps.length)
for (var i = 0; i < recentToAdd; i++) {
unpinnedApps.push(recentApps[i].appId)
}
}
if (pinnedApps.length > 0 && unpinnedApps.length > 0) {
items.push({
"appId": "__SEPARATOR__",
"windows": [],
"isPinned": false,
"isRunning": false
})
}
unpinnedApps.forEach(appId => {
var windows = NiriService.getWindowsByAppId(
appId)
items.push({
"appId": appId,
"windows": windows,
"isPinned": false,
"isRunning": windows.length > 0
})
})
items.forEach(item => {
append(item)
})
}
}
delegate: Item {
id: delegateItem
property alias dockButton: button
width: model.appId === "__SEPARATOR__" ? 16 : 40
height: 40 height: 40
Repeater { Rectangle {
id: repeater visible: model.appId === "__SEPARATOR__"
model: ListModel { width: 2
id: dockModel height: 20
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
Component.onCompleted: updateModel() radius: 1
anchors.centerIn: parent
function updateModel() {
clear()
var items = []
var runningApps = NiriService.getRunningAppIds()
var pinnedApps = [...(SessionData.pinnedApps || [])]
var addedApps = new Set()
pinnedApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
var windows = NiriService.getWindowsByAppId(appId)
items.push({
appId: appId,
windows: windows,
isPinned: true,
isRunning: windows.length > 0
})
addedApps.add(lowerAppId)
}
})
root.pinnedAppCount = pinnedApps.length
var appUsageRanking = AppUsageHistoryData.appUsageRanking || {}
var unpinnedApps = []
var unpinnedAppsSet = new Set()
// First: Add ALL currently running apps that aren't pinned
runningApps.forEach(appId => {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId)) {
unpinnedApps.push(appId)
unpinnedAppsSet.add(lowerAppId)
}
})
// Then: Fill remaining slots up to 3 with recently used apps
var remainingSlots = Math.max(0, 3 - unpinnedApps.length)
if (remainingSlots > 0) {
// Sort recent apps by usage
var recentApps = []
for (var appId in appUsageRanking) {
var lowerAppId = appId.toLowerCase()
if (!addedApps.has(lowerAppId) && !unpinnedAppsSet.has(lowerAppId)) {
recentApps.push({
appId: appId,
lastUsed: appUsageRanking[appId].lastUsed || 0
})
}
}
recentApps.sort((a, b) => b.lastUsed - a.lastUsed)
var recentToAdd = Math.min(remainingSlots, recentApps.length)
for (var i = 0; i < recentToAdd; i++) {
unpinnedApps.push(recentApps[i].appId)
}
}
if (pinnedApps.length > 0 && unpinnedApps.length > 0) {
items.push({
appId: "__SEPARATOR__",
windows: [],
isPinned: false,
isRunning: false
})
}
unpinnedApps.forEach(appId => {
var windows = NiriService.getWindowsByAppId(appId)
items.push({
appId: appId,
windows: windows,
isPinned: false,
isRunning: windows.length > 0
})
})
items.forEach(item => {
append(item)
})
}
}
delegate: Item {
id: delegateItem
property alias dockButton: button
width: model.appId === "__SEPARATOR__" ? 16 : 40
height: 40
Rectangle {
visible: model.appId === "__SEPARATOR__"
width: 2
height: 20
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
radius: 1
anchors.centerIn: parent
}
DockAppButton {
id: button
visible: model.appId !== "__SEPARATOR__"
anchors.centerIn: parent
width: 40
height: 40
appData: model
contextMenu: root.contextMenu
windowsMenu: root.windowsMenu
dockApps: root
index: model.index
}
}
} }
}
Connections { DockAppButton {
target: NiriService id: button
function onWindowsChanged() { dockModel.updateModel() } visible: model.appId !== "__SEPARATOR__"
function onWindowOpenedOrChanged() { dockModel.updateModel() } anchors.centerIn: parent
}
Connections { width: 40
target: SessionData height: 40
function onPinnedAppsChanged() { dockModel.updateModel() }
appData: model
contextMenu: root.contextMenu
windowsMenu: root.windowsMenu
dockApps: root
index: model.index
}
}
} }
}
Connections {
target: NiriService
function onWindowsChanged() {
dockModel.updateModel()
}
function onWindowOpenedOrChanged() {
dockModel.updateModel()
}
}
Connections {
target: SessionData
function onPinnedAppsChanged() {
dockModel.updateModel()
}
}
} }

View File

@@ -8,276 +8,304 @@ import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
property bool showContextMenu: false property bool showContextMenu: false
property var appData: null property var appData: null
property var anchorItem: null property var anchorItem: null
property real dockVisibleHeight: 40 property real dockVisibleHeight: 40
property int margin: 10 property int margin: 10
function showForButton(button, data, dockHeight) { function showForButton(button, data, dockHeight) {
anchorItem = button anchorItem = button
appData = data appData = data
dockVisibleHeight = dockHeight || 40 dockVisibleHeight = dockHeight || 40
var dockWindow = button.Window.window var dockWindow = button.Window.window
if (dockWindow) { if (dockWindow) {
for (var i = 0; i < Quickshell.screens.length; i++) { for (var i = 0; i < Quickshell.screens.length; i++) {
var s = Quickshell.screens[i] var s = Quickshell.screens[i]
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) { if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
root.screen = s root.screen = s
break break
}
}
} }
}
showContextMenu = true
} }
function close() { showContextMenu = false }
screen: Quickshell.screens[0] showContextMenu = true
}
function close() {
showContextMenu = false
}
visible: showContextMenu screen: Quickshell.screens[0]
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors { top: true; left: true; right: true; bottom: true }
property point anchorPos: Qt.point(screen.width/2, screen.height - 100) visible: showContextMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
onAnchorItemChanged: updatePosition() property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
onVisibleChanged: if (visible) updatePosition()
function updatePosition() { onAnchorItemChanged: updatePosition()
if (!anchorItem) { onVisibleChanged: if (visible)
anchorPos = Qt.point(screen.width/2, screen.height - 100) updatePosition()
return
}
var dockWindow = anchorItem.Window.window function updatePosition() {
if (!dockWindow) { if (!anchorItem) {
anchorPos = Qt.point(screen.width/2, screen.height - 100) anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return return
}
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
var actualDockHeight = root.dockVisibleHeight // fallback
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item
}
for (var i = 0; i < item.children.length; i++) {
var found = findDockBackground(item.children[i])
if (found) return found
}
return null
}
var dockBackground = findDockBackground(dockWindow.contentItem)
if (dockBackground) {
actualDockHeight = dockBackground.height
}
var dockBottomMargin = 16 // The dock has bottom margin
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
var dockContentWidth = dockWindow.width
var screenWidth = root.screen.width
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width/2
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
} }
var dockWindow = anchorItem.Window.window
if (!dockWindow) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
}
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
var actualDockHeight = root.dockVisibleHeight // fallback
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item
}
for (var i = 0; i < item.children.length; i++) {
var found = findDockBackground(item.children[i])
if (found)
return found
}
return null
}
var dockBackground = findDockBackground(dockWindow.contentItem)
if (dockBackground) {
actualDockHeight = dockBackground.height
}
var dockBottomMargin = 16 // The dock has bottom margin
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
var dockContentWidth = dockWindow.width
var screenWidth = root.screen.width
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
}
Rectangle {
id: menuContainer
width: Math.min(400,
Math.max(200,
menuColumn.implicitWidth + Theme.spacingS * 2))
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
x: {
var left = 10
var right = root.width - width - 10
var want = root.anchorPos.x - width / 2
return Math.max(left, Math.min(right, want))
}
y: Math.max(10, root.anchorPos.y - height + 30)
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
opacity: showContextMenu ? 1 : 0
scale: showContextMenu ? 1 : 0.85
Rectangle { Rectangle {
id: menuContainer anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
width: Math.min(400, Math.max(200, menuColumn.implicitWidth + Theme.spacingS * 2)) Column {
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2) id: menuColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
x: { Rectangle {
var left = 10 width: parent.width
var right = root.width - width - 10 height: 28
var want = root.anchorPos.x - width/2 radius: Theme.cornerRadiusSmall
return Math.max(left, Math.min(right, want)) color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.appData
&& root.appData.isPinned ? "Unpin from Dock" : "Pin to Dock"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
} }
y: Math.max(10, root.anchorPos.y - height + 30)
color: Theme.popupBackground() MouseArea {
radius: Theme.cornerRadiusLarge id: pinArea
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) anchors.fill: parent
border.width: 1 hoverEnabled: true
opacity: showContextMenu ? 1 : 0 cursorShape: Qt.PointingHandCursor
scale: showContextMenu ? 1 : 0.85 onClicked: {
if (!root.appData)
return
if (root.appData.isPinned) {
SessionData.removePinnedApp(root.appData.appId)
} else {
SessionData.addPinnedApp(root.appData.appId)
}
root.close()
}
}
}
Rectangle {
visible: root.appData && root.appData.windows
&& root.appData.windows.count > 0
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Repeater {
model: root.appData
&& root.appData.windows ? root.appData.windows : null
Rectangle { Rectangle {
required property var model
width: menuColumn.width
height: 28
radius: Theme.cornerRadiusSmall
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: model.title || "Untitled Window"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: windowArea
anchors.fill: parent anchors.fill: parent
anchors.topMargin: 4 hoverEnabled: true
anchors.leftMargin: 2 cursorShape: Qt.PointingHandCursor
anchors.rightMargin: -2 onClicked: {
anchors.bottomMargin: -4 NiriService.focusWindow(model.id)
radius: parent.radius root.close()
color: Qt.rgba(0, 0, 0, 0.15) }
z: parent.z - 1 }
}
}
Rectangle {
visible: root.appData && root.appData.windows
&& root.appData.windows.count > 1
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: root.appData && root.appData.windows
&& root.appData.windows.count > 1
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: closeAllArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Close All Windows"
font.pixelSize: Theme.fontSizeSmall
color: closeAllArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
} }
Column { MouseArea {
id: menuColumn id: closeAllArea
width: parent.width - Theme.spacingS * 2 anchors.fill: parent
anchors.horizontalCenter: parent.horizontalCenter hoverEnabled: true
anchors.top: parent.top cursorShape: Qt.PointingHandCursor
anchors.topMargin: Theme.spacingS onClicked: {
spacing: 1 if (!root.appData || !root.appData.windows)
return
Rectangle { for (var i = 0; i < root.appData.windows.count; i++) {
width: parent.width var window = root.appData.windows.get(i)
height: 28 NiriService.closeWindow(window.id)
radius: Theme.cornerRadiusSmall
color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.appData && root.appData.isPinned ? "Unpin from Dock" : "Pin to Dock"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: pinArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.appData) return
if (root.appData.isPinned) {
SessionData.removePinnedApp(root.appData.appId)
} else {
SessionData.addPinnedApp(root.appData.appId)
}
root.close()
}
}
} }
Rectangle {
visible: root.appData && root.appData.windows && root.appData.windows.count > 0
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Repeater {
model: root.appData && root.appData.windows ? root.appData.windows : null
Rectangle {
required property var model
width: menuColumn.width
height: 28
radius: Theme.cornerRadiusSmall
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: model.title || "Untitled Window"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: windowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NiriService.focusWindow(model.id)
root.close()
}
}
}
}
Rectangle {
visible: root.appData && root.appData.windows && root.appData.windows.count > 1
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: root.appData && root.appData.windows && root.appData.windows.count > 1
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: closeAllArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Close All Windows"
font.pixelSize: Theme.fontSizeSmall
color: closeAllArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: closeAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.appData || !root.appData.windows) return
for (var i = 0; i < root.appData.windows.count; i++) {
var window = root.appData.windows.get(i)
NiriService.closeWindow(window.id)
}
root.close()
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.close() root.close()
}
} }
}
} }
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.close()
}
}
} }

View File

@@ -8,193 +8,207 @@ import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
property bool showWindowsMenu: false property bool showWindowsMenu: false
property var appData: null property var appData: null
property var anchorItem: null property var anchorItem: null
property real dockVisibleHeight: 40 property real dockVisibleHeight: 40
property int margin: 10 property int margin: 10
function showForButton(button, data, dockHeight) { function showForButton(button, data, dockHeight) {
anchorItem = button anchorItem = button
appData = data appData = data
dockVisibleHeight = dockHeight || 40 dockVisibleHeight = dockHeight || 40
var dockWindow = button.Window.window var dockWindow = button.Window.window
if (dockWindow) { if (dockWindow) {
for (var i = 0; i < Quickshell.screens.length; i++) { for (var i = 0; i < Quickshell.screens.length; i++) {
var s = Quickshell.screens[i] var s = Quickshell.screens[i]
if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) { if (dockWindow.x >= s.x && dockWindow.x < s.x + s.width) {
root.screen = s root.screen = s
break break
}
}
} }
}
showWindowsMenu = true
} }
function close() { showWindowsMenu = false } showWindowsMenu = true
}
screen: Quickshell.screens[0] function close() {
showWindowsMenu = false
}
visible: showWindowsMenu screen: Quickshell.screens[0]
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors { top: true; left: true; right: true; bottom: true }
property point anchorPos: Qt.point(screen.width/2, screen.height - 100) visible: showWindowsMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
onAnchorItemChanged: updatePosition() property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
onVisibleChanged: if (visible) updatePosition()
function updatePosition() { onAnchorItemChanged: updatePosition()
if (!anchorItem) { onVisibleChanged: if (visible)
anchorPos = Qt.point(screen.width/2, screen.height - 100) updatePosition()
return
}
var dockWindow = anchorItem.Window.window function updatePosition() {
if (!dockWindow) { if (!anchorItem) {
anchorPos = Qt.point(screen.width/2, screen.height - 100) anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return return
}
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
var actualDockHeight = root.dockVisibleHeight // fallback
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item
}
for (var i = 0; i < item.children.length; i++) {
var found = findDockBackground(item.children[i])
if (found) return found
}
return null
}
var dockBackground = findDockBackground(dockWindow.contentItem)
if (dockBackground) {
actualDockHeight = dockBackground.height
}
var dockBottomMargin = 16 // The dock has bottom margin
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
var dockContentWidth = dockWindow.width
var screenWidth = root.screen.width
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width/2
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
} }
var dockWindow = anchorItem.Window.window
if (!dockWindow) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
}
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
var actualDockHeight = root.dockVisibleHeight // fallback
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item
}
for (var i = 0; i < item.children.length; i++) {
var found = findDockBackground(item.children[i])
if (found)
return found
}
return null
}
var dockBackground = findDockBackground(dockWindow.contentItem)
if (dockBackground) {
actualDockHeight = dockBackground.height
}
var dockBottomMargin = 16 // The dock has bottom margin
var buttonScreenY = root.screen.height - actualDockHeight - dockBottomMargin - 20
var dockContentWidth = dockWindow.width
var screenWidth = root.screen.width
var dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
var buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
}
Rectangle {
id: menuContainer
width: Math.min(600,
Math.max(250,
windowColumn.implicitWidth + Theme.spacingS * 2))
height: Math.max(60, windowColumn.implicitHeight + Theme.spacingS * 2)
x: {
var left = 10
var right = root.width - width - 10
var want = root.anchorPos.x - width / 2
return Math.max(left, Math.min(right, want))
}
y: Math.max(10, root.anchorPos.y - height + 30)
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
opacity: showWindowsMenu ? 1 : 0
scale: showWindowsMenu ? 1 : 0.85
Rectangle { Rectangle {
id: menuContainer anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
width: Math.min(600, Math.max(250, windowColumn.implicitWidth + Theme.spacingS * 2)) Column {
height: Math.max(60, windowColumn.implicitHeight + Theme.spacingS * 2) id: windowColumn
width: parent.width - Theme.spacingS * 2
x: { anchors.horizontalCenter: parent.horizontalCenter
var left = 10 anchors.top: parent.top
var right = root.width - width - 10 anchors.topMargin: Theme.spacingS
var want = root.anchorPos.x - width/2 spacing: 1
return Math.max(left, Math.min(right, want))
}
y: Math.max(10, root.anchorPos.y - height + 30)
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: showWindowsMenu ? 1 : 0
scale: showWindowsMenu ? 1 : 0.85
Repeater {
model: root.appData
&& root.appData.windows ? root.appData.windows : null
Rectangle { Rectangle {
required property var model
width: windowColumn.width
height: 32
radius: Theme.cornerRadiusSmall
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: model.title || "Untitled Window"
font.pixelSize: Theme.fontSizeSmall
color: model.is_focused ? Theme.primary : Theme.surfaceText
font.weight: model.is_focused ? Font.Medium : Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: windowArea
anchors.fill: parent anchors.fill: parent
anchors.topMargin: 4 hoverEnabled: true
anchors.leftMargin: 2 cursorShape: Qt.PointingHandCursor
anchors.rightMargin: -2 onClicked: {
anchors.bottomMargin: -4 NiriService.focusWindow(model.id)
radius: parent.radius root.close()
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Column {
id: windowColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
Repeater {
model: root.appData && root.appData.windows ? root.appData.windows : null
Rectangle {
required property var model
width: windowColumn.width
height: 32
radius: Theme.cornerRadiusSmall
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: model.title || "Untitled Window"
font.pixelSize: Theme.fontSizeSmall
color: model.is_focused ? Theme.primary : Theme.surfaceText
font.weight: model.is_focused ? Font.Medium : Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: windowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NiriService.focusWindow(model.id)
root.close()
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
} }
}
} }
}
} }
MouseArea { Behavior on opacity {
anchors.fill: parent NumberAnimation {
z: -1 duration: Theme.shortDuration
hoverEnabled: false easing.type: Theme.emphasizedEasing
acceptedButtons: Qt.LeftButton | Qt.RightButton }
onClicked: {
root.close()
}
} }
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
hoverEnabled: false
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
root.close()
}
}
} }

View File

@@ -1,4 +1,4 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -7,57 +7,57 @@ import Quickshell.Wayland
import qs.Common import qs.Common
Item { Item {
id: root id: root
function activate() { function activate() {
loader.activeAsync = true loader.activeAsync = true
}
LazyLoader {
id: loader
WlSessionLock {
id: lock
property bool unlocked: false
property string sharedPasswordBuffer: ""
locked: true
onLockedChanged: {
if (!locked)
loader.active = false
}
LockSurface {
lock: lock
sharedPasswordBuffer: lock.sharedPasswordBuffer
onPasswordChanged: newPassword => {
lock.sharedPasswordBuffer = newPassword
}
}
}
}
LockScreenDemo {
id: demoWindow
}
IpcHandler {
target: "lock"
function lock(): void {
console.log("Lock screen requested via IPC")
loader.activeAsync = true
} }
LazyLoader { function demo(): void {
id: loader console.log("Lock screen DEMO mode requested via IPC")
demoWindow.showDemo()
WlSessionLock {
id: lock
property bool unlocked: false
property string sharedPasswordBuffer: ""
locked: true
onLockedChanged: {
if (!locked)
loader.active = false
}
LockSurface {
lock: lock
sharedPasswordBuffer: lock.sharedPasswordBuffer
onPasswordChanged: (newPassword) => {
lock.sharedPasswordBuffer = newPassword
}
}
}
} }
LockScreenDemo { function isLocked(): bool {
id: demoWindow return loader.active
}
IpcHandler {
target: "lock"
function lock(): void {
console.log("Lock screen requested via IPC")
loader.activeAsync = true
}
function demo(): void {
console.log("Lock screen DEMO mode requested via IPC")
demoWindow.showDemo()
}
function isLocked(): bool {
return loader.active
}
} }
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -7,46 +7,46 @@ import qs.Common
import qs.Modals import qs.Modals
PanelWindow { PanelWindow {
id: root id: root
property bool demoActive: false property bool demoActive: false
visible: demoActive visible: demoActive
anchors { anchors {
top: true top: true
bottom: true bottom: true
left: true left: true
right: true right: true
} }
WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent" color: "transparent"
function showDemo(): void { function showDemo(): void {
console.log("Showing lock screen demo") console.log("Showing lock screen demo")
demoActive = true demoActive = true
} }
function hideDemo(): void { function hideDemo(): void {
console.log("Hiding lock screen demo") console.log("Hiding lock screen demo")
demoActive = false demoActive = false
} }
PowerConfirmModal { PowerConfirmModal {
id: powerModal id: powerModal
} }
Loader { Loader {
anchors.fill: parent anchors.fill: parent
active: demoActive active: demoActive
sourceComponent: LockScreenContent { sourceComponent: LockScreenContent {
demoMode: true demoMode: true
powerModal: powerModal powerModal: powerModal
onUnlockRequested: root.hideDemo() onUnlockRequested: root.hideDemo()
}
} }
}
} }

View File

@@ -1,4 +1,4 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -7,50 +7,50 @@ import qs.Common
import qs.Modals import qs.Modals
WlSessionLockSurface { WlSessionLockSurface {
id: root id: root
required property WlSessionLock lock required property WlSessionLock lock
required property string sharedPasswordBuffer required property string sharedPasswordBuffer
signal passwordChanged(string newPassword) signal passwordChanged(string newPassword)
property bool thisLocked: false property bool thisLocked: false
readonly property bool locked: thisLocked && !lock.unlocked readonly property bool locked: thisLocked && !lock.unlocked
function unlock(): void { function unlock(): void {
console.log("LockSurface.unlock() called") console.log("LockSurface.unlock() called")
lock.unlocked = true lock.unlocked = true
animDelay.start() animDelay.start()
} }
Component.onCompleted: { Component.onCompleted: {
thisLocked = true thisLocked = true
} }
color: "transparent" color: "transparent"
Timer { Timer {
id: animDelay id: animDelay
interval: 1500 // Longer delay for success feedback interval: 1500 // Longer delay for success feedback
onTriggered: root.lock.locked = false onTriggered: root.lock.locked = false
} }
PowerConfirmModal { PowerConfirmModal {
id: powerModal id: powerModal
} }
Loader { Loader {
anchors.fill: parent anchors.fill: parent
sourceComponent: LockScreenContent { sourceComponent: LockScreenContent {
demoMode: false demoMode: false
powerModal: powerModal powerModal: powerModal
passwordBuffer: root.sharedPasswordBuffer passwordBuffer: root.sharedPasswordBuffer
onUnlockRequested: root.unlock() onUnlockRequested: root.unlock()
onPasswordBufferChanged: { onPasswordBufferChanged: {
if (root.sharedPasswordBuffer !== passwordBuffer) { if (root.sharedPasswordBuffer !== passwordBuffer) {
root.passwordChanged(passwordBuffer) root.passwordChanged(passwordBuffer)
}
}
} }
}
} }
}
} }

View File

@@ -8,109 +8,112 @@ import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
property var modelData property var modelData
property bool micPopupVisible: false property bool micPopupVisible: false
function show() { function show() {
root.micPopupVisible = true; root.micPopupVisible = true
hideTimer.restart(); hideTimer.restart()
}
screen: modelData
visible: micPopupVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Timer {
id: hideTimer
interval: 2000
repeat: false
onTriggered: {
root.micPopupVisible = false
}
}
Connections {
function onMicMuteChanged() {
root.show()
} }
screen: modelData target: AudioService
visible: micPopupVisible }
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors { Rectangle {
top: true id: micPopup
left: true
right: true width: Theme.iconSize + Theme.spacingS * 2
bottom: true height: Theme.iconSize + Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
opacity: root.micPopupVisible ? 1 : 0
scale: root.micPopupVisible ? 1 : 0.9
layer.enabled: true
DankIcon {
id: micContent
anchors.centerIn: parent
name: AudioService.source && AudioService.source.audio
&& AudioService.source.audio.muted ? "mic_off" : "mic"
size: Theme.iconSize
color: AudioService.source && AudioService.source.audio
&& AudioService.source.audio.muted ? Theme.error : Theme.primary
} }
Timer { layer.effect: MultiEffect {
id: hideTimer shadowEnabled: true
shadowHorizontalOffset: 0
interval: 2000 shadowVerticalOffset: 4
repeat: false shadowBlur: 0.8
onTriggered: { shadowColor: Qt.rgba(0, 0, 0, 0.3)
root.micPopupVisible = false; shadowOpacity: 0.3
}
} }
Connections { transform: Translate {
function onMicMuteChanged() { y: root.micPopupVisible ? 0 : 20
root.show();
}
target: AudioService
} }
Rectangle { Behavior on opacity {
id: micPopup NumberAnimation {
duration: Theme.mediumDuration
width: Theme.iconSize + Theme.spacingS * 2 easing.type: Theme.emphasizedEasing
height: Theme.iconSize + Theme.spacingS * 2 }
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: root.micPopupVisible ? 1 : 0
scale: root.micPopupVisible ? 1 : 0.9
layer.enabled: true
DankIcon {
id: micContent
anchors.centerIn: parent
name: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? "mic_off" : "mic"
size: Theme.iconSize
color: AudioService.source && AudioService.source.audio && AudioService.source.audio.muted ? Theme.error : Theme.primary
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
transform: Translate {
y: root.micPopupVisible ? 0 : 20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on transform {
PropertyAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
} }
mask: Region { Behavior on scale {
item: micPopup NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
} }
Behavior on transform {
PropertyAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
mask: Region {
item: micPopup
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,186 +9,183 @@ import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
property bool notificationHistoryVisible: false property bool notificationHistoryVisible: false
property real triggerX: Screen.width - 400 - Theme.spacingL property real triggerX: Screen.width - 400 - Theme.spacingL
property real triggerY: Theme.barHeight + Theme.spacingXS property real triggerY: Theme.barHeight + Theme.spacingXS
property real triggerWidth: 40 property real triggerWidth: 40
property string triggerSection: "right" property string triggerSection: "right"
function setTriggerPosition(x, y, width, section) { function setTriggerPosition(x, y, width, section) {
triggerX = x; triggerX = x
triggerY = y; triggerY = y
triggerWidth = width; triggerWidth = width
triggerSection = section; triggerSection = section
}
visible: notificationHistoryVisible
onNotificationHistoryVisibleChanged: {
NotificationService.disablePopups(notificationHistoryVisible)
}
implicitWidth: 400
implicitHeight: Math.min(Screen.height * 0.8, 400)
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: notificationHistoryVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: {
notificationHistoryVisible = false
}
}
Rectangle {
id: mainRect
function calculateHeight() {
let baseHeight = Theme.spacingL * 2
baseHeight += notificationHeader.height
baseHeight += Theme.spacingM
let listHeight = notificationList.listContentHeight
if (NotificationService.groupedNotifications.length === 0)
listHeight = 200
baseHeight += Math.min(listHeight, 600)
return Math.max(300, baseHeight)
} }
visible: notificationHistoryVisible readonly property real popupWidth: 400
onNotificationHistoryVisibleChanged: { readonly property real calculatedX: {
NotificationService.disablePopups(notificationHistoryVisible); var centerX = root.triggerX + (root.triggerWidth / 2) - (popupWidth / 2)
}
implicitWidth: 400
implicitHeight: Math.min(Screen.height * 0.8, 400)
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: notificationHistoryVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
anchors { if (centerX >= Theme.spacingM
top: true && centerX + popupWidth <= Screen.width - Theme.spacingM) {
left: true return centerX
right: true }
bottom: true
if (centerX < Theme.spacingM) {
return Theme.spacingM
}
if (centerX + popupWidth > Screen.width - Theme.spacingM) {
return Screen.width - popupWidth - Theme.spacingM
}
return centerX
} }
width: popupWidth
height: calculateHeight()
x: calculatedX
y: root.triggerY
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
opacity: notificationHistoryVisible ? 1 : 0
scale: notificationHistoryVisible ? 1 : 0.9
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
notificationHistoryVisible = false;
} }
} }
Rectangle { Column {
id: mainRect id: contentColumn
function calculateHeight() { anchors.fill: parent
let baseHeight = Theme.spacingL * 2; anchors.margins: Theme.spacingL
baseHeight += notificationHeader.height; spacing: Theme.spacingM
baseHeight += Theme.spacingM; focus: true
let listHeight = notificationList.listContentHeight; Component.onCompleted: {
if (NotificationService.groupedNotifications.length === 0) if (notificationHistoryVisible)
listHeight = 200; forceActiveFocus()
}
baseHeight += Math.min(listHeight, 600); Keys.onPressed: function (event) {
return Math.max(300, baseHeight); if (event.key === Qt.Key_Escape) {
notificationHistoryVisible = false
event.accepted = true
} }
}
readonly property real popupWidth: 400 Connections {
readonly property real calculatedX: { function onNotificationHistoryVisibleChanged() {
var centerX = root.triggerX + (root.triggerWidth / 2) - (popupWidth / 2); if (notificationHistoryVisible)
Qt.callLater(function () {
if (centerX >= Theme.spacingM && centerX + popupWidth <= Screen.width - Theme.spacingM) { contentColumn.forceActiveFocus()
return centerX; })
} else
contentColumn.focus = false
if (centerX < Theme.spacingM) {
return Theme.spacingM;
}
if (centerX + popupWidth > Screen.width - Theme.spacingM) {
return Screen.width - popupWidth - Theme.spacingM;
}
return centerX;
} }
target: root
}
width: popupWidth NotificationHeader {
height: calculateHeight() id: notificationHeader
x: calculatedX }
y: root.triggerY
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: notificationHistoryVisible ? 1 : 0
scale: notificationHistoryVisible ? 1 : 0.9
MouseArea { NotificationList {
anchors.fill: parent id: notificationList
onClicked: {
}
}
Column {
id: contentColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
focus: true
Component.onCompleted: {
if (notificationHistoryVisible)
forceActiveFocus();
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
notificationHistoryVisible = false;
event.accepted = true;
}
}
Connections {
function onNotificationHistoryVisibleChanged() {
if (notificationHistoryVisible)
Qt.callLater(function() {
contentColumn.forceActiveFocus();
});
else
contentColumn.focus = false;
}
target: root
}
NotificationHeader {
id: notificationHeader
}
NotificationList {
id: notificationList
width: parent.width
height: parent.height - notificationHeader.height - contentColumn.spacing
}
}
Connections {
function onNotificationsChanged() {
mainRect.height = mainRect.calculateHeight();
}
function onGroupedNotificationsChanged() {
mainRect.height = mainRect.calculateHeight();
}
function onExpandedGroupsChanged() {
mainRect.height = mainRect.calculateHeight();
}
function onExpandedMessagesChanged() {
mainRect.height = mainRect.calculateHeight();
}
target: NotificationService
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
width: parent.width
height: parent.height - notificationHeader.height - contentColumn.spacing
}
} }
Connections {
function onNotificationsChanged() {
mainRect.height = mainRect.calculateHeight()
}
function onGroupedNotificationsChanged() {
mainRect.height = mainRect.calculateHeight()
}
function onExpandedGroupsChanged() {
mainRect.height = mainRect.calculateHeight()
}
function onExpandedMessagesChanged() {
mainRect.height = mainRect.calculateHeight()
}
target: NotificationService
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
}
} }

View File

@@ -4,33 +4,33 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
width: parent.width width: parent.width
height: 200 height: 200
visible: NotificationService.notifications.length === 0 visible: NotificationService.notifications.length === 0
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
width: parent.width * 0.8 width: parent.width * 0.8
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "notifications_none"
size: Theme.iconSizeLarge + 16
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Nothing to see here"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "notifications_none"
size: Theme.iconSizeLarge + 16
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
} }
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: "Nothing to see here"
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
}
} }

View File

@@ -5,132 +5,129 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
width: parent.width width: parent.width
height: 32 height: 32
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
StyledText {
text: "Notifications"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
DankActionButton {
id: doNotDisturbButton
iconName: SessionData.doNotDisturb ? "notifications_off" : "notifications"
iconColor: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText
buttonSize: 28
anchors.verticalCenter: parent.verticalCenter
onClicked: SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
Rectangle {
id: doNotDisturbTooltip
width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadiusSmall
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: doNotDisturbButton.children[1].containsMouse // Access StateLayer's containsMouse
opacity: visible ? 1 : 0
StyledText { StyledText {
text: "Notifications" id: tooltipText
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText text: "Do Not Disturb"
font.weight: Font.Medium font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
font.hintingPreference: Font.PreferFullHinting
} }
DankActionButton { Behavior on opacity {
id: doNotDisturbButton NumberAnimation {
duration: Theme.shortDuration
iconName: SessionData.doNotDisturb ? "notifications_off" : "notifications" easing.type: Theme.standardEasing
iconColor: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText }
buttonSize: 28
anchors.verticalCenter: parent.verticalCenter
onClicked: SessionData.setDoNotDisturb(!SessionData.doNotDisturb)
Rectangle {
id: doNotDisturbTooltip
width: tooltipText.contentWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadiusSmall
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: doNotDisturbButton.children[1].containsMouse // Access StateLayer's containsMouse
opacity: visible ? 1 : 0
StyledText {
id: tooltipText
text: "Do Not Disturb"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
font.hintingPreference: Font.PreferFullHinting
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
} }
}
} }
}
Rectangle { Rectangle {
id: clearAllButton id: clearAllButton
width: 120 width: 120
height: 28 height: 28
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: NotificationService.notifications.length > 0
color: clearArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: clearArea.containsMouse ? Theme.primary : Qt.rgba(
Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "delete_sweep"
size: Theme.iconSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NotificationService.notifications.length > 0 }
color: clearArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: clearArea.containsMouse ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "delete_sweep"
size: Theme.iconSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Clear All"
font.pixelSize: Theme.fontSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.clearAllNotifications()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
StyledText {
text: "Clear All"
font.pixelSize: Theme.fontSizeSmall
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
} }
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.clearAllNotifications()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
} }

View File

@@ -5,24 +5,23 @@ import qs.Services
import qs.Widgets import qs.Widgets
DankListView { DankListView {
id: root id: root
property alias count: root.count property alias count: root.count
property alias listContentHeight: root.contentHeight property alias listContentHeight: root.contentHeight
width: parent.width width: parent.width
height: parent.height height: parent.height
clip: true clip: true
model: NotificationService.groupedNotifications model: NotificationService.groupedNotifications
spacing: Theme.spacingL spacing: Theme.spacingL
NotificationEmptyState { NotificationEmptyState {
visible: root.count === 0 visible: root.count === 0
anchors.centerIn: parent anchors.centerIn: parent
} }
delegate: NotificationCard {
delegate: NotificationCard { notificationGroup: modelData
notificationGroup: modelData }
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,242 +4,246 @@ import qs.Common
import qs.Services import qs.Services
QtObject { QtObject {
id: manager id: manager
property var modelData property var modelData
property int topMargin: 0 property int topMargin: 0
property int baseNotificationHeight: 120 property int baseNotificationHeight: 120
property int maxTargetNotifications: 3 property int maxTargetNotifications: 3
property var popupWindows: [] // strong refs to windows (live until exitFinished) property var popupWindows: [] // strong refs to windows (live until exitFinished)
property var destroyingWindows: new Set() property var destroyingWindows: new Set()
property Component popupComponent property Component popupComponent
popupComponent: Component { popupComponent: Component {
NotificationPopup { NotificationPopup {
onEntered: manager._onPopupEntered(this) onEntered: manager._onPopupEntered(this)
onExitFinished: manager._onPopupExitFinished(this) onExitFinished: manager._onPopupExitFinished(this)
} }
}
property Connections notificationConnections
notificationConnections: Connections {
function onVisibleNotificationsChanged() {
manager._sync(NotificationService.visibleNotifications)
} }
property Connections notificationConnections target: NotificationService
}
notificationConnections: Connections { property Timer sweeper
function onVisibleNotificationsChanged() {
manager._sync(NotificationService.visibleNotifications); sweeper: Timer {
interval: 2000
running: false // Not running by default
repeat: true
onTriggered: {
let toRemove = []
for (let p of popupWindows) {
if (!p) {
toRemove.push(p)
continue
} }
const isZombie = p.status === Component.Null || (!p.visible
&& !p.exiting)
|| (!p.notificationData && !p._isDestroying)
|| (!p.hasValidData && !p._isDestroying)
if (isZombie) {
target: NotificationService toRemove.push(p)
} if (p.forceExit) {
p.forceExit()
} else if (p.destroy) {
try {
p.destroy()
} catch (e) {
property Timer sweeper
sweeper: Timer {
interval: 2000
running: false // Not running by default
repeat: true
onTriggered: {
let toRemove = [];
for (let p of popupWindows) {
if (!p) {
toRemove.push(p);
continue;
}
const isZombie = p.status === Component.Null || (!p.visible && !p.exiting) || (!p.notificationData && !p._isDestroying) || (!p.hasValidData && !p._isDestroying);
if (isZombie) {
toRemove.push(p);
if (p.forceExit) {
p.forceExit();
} else if (p.destroy) {
try {
p.destroy();
} catch (e) {
}
}
}
} }
if (toRemove.length > 0) { }
for (let zombie of toRemove) {
const i = popupWindows.indexOf(zombie);
if (i !== -1)
popupWindows.splice(i, 1);
}
popupWindows = popupWindows.slice();
const survivors = _active().sort((a, b) => {
return a.screenY - b.screenY;
});
for (let k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight;
}
}
if (popupWindows.length === 0)
sweeper.stop();
} }
} }
if (toRemove.length > 0) {
function _hasWindowFor(w) { for (let zombie of toRemove) {
return popupWindows.some((p) => { const i = popupWindows.indexOf(zombie)
return p && p.notificationData === w && !p._isDestroying && p.status !== Component.Null; if (i !== -1)
}); popupWindows.splice(i, 1)
}
function _isValidWindow(p) {
return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData;
}
function _sync(newWrappers) {
for (let w of newWrappers) {
if (w && !_hasWindowFor(w))
insertNewestAtTop(w);
} }
for (let p of popupWindows.slice()) { popupWindows = popupWindows.slice()
if (!_isValidWindow(p))
continue;
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) {
p.notificationData.removedByLimit = true;
p.notificationData.popup = false;
}
}
}
function insertNewestAtTop(wrapper) {
if (!wrapper) {
return ;
}
for (let p of popupWindows) {
if (!_isValidWindow(p))
continue;
if (p.exiting)
continue;
p.screenY = p.screenY + baseNotificationHeight;
}
const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : "";
const win = popupComponent.createObject(null, {
"notificationData": wrapper,
"notificationId": notificationId,
"screenY": topMargin,
"screen": manager.modelData
});
if (!win) {
return ;
}
if (!win.hasValidData) {
win.destroy();
return ;
}
popupWindows.push(win);
if (!sweeper.running)
sweeper.start();
_maybeStartOverflow();
}
function _active() {
return popupWindows.filter((p) => {
return _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting;
});
}
function _bottom() {
let b = null, maxY = -1;
for (let p of _active()) {
if (p.screenY > maxY) {
maxY = p.screenY;
b = p;
}
}
return b;
}
function _maybeStartOverflow() {
const activeWindows = _active();
if (activeWindows.length <= maxTargetNotifications + 1)
return ;
const b = _bottom();
if (b && !b.exiting) {
b.notificationData.removedByLimit = true;
b.notificationData.popup = false;
}
}
function _onPopupEntered(p) {
if (_isValidWindow(p))
_maybeStartOverflow();
}
function _onPopupExitFinished(p) {
if (!p)
return ;
const windowId = p.toString();
if (destroyingWindows.has(windowId))
return ;
destroyingWindows.add(windowId);
const i = popupWindows.indexOf(p);
if (i !== -1) {
popupWindows.splice(i, 1);
popupWindows = popupWindows.slice();
}
if (NotificationService.releaseWrapper && p.notificationData)
NotificationService.releaseWrapper(p.notificationData);
Qt.callLater(() => {
if (p && p.destroy) {
try {
p.destroy();
} catch (e) {
}
}
Qt.callLater(() => {
destroyingWindows.delete(windowId);
});
});
const survivors = _active().sort((a, b) => { const survivors = _active().sort((a, b) => {
return a.screenY - b.screenY; return a.screenY - b.screenY
}); })
for (let k = 0; k < survivors.length; ++k) { for (var k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight; survivors[k].screenY = topMargin + k * baseNotificationHeight
} }
_maybeStartOverflow(); }
if (popupWindows.length === 0)
sweeper.stop()
} }
}
function cleanupAllWindows() { function _hasWindowFor(w) {
sweeper.stop(); return popupWindows.some(p => {
for (let p of popupWindows.slice()) { return p && p.notificationData === w
if (p) { && !p._isDestroying
try { && p.status !== Component.Null
if (p.forceExit) })
p.forceExit(); }
else if (p.destroy)
p.destroy(); function _isValidWindow(p) {
} catch (e) { return p && p.status !== Component.Null && !p._isDestroying
&& p.hasValidData
}
function _sync(newWrappers) {
for (let w of newWrappers) {
if (w && !_hasWindowFor(w))
insertNewestAtTop(w)
}
for (let p of popupWindows.slice()) {
if (!_isValidWindow(p))
continue
if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1
&& !p.exiting) {
p.notificationData.removedByLimit = true
p.notificationData.popup = false
}
}
}
function insertNewestAtTop(wrapper) {
if (!wrapper) {
return
}
for (let p of popupWindows) {
if (!_isValidWindow(p))
continue
if (p.exiting)
continue
p.screenY = p.screenY + baseNotificationHeight
}
const notificationId = wrapper
&& wrapper.notification ? wrapper.notification.id : ""
const win = popupComponent.createObject(null, {
"notificationData": wrapper,
"notificationId": notificationId,
"screenY": topMargin,
"screen": manager.modelData
})
if (!win) {
return
}
if (!win.hasValidData) {
win.destroy()
return
}
popupWindows.push(win)
if (!sweeper.running)
sweeper.start()
_maybeStartOverflow()
}
function _active() {
return popupWindows.filter(p => {
return _isValidWindow(p) && p.notificationData
&& p.notificationData.popup && !p.exiting
})
}
function _bottom() {
let b = null, maxY = -1
for (let p of _active()) {
if (p.screenY > maxY) {
maxY = p.screenY
b = p
}
}
return b
}
function _maybeStartOverflow() {
const activeWindows = _active()
if (activeWindows.length <= maxTargetNotifications + 1)
return
const b = _bottom()
if (b && !b.exiting) {
b.notificationData.removedByLimit = true
b.notificationData.popup = false
}
}
function _onPopupEntered(p) {
if (_isValidWindow(p))
_maybeStartOverflow()
}
function _onPopupExitFinished(p) {
if (!p)
return
const windowId = p.toString()
if (destroyingWindows.has(windowId))
return
destroyingWindows.add(windowId)
const i = popupWindows.indexOf(p)
if (i !== -1) {
popupWindows.splice(i, 1)
popupWindows = popupWindows.slice()
}
if (NotificationService.releaseWrapper && p.notificationData)
NotificationService.releaseWrapper(p.notificationData)
Qt.callLater(() => {
if (p && p.destroy) {
try {
p.destroy()
} catch (e) {
}
}
Qt.callLater(() => {
destroyingWindows.delete(windowId)
})
})
const survivors = _active().sort((a, b) => {
return a.screenY - b.screenY
})
for (var k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight
}
_maybeStartOverflow()
}
function cleanupAllWindows() {
sweeper.stop()
for (let p of popupWindows.slice()) {
if (p) {
try {
if (p.forceExit)
p.forceExit()
else if (p.destroy)
p.destroy()
} catch (e) {
}
}
} }
popupWindows = []; }
destroyingWindows.clear();
} }
popupWindows = []
destroyingWindows.clear()
}
onPopupWindowsChanged: { onPopupWindowsChanged: {
if (popupWindows.length > 0 && !sweeper.running) if (popupWindows.length > 0 && !sweeper.running)
sweeper.start(); sweeper.start()
else if (popupWindows.length === 0 && sweeper.running) else if (popupWindows.length === 0 && sweeper.running)
sweeper.stop(); sweeper.stop()
} }
} }

View File

@@ -5,476 +5,474 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
function formatNetworkSpeed(bytesPerSec) { function formatNetworkSpeed(bytesPerSec) {
if (bytesPerSec < 1024) if (bytesPerSec < 1024)
return bytesPerSec.toFixed(0) + " B/s"; return bytesPerSec.toFixed(0) + " B/s"
else if (bytesPerSec < 1024 * 1024) else if (bytesPerSec < 1024 * 1024)
return (bytesPerSec / 1024).toFixed(1) + " KB/s"; return (bytesPerSec / 1024).toFixed(1) + " KB/s"
else if (bytesPerSec < 1024 * 1024 * 1024) else if (bytesPerSec < 1024 * 1024 * 1024)
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"; return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
else else
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"; return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
} }
function formatDiskSpeed(bytesPerSec) { function formatDiskSpeed(bytesPerSec) {
if (bytesPerSec < 1024 * 1024) if (bytesPerSec < 1024 * 1024)
return (bytesPerSec / 1024).toFixed(1) + " KB/s"; return (bytesPerSec / 1024).toFixed(1) + " KB/s"
else if (bytesPerSec < 1024 * 1024 * 1024) else if (bytesPerSec < 1024 * 1024 * 1024)
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"; return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
else else
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"; return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
} }
anchors.fill: parent anchors.fill: parent
spacing: Theme.spacingM spacing: Theme.spacingM
Component.onCompleted: { Component.onCompleted: {
SysMonitorService.addRef(); SysMonitorService.addRef()
} }
Component.onDestruction: { Component.onDestruction: {
SysMonitorService.removeRef(); SysMonitorService.removeRef()
} }
Rectangle { Rectangle {
width: parent.width
height: 200
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width width: parent.width
height: 200 height: 32
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
height: 32
spacing: Theme.spacingM
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 80
height: 24
radius: Theme.cornerRadiusSmall
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.totalCpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
anchors.centerIn: parent
}
}
Item {
width: parent.width - 280
height: 1
}
StyledText {
text: SysMonitorService.cpuCount + " cores"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
ScrollView {
width: parent.width
height: parent.height - 40
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width
spacing: 6
Repeater {
model: SysMonitorService.perCoreCpuUsage
Row {
width: parent.width
height: 20
spacing: Theme.spacingS
StyledText {
text: "C" + index
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 24
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: parent.width - 80
height: 6
radius: 3
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: parent.width * Math.min(1, modelData / 100)
height: parent.height
radius: parent.radius
color: {
const usage = modelData;
if (usage > 80)
return Theme.error;
if (usage > 60)
return Theme.warning;
return Theme.primary;
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
}
StyledText {
text: modelData ? modelData.toFixed(0) + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: 32
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Row {
anchors.centerIn: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: SysMonitorService.formatSystemMemory(SysMonitorService.usedMemoryKB) + " / " + SysMonitorService.formatSystemMemory(SysMonitorService.totalMemoryKB)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: SysMonitorService.totalMemoryKB > 0 ? parent.width * (SysMonitorService.usedMemoryKB / SysMonitorService.totalMemoryKB) : 0
height: parent.height
radius: parent.radius
color: {
const usage = SysMonitorService.totalMemoryKB > 0 ? (SysMonitorService.usedMemoryKB / SysMonitorService.totalMemoryKB) : 0;
if (usage > 0.9)
return Theme.error;
if (usage > 0.7)
return Theme.warning;
return Theme.secondary;
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: SysMonitorService.totalMemoryKB > 0 ? ((SysMonitorService.usedMemoryKB / SysMonitorService.totalMemoryKB) * 100).toFixed(1) + "% used" : "No data"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "Swap"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: SysMonitorService.totalSwapKB > 0 ? SysMonitorService.formatSystemMemory(SysMonitorService.usedSwapKB) + " / " + SysMonitorService.formatSystemMemory(SysMonitorService.totalSwapKB) : "No swap configured"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: SysMonitorService.totalSwapKB > 0 ? parent.width * (SysMonitorService.usedSwapKB / SysMonitorService.totalSwapKB) : 0
height: parent.height
radius: parent.radius
color: {
if (!SysMonitorService.totalSwapKB)
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3);
const usage = SysMonitorService.usedSwapKB / SysMonitorService.totalSwapKB;
if (usage > 0.9)
return Theme.error;
if (usage > 0.7)
return Theme.warning;
return Theme.info;
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: SysMonitorService.totalSwapKB > 0 ? ((SysMonitorService.usedSwapKB / SysMonitorService.totalSwapKB) * 100).toFixed(1) + "% used" : "Not available"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
Row {
width: parent.width
height: 80
spacing: Theme.spacingM spacing: Theme.spacingM
Rectangle { StyledText {
width: (parent.width - Theme.spacingM) / 2 text: "CPU"
height: 80 font.pixelSize: Theme.fontSizeLarge
radius: Theme.cornerRadiusLarge font.weight: Font.Bold
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) color: Theme.surfaceText
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) anchors.verticalCenter: parent.verticalCenter
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Network"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "↓"
font.pixelSize: Theme.fontSizeSmall
color: Theme.info
}
StyledText {
text: SysMonitorService.networkRxRate > 0 ? formatNetworkSpeed(SysMonitorService.networkRxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "↑"
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
}
StyledText {
text: SysMonitorService.networkTxRate > 0 ? formatNetworkSpeed(SysMonitorService.networkTxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
} }
Rectangle { Rectangle {
width: (parent.width - Theme.spacingM) / 2 width: 80
height: 80 height: 24
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusSmall
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04) color: Qt.rgba(Theme.primary.r, Theme.primary.g,
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06) Theme.primary.b, 0.12)
border.width: 1 anchors.verticalCenter: parent.verticalCenter
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Disk"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "R"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
}
StyledText {
text: formatDiskSpeed(SysMonitorService.diskReadRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "W"
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
}
StyledText {
text: formatDiskSpeed(SysMonitorService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
StyledText {
text: SysMonitorService.totalCpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
anchors.centerIn: parent
}
} }
Item {
width: parent.width - 280
height: 1
}
StyledText {
text: SysMonitorService.cpuCount + " cores"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
ScrollView {
width: parent.width
height: parent.height - 40
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width
spacing: 6
Repeater {
model: SysMonitorService.perCoreCpuUsage
Row {
width: parent.width
height: 20
spacing: Theme.spacingS
StyledText {
text: "C" + index
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 24
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: parent.width - 80
height: 6
radius: 3
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: parent.width * Math.min(1, modelData / 100)
height: parent.height
radius: parent.radius
color: {
const usage = modelData
if (usage > 80)
return Theme.error
if (usage > 60)
return Theme.warning
return Theme.primary
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
}
}
}
}
StyledText {
text: modelData ? modelData.toFixed(0) + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: 32
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06)
border.width: 1
Row {
anchors.centerIn: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: SysMonitorService.formatSystemMemory(
SysMonitorService.usedMemoryKB) + " / " + SysMonitorService.formatSystemMemory(
SysMonitorService.totalMemoryKB)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: SysMonitorService.totalMemoryKB
> 0 ? parent.width * (SysMonitorService.usedMemoryKB
/ SysMonitorService.totalMemoryKB) : 0
height: parent.height
radius: parent.radius
color: {
const usage = SysMonitorService.totalMemoryKB
> 0 ? (SysMonitorService.usedMemoryKB
/ SysMonitorService.totalMemoryKB) : 0
if (usage > 0.9)
return Theme.error
if (usage > 0.7)
return Theme.warning
return Theme.secondary
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: SysMonitorService.totalMemoryKB
> 0 ? ((SysMonitorService.usedMemoryKB
/ SysMonitorService.totalMemoryKB) * 100).toFixed(
1) + "% used" : "No data"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "Swap"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
text: SysMonitorService.totalSwapKB
> 0 ? SysMonitorService.formatSystemMemory(
SysMonitorService.usedSwapKB) + " / "
+ SysMonitorService.formatSystemMemory(
SysMonitorService.totalSwapKB) : "No swap configured"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: SysMonitorService.totalSwapKB
> 0 ? parent.width * (SysMonitorService.usedSwapKB
/ SysMonitorService.totalSwapKB) : 0
height: parent.height
radius: parent.radius
color: {
if (!SysMonitorService.totalSwapKB)
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
const usage = SysMonitorService.usedSwapKB / SysMonitorService.totalSwapKB
if (usage > 0.9)
return Theme.error
if (usage > 0.7)
return Theme.warning
return Theme.info
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
}
StyledText {
text: SysMonitorService.totalSwapKB
> 0 ? ((SysMonitorService.usedSwapKB
/ SysMonitorService.totalSwapKB) * 100).toFixed(
1) + "% used" : "Not available"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
Row {
width: parent.width
height: 80
spacing: Theme.spacingM
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Network"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "↓"
font.pixelSize: Theme.fontSizeSmall
color: Theme.info
}
StyledText {
text: SysMonitorService.networkRxRate
> 0 ? formatNetworkSpeed(
SysMonitorService.networkRxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "↑"
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
}
StyledText {
text: SysMonitorService.networkTxRate
> 0 ? formatNetworkSpeed(
SysMonitorService.networkTxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
} }
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: "Disk"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "R"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
}
StyledText {
text: formatDiskSpeed(SysMonitorService.diskReadRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
StyledText {
text: "W"
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
}
StyledText {
text: formatDiskSpeed(SysMonitorService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
}
}
} }

View File

@@ -7,223 +7,238 @@ import qs.Services
import qs.Widgets import qs.Widgets
Popup { Popup {
id: processContextMenu id: processContextMenu
property var processData: null property var processData: null
function show(x, y) { function show(x, y) {
if (!processContextMenu.parent && typeof Overlay !== "undefined" && Overlay.overlay) if (!processContextMenu.parent && typeof Overlay !== "undefined"
processContextMenu.parent = Overlay.overlay; && Overlay.overlay)
processContextMenu.parent = Overlay.overlay
const menuWidth = 180; const menuWidth = 180
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2; const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
const screenWidth = Screen.width; const screenWidth = Screen.width
const screenHeight = Screen.height; const screenHeight = Screen.height
let finalX = x; let finalX = x
let finalY = y; let finalY = y
if (x + menuWidth > screenWidth - 20) if (x + menuWidth > screenWidth - 20)
finalX = x - menuWidth; finalX = x - menuWidth
if (y + menuHeight > screenHeight - 20) if (y + menuHeight > screenHeight - 20)
finalY = y - menuHeight; finalY = y - menuHeight
processContextMenu.x = Math.max(20, finalX); processContextMenu.x = Math.max(20, finalX)
processContextMenu.y = Math.max(20, finalY); processContextMenu.y = Math.max(20, finalY)
open(); open()
}
width: 180
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape
}
onOpened: {
outsideClickTimer.start()
}
Timer {
id: outsideClickTimer
interval: 100
onTriggered: {
processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
} }
}
width: 180 background: Rectangle {
height: menuColumn.implicitHeight + Theme.spacingS * 2 color: "transparent"
padding: 0 }
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape;
}
onOpened: {
outsideClickTimer.start();
}
Timer { contentItem: Rectangle {
id: outsideClickTimer id: menuContent
interval: 100 color: Theme.popupBackground()
onTriggered: { radius: Theme.cornerRadiusLarge
processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside; border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: copyPidArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Copy PID"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
} }
}
background: Rectangle { MouseArea {
id: copyPidArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(
["wl-copy", processContextMenu.processData.pid.toString()])
processContextMenu.close()
}
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: copyNameArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Copy Process Name"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
MouseArea {
id: copyNameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData) {
let processName = processContextMenu.processData.displayName
|| processContextMenu.processData.command
Quickshell.execDetached(["wl-copy", processName])
}
processContextMenu.close()
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent" color: "transparent"
}
contentItem: Rectangle { Rectangle {
id: menuContent anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
color: Theme.popupBackground() Rectangle {
radius: Theme.cornerRadiusLarge width: parent.width
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) height: 28
border.width: 1 radius: Theme.cornerRadiusSmall
color: killArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g,
Column { Theme.error.b,
id: menuColumn 0.12) : "transparent"
enabled: processContextMenu.processData
anchors.fill: parent opacity: enabled ? 1 : 0.5
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: copyPidArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Copy PID"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
MouseArea {
id: copyPidArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(["wl-copy", processContextMenu.processData.pid.toString()]);
processContextMenu.close();
}
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: copyNameArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Copy Process Name"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
}
MouseArea {
id: copyNameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenu.processData) {
let processName = processContextMenu.processData.displayName || processContextMenu.processData.command;
Quickshell.execDetached(["wl-copy", processName]);
}
processContextMenu.close();
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: killArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
enabled: processContextMenu.processData
opacity: enabled ? 1 : 0.5
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Kill Process"
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (killArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
}
MouseArea {
id: killArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(["kill", processContextMenu.processData.pid.toString()]);
processContextMenu.close();
}
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: forceKillArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
enabled: processContextMenu.processData && processContextMenu.processData.pid > 1000
opacity: enabled ? 1 : 0.5
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Force Kill Process"
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (forceKillArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
}
MouseArea {
id: forceKillArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(["kill", "-9", processContextMenu.processData.pid.toString()]);
processContextMenu.close();
}
}
}
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Kill Process"
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (killArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(
Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
} }
} MouseArea {
id: killArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(
["kill", processContextMenu.processData.pid.toString()])
processContextMenu.close()
}
}
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadiusSmall
color: forceKillArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.12) : "transparent"
enabled: processContextMenu.processData
&& processContextMenu.processData.pid > 1000
opacity: enabled ? 1 : 0.5
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: "Force Kill Process"
font.pixelSize: Theme.fontSizeSmall
color: parent.enabled ? (forceKillArea.containsMouse ? Theme.error : Theme.surfaceText) : Qt.rgba(
Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
font.weight: Font.Normal
}
MouseArea {
id: forceKillArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenu.processData)
Quickshell.execDetached(
["kill", "-9", processContextMenu.processData.pid.toString(
)])
processContextMenu.close()
}
}
}
}
}
} }

View File

@@ -4,212 +4,227 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: processItem id: processItem
property var process: null property var process: null
property var contextMenu: null property var contextMenu: null
width: parent ? parent.width : 0 width: parent ? parent.width : 0
height: 40 height: 40
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent" color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r,
border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" Theme.primary.g,
border.width: 1 Theme.primary.b,
0.08) : "transparent"
border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
border.width: 1
MouseArea { MouseArea {
id: processMouseArea id: processMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process
let globalPos = processMouseArea.mapToGlobal(mouse.x,
mouse.y)
let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(
globalPos.x,
globalPos.y) : globalPos
contextMenu.show(localPos.x, localPos.y)
}
}
}
onPressAndHold: {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process
let globalPos = processMouseArea.mapToGlobal(
processMouseArea.width / 2, processMouseArea.height / 2)
contextMenu.show(globalPos.x, globalPos.y)
}
}
}
Item {
anchors.fill: parent
anchors.margins: 8
DankIcon {
id: processIcon
name: SysMonitorService.getProcessIcon(process ? process.command : "")
size: Theme.iconSize - 4
color: {
if (process && process.cpu > 80)
return Theme.error
if (process && process.cpu > 50)
return Theme.warning
return Theme.surfaceText
}
opacity: 0.8
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: process ? process.displayName : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: 250
elide: Text.ElideRight
anchors.left: processIcon.right
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: cpuBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.cpu > 80)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
if (process && process.cpu > 50)
return Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.12)
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.08)
}
anchors.right: parent.right
anchors.rightMargin: 194
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.formatCpuUsage(process ? process.cpu : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.cpu > 80)
return Theme.error
if (process && process.cpu > 50)
return Theme.warning
return Theme.surfaceText
}
anchors.centerIn: parent
}
}
Rectangle {
id: memoryBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.memoryKB > 1024 * 1024)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
if (process && process.memoryKB > 512 * 1024)
return Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.12)
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.08)
}
anchors.right: parent.right
anchors.rightMargin: 102
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.formatMemoryUsage(
process ? process.memoryKB : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.memoryKB > 1024 * 1024)
return Theme.error
if (process && process.memoryKB > 512 * 1024)
return Theme.warning
return Theme.surfaceText
}
anchors.centerIn: parent
}
}
StyledText {
text: process ? process.pid.toString() : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
width: 50
horizontalAlignment: Text.AlignRight
anchors.right: parent.right
anchors.rightMargin: 40
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: menuButton
width: 28
height: 28
radius: Theme.cornerRadius
color: menuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "more_vert"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: menuButtonArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: {
onClicked: (mouse) => { if (process && process.pid > 0 && contextMenu) {
if (mouse.button === Qt.RightButton) { contextMenu.processData = process
if (process && process.pid > 0 && contextMenu) { let globalPos = menuButtonArea.mapToGlobal(
contextMenu.processData = process; menuButtonArea.width / 2, menuButtonArea.height)
let globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y); let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(
let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos; globalPos.x,
contextMenu.show(localPos.x, localPos.y); globalPos.y) : globalPos
} contextMenu.show(localPos.x, localPos.y)
} }
} }
onPressAndHold: { }
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process; Behavior on color {
let globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2); ColorAnimation {
contextMenu.show(globalPos.x, globalPos.y); duration: Theme.shortDuration
}
} }
}
} }
}
Item {
anchors.fill: parent
anchors.margins: 8
DankIcon {
id: processIcon
name: SysMonitorService.getProcessIcon(process ? process.command : "")
size: Theme.iconSize - 4
color: {
if (process && process.cpu > 80)
return Theme.error;
if (process && process.cpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
opacity: 0.8
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: process ? process.displayName : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: 250
elide: Text.ElideRight
anchors.left: processIcon.right
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: cpuBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.cpu > 80)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
if (process && process.cpu > 50)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
}
anchors.right: parent.right
anchors.rightMargin: 194
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.formatCpuUsage(process ? process.cpu : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.cpu > 80)
return Theme.error;
if (process && process.cpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
anchors.centerIn: parent
}
}
Rectangle {
id: memoryBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (process && process.memoryKB > 1024 * 1024)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
if (process && process.memoryKB > 512 * 1024)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
}
anchors.right: parent.right
anchors.rightMargin: 102
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.formatMemoryUsage(process ? process.memoryKB : 0)
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (process && process.memoryKB > 1024 * 1024)
return Theme.error;
if (process && process.memoryKB > 512 * 1024)
return Theme.warning;
return Theme.surfaceText;
}
anchors.centerIn: parent
}
}
StyledText {
text: process ? process.pid.toString() : ""
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
width: 50
horizontalAlignment: Text.AlignRight
anchors.right: parent.right
anchors.rightMargin: 40
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: menuButton
width: 28
height: 28
radius: Theme.cornerRadius
color: menuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "more_vert"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: menuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (process && process.pid > 0 && contextMenu) {
contextMenu.processData = process;
let globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height);
let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(globalPos.x, globalPos.y) : globalPos;
contextMenu.show(localPos.x, localPos.y);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
} }

View File

@@ -12,202 +12,203 @@ import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: processListPopout id: processListPopout
property bool isVisible: false property bool isVisible: false
property var parentWidget: null property var parentWidget: null
property real triggerX: Screen.width - 600 - Theme.spacingL property real triggerX: Screen.width - 600 - Theme.spacingL
property real triggerY: Theme.barHeight + Theme.spacingXS property real triggerY: Theme.barHeight + Theme.spacingXS
property real triggerWidth: 55 property real triggerWidth: 55
property string triggerSection: "right" property string triggerSection: "right"
property var triggerScreen: null property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) { function setTriggerPosition(x, y, width, section, screen) {
triggerX = x; triggerX = x
triggerY = y; triggerY = y
triggerWidth = width; triggerWidth = width
triggerSection = section; triggerSection = section
triggerScreen = screen; triggerScreen = screen
}
function hide() {
isVisible = false
if (processContextMenu.visible)
processContextMenu.close()
}
function show() {
isVisible = true
}
function toggle() {
if (isVisible)
hide()
else
show()
}
visible: isVisible
screen: triggerScreen
implicitWidth: 600
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
Ref {
service: SysMonitorService
}
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: function (mouse) {
var localPos = mapToItem(contentLoader, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > contentLoader.width || localPos.y < 0
|| localPos.y > contentLoader.height)
processListPopout.hide()
}
}
Loader {
id: contentLoader
readonly property real screenWidth: processListPopout.screen ? processListPopout.screen.width : Screen.width
readonly property real screenHeight: processListPopout.screen ? processListPopout.screen.height : Screen.height
readonly property real targetWidth: Math.min(
600, screenWidth - Theme.spacingL * 2)
readonly property real targetHeight: Math.min(
600,
screenHeight - Theme.barHeight - Theme.spacingS * 2)
readonly property real calculatedX: {
var centerX = processListPopout.triggerX + (processListPopout.triggerWidth
/ 2) - (targetWidth / 2)
if (centerX >= Theme.spacingM
&& centerX + targetWidth <= screenWidth - Theme.spacingM) {
return centerX
}
if (centerX < Theme.spacingM) {
return Theme.spacingM
}
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
return screenWidth - targetWidth - Theme.spacingM
}
return centerX
} }
function hide() { asynchronous: true
isVisible = false; active: processListPopout.isVisible
if (processContextMenu.visible) width: targetWidth
processContextMenu.close(); height: targetHeight
y: processListPopout.triggerY
x: calculatedX
opacity: processListPopout.isVisible ? 1 : 0
scale: processListPopout.isVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
} }
function show() { Behavior on scale {
isVisible = true; NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
} }
function toggle() { sourceComponent: Rectangle {
if (isVisible) id: dropdownContent
hide();
else
show();
}
visible: isVisible radius: Theme.cornerRadiusLarge
screen: triggerScreen color: Theme.popupBackground()
implicitWidth: 600 border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
implicitHeight: 600 Theme.outline.b, 0.08)
WlrLayershell.layer: WlrLayershell.Overlay border.width: 1
WlrLayershell.exclusiveZone: -1 clip: true
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None antialiasing: true
color: "transparent" smooth: true
focus: true
Component.onCompleted: {
if (processListPopout.isVisible)
forceActiveFocus()
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
processListPopout.hide()
event.accepted = true
}
}
Ref { Connections {
service: SysMonitorService function onIsVisibleChanged() {
} if (processListPopout.isVisible)
Qt.callLater(function () {
dropdownContent.forceActiveFocus()
})
}
target: processListPopout
}
anchors { ColumnLayout {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: function(mouse) { anchors.margins: Theme.spacingL
var localPos = mapToItem(contentLoader, mouse.x, mouse.y); spacing: Theme.spacingL
if (localPos.x < 0 || localPos.x > contentLoader.width || localPos.y < 0 || localPos.y > contentLoader.height)
processListPopout.hide();
Rectangle {
Layout.fillWidth: true
height: systemOverview.height + Theme.spacingM * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
SystemOverview {
id: systemOverview
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
}
} }
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.1)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.05)
border.width: 1
ProcessListView {
anchors.fill: parent
anchors.margins: Theme.spacingS
contextMenu: processContextMenu
}
}
}
} }
}
Loader { ProcessContextMenu {
id: contentLoader id: processContextMenu
}
readonly property real screenWidth: processListPopout.screen ? processListPopout.screen.width : Screen.width
readonly property real screenHeight: processListPopout.screen ? processListPopout.screen.height : Screen.height
readonly property real targetWidth: Math.min(600, screenWidth - Theme.spacingL * 2)
readonly property real targetHeight: Math.min(600, screenHeight - Theme.barHeight - Theme.spacingS * 2)
readonly property real calculatedX: {
var centerX = processListPopout.triggerX + (processListPopout.triggerWidth / 2) - (targetWidth / 2);
if (centerX >= Theme.spacingM && centerX + targetWidth <= screenWidth - Theme.spacingM) {
return centerX;
}
if (centerX < Theme.spacingM) {
return Theme.spacingM;
}
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
return screenWidth - targetWidth - Theme.spacingM;
}
return centerX;
}
asynchronous: true
active: processListPopout.isVisible
width: targetWidth
height: targetHeight
y: processListPopout.triggerY
x: calculatedX
opacity: processListPopout.isVisible ? 1 : 0
scale: processListPopout.isVisible ? 1 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durMed
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
sourceComponent: Rectangle {
id: dropdownContent
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
clip: true
antialiasing: true
smooth: true
focus: true
Component.onCompleted: {
if (processListPopout.isVisible)
forceActiveFocus();
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
processListPopout.hide();
event.accepted = true;
}
}
Connections {
function onIsVisibleChanged() {
if (processListPopout.isVisible)
Qt.callLater(function() {
dropdownContent.forceActiveFocus();
});
}
target: processListPopout
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
Rectangle {
Layout.fillWidth: true
height: systemOverview.height + Theme.spacingM * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
SystemOverview {
id: systemOverview
anchors.centerIn: parent
width: parent.width - Theme.spacingM * 2
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: 1
ProcessListView {
anchors.fill: parent
anchors.margins: Theme.spacingS
contextMenu: processContextMenu
}
}
}
}
}
ProcessContextMenu {
id: processContextMenu
}
} }

View File

@@ -5,234 +5,237 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property var contextMenu: null property var contextMenu: null
Component.onCompleted: { Component.onCompleted: {
SysMonitorService.addRef(); SysMonitorService.addRef()
} }
Component.onDestruction: { Component.onDestruction: {
SysMonitorService.removeRef(); SysMonitorService.removeRef()
}
Item {
id: columnHeaders
width: parent.width
anchors.leftMargin: 8
height: 24
Rectangle {
width: 60
height: 20
color: processHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
radius: Theme.cornerRadius
anchors.left: parent.left
anchors.leftMargin: 0
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Process"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: SysMonitorService.sortBy === "name" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "name" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: processHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("name")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
} }
Item { Rectangle {
id: columnHeaders width: 80
height: 20
color: cpuHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 200
anchors.verticalCenter: parent.verticalCenter
width: parent.width StyledText {
anchors.leftMargin: 8 text: "CPU"
height: 24 font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: SysMonitorService.sortBy === "cpu" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "cpu" ? 1 : 0.7
anchors.centerIn: parent
}
Rectangle { MouseArea {
width: 60 id: cpuHeaderArea
height: 20
color: processHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
radius: Theme.cornerRadius
anchors.left: parent.left
anchors.leftMargin: 0
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Process"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: SysMonitorService.sortBy === "name" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "name" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: processHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("name");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("cpu")
} }
}
Rectangle { Behavior on color {
width: 80 ColorAnimation {
height: 20 duration: Theme.shortDuration
color: cpuHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 200
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: SysMonitorService.sortBy === "cpu" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "cpu" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: cpuHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("cpu");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
} }
}
Rectangle {
width: 80
height: 20
color: memoryHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 112
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "RAM"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: SysMonitorService.sortBy === "memory" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "memory" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: memoryHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("memory");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 50
height: 20
color: pidHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 53
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "PID"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: SysMonitorService.sortBy === "pid" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "pid" ? 1 : 0.7
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
MouseArea {
id: pidHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("pid");
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: sortOrderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.sortDescending ? "↓" : "↑"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: sortOrderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.toggleSortOrder();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
} }
DankListView { Rectangle {
id: processListView width: 80
height: 20
color: memoryHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 112
anchors.verticalCenter: parent.verticalCenter
property string keyRoleName: "pid" StyledText {
text: "RAM"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: SysMonitorService.sortBy === "memory" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "memory" ? 1 : 0.7
anchors.centerIn: parent
}
width: parent.width MouseArea {
height: parent.height - columnHeaders.height id: memoryHeaderArea
clip: true
spacing: 4
model: SysMonitorService.processes
delegate: ProcessListItem { anchors.fill: parent
process: modelData hoverEnabled: true
contextMenu: root.contextMenu cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("memory")
} }
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
} }
Rectangle {
width: 50
height: 20
color: pidHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
radius: Theme.cornerRadius
anchors.right: parent.right
anchors.rightMargin: 53
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "PID"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: SysMonitorService.sortBy === "pid" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: SysMonitorService.sortBy === "pid" ? 1 : 0.7
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
MouseArea {
id: pidHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.setSortBy("pid")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: sortOrderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: SysMonitorService.sortDescending ? "↓" : "↑"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: sortOrderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SysMonitorService.toggleSortOrder()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
DankListView {
id: processListView
property string keyRoleName: "pid"
width: parent.width
height: parent.height - columnHeaders.height
clip: true
spacing: 4
model: SysMonitorService.processes
delegate: ProcessListItem {
process: modelData
contextMenu: root.contextMenu
}
}
} }

View File

@@ -5,25 +5,24 @@ import qs.Modules.ProcessList
import qs.Services import qs.Services
ColumnLayout { ColumnLayout {
id: processesTab id: processesTab
property var contextMenu: null property var contextMenu: null
anchors.fill: parent anchors.fill: parent
spacing: Theme.spacingM spacing: Theme.spacingM
SystemOverview { SystemOverview {
Layout.fillWidth: true Layout.fillWidth: true
} }
ProcessListView { ProcessListView {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
contextMenu: processesTab.contextMenu || localContextMenu contextMenu: processesTab.contextMenu || localContextMenu
} }
ProcessContextMenu {
id: localContextMenu
}
ProcessContextMenu {
id: localContextMenu
}
} }

View File

@@ -4,310 +4,334 @@ import qs.Services
import qs.Widgets import qs.Widgets
Row { Row {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
Component.onCompleted: { Component.onCompleted: {
SysMonitorService.addRef(); SysMonitorService.addRef()
}
Component.onDestruction: {
SysMonitorService.removeRef()
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadiusLarge
color: {
if (SysMonitorService.sortBy === "cpu")
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16)
else if (cpuCardMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
else
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
} }
Component.onDestruction: { border.color: SysMonitorService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r,
SysMonitorService.removeRef(); Theme.primary.g,
Theme.primary.b,
0.4) : Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b, 0.2)
border.width: SysMonitorService.sortBy === "cpu" ? 2 : 1
MouseArea {
id: cpuCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: SysMonitorService.setSortBy("cpu")
} }
Rectangle { Column {
width: (parent.width - Theme.spacingM * 2) / 3 anchors.left: parent.left
height: 80 anchors.leftMargin: Theme.spacingM
radius: Theme.cornerRadiusLarge anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: SysMonitorService.sortBy === "cpu" ? Theme.primary : Theme.secondary
opacity: SysMonitorService.sortBy === "cpu" ? 1 : 0.8
}
Row {
spacing: Theme.spacingS
StyledText {
text: SysMonitorService.totalCpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 1
height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (SysMonitorService.cpuTemperature === undefined
|| SysMonitorService.cpuTemperature === null
|| SysMonitorService.cpuTemperature < 0) {
return "--°"
}
return Math.round(SysMonitorService.cpuTemperature) + "°"
}
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: {
if (SysMonitorService.cpuTemperature > 80)
return Theme.error
if (SysMonitorService.cpuTemperature > 60)
return Theme.warning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: SysMonitorService.cpuCount + " cores"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadiusLarge
color: {
if (SysMonitorService.sortBy === "memory")
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16)
else if (memoryCardMouseArea.containsMouse)
return Qt.rgba(Theme.secondary.r, Theme.secondary.g,
Theme.secondary.b, 0.12)
else
return Qt.rgba(Theme.secondary.r, Theme.secondary.g,
Theme.secondary.b, 0.08)
}
border.color: SysMonitorService.sortBy === "memory" ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.4) : Qt.rgba(
Theme.secondary.r,
Theme.secondary.g,
Theme.secondary.b,
0.2)
border.width: SysMonitorService.sortBy === "memory" ? 2 : 1
MouseArea {
id: memoryCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: SysMonitorService.setSortBy("memory")
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: SysMonitorService.sortBy === "memory" ? Theme.primary : Theme.secondary
opacity: SysMonitorService.sortBy === "memory" ? 1 : 0.8
}
Row {
spacing: Theme.spacingS
StyledText {
text: SysMonitorService.formatSystemMemory(
SysMonitorService.usedMemoryKB)
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 1
height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
visible: SysMonitorService.totalSwapKB > 0
}
StyledText {
text: SysMonitorService.totalSwapKB > 0 ? SysMonitorService.formatSystemMemory(
SysMonitorService.usedSwapKB) : ""
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: SysMonitorService.usedSwapKB > 0 ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: SysMonitorService.totalSwapKB > 0
}
}
StyledText {
text: {
if (SysMonitorService.totalSwapKB > 0) {
return "of " + SysMonitorService.formatSystemMemory(
SysMonitorService.totalMemoryKB) + " + swap"
}
return "of " + SysMonitorService.formatSystemMemory(
SysMonitorService.totalMemoryKB)
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
border.color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
border.width: 1
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "Graphics"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.secondary
opacity: 0.8
}
StyledText {
text: {
if (!SysMonitorService.availableGpus
|| SysMonitorService.availableGpus.length === 0) {
return "None"
}
if (SysMonitorService.availableGpus.length === 1) {
var gpu = SysMonitorService.availableGpus[0]
var temp = gpu.temperature
var tempText = (temp === undefined || temp === null
|| temp === 0) ? "--°" : Math.round(temp) + "°"
return tempText
}
// Multiple GPUs - show average temp
var totalTemp = 0
var validTemps = 0
for (var i = 0; i < SysMonitorService.availableGpus.length; i++) {
var temp = SysMonitorService.availableGpus[i].temperature
if (temp !== undefined && temp !== null && temp > 0) {
totalTemp += temp
validTemps++
}
}
if (validTemps > 0) {
return Math.round(totalTemp / validTemps) + "°"
}
return "--°"
}
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: { color: {
if (SysMonitorService.sortBy === "cpu") if (!SysMonitorService.availableGpus
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16); || SysMonitorService.availableGpus.length === 0) {
else if (cpuCardMouseArea.containsMouse) return Theme.surfaceText
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); }
else if (SysMonitorService.availableGpus.length === 1) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08); var temp = SysMonitorService.availableGpus[0].temperature || 0
if (temp > 80)
return Theme.tempDanger
if (temp > 60)
return Theme.tempWarning
return Theme.surfaceText
}
// Multiple GPUs - get max temp for coloring
var maxTemp = 0
for (var i = 0; i < SysMonitorService.availableGpus.length; i++) {
var temp = SysMonitorService.availableGpus[i].temperature || 0
if (temp > maxTemp)
maxTemp = temp
}
if (maxTemp > 80)
return Theme.tempDanger
if (maxTemp > 60)
return Theme.tempWarning
return Theme.surfaceText
} }
border.color: SysMonitorService.sortBy === "cpu" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) }
border.width: SysMonitorService.sortBy === "cpu" ? 2 : 1
MouseArea { StyledText {
id: cpuCardMouseArea text: {
if (!SysMonitorService.availableGpus
anchors.fill: parent || SysMonitorService.availableGpus.length === 0) {
hoverEnabled: true return "No GPUs detected"
cursorShape: Qt.PointingHandCursor }
onClicked: SysMonitorService.setSortBy("cpu") if (SysMonitorService.availableGpus.length === 1) {
return SysMonitorService.availableGpus[0].driver.toUpperCase()
}
return SysMonitorService.availableGpus.length + " GPUs detected"
} }
font.pixelSize: Theme.fontSizeSmall
Column { font.family: SettingsData.monoFontFamily
anchors.left: parent.left color: Theme.surfaceText
anchors.leftMargin: Theme.spacingM opacity: 0.7
anchors.verticalCenter: parent.verticalCenter }
spacing: 2
StyledText {
text: "CPU"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: SysMonitorService.sortBy === "cpu" ? Theme.primary : Theme.secondary
opacity: SysMonitorService.sortBy === "cpu" ? 1 : 0.8
}
Row {
spacing: Theme.spacingS
StyledText {
text: SysMonitorService.totalCpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 1
height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (SysMonitorService.cpuTemperature === undefined || SysMonitorService.cpuTemperature === null || SysMonitorService.cpuTemperature < 0) {
return "--°";
}
return Math.round(SysMonitorService.cpuTemperature) + "°";
}
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: {
if (SysMonitorService.cpuTemperature > 80)
return Theme.error;
if (SysMonitorService.cpuTemperature > 60)
return Theme.warning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: SysMonitorService.cpuCount + " cores"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
} }
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadiusLarge
color: {
if (SysMonitorService.sortBy === "memory")
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
else if (memoryCardMouseArea.containsMouse)
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12);
else
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08);
}
border.color: SysMonitorService.sortBy === "memory" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.2)
border.width: SysMonitorService.sortBy === "memory" ? 2 : 1
MouseArea {
id: memoryCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: SysMonitorService.setSortBy("memory")
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: SysMonitorService.sortBy === "memory" ? Theme.primary : Theme.secondary
opacity: SysMonitorService.sortBy === "memory" ? 1 : 0.8
}
Row {
spacing: Theme.spacingS
StyledText {
text: SysMonitorService.formatSystemMemory(SysMonitorService.usedMemoryKB)
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 1
height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
visible: SysMonitorService.totalSwapKB > 0
}
StyledText {
text: SysMonitorService.totalSwapKB > 0 ? SysMonitorService.formatSystemMemory(SysMonitorService.usedSwapKB) : ""
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: SysMonitorService.usedSwapKB > 0 ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: SysMonitorService.totalSwapKB > 0
}
}
StyledText {
text: {
if (SysMonitorService.totalSwapKB > 0) {
return "of " + SysMonitorService.formatSystemMemory(SysMonitorService.totalMemoryKB) + " + swap";
}
return "of " + SysMonitorService.formatSystemMemory(SysMonitorService.totalMemoryKB);
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
border.color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2)
border.width: 1
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "Graphics"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.secondary
opacity: 0.8
}
StyledText {
text: {
if (!SysMonitorService.availableGpus || SysMonitorService.availableGpus.length === 0) {
return "None";
}
if (SysMonitorService.availableGpus.length === 1) {
var gpu = SysMonitorService.availableGpus[0];
var temp = gpu.temperature;
var tempText = (temp === undefined || temp === null || temp === 0) ? "--°" : Math.round(temp) + "°";
return tempText;
}
// Multiple GPUs - show average temp
var totalTemp = 0;
var validTemps = 0;
for (var i = 0; i < SysMonitorService.availableGpus.length; i++) {
var temp = SysMonitorService.availableGpus[i].temperature;
if (temp !== undefined && temp !== null && temp > 0) {
totalTemp += temp;
validTemps++;
}
}
if (validTemps > 0) {
return Math.round(totalTemp / validTemps) + "°";
}
return "--°";
}
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: {
if (!SysMonitorService.availableGpus || SysMonitorService.availableGpus.length === 0) {
return Theme.surfaceText;
}
if (SysMonitorService.availableGpus.length === 1) {
var temp = SysMonitorService.availableGpus[0].temperature || 0;
if (temp > 80) return Theme.tempDanger;
if (temp > 60) return Theme.tempWarning;
return Theme.surfaceText;
}
// Multiple GPUs - get max temp for coloring
var maxTemp = 0;
for (var i = 0; i < SysMonitorService.availableGpus.length; i++) {
var temp = SysMonitorService.availableGpus[i].temperature || 0;
if (temp > maxTemp) maxTemp = temp;
}
if (maxTemp > 80) return Theme.tempDanger;
if (maxTemp > 60) return Theme.tempWarning;
return Theme.surfaceText;
}
}
StyledText {
text: {
if (!SysMonitorService.availableGpus || SysMonitorService.availableGpus.length === 0) {
return "No GPUs detected";
}
if (SysMonitorService.availableGpus.length === 1) {
return SysMonitorService.availableGpus[0].driver.toUpperCase();
}
return SysMonitorService.availableGpus.length + " GPUs detected";
}
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
opacity: 0.7
}
}
}
} }

View File

@@ -5,475 +5,481 @@ import qs.Services
import qs.Widgets import qs.Widgets
ScrollView { ScrollView {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Component.onCompleted: { Component.onCompleted: {
SysMonitorService.addRef(); SysMonitorService.addRef()
} }
Component.onDestruction: { Component.onDestruction: {
SysMonitorService.removeRef(); SysMonitorService.removeRef()
} }
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
Rectangle { Rectangle {
width: parent.width width: parent.width
height: systemInfoColumn.implicitHeight + 2 * Theme.spacingL height: systemInfoColumn.implicitHeight + 2 * Theme.spacingL
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.6) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
border.width: 0 Theme.surfaceContainer.b, 0.6)
border.width: 0
Column { Column {
id: systemInfoColumn id: systemInfoColumn
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: Theme.spacingL anchors.margins: Theme.spacingL
spacing: Theme.spacingL spacing: Theme.spacingL
Row { Row {
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
SystemLogo { SystemLogo {
width: 80 width: 80
height: 80 height: 80
} }
Column { Column {
width: parent.width - 80 - Theme.spacingL width: parent.width - 80 - Theme.spacingL
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS spacing: Theme.spacingS
StyledText {
text: SysMonitorService.hostname
font.pixelSize: Theme.fontSizeXLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Light
color: Theme.surfaceText
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: SysMonitorService.distribution + " • " + SysMonitorService.architecture + " • " + SysMonitorService.kernelVersion
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Up " + UserInfoService.uptime + " • Boot: " + SysMonitorService.bootTime
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Load: " + SysMonitorService.loadAverage + " • " + SysMonitorService.processCount + " processes, " + SysMonitorService.threadCount + " threads"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Row {
width: parent.width
spacing: Theme.spacingXL
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: Math.max(hardwareColumn.implicitHeight, memoryColumn.implicitHeight) + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.4)
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
Column {
id: hardwareColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: Theme.iconSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Hardware"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: SysMonitorService.cpuModel
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: SysMonitorService.motherboard
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "BIOS " + SysMonitorService.biosVersion
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
}
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: Math.max(hardwareColumn.implicitHeight, memoryColumn.implicitHeight) + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.4)
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
Column {
id: memoryColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "developer_board"
size: Theme.iconSizeSmall
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: SysMonitorService.formatSystemMemory(SysMonitorService.totalMemoryKB) + " Total"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: SysMonitorService.formatSystemMemory(SysMonitorService.usedMemoryKB) + " Used • " + SysMonitorService.formatSystemMemory(SysMonitorService.totalMemoryKB - SysMonitorService.usedMemoryKB) + " Available"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
Item {
width: parent.width
height: Theme.fontSizeSmall + Theme.spacingXS
}
}
}
}
StyledText {
text: SysMonitorService.hostname
font.pixelSize: Theme.fontSizeXLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Light
color: Theme.surfaceText
verticalAlignment: Text.AlignVCenter
} }
StyledText {
text: SysMonitorService.distribution + " • " + SysMonitorService.architecture
+ " • " + SysMonitorService.kernelVersion
font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Up " + UserInfoService.uptime + " • Boot: " + SysMonitorService.bootTime
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Load: " + SysMonitorService.loadAverage + " • "
+ SysMonitorService.processCount + " processes, "
+ SysMonitorService.threadCount + " threads"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
verticalAlignment: Text.AlignVCenter
}
}
} }
Rectangle { Rectangle {
width: parent.width width: parent.width
height: storageColumn.implicitHeight + 2 * Theme.spacingL height: 1
radius: Theme.cornerRadiusLarge color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.6) }
border.width: 0
Row {
width: parent.width
spacing: Theme.spacingXL
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: Math.max(hardwareColumn.implicitHeight,
memoryColumn.implicitHeight) + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainerHigh.r,
Theme.surfaceContainerHigh.g,
Theme.surfaceContainerHigh.b, 0.4)
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
Column { Column {
id: storageColumn id: hardwareColumn
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: Theme.spacingL anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS spacing: Theme.spacingS
Row { DankIcon {
width: parent.width name: "memory"
spacing: Theme.spacingS size: Theme.iconSizeSmall
color: Theme.primary
DankIcon { anchors.verticalCenter: parent.verticalCenter
name: "storage"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Storage & Disks"
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
} }
Column { StyledText {
width: parent.width text: "Hardware"
spacing: 2 font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
Row { font.weight: Font.Bold
width: parent.width color: Theme.primary
height: 24 anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
text: "Device"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Mount"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Size"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Used"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Available"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Use%"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.1
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
Repeater {
id: diskMountRepeater
model: SysMonitorService.diskMounts
Rectangle {
width: parent.width
height: 24
radius: Theme.cornerRadiusSmall
color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) : "transparent"
MouseArea {
id: diskMouseArea
anchors.fill: parent
hoverEnabled: true
}
Row {
anchors.fill: parent
spacing: Theme.spacingS
StyledText {
text: modelData.device
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.size
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.used
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.avail
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.percent
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: {
const percent = parseInt(modelData.percent);
if (percent > 90)
return Theme.error;
if (percent > 75)
return Theme.warning;
return Theme.surfaceText;
}
width: parent.width * 0.1
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
} }
}
StyledText {
text: SysMonitorService.cpuModel
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: SysMonitorService.motherboard
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "BIOS " + SysMonitorService.biosVersion
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
} }
}
Rectangle {
width: (parent.width - Theme.spacingXL) / 2
height: Math.max(hardwareColumn.implicitHeight,
memoryColumn.implicitHeight) + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainerHigh.r,
Theme.surfaceContainerHigh.g,
Theme.surfaceContainerHigh.b, 0.4)
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
Column {
id: memoryColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "developer_board"
size: Theme.iconSizeSmall
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Memory"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: SysMonitorService.formatSystemMemory(
SysMonitorService.totalMemoryKB) + " Total"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: SysMonitorService.formatSystemMemory(
SysMonitorService.usedMemoryKB) + " Used • "
+ SysMonitorService.formatSystemMemory(
SysMonitorService.totalMemoryKB
- SysMonitorService.usedMemoryKB) + " Available"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
Item {
width: parent.width
height: Theme.fontSizeSmall + Theme.spacingXS
}
}
}
} }
}
} }
Rectangle {
width: parent.width
height: storageColumn.implicitHeight + 2 * Theme.spacingL
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.6)
border.width: 0
Column {
id: storageColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
DankIcon {
name: "storage"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Storage & Disks"
font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Column {
width: parent.width
spacing: 2
Row {
width: parent.width
height: 24
spacing: Theme.spacingS
StyledText {
text: "Device"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Mount"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Size"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Used"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Available"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: "Use%"
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.1
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
Repeater {
id: diskMountRepeater
model: SysMonitorService.diskMounts
Rectangle {
width: parent.width
height: 24
radius: Theme.cornerRadiusSmall
color: diskMouseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b,
0.04) : "transparent"
MouseArea {
id: diskMouseArea
anchors.fill: parent
hoverEnabled: true
}
Row {
anchors.fill: parent
spacing: Theme.spacingS
StyledText {
text: modelData.device
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.size
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.used
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.avail
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
StyledText {
text: modelData.percent
font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily
color: {
const percent = parseInt(modelData.percent)
if (percent > 90)
return Theme.error
if (percent > 75)
return Theme.warning
return Theme.surfaceText
}
width: parent.width * 0.1
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
}
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -5,493 +5,497 @@ import qs.Common
import qs.Widgets import qs.Widgets
Item { Item {
id: launcherTab id: launcherTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingXL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
Loader {
width: parent.width
sourceComponent: appLauncherComponent
}
Loader {
width: parent.width
sourceComponent: dockComponent
}
Loader {
width: parent.width
sourceComponent: recentlyUsedComponent
}
}
}
// App Launcher Component
Component {
id: appLauncherComponent
StyledRect {
width: parent.width
height: appLauncherSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: appLauncherSection
DankFlickable {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Theme.spacingL anchors.margins: Theme.spacingL
anchors.bottomMargin: Theme.spacingXL spacing: Theme.spacingM
clip: true
contentHeight: mainColumn.height DankToggle {
contentWidth: width width: parent.width
text: "Use OS Logo"
description: "Display operating system logo instead of apps icon"
checked: SettingsData.useOSLogo
onToggled: checked => {
return SettingsData.setUseOSLogo(checked)
}
}
Row {
width: parent.width - Theme.spacingL
spacing: Theme.spacingL
visible: SettingsData.useOSLogo
opacity: visible ? 1 : 0
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Color Override"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
width: 100
height: 28
placeholderText: "#ffffff"
text: SettingsData.osLogoColorOverride
maximumLength: 7
font.pixelSize: Theme.fontSizeSmall
topPadding: Theme.spacingXS
bottomPadding: Theme.spacingXS
onEditingFinished: {
var color = text.trim()
if (color === "" || /^#[0-9A-Fa-f]{6}$/.test(color))
SettingsData.setOSLogoColorOverride(color)
else
text = SettingsData.osLogoColorOverride
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Brightness"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 100
value: Math.round(SettingsData.osLogoBrightness * 100)
unit: "%"
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setOSLogoBrightness(
newValue / 100)
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Contrast"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 200
value: Math.round(SettingsData.osLogoContrast * 100)
unit: "%"
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setOSLogoContrast(
newValue / 100)
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
// Dock Component
Component {
id: dockComponent
StyledRect {
width: parent.width
height: dockSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: dockSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "dock_to_bottom"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Dock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Show Dock"
description: "Display a dock at the bottom of the screen with pinned and running applications"
checked: SettingsData.showDock
onToggled: checked => {
SettingsData.setShowDock(checked)
}
}
DankToggle {
width: parent.width
text: "Auto-hide Dock"
description: "Hide the dock when not in use and reveal it when hovering near the bottom of the screen"
checked: SettingsData.dockAutoHide
visible: SettingsData.showDock
opacity: visible ? 1 : 0
onToggled: checked => {
SettingsData.setDockAutoHide(checked)
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Column { Column {
id: mainColumn width: parent.width
spacing: Theme.spacingS
visible: SettingsData.showDock
opacity: visible ? 1 : 0
StyledText {
text: "Dock Transparency"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width width: parent.width
spacing: Theme.spacingXL height: 24
value: Math.round(SettingsData.dockTransparency * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
onSliderValueChanged: newValue => {
SettingsData.setDockTransparency(
newValue / 100)
}
}
Loader { Behavior on opacity {
width: parent.width NumberAnimation {
sourceComponent: appLauncherComponent duration: Theme.mediumDuration
} easing.type: Theme.emphasizedEasing
Loader {
width: parent.width
sourceComponent: dockComponent
}
Loader {
width: parent.width
sourceComponent: recentlyUsedComponent
} }
}
} }
}
} }
}
// App Launcher Component // Recently Used Apps Component
Component { Component {
id: appLauncherComponent id: recentlyUsedComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: appLauncherSection.implicitHeight + Theme.spacingL * 2 height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) Theme.surfaceVariant.b, 0.3)
border.width: 1 border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column { Column {
id: appLauncherSection id: recentlyUsedSection
anchors.fill: parent property var rankedAppsModel: {
anchors.margins: Theme.spacingL var apps = []
spacing: Theme.spacingM for (var appId in (AppUsageHistoryData.appUsageRanking || {})) {
var appData = (AppUsageHistoryData.appUsageRanking || {})[appId]
apps.push({
"id": appId,
"name": appData.name,
"exec": appData.exec,
"icon": appData.icon,
"comment": appData.comment,
"usageCount": appData.usageCount,
"lastUsed": appData.lastUsed
})
}
apps.sort(function (a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount
DankToggle { return a.name.localeCompare(b.name)
width: parent.width })
text: "Use OS Logo" return apps.slice(0, 20)
description: "Display operating system logo instead of apps icon"
checked: SettingsData.useOSLogo
onToggled: (checked) => {
return SettingsData.setUseOSLogo(checked);
}
}
Row {
width: parent.width - Theme.spacingL
spacing: Theme.spacingL
visible: SettingsData.useOSLogo
opacity: visible ? 1 : 0
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Color Override"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
width: 100
height: 28
placeholderText: "#ffffff"
text: SettingsData.osLogoColorOverride
maximumLength: 7
font.pixelSize: Theme.fontSizeSmall
topPadding: Theme.spacingXS
bottomPadding: Theme.spacingXS
onEditingFinished: {
var color = text.trim();
if (color === "" || /^#[0-9A-Fa-f]{6}$/.test(color))
SettingsData.setOSLogoColorOverride(color);
else
text = SettingsData.osLogoColorOverride;
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Brightness"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 100
value: Math.round(SettingsData.osLogoBrightness * 100)
unit: "%"
showValue: true
onSliderValueChanged: (newValue) => {
SettingsData.setOSLogoBrightness(newValue / 100);
}
}
}
Column {
width: 120
spacing: Theme.spacingS
StyledText {
text: "Contrast"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: 100
height: 20
minimum: 0
maximum: 200
value: Math.round(SettingsData.osLogoContrast * 100)
unit: "%"
showValue: true
onSliderValueChanged: (newValue) => {
SettingsData.setOSLogoContrast(newValue / 100);
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
} }
}
// Dock Component anchors.fill: parent
Component { anchors.margins: Theme.spacingL
id: dockComponent spacing: Theme.spacingM
StyledRect { Row {
width: parent.width width: parent.width
height: dockSection.implicitHeight + Theme.spacingL * 2 spacing: Theme.spacingM
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
Column { DankIcon {
id: dockSection name: "history"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
anchors.fill: parent StyledText {
anchors.margins: Theme.spacingL text: "Recently Used Apps"
spacing: Theme.spacingM font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Row { Item {
width: parent.width width: parent.width - parent.children[0].width - parent.children[1].width
spacing: Theme.spacingM - clearAllButton.width - Theme.spacingM * 3
height: 1
}
DankIcon { DankActionButton {
name: "dock_to_bottom" id: clearAllButton
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Dock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Show Dock"
description: "Display a dock at the bottom of the screen with pinned and running applications"
checked: SettingsData.showDock
onToggled: (checked) => {
SettingsData.setShowDock(checked)
}
}
DankToggle {
width: parent.width
text: "Auto-hide Dock"
description: "Hide the dock when not in use and reveal it when hovering near the bottom of the screen"
checked: SettingsData.dockAutoHide
visible: SettingsData.showDock
opacity: visible ? 1 : 0
onToggled: (checked) => {
SettingsData.setDockAutoHide(checked)
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: SettingsData.showDock
opacity: visible ? 1 : 0
StyledText {
text: "Dock Transparency"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankSlider {
width: parent.width
height: 24
value: Math.round(SettingsData.dockTransparency * 100)
minimum: 0
maximum: 100
unit: ""
showValue: true
onSliderValueChanged: (newValue) => {
SettingsData.setDockTransparency(newValue / 100);
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
iconName: "delete_sweep"
iconSize: Theme.iconSize - 2
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
onClicked: {
AppUsageHistoryData.appUsageRanking = {}
SettingsData.saveSettings()
} }
}
} }
}
// Recently Used Apps Component StyledText {
Component { width: parent.width
id: recentlyUsedComponent text: "Apps are ordered by usage frequency, then last used, then alphabetically."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
StyledRect { Column {
width: parent.width id: rankedAppsList
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
Column { width: parent.width
id: recentlyUsedSection spacing: Theme.spacingS
property var rankedAppsModel: { Repeater {
var apps = []; model: recentlyUsedSection.rankedAppsModel
for (var appId in (AppUsageHistoryData.appUsageRanking || {})) {
var appData = (AppUsageHistoryData.appUsageRanking || {})[appId];
apps.push({
"id": appId,
"name": appData.name,
"exec": appData.exec,
"icon": appData.icon,
"comment": appData.comment,
"usageCount": appData.usageCount,
"lastUsed": appData.lastUsed
});
}
apps.sort(function(a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount;
return a.name.localeCompare(b.name); delegate: Rectangle {
}); width: rankedAppsList.width
return apps.slice(0, 20); height: 48
} radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
border.width: 1
anchors.fill: parent Row {
anchors.margins: Theme.spacingL anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "history"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Recently Used Apps"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - parent.children[0].width - parent.children[1].width - clearAllButton.width - Theme.spacingM * 3
height: 1
}
DankActionButton {
id: clearAllButton
iconName: "delete_sweep"
iconSize: Theme.iconSize - 2
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
onClicked: {
AppUsageHistoryData.appUsageRanking = {};
SettingsData.saveSettings();
}
}
}
StyledText { StyledText {
width: parent.width text: (index + 1).toString()
text: "Apps are ordered by usage frequency, then last used, then alphabetically." font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.primary
wrapMode: Text.WordWrap width: 20
anchors.verticalCenter: parent.verticalCenter
}
Image {
width: 24
height: 24
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable"
}
} }
Column { Column {
id: rankedAppsList anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width StyledText {
spacing: Theme.spacingS text: modelData.name || "Unknown App"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
Repeater { StyledText {
model: recentlyUsedSection.rankedAppsModel text: {
if (!modelData.lastUsed)
return "Never used"
delegate: Rectangle { var date = new Date(modelData.lastUsed)
width: rankedAppsList.width var now = new Date()
height: 48 var diffMs = now - date
radius: Theme.cornerRadius var diffMins = Math.floor(diffMs / (1000 * 60))
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) var diffHours = Math.floor(diffMs / (1000 * 60 * 60))
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
border.width: 1 if (diffMins < 1)
return "Last launched just now"
Row { if (diffMins < 60)
anchors.left: parent.left return "Last launched " + diffMins + " minute"
anchors.leftMargin: Theme.spacingM + (diffMins === 1 ? "" : "s") + " ago"
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
StyledText { if (diffHours < 24)
text: (index + 1).toString() return "Last launched " + diffHours + " hour"
font.pixelSize: Theme.fontSizeSmall + (diffHours === 1 ? "" : "s") + " ago"
font.weight: Font.Medium
color: Theme.primary
width: 20
anchors.verticalCenter: parent.verticalCenter
}
Image { if (diffDays < 7)
width: 24 return "Last launched " + diffDays + " day"
height: 24 + (diffDays === 1 ? "" : "s") + " ago"
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Image.Error)
source = "image://icon/application-x-executable";
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.name || "Unknown App"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: {
if (!modelData.lastUsed)
return "Never used";
var date = new Date(modelData.lastUsed);
var now = new Date();
var diffMs = now - date;
var diffMins = Math.floor(diffMs / (1000 * 60));
var diffHours = Math.floor(diffMs / (1000 * 60 * 60));
var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 1)
return "Last launched just now";
if (diffMins < 60)
return "Last launched " + diffMins + " minute" + (diffMins === 1 ? "" : "s") + " ago";
if (diffHours < 24)
return "Last launched " + diffHours + " hour" + (diffHours === 1 ? "" : "s") + " ago";
if (diffDays < 7)
return "Last launched " + diffDays + " day" + (diffDays === 1 ? "" : "s") + " ago";
return "Last launched " + date.toLocaleDateString();
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
circular: true
iconName: "close"
iconSize: 16
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
onClicked: {
var currentRanking = Object.assign({
}, AppUsageHistoryData.appUsageRanking || {});
delete currentRanking[modelData.id];
AppUsageHistoryData.appUsageRanking = currentRanking;
SettingsData.saveSettings();
}
}
}
return "Last launched " + date.toLocaleDateString()
} }
font.pixelSize: Theme.fontSizeSmall
StyledText { color: Theme.surfaceVariantText
width: parent.width }
text: recentlyUsedSection.rankedAppsModel.length === 0 ? "No apps have been launched yet." : ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: recentlyUsedSection.rankedAppsModel.length === 0
}
} }
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
circular: true
iconName: "close"
iconSize: 16
iconColor: Theme.error
hoverColor: Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.12)
onClicked: {
var currentRanking = Object.assign(
{}, AppUsageHistoryData.appUsageRanking || {})
delete currentRanking[modelData.id]
AppUsageHistoryData.appUsageRanking = currentRanking
SettingsData.saveSettings()
}
}
} }
}
StyledText {
width: parent.width
text: recentlyUsedSection.rankedAppsModel.length
=== 0 ? "No apps have been launched yet." : ""
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
visible: recentlyUsedSection.rankedAppsModel.length === 0
}
} }
}
} }
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,114 +1,107 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property string title: "" property string title: ""
property string iconName: "" property string iconName: ""
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
property bool expanded: false property bool expanded: false
property bool collapsible: true property bool collapsible: true
property bool lazyLoad: true property bool lazyLoad: true
width: parent.width
spacing: expanded ? Theme.spacingM : 0
Component.onCompleted: {
if (!collapsible)
expanded = true
}
MouseArea {
width: parent.width width: parent.width
spacing: expanded ? Theme.spacingM : 0 height: headerRow.height
Component.onCompleted: { enabled: collapsible
if (!collapsible) hoverEnabled: collapsible
expanded = true; onClicked: {
if (collapsible)
} expanded = !expanded
MouseArea {
width: parent.width
height: headerRow.height
enabled: collapsible
hoverEnabled: collapsible
onClicked: {
if (collapsible)
expanded = !expanded;
}
Rectangle {
anchors.fill: parent
color: parent.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.radiusS
}
Row {
id: headerRow
width: parent.width
spacing: Theme.spacingS
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
DankIcon {
name: root.collapsible ? (root.expanded ? "expand_less" : "expand_more") : root.iconName
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on rotation {
NumberAnimation {
duration: Appearance.anim.durations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
DankIcon {
name: root.iconName
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.collapsible
}
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
} }
Rectangle { Rectangle {
width: parent.width anchors.fill: parent
height: 1 color: parent.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g,
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) Theme.primary.b,
visible: expanded || !collapsible 0.08) : "transparent"
radius: Theme.radiusS
} }
Loader { Row {
id: contentLoader id: headerRow
width: parent.width width: parent.width
active: lazyLoad ? expanded || !collapsible : true spacing: Theme.spacingS
visible: expanded || !collapsible topPadding: Theme.spacingS
asynchronous: true bottomPadding: Theme.spacingS
opacity: visible ? 1 : 0
Behavior on opacity { DankIcon {
NumberAnimation { name: root.collapsible ? (root.expanded ? "expand_less" : "expand_more") : root.iconName
duration: Appearance.anim.durations.normal size: Theme.iconSize - 2
easing.type: Easing.BezierSpline color: Theme.primary
easing.bezierCurve: Appearance.anim.curves.standard anchors.verticalCenter: parent.verticalCenter
}
Behavior on rotation {
NumberAnimation {
duration: Appearance.anim.durations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
} }
}
DankIcon {
name: root.iconName
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.collapsible
}
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
} }
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: expanded || !collapsible
}
Loader {
id: contentLoader
width: parent.width
active: lazyLoad ? expanded || !collapsible : true
visible: expanded || !collapsible
asynchronous: true
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}
} }

View File

@@ -4,183 +4,183 @@ import qs.Common
import qs.Widgets import qs.Widgets
Item { Item {
id: timeWeatherTab id: timeWeatherTab
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingXL
clip: true
contentHeight: mainColumn.height
contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
Loader {
width: parent.width
sourceComponent: timeComponent
}
Loader {
width: parent.width
sourceComponent: weatherComponent
}
}
}
// Time Format Component
Component {
id: timeComponent
StyledRect {
width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: timeSection
DankFlickable {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Theme.spacingL anchors.margins: Theme.spacingL
anchors.bottomMargin: Theme.spacingXL spacing: Theme.spacingM
clip: true
contentHeight: mainColumn.height Row {
contentWidth: width width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Time Format"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "24-Hour Format"
description: "Use 24-hour time format instead of 12-hour AM/PM"
checked: SettingsData.use24HourClock
onToggled: checked => {
return SettingsData.setClockFormat(checked)
}
}
}
}
}
// Weather Component
Component {
id: weatherComponent
StyledRect {
width: parent.width
height: weatherSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
Column {
id: weatherSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "cloud"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Weather"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Enable Weather"
description: "Show weather information in top bar and centcom center"
checked: SettingsData.weatherEnabled
onToggled: checked => {
return SettingsData.setWeatherEnabled(checked)
}
}
DankToggle {
width: parent.width
text: "Fahrenheit"
description: "Use Fahrenheit instead of Celsius for temperature"
checked: SettingsData.useFahrenheit
enabled: SettingsData.weatherEnabled
onToggled: checked => {
return SettingsData.setTemperatureUnit(checked)
}
}
DankToggle {
width: parent.width
text: "Auto Location"
description: "Allow wttr.in to determine location based on IP address"
checked: SettingsData.useAutoLocation
enabled: SettingsData.weatherEnabled
onToggled: checked => {
return SettingsData.setAutoLocation(checked)
}
}
Column { Column {
id: mainColumn width: parent.width
spacing: Theme.spacingXS
visible: !SettingsData.useAutoLocation && SettingsData.weatherEnabled
StyledText {
text: "Location"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
DankLocationSearch {
width: parent.width width: parent.width
spacing: Theme.spacingXL currentLocation: SettingsData.weatherLocation
placeholderText: "New York, NY"
Loader { onLocationSelected: (displayName, coordinates) => {
width: parent.width SettingsData.setWeatherLocation(displayName,
sourceComponent: timeComponent coordinates)
} }
}
Loader {
width: parent.width
sourceComponent: weatherComponent
}
}
}
// Time Format Component
Component {
id: timeComponent
StyledRect {
width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
Column {
id: timeSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "schedule"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Time Format"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "24-Hour Format"
description: "Use 24-hour time format instead of 12-hour AM/PM"
checked: SettingsData.use24HourClock
onToggled: (checked) => {
return SettingsData.setClockFormat(checked);
}
}
}
}
}
// Weather Component
Component {
id: weatherComponent
StyledRect {
width: parent.width
height: weatherSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
Column {
id: weatherSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "cloud"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Weather"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Enable Weather"
description: "Show weather information in top bar and centcom center"
checked: SettingsData.weatherEnabled
onToggled: (checked) => {
return SettingsData.setWeatherEnabled(checked);
}
}
DankToggle {
width: parent.width
text: "Fahrenheit"
description: "Use Fahrenheit instead of Celsius for temperature"
checked: SettingsData.useFahrenheit
enabled: SettingsData.weatherEnabled
onToggled: (checked) => {
return SettingsData.setTemperatureUnit(checked);
}
}
DankToggle {
width: parent.width
text: "Auto Location"
description: "Allow wttr.in to determine location based on IP address"
checked: SettingsData.useAutoLocation
enabled: SettingsData.weatherEnabled
onToggled: (checked) => {
return SettingsData.setAutoLocation(checked);
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: !SettingsData.useAutoLocation && SettingsData.weatherEnabled
StyledText {
text: "Location"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
DankLocationSearch {
width: parent.width
currentLocation: SettingsData.weatherLocation
placeholderText: "New York, NY"
onLocationSelected: (displayName, coordinates) => {
SettingsData.setWeatherLocation(displayName, coordinates);
}
}
}
}
} }
}
} }
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -5,380 +5,401 @@ import qs.Widgets
import qs.Services import qs.Services
Column { Column {
id: root id: root
property var items: [] property var items: []
property var allWidgets: [] property var allWidgets: []
property string title: "" property string title: ""
property string titleIcon: "widgets" property string titleIcon: "widgets"
property string sectionId: "" property string sectionId: ""
signal itemEnabledChanged(string sectionId, string itemId, bool enabled) signal itemEnabledChanged(string sectionId, string itemId, bool enabled)
signal itemOrderChanged(var newOrder) signal itemOrderChanged(var newOrder)
signal addWidget(string sectionId) signal addWidget(string sectionId)
signal removeWidget(string sectionId, int widgetIndex) signal removeWidget(string sectionId, int widgetIndex)
signal spacerSizeChanged(string sectionId, string itemId, int newSize) signal spacerSizeChanged(string sectionId, string itemId, int newSize)
signal compactModeChanged(string widgetId, bool enabled) signal compactModeChanged(string widgetId, bool enabled)
signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex) signal gpuSelectionChanged(string sectionId, int widgetIndex, int selectedIndex)
width: parent.width
height: implicitHeight
spacing: Theme.spacingM
Row {
width: parent.width width: parent.width
height: implicitHeight
spacing: Theme.spacingM spacing: Theme.spacingM
Row { DankIcon {
width: parent.width name: root.titleIcon
spacing: Theme.spacingM size: Theme.iconSize
color: Theme.primary
DankIcon { anchors.verticalCenter: parent.verticalCenter
name: root.titleIcon
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 60
height: 1
}
} }
Column { StyledText {
id: itemsList text: root.title
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
width: parent.width Item {
spacing: Theme.spacingS width: parent.width - 60
height: 1
}
}
Repeater { Column {
model: root.items id: itemsList
delegate: Item { width: parent.width
id: delegateItem spacing: Theme.spacingS
property bool held: dragArea.pressed Repeater {
property real originalY: y model: root.items
width: itemsList.width delegate: Item {
height: 70 id: delegateItem
z: held ? 2 : 1
Rectangle { property bool held: dragArea.pressed
id: itemBackground property real originalY: y
anchors.fill: parent width: itemsList.width
anchors.margins: 2 height: 70
radius: Theme.cornerRadius z: held ? 2 : 1
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.8)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
DankIcon { Rectangle {
name: "drag_indicator" id: itemBackground
size: Theme.iconSize - 4
color: Theme.outline anchors.fill: parent
anchors.left: parent.left anchors.margins: 2
anchors.leftMargin: Theme.spacingM + 8 radius: Theme.cornerRadius
anchors.verticalCenter: parent.verticalCenter color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
opacity: 0.8 Theme.surfaceContainer.b, 0.8)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
DankIcon {
name: "drag_indicator"
size: Theme.iconSize - 4
color: Theme.outline
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM + 8
anchors.verticalCenter: parent.verticalCenter
opacity: 0.8
}
DankIcon {
name: modelData.icon
size: Theme.iconSize
color: modelData.enabled ? Theme.primary : Theme.outline
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM * 2 + 40
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize
anchors.right: actionButtons.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.text
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: modelData.enabled ? Theme.surfaceText : Theme.outline
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: modelData.enabled ? Theme.outline : Qt.rgba(
Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.6)
elide: Text.ElideRight
width: parent.width
wrapMode: Text.WordWrap
}
}
Row {
id: actionButtons
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Item {
width: 120
height: 32
visible: modelData.id === "gpuTemp"
DankDropdown {
id: gpuDropdown
anchors.fill: parent
currentValue: {
var selectedIndex = modelData.selectedGpuIndex
!== undefined ? modelData.selectedGpuIndex : 0
if (SysMonitorService.availableGpus
&& SysMonitorService.availableGpus.length > selectedIndex
&& selectedIndex >= 0) {
var gpu = SysMonitorService.availableGpus[selectedIndex]
return gpu.driver.toUpperCase() + " (" + Math.round(
gpu.temperature || 0) + "°C)"
}
return SysMonitorService.availableGpus
&& SysMonitorService.availableGpus.length
> 0 ? SysMonitorService.availableGpus[0].driver.toUpperCase(
) + " (" + Math.round(
SysMonitorService.availableGpus[0].temperature
|| 0) + "°C)" : ""
}
options: {
var gpuOptions = []
if (SysMonitorService.availableGpus
&& SysMonitorService.availableGpus.length > 0) {
for (var i = 0; i < SysMonitorService.availableGpus.length; i++) {
var gpu = SysMonitorService.availableGpus[i]
gpuOptions.push(gpu.driver.toUpperCase(
) + " (" + Math.round(gpu.temperature
|| 0) + "°C)")
} }
}
DankIcon { return gpuOptions
name: modelData.icon }
size: Theme.iconSize onValueChanged: value => {
color: modelData.enabled ? Theme.primary : Theme.outline var gpuIndex = options.indexOf(value)
anchors.left: parent.left if (gpuIndex >= 0) {
anchors.leftMargin: Theme.spacingM * 2 + 40 root.gpuSelectionChanged(root.sectionId,
anchors.verticalCenter: parent.verticalCenter index, gpuIndex)
} }
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM * 3 + 40 + Theme.iconSize
anchors.right: actionButtons.left
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.text
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: modelData.enabled ? Theme.surfaceText : Theme.outline
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: modelData.enabled ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
elide: Text.ElideRight
width: parent.width
wrapMode: Text.WordWrap
}
}
Row {
id: actionButtons
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Item {
width: 120
height: 32
visible: modelData.id === "gpuTemp"
DankDropdown {
id: gpuDropdown
anchors.fill: parent
currentValue: {
var selectedIndex = modelData.selectedGpuIndex !== undefined ? modelData.selectedGpuIndex : 0;
if (SysMonitorService.availableGpus && SysMonitorService.availableGpus.length > selectedIndex && selectedIndex >= 0) {
var gpu = SysMonitorService.availableGpus[selectedIndex];
return gpu.driver.toUpperCase() + " (" + Math.round(gpu.temperature || 0) + "°C)";
}
return SysMonitorService.availableGpus && SysMonitorService.availableGpus.length > 0 ? SysMonitorService.availableGpus[0].driver.toUpperCase() + " (" + Math.round(SysMonitorService.availableGpus[0].temperature || 0) + "°C)" : "";
} }
options: { }
var gpuOptions = []; }
if (SysMonitorService.availableGpus && SysMonitorService.availableGpus.length > 0) {
for (var i = 0; i < SysMonitorService.availableGpus.length; i++) {
var gpu = SysMonitorService.availableGpus[i];
gpuOptions.push(gpu.driver.toUpperCase() + " (" + Math.round(gpu.temperature || 0) + "°C)");
}
}
return gpuOptions;
}
onValueChanged: (value) => {
var gpuIndex = options.indexOf(value);
if (gpuIndex >= 0) {
root.gpuSelectionChanged(root.sectionId, index, gpuIndex);
}
}
}
}
Item { Item {
width: 32 width: 32
height: 32 height: 32
visible: modelData.id === "clock" || modelData.id === "music" visible: modelData.id === "clock" || modelData.id === "music"
DankActionButton { DankActionButton {
id: compactModeButton id: compactModeButton
anchors.fill: parent anchors.fill: parent
buttonSize: 32 buttonSize: 32
iconName: (modelData.id === "clock" && SettingsData.clockCompactMode) || (modelData.id === "music" && SettingsData.mediaCompactMode) ? "zoom_out" : "zoom_in" iconName: (modelData.id === "clock"
iconSize: 18 && SettingsData.clockCompactMode)
iconColor: ((modelData.id === "clock" && SettingsData.clockCompactMode) || (modelData.id === "music" && SettingsData.mediaCompactMode)) ? Theme.primary : Theme.outline || (modelData.id === "music"
onClicked: { && SettingsData.mediaCompactMode) ? "zoom_out" : "zoom_in"
if (modelData.id === "clock") { iconSize: 18
root.compactModeChanged("clock", !SettingsData.clockCompactMode); iconColor: ((modelData.id === "clock"
} else if (modelData.id === "music") { && SettingsData.clockCompactMode)
root.compactModeChanged("music", !SettingsData.mediaCompactMode); || (modelData.id === "music"
} && SettingsData.mediaCompactMode)) ? Theme.primary : Theme.outline
} onClicked: {
} if (modelData.id === "clock") {
root.compactModeChanged("clock",
!SettingsData.clockCompactMode)
} else if (modelData.id === "music") {
root.compactModeChanged("music",
!SettingsData.mediaCompactMode)
}
}
}
Rectangle { Rectangle {
id: compactModeTooltip id: compactModeTooltip
width: tooltipText.contentWidth + Theme.spacingM * 2 width: tooltipText.contentWidth + Theme.spacingM * 2
height: tooltipText.contentHeight + Theme.spacingS * 2 height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.surfaceContainer color: Theme.surfaceContainer
border.color: Theme.outline border.color: Theme.outline
border.width: 1 border.width: 1
visible: compactModeButton.children[1] && compactModeButton.children[1].containsMouse visible: compactModeButton.children[1]
opacity: visible ? 1 : 0 && compactModeButton.children[1].containsMouse
x: -width - Theme.spacingS opacity: visible ? 1 : 0
y: (parent.height - height) / 2 x: -width - Theme.spacingS
z: 100 y: (parent.height - height) / 2
z: 100
StyledText {
id: tooltipText
anchors.centerIn: parent
text: "Compact Mode"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
DankActionButton {
visible: modelData.id !== "spacer"
buttonSize: 32
iconName: modelData.enabled ? "visibility" : "visibility_off"
iconSize: 18
iconColor: modelData.enabled ? Theme.primary : Theme.outline
onClicked: {
root.itemEnabledChanged(root.sectionId, modelData.id, !modelData.enabled);
}
}
Row {
visible: modelData.id === "spacer"
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 24
iconName: "remove"
iconSize: 14
iconColor: Theme.outline
onClicked: {
var currentSize = modelData.size || 20;
var newSize = Math.max(5, currentSize - 5);
root.spacerSizeChanged(root.sectionId, modelData.id, newSize);
}
}
StyledText {
text: (modelData.size || 20).toString()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
DankActionButton {
buttonSize: 24
iconName: "add"
iconSize: 14
iconColor: Theme.outline
onClicked: {
var currentSize = modelData.size || 20;
var newSize = Math.min(5000, currentSize + 5);
root.spacerSizeChanged(root.sectionId, modelData.id, newSize);
}
}
}
DankActionButton {
buttonSize: 32
iconName: "close"
iconSize: 18
iconColor: Theme.error
onClicked: {
root.removeWidget(root.sectionId, index);
}
}
}
MouseArea {
id: dragArea
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 60
hoverEnabled: true
cursorShape: Qt.SizeVerCursor
drag.target: held ? delegateItem : undefined
drag.axis: Drag.YAxis
drag.minimumY: -delegateItem.height
drag.maximumY: itemsList.height
preventStealing: true
onPressed: {
delegateItem.z = 2;
delegateItem.originalY = delegateItem.y;
}
onReleased: {
delegateItem.z = 1;
if (drag.active) {
var newIndex = Math.round(delegateItem.y / (delegateItem.height + itemsList.spacing));
newIndex = Math.max(0, Math.min(newIndex, root.items.length - 1));
if (newIndex !== index) {
var newItems = root.items.slice();
var draggedItem = newItems.splice(index, 1)[0];
newItems.splice(newIndex, 0, draggedItem);
root.itemOrderChanged(newItems.map((item) => {
return ({
"id": item.id,
"enabled": item.enabled,
"size": item.size
});
}));
}
}
delegateItem.x = 0;
delegateItem.y = delegateItem.originalY;
}
}
Behavior on y {
enabled: !dragArea.held && !dragArea.drag.active
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
StyledText {
id: tooltipText
anchors.centerIn: parent
text: "Compact Mode"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
} }
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
} }
} DankActionButton {
visible: modelData.id !== "spacer"
buttonSize: 32
iconName: modelData.enabled ? "visibility" : "visibility_off"
iconSize: 18
iconColor: modelData.enabled ? Theme.primary : Theme.outline
onClicked: {
root.itemEnabledChanged(root.sectionId, modelData.id,
!modelData.enabled)
}
}
} Row {
visible: modelData.id === "spacer"
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
Rectangle { DankActionButton {
width: 200 buttonSize: 24
height: 40 iconName: "remove"
radius: Theme.cornerRadius iconSize: 14
color: addButtonArea.containsMouse ? Theme.primaryContainer : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) iconColor: Theme.outline
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) onClicked: {
border.width: 1 var currentSize = modelData.size || 20
anchors.horizontalCenter: parent.horizontalCenter var newSize = Math.max(5, currentSize - 5)
root.spacerSizeChanged(root.sectionId, modelData.id, newSize)
}
}
StyledText { StyledText {
text: "Add Widget" text: (modelData.size || 20).toString()
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium color: Theme.surfaceText
color: Theme.primary anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: parent.verticalCenter }
anchors.centerIn: parent
}
MouseArea { DankActionButton {
id: addButtonArea buttonSize: 24
iconName: "add"
iconSize: 14
iconColor: Theme.outline
onClicked: {
var currentSize = modelData.size || 20
var newSize = Math.min(5000, currentSize + 5)
root.spacerSizeChanged(root.sectionId, modelData.id, newSize)
}
}
}
anchors.fill: parent DankActionButton {
buttonSize: 32
iconName: "close"
iconSize: 18
iconColor: Theme.error
onClicked: {
root.removeWidget(root.sectionId, index)
}
}
}
MouseArea {
id: dragArea
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: 60
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.SizeVerCursor
onClicked: { drag.target: held ? delegateItem : undefined
root.addWidget(root.sectionId); drag.axis: Drag.YAxis
drag.minimumY: -delegateItem.height
drag.maximumY: itemsList.height
preventStealing: true
onPressed: {
delegateItem.z = 2
delegateItem.originalY = delegateItem.y
} }
} onReleased: {
delegateItem.z = 1
Behavior on color { if (drag.active) {
ColorAnimation { var newIndex = Math.round(
duration: Theme.shortDuration delegateItem.y / (delegateItem.height + itemsList.spacing))
easing.type: Theme.standardEasing newIndex = Math.max(0, Math.min(newIndex,
root.items.length - 1))
if (newIndex !== index) {
var newItems = root.items.slice()
var draggedItem = newItems.splice(index, 1)[0]
newItems.splice(newIndex, 0, draggedItem)
root.itemOrderChanged(newItems.map(item => {
return ({
"id": item.id,
"enabled": item.enabled,
"size": item.size
})
}))
}
}
delegateItem.x = 0
delegateItem.y = delegateItem.originalY
} }
}
Behavior on y {
enabled: !dragArea.held && !dragArea.drag.active
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
}
}
}
Rectangle {
width: 200
height: 40
radius: Theme.cornerRadius
color: addButtonArea.containsMouse ? Theme.primaryContainer : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: "Add Widget"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
anchors.centerIn: parent
} }
MouseArea {
id: addButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.addWidget(root.sectionId)
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
} }

View File

@@ -8,130 +8,124 @@ import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
property var modelData property var modelData
screen: modelData screen: modelData
visible: ToastService.toastVisible visible: ToastService.toastVisible
WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent" color: "transparent"
anchors { anchors {
top: true top: true
left: true left: true
right: true right: true
bottom: true bottom: true
}
Rectangle {
id: toast
width: Math.min(400, Screen.width - Theme.spacingL * 2)
height: toastContent.height + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
y: Theme.barHeight + Theme.spacingL
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
return Theme.error
case ToastService.levelWarn:
return Theme.warning
case ToastService.levelInfo:
return Theme.primary
default:
return Theme.primary
}
}
radius: Theme.cornerRadiusLarge
layer.enabled: true
opacity: ToastService.toastVisible ? 0.9 : 0
scale: ToastService.toastVisible ? 1 : 0.9
Row {
id: toastContent
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
return "error"
case ToastService.levelWarn:
return "warning"
case ToastService.levelInfo:
return "info"
default:
return "info"
}
}
size: Theme.iconSize
color: Theme.background
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: ToastService.currentMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
width: 300
wrapMode: Text.WordWrap
}
} }
Rectangle { MouseArea {
id: toast anchors.fill: parent
onClicked: ToastService.hideToast()
width: Math.min(400, Screen.width - Theme.spacingL * 2)
height: toastContent.height + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
y: Theme.barHeight + Theme.spacingL
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
return Theme.error;
case ToastService.levelWarn:
return Theme.warning;
case ToastService.levelInfo:
return Theme.primary;
default:
return Theme.primary;
}
}
radius: Theme.cornerRadiusLarge
layer.enabled: true
opacity: ToastService.toastVisible ? 0.9 : 0
scale: ToastService.toastVisible ? 1 : 0.9
Row {
id: toastContent
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: {
switch (ToastService.currentLevel) {
case ToastService.levelError:
return "error";
case ToastService.levelWarn:
return "warning";
case ToastService.levelInfo:
return "info";
default:
return "info";
}
}
size: Theme.iconSize
color: Theme.background
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: ToastService.currentMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
width: 300
wrapMode: Text.WordWrap
}
}
MouseArea {
anchors.fill: parent
onClicked: ToastService.hideToast()
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
transform: Translate {
y: ToastService.toastVisible ? 0 : -20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
mask: Region { layer.effect: MultiEffect {
item: toast shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
} }
transform: Translate {
y: ToastService.toastVisible ? 0 : -20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
mask: Region {
item: toast
}
} }

View File

@@ -4,66 +4,66 @@ import qs.Common
import qs.Services import qs.Services
Item { Item {
id: root id: root
readonly property MprisPlayer activePlayer: MprisController.activePlayer readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool hasActiveMedia: activePlayer !== null readonly property bool hasActiveMedia: activePlayer !== null
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing readonly property bool isPlaying: hasActiveMedia && activePlayer
&& activePlayer.playbackState === MprisPlaybackState.Playing
width: 20 width: 20
height: Theme.iconSize height: Theme.iconSize
Ref { Ref {
service: CavaService service: CavaService
}
Timer {
id: fallbackTimer
running: !CavaService.cavaAvailable && isPlaying
interval: 256
repeat: true
onTriggered: {
CavaService.values = [Math.random() * 40 + 10, Math.random(
) * 60 + 20, Math.random() * 50 + 15, Math.random(
) * 35 + 20, Math.random() * 45 + 15, Math.random(
) * 55 + 25]
} }
}
Timer { Row {
id: fallbackTimer anchors.centerIn: parent
spacing: 1.5
running: !CavaService.cavaAvailable && isPlaying Repeater {
interval: 256 model: 6
repeat: true
onTriggered: { Rectangle {
CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25]; width: 2
height: {
if (root.isPlaying && CavaService.values.length > index) {
const rawLevel = CavaService.values[index] || 0
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0),
100) / 100) * 100
const maxHeight = Theme.iconSize - 2
const minHeight = 3
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight)
}
return 3
} }
} radius: 1.5
color: Theme.primary
Row { anchors.verticalCenter: parent.verticalCenter
anchors.centerIn: parent
spacing: 1.5
Repeater {
model: 6
Rectangle {
width: 2
height: {
if (root.isPlaying && CavaService.values.length > index) {
const rawLevel = CavaService.values[index] || 0;
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
const maxHeight = Theme.iconSize - 2;
const minHeight = 3;
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
}
return 3;
}
radius: 1.5
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
} }
}
} }
}
} }

View File

@@ -5,174 +5,172 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: battery id: battery
property bool batteryPopupVisible: false property bool batteryPopupVisible: false
property string section: "right" property string section: "right"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
signal toggleBatteryPopup() signal toggleBatteryPopup
width: BatteryService.batteryAvailable ? 70 : 40 width: BatteryService.batteryAvailable ? 70 : 40
height: 30 height: 30
radius: Theme.cornerRadius
color: {
const baseColor = batteryArea.containsMouse
|| batteryPopupVisible ? Theme.primaryPressed : Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
visible: true
Row {
anchors.centerIn: parent
spacing: 4
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel,
BatteryService.isCharging,
BatteryService.batteryAvailable)
size: Theme.iconSize - 6
color: {
if (!BatteryService.batteryAvailable)
return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error
if (BatteryService.isCharging)
return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
SequentialAnimation on opacity {
running: BatteryService.isCharging
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: Anims.durLong
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
NumberAnimation {
to: 1
duration: Anims.durLong
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
StyledText {
text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: {
if (!BatteryService.batteryAvailable)
return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error
if (BatteryService.isCharging)
return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
}
}
MouseArea {
id: batteryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX,
Theme.barHeight + Theme.spacingXS,
width, section, currentScreen)
}
toggleBatteryPopup()
}
}
Rectangle {
id: batteryTooltip
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: Theme.surfaceContainer
const baseColor = batteryArea.containsMouse || batteryPopupVisible ? Theme.primaryPressed : Theme.secondaryHover; border.color: Theme.surfaceVariantAlpha
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); border.width: 1
} visible: batteryArea.containsMouse && !batteryPopupVisible
visible: true anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
opacity: batteryArea.containsMouse ? 1 : 0
Row { Column {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 4 spacing: 2
DankIcon { StyledText {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable) id: tooltipText
size: Theme.iconSize - 6
color: {
if (!BatteryService.batteryAvailable)
return Theme.surfaceText;
if (BatteryService.isLowBattery && !BatteryService.isCharging) text: {
return Theme.error; if (!BatteryService.batteryAvailable) {
if (typeof PowerProfiles === "undefined")
return "Power Management"
if (BatteryService.isCharging) switch (PowerProfiles.profile) {
return Theme.primary; case PowerProfile.PowerSaver:
return "Power Profile: Power Saver"
return Theme.surfaceText; case PowerProfile.Performance:
return "Power Profile: Performance"
default:
return "Power Profile: Balanced"
} }
anchors.verticalCenter: parent.verticalCenter }
let status = BatteryService.batteryStatus
SequentialAnimation on opacity { let level = BatteryService.batteryLevel + "%"
running: BatteryService.isCharging let time = BatteryService.formatTimeRemaining()
loops: Animation.Infinite if (time !== "Unknown")
return status + " • " + level + " • " + time
NumberAnimation { else
to: 0.6 return status + " • " + level
duration: Anims.durLong
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
NumberAnimation {
to: 1
duration: Anims.durLong
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
} }
font.pixelSize: Theme.fontSizeSmall
StyledText { color: Theme.surfaceText
text: BatteryService.batteryLevel + "%" horizontalAlignment: Text.AlignHCenter
font.pixelSize: Theme.fontSizeSmall }
font.weight: Font.Medium
color: {
if (!BatteryService.batteryAvailable)
return Theme.surfaceText;
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
}
} }
MouseArea { Behavior on opacity {
id: batteryArea NumberAnimation {
duration: Theme.shortDuration
anchors.fill: parent easing.type: Theme.standardEasing
hoverEnabled: true }
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0);
var currentScreen = parentScreen || Screen;
var screenX = currentScreen.x || 0;
var relativeX = globalPos.x - screenX;
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen);
}
toggleBatteryPopup();
}
} }
}
Rectangle { Behavior on color {
id: batteryTooltip ColorAnimation {
duration: Theme.shortDuration
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2) easing.type: Theme.standardEasing
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.surfaceVariantAlpha
border.width: 1
visible: batteryArea.containsMouse && !batteryPopupVisible
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
opacity: batteryArea.containsMouse ? 1 : 0
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
id: tooltipText
text: {
if (!BatteryService.batteryAvailable) {
if (typeof PowerProfiles === "undefined")
return "Power Management";
switch (PowerProfiles.profile) {
case PowerProfile.PowerSaver:
return "Power Profile: Power Saver";
case PowerProfile.Performance:
return "Power Profile: Performance";
default:
return "Power Profile: Balanced";
}
}
let status = BatteryService.batteryStatus;
let level = BatteryService.batteryLevel + "%";
let time = BatteryService.formatTimeRemaining();
if (time !== "Unknown")
return status + " • " + level + " • " + time;
else
return status + " • " + level;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,89 +4,91 @@ import qs.Common
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property date currentDate: new Date() property date currentDate: new Date()
property bool compactMode: false property bool compactMode: false
property string section: "center" property string section: "center"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
signal clockClicked() signal clockClicked
width: clockRow.implicitWidth + Theme.spacingS * 2 width: clockRow.implicitWidth + Theme.spacingS * 2
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = clockMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover; const baseColor = clockMouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
} baseColor.a * Theme.widgetTransparency)
Component.onCompleted: { }
root.currentDate = systemClock.date; Component.onCompleted: {
root.currentDate = systemClock.date
}
Row {
id: clockRow
anchors.centerIn: parent
spacing: Theme.spacingS
StyledText {
text: SettingsData.use24HourClock ? Qt.formatTime(root.currentDate,
"H:mm") : Qt.formatTime(
root.currentDate, "h:mm AP")
font.pixelSize: Theme.fontSizeMedium - 1
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
Row { StyledText {
id: clockRow text: "•"
font.pixelSize: Theme.fontSizeSmall
anchors.centerIn: parent color: Theme.outlineButton
spacing: Theme.spacingS anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.clockCompactMode
StyledText {
text: SettingsData.use24HourClock ? Qt.formatTime(root.currentDate, "H:mm") : Qt.formatTime(root.currentDate, "h:mm AP")
font.pixelSize: Theme.fontSizeMedium - 1
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.clockCompactMode
}
StyledText {
text: Qt.formatDate(root.currentDate, "ddd d")
font.pixelSize: Theme.fontSizeMedium - 1
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.clockCompactMode
}
} }
SystemClock { StyledText {
id: systemClock text: Qt.formatDate(root.currentDate, "ddd d")
font.pixelSize: Theme.fontSizeMedium - 1
precision: SystemClock.Seconds color: Theme.surfaceText
onDateChanged: root.currentDate = systemClock.date anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.clockCompactMode
} }
}
MouseArea { SystemClock {
id: clockMouseArea id: systemClock
anchors.fill: parent precision: SystemClock.Seconds
hoverEnabled: true onDateChanged: root.currentDate = systemClock.date
cursorShape: Qt.PointingHandCursor }
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) { MouseArea {
var globalPos = mapToGlobal(0, 0); id: clockMouseArea
var currentScreen = parentScreen || Screen;
var screenX = currentScreen.x || 0; anchors.fill: parent
var relativeX = globalPos.x - screenX; hoverEnabled: true
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen); cursorShape: Qt.PointingHandCursor
} onClicked: {
root.clockClicked(); if (popupTarget && popupTarget.setTriggerPosition) {
} var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX,
Theme.barHeight + Theme.spacingXS,
width, section, currentScreen)
}
root.clockClicked()
} }
}
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
}
} }
}
} }

View File

@@ -4,140 +4,145 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool isActive: false property bool isActive: false
property string section: "right" property string section: "right"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
signal clicked() signal clicked
function getWiFiSignalIcon(signalStrength) { function getWiFiSignalIcon(signalStrength) {
switch (signalStrength) { switch (signalStrength) {
case "excellent": case "excellent":
return "wifi"; return "wifi"
case "good": case "good":
return "wifi_2_bar"; return "wifi_2_bar"
case "fair": case "fair":
return "wifi_1_bar"; return "wifi_1_bar"
case "poor": case "poor":
return "signal_wifi_0_bar"; return "signal_wifi_0_bar"
default: default:
return "wifi"; return "wifi"
} }
}
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = controlCenterArea.containsMouse
|| root.isActive ? Theme.primaryPressed : Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
Row {
id: controlIndicators
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
if (NetworkService.networkStatus === "ethernet")
return "lan"
else if (NetworkService.networkStatus === "wifi")
return getWiFiSignalIcon(NetworkService.wifiSignalStrength)
else
return "wifi_off"
}
size: Theme.iconSize - 8
color: NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: true
} }
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2) DankIcon {
height: 30 name: "bluetooth"
radius: Theme.cornerRadius size: Theme.iconSize - 8
color: { color: BluetoothService.enabled ? Theme.primary : Theme.outlineButton
const baseColor = controlCenterArea.containsMouse || root.isActive ? Theme.primaryPressed : Theme.secondaryHover; anchors.verticalCenter: parent.verticalCenter
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); visible: BluetoothService.available && BluetoothService.enabled
} }
Row { Rectangle {
id: controlIndicators width: audioIcon.implicitWidth + 4
height: audioIcon.implicitHeight + 4
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
id: audioIcon
name: (AudioService.sink && AudioService.sink.audio
&& AudioService.sink.audio.muted) ? "volume_off" : (AudioService.sink
&& AudioService.sink.audio
&& AudioService.sink.audio.volume * 100) < 33 ? "volume_down" : "volume_up"
size: Theme.iconSize - 8
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse
|| root.isActive ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS }
DankIcon { MouseArea {
name: { id: audioWheelArea
if (NetworkService.networkStatus === "ethernet")
return "lan";
else if (NetworkService.networkStatus === "wifi")
return getWiFiSignalIcon(NetworkService.wifiSignalStrength);
else
return "wifi_off";
}
size: Theme.iconSize - 8
color: NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: true
}
DankIcon {
name: "bluetooth"
size: Theme.iconSize - 8
color: BluetoothService.enabled ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: BluetoothService.available && BluetoothService.enabled
}
Rectangle {
width: audioIcon.implicitWidth + 4
height: audioIcon.implicitHeight + 4
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
DankIcon {
id: audioIcon
name: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted) ? "volume_off" : (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) < 33 ? "volume_down" : "volume_up"
size: Theme.iconSize - 8
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: audioWheelArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y;
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
let newVolume;
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5);
else
newVolume = Math.max(0, currentVolume - 5);
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newVolume / 100;
}
wheelEvent.accepted = true;
}
}
}
DankIcon {
name: "mic"
size: Theme.iconSize - 8
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: false // TODO: Add mic detection
}
}
MouseArea {
id: controlCenterArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.NoButton
onClicked: { onWheel: function (wheelEvent) {
if (popupTarget && popupTarget.setTriggerPosition) { let delta = wheelEvent.angleDelta.y
var globalPos = mapToGlobal(0, 0); let currentVolume = (AudioService.sink && AudioService.sink.audio
var currentScreen = parentScreen || Screen; && AudioService.sink.audio.volume * 100) || 0
var screenX = currentScreen.x || 0; let newVolume
var relativeX = globalPos.x - screenX; if (delta > 0)
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen); newVolume = Math.min(100, currentVolume + 5)
} else
root.clicked(); newVolume = Math.max(0, currentVolume - 5)
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newVolume / 100
}
wheelEvent.accepted = true
} }
}
} }
Behavior on color { DankIcon {
ColorAnimation { name: "mic"
duration: Theme.shortDuration size: Theme.iconSize - 8
easing.type: Theme.standardEasing color: Theme.primary
} anchors.verticalCenter: parent.verticalCenter
visible: false // TODO: Add mic detection
} }
}
MouseArea {
id: controlCenterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX,
Theme.barHeight + Theme.spacingXS,
width, section, currentScreen)
}
root.clicked()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }

View File

@@ -5,81 +5,84 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool showPercentage: true property bool showPercentage: true
property bool showIcon: true property bool showIcon: true
property var toggleProcessList property var toggleProcessList
property string section: "right" property string section: "right"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
width: 55 width: 55
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = cpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover; const baseColor = cpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
Component.onCompleted: {
SysMonitorService.addRef()
}
Component.onDestruction: {
SysMonitorService.removeRef()
}
MouseArea {
id: cpuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX,
Theme.barHeight + Theme.spacingXS,
width, section, currentScreen)
}
SysMonitorService.setSortBy("cpu")
if (root.toggleProcessList)
root.toggleProcessList()
} }
Component.onCompleted: { }
SysMonitorService.addRef();
} Row {
Component.onDestruction: { anchors.centerIn: parent
SysMonitorService.removeRef(); spacing: 3
DankIcon {
name: "memory"
size: Theme.iconSize - 8
color: {
if (SysMonitorService.cpuUsage > 80)
return Theme.tempDanger
if (SysMonitorService.cpuUsage > 60)
return Theme.tempWarning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
} }
MouseArea { StyledText {
id: cpuArea text: {
if (SysMonitorService.cpuUsage === undefined
anchors.fill: parent || SysMonitorService.cpuUsage === null
hoverEnabled: true || SysMonitorService.cpuUsage === 0) {
cursorShape: Qt.PointingHandCursor return "--%"
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0);
var currentScreen = parentScreen || Screen;
var screenX = currentScreen.x || 0;
var relativeX = globalPos.x - screenX;
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen);
}
SysMonitorService.setSortBy("cpu");
if (root.toggleProcessList)
root.toggleProcessList();
} }
return SysMonitorService.cpuUsage.toFixed(0) + "%"
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
}
Row {
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "memory"
size: Theme.iconSize - 8
color: {
if (SysMonitorService.cpuUsage > 80)
return Theme.tempDanger;
if (SysMonitorService.cpuUsage > 60)
return Theme.tempWarning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (SysMonitorService.cpuUsage === undefined || SysMonitorService.cpuUsage === null || SysMonitorService.cpuUsage === 0) {
return "--%";
}
return SysMonitorService.cpuUsage.toFixed(0) + "%";
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
} }

View File

@@ -5,87 +5,91 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool showPercentage: true property bool showPercentage: true
property bool showIcon: true property bool showIcon: true
property var toggleProcessList property var toggleProcessList
property string section: "right" property string section: "right"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
width: 55 width: 55
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = cpuTempArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover; const baseColor = cpuTempArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
Component.onCompleted: {
SysMonitorService.addRef()
}
Component.onDestruction: {
SysMonitorService.removeRef()
}
MouseArea {
id: cpuTempArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX,
Theme.barHeight + Theme.spacingXS,
width, section, currentScreen)
}
SysMonitorService.setSortBy("cpu")
if (root.toggleProcessList)
root.toggleProcessList()
} }
Component.onCompleted: { }
SysMonitorService.addRef();
} Row {
Component.onDestruction: { anchors.centerIn: parent
SysMonitorService.removeRef(); spacing: 3
DankIcon {
name: "memory"
size: Theme.iconSize - 8
color: {
if (SysMonitorService.cpuTemperature > 85)
return Theme.tempDanger
if (SysMonitorService.cpuTemperature > 69)
return Theme.tempWarning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
} }
MouseArea { StyledText {
id: cpuTempArea text: {
if (SysMonitorService.cpuTemperature === undefined
anchors.fill: parent || SysMonitorService.cpuTemperature === null
hoverEnabled: true || SysMonitorService.cpuTemperature < 0) {
cursorShape: Qt.PointingHandCursor return "--°"
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0);
var currentScreen = parentScreen || Screen;
var screenX = currentScreen.x || 0;
var relativeX = globalPos.x - screenX;
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen);
}
SysMonitorService.setSortBy("cpu");
if (root.toggleProcessList)
root.toggleProcessList();
} }
return Math.round(SysMonitorService.cpuTemperature) + "°"
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
}
Row { Behavior on color {
anchors.centerIn: parent ColorAnimation {
spacing: 3 duration: Theme.shortDuration
easing.type: Theme.standardEasing
DankIcon {
name: "memory"
size: Theme.iconSize - 8
color: {
if (SysMonitorService.cpuTemperature > 85)
return Theme.tempDanger;
if (SysMonitorService.cpuTemperature > 69)
return Theme.tempWarning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (SysMonitorService.cpuTemperature === undefined || SysMonitorService.cpuTemperature === null || SysMonitorService.cpuTemperature < 0) {
return "--°";
}
return Math.round(SysMonitorService.cpuTemperature) + "°";
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
} }
}
} }

View File

@@ -4,91 +4,93 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool compactMode: false property bool compactMode: false
property int availableWidth: 400 property int availableWidth: 400
readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2 readonly property int baseWidth: contentRow.implicitWidth + Theme.spacingS * 2
readonly property int maxNormalWidth: 456 readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288 readonly property int maxCompactWidth: 288
width: compactMode ? Math.min(baseWidth, maxCompactWidth) : Math.min(baseWidth, maxNormalWidth) width: compactMode ? Math.min(baseWidth,
height: 30 maxCompactWidth) : Math.min(baseWidth,
radius: Theme.cornerRadius maxNormalWidth)
color: { height: 30
if (!FocusedWindowService.focusedAppName && !FocusedWindowService.focusedWindowTitle) radius: Theme.cornerRadius
return "transparent"; color: {
if (!FocusedWindowService.focusedAppName
&& !FocusedWindowService.focusedWindowTitle)
return "transparent"
const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover; const baseColor = mouseArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
} baseColor.a * Theme.widgetTransparency)
clip: true }
visible: FocusedWindowService.niriAvailable && (FocusedWindowService.focusedAppName || FocusedWindowService.focusedWindowTitle) clip: true
visible: FocusedWindowService.niriAvailable
&& (FocusedWindowService.focusedAppName
|| FocusedWindowService.focusedWindowTitle)
Row { Row {
id: contentRow id: contentRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
StyledText { StyledText {
id: appText id: appText
text: FocusedWindowService.focusedAppName || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180)
}
StyledText {
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: appText.text && titleText.text
}
StyledText {
id: titleText
text: FocusedWindowService.focusedWindowTitle || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 180 : 250)
visible: text.length > 0
}
text: FocusedWindowService.focusedAppName || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180)
} }
MouseArea { StyledText {
id: mouseArea text: "•"
font.pixelSize: Theme.fontSizeSmall
anchors.fill: parent color: Theme.outlineButton
hoverEnabled: true anchors.verticalCenter: parent.verticalCenter
visible: appText.text && titleText.text
} }
Behavior on color { StyledText {
ColorAnimation { id: titleText
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
text: FocusedWindowService.focusedWindowTitle || ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 180 : 250)
visible: text.length > 0
} }
}
Behavior on width { MouseArea {
NumberAnimation { id: mouseArea
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
anchors.fill: parent
hoverEnabled: true
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
} }
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }

View File

@@ -5,107 +5,115 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool showPercentage: true property bool showPercentage: true
property bool showIcon: true property bool showIcon: true
property var toggleProcessList property var toggleProcessList
property string section: "right" property string section: "right"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
property var widgetData: null property var widgetData: null
property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex !== undefined) ? widgetData.selectedGpuIndex : 0 property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex
!== undefined) ? widgetData.selectedGpuIndex : 0
Connections { Connections {
target: SettingsData target: SettingsData
function onWidgetDataChanged() { function onWidgetDataChanged() {
// Force property re-evaluation by triggering change detection // Force property re-evaluation by triggering change detection
root.selectedGpuIndex = Qt.binding(() => { root.selectedGpuIndex = Qt.binding(() => {
return (root.widgetData && root.widgetData.selectedGpuIndex !== undefined) ? root.widgetData.selectedGpuIndex : 0; return (root.widgetData
}); && root.widgetData.selectedGpuIndex !== undefined) ? root.widgetData.selectedGpuIndex : 0
})
}
}
width: 55
height: 30
radius: Theme.cornerRadius
color: {
const baseColor = gpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
Component.onCompleted: {
SysMonitorService.addRef()
}
Component.onDestruction: {
SysMonitorService.removeRef()
}
property real displayTemp: {
if (!SysMonitorService.availableGpus
|| SysMonitorService.availableGpus.length === 0)
return 0
if (selectedGpuIndex >= 0
&& selectedGpuIndex < SysMonitorService.availableGpus.length) {
return SysMonitorService.availableGpus[selectedGpuIndex].temperature || 0
}
return 0
}
MouseArea {
id: gpuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX,
Theme.barHeight + Theme.spacingXS,
width, section, currentScreen)
}
SysMonitorService.setSortBy("cpu")
if (root.toggleProcessList)
root.toggleProcessList()
}
}
Row {
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "auto_awesome_mosaic"
size: Theme.iconSize - 8
color: {
if (root.displayTemp > 80)
return Theme.tempDanger
if (root.displayTemp > 65)
return Theme.tempWarning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (root.displayTemp === undefined || root.displayTemp === null
|| root.displayTemp === 0) {
return "--°"
} }
return Math.round(root.displayTemp) + "°"
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
}
width: 55 Behavior on color {
height: 30 ColorAnimation {
radius: Theme.cornerRadius duration: Theme.shortDuration
color: { easing.type: Theme.standardEasing
const baseColor = gpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
Component.onCompleted: {
SysMonitorService.addRef();
}
Component.onDestruction: {
SysMonitorService.removeRef();
}
property real displayTemp: {
if (!SysMonitorService.availableGpus || SysMonitorService.availableGpus.length === 0) return 0;
if (selectedGpuIndex >= 0 && selectedGpuIndex < SysMonitorService.availableGpus.length) {
return SysMonitorService.availableGpus[selectedGpuIndex].temperature || 0;
}
return 0;
}
MouseArea {
id: gpuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0);
var currentScreen = parentScreen || Screen;
var screenX = currentScreen.x || 0;
var relativeX = globalPos.x - screenX;
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen);
}
SysMonitorService.setSortBy("cpu");
if (root.toggleProcessList)
root.toggleProcessList();
}
}
Row {
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "auto_awesome_mosaic"
size: Theme.iconSize - 8
color: {
if (root.displayTemp > 80)
return Theme.tempDanger;
if (root.displayTemp > 65)
return Theme.tempWarning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) {
return "--°";
}
return Math.round(root.displayTemp) + "°";
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
} }
}
} }

View File

@@ -4,65 +4,67 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool isActive: false property bool isActive: false
property string section: "left" // Which section this button is in property string section: "left" // Which section this button is in
property var popupTarget: null // Reference to the popup to position property var popupTarget: null // Reference to the popup to position
property var parentScreen: null // The screen this button is on property var parentScreen: null // The screen this button is on
signal clicked() signal clicked
width: 40 width: 40
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = launcherArea.containsMouse || isActive ? Theme.surfaceTextPressed : Theme.surfaceTextHover; const baseColor = launcherArea.containsMouse
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); || isActive ? Theme.surfaceTextPressed : Theme.surfaceTextHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
SystemLogo {
visible: SettingsData.useOSLogo
anchors.centerIn: parent
width: Theme.iconSize - 3
height: Theme.iconSize - 3
colorOverride: SettingsData.osLogoColorOverride
brightnessOverride: SettingsData.osLogoBrightness
contrastOverride: SettingsData.osLogoContrast
}
DankIcon {
visible: !SettingsData.useOSLogo
anchors.centerIn: parent
name: "apps"
size: Theme.iconSize - 6
color: Theme.surfaceText
}
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX,
Theme.barHeight + Theme.spacingXS,
width, section, currentScreen)
}
root.clicked()
} }
}
SystemLogo { Behavior on color {
visible: SettingsData.useOSLogo ColorAnimation {
anchors.centerIn: parent duration: Theme.shortDuration
width: Theme.iconSize - 3 easing.type: Theme.standardEasing
height: Theme.iconSize - 3
colorOverride: SettingsData.osLogoColorOverride
brightnessOverride: SettingsData.osLogoBrightness
contrastOverride: SettingsData.osLogoContrast
} }
}
DankIcon {
visible: !SettingsData.useOSLogo
anchors.centerIn: parent
name: "apps"
size: Theme.iconSize - 6
color: Theme.surfaceText
}
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0);
var currentScreen = parentScreen || Screen;
var screenX = currentScreen.x || 0;
var relativeX = globalPos.x - screenX;
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen);
}
root.clicked();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }

View File

@@ -5,260 +5,252 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
readonly property MprisPlayer activePlayer: MprisController.activePlayer readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool playerAvailable: activePlayer !== null readonly property bool playerAvailable: activePlayer !== null
property bool compactMode: false property bool compactMode: false
readonly property int baseContentWidth: mediaRow.implicitWidth + Theme.spacingS * 2 readonly property int baseContentWidth: mediaRow.implicitWidth + Theme.spacingS * 2
readonly property int normalContentWidth: Math.min(280, baseContentWidth) readonly property int normalContentWidth: Math.min(280, baseContentWidth)
readonly property int compactContentWidth: Math.min(120, baseContentWidth) readonly property int compactContentWidth: Math.min(120, baseContentWidth)
property string section: "center" property string section: "center"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
signal clicked() signal clicked
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = Theme.surfaceTextHover; const baseColor = Theme.surfaceTextHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
states: [
State {
name: "shown"
when: playerAvailable
PropertyChanges {
target: root
opacity: 1
width: SettingsData.mediaCompactMode ? compactContentWidth : normalContentWidth
}
},
State {
name: "hidden"
when: !playerAvailable
PropertyChanges {
target: root
opacity: 0
width: 0
}
} }
states: [ ]
State { transitions: [
name: "shown" Transition {
when: playerAvailable from: "shown"
to: "hidden"
PropertyChanges {
target: root
opacity: 1
width: SettingsData.mediaCompactMode ? compactContentWidth : normalContentWidth
}
},
State {
name: "hidden"
when: !playerAvailable
PropertyChanges {
target: root
opacity: 0
width: 0
}
SequentialAnimation {
PauseAnimation {
duration: 500
} }
]
transitions: [
Transition {
from: "shown"
to: "hidden"
SequentialAnimation {
PauseAnimation {
duration: 500
}
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
},
Transition {
from: "hidden"
to: "shown"
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
} }
] }
},
Transition {
from: "hidden"
to: "shown"
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
]
Row {
id: mediaRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Row { Row {
id: mediaRow id: mediaInfo
anchors.centerIn: parent spacing: Theme.spacingXS
spacing: Theme.spacingXS
Row { AudioVisualization {
id: mediaInfo anchors.verticalCenter: parent.verticalCenter
}
spacing: Theme.spacingXS StyledText {
id: mediaText
AudioVisualization { anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenter: parent.verticalCenter width: SettingsData.mediaCompactMode ? 60 : 140
} visible: !SettingsData.mediaCompactMode
text: {
StyledText { if (!activePlayer || !activePlayer.trackTitle)
id: mediaText return ""
anchors.verticalCenter: parent.verticalCenter
width: SettingsData.mediaCompactMode ? 60 : 140
visible: !SettingsData.mediaCompactMode
text: {
if (!activePlayer || !activePlayer.trackTitle)
return "";
let identity = activePlayer.identity || "";
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari");
let title = "";
let subtitle = "";
if (isWebMedia && activePlayer.trackTitle) {
title = activePlayer.trackTitle;
subtitle = activePlayer.trackArtist || identity;
} else {
title = activePlayer.trackTitle || "Unknown Track";
subtitle = activePlayer.trackArtist || "";
}
return subtitle.length > 0 ? title + " • " + subtitle : title;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.popupTarget && root.popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0);
var currentScreen = root.parentScreen || Screen;
var screenX = currentScreen.x || 0;
var relativeX = globalPos.x - screenX;
root.popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, root.width, root.section, currentScreen);
}
root.clicked();
}
}
}
let identity = activePlayer.identity || ""
let isWebMedia = identity.toLowerCase().includes("firefox")
|| identity.toLowerCase().includes(
"chrome") || identity.toLowerCase().includes("chromium")
|| identity.toLowerCase().includes(
"edge") || identity.toLowerCase().includes("safari")
let title = ""
let subtitle = ""
if (isWebMedia && activePlayer.trackTitle) {
title = activePlayer.trackTitle
subtitle = activePlayer.trackArtist || identity
} else {
title = activePlayer.trackTitle || "Unknown Track"
subtitle = activePlayer.trackArtist || ""
}
return subtitle.length > 0 ? title + " • " + subtitle : title
} }
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
Row { MouseArea {
spacing: Theme.spacingXS anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter hoverEnabled: true
cursorShape: Qt.PointingHandCursor
Rectangle { onClicked: {
width: 20 if (root.popupTarget && root.popupTarget.setTriggerPosition) {
height: 20 var globalPos = mapToGlobal(0, 0)
radius: 10 var currentScreen = root.parentScreen || Screen
anchors.verticalCenter: parent.verticalCenter var screenX = currentScreen.x || 0
color: prevArea.containsMouse ? Theme.primaryHover : "transparent" var relativeX = globalPos.x - screenX
visible: root.playerAvailable root.popupTarget.setTriggerPosition(
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3 relativeX, Theme.barHeight + Theme.spacingXS, root.width,
root.section, currentScreen)
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 12
color: Theme.surfaceText
}
MouseArea {
id: prevArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer)
activePlayer.previous();
}
}
} }
root.clicked()
Rectangle { }
width: 24
height: 24
radius: 12
anchors.verticalCenter: parent.verticalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer)
activePlayer.togglePlaying();
}
}
}
Rectangle {
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
color: nextArea.containsMouse ? Theme.primaryHover : "transparent"
visible: playerAvailable
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 12
color: Theme.surfaceText
}
MouseArea {
id: nextArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer)
activePlayer.next();
}
}
}
} }
}
} }
Behavior on color { Row {
ColorAnimation { spacing: Theme.spacingXS
duration: Theme.shortDuration anchors.verticalCenter: parent.verticalCenter
easing.type: Theme.standardEasing
Rectangle {
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
color: prevArea.containsMouse ? Theme.primaryHover : "transparent"
visible: root.playerAvailable
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 12
color: Theme.surfaceText
} }
} MouseArea {
id: prevArea
Behavior on width { anchors.fill: parent
NumberAnimation { hoverEnabled: true
duration: Theme.shortDuration cursorShape: Qt.PointingHandCursor
easing.type: Theme.standardEasing onClicked: {
if (activePlayer)
activePlayer.previous()
}
}
}
Rectangle {
width: 24
height: 24
radius: 12
anchors.verticalCenter: parent.verticalCenter
color: activePlayer
&& activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: activePlayer
&& activePlayer.playbackState === 1 ? "pause" : "play_arrow"
size: 14
color: activePlayer
&& activePlayer.playbackState === 1 ? Theme.background : Theme.primary
} }
} MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer)
activePlayer.togglePlaying()
}
}
}
Rectangle {
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
color: nextArea.containsMouse ? Theme.primaryHover : "transparent"
visible: playerAvailable
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 12
color: Theme.surfaceText
}
MouseArea {
id: nextArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer)
activePlayer.next()
}
}
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }

View File

@@ -3,67 +3,70 @@ import qs.Common
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool hasUnread: false property bool hasUnread: false
property bool isActive: false property bool isActive: false
property string section: "right" property string section: "right"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
signal clicked() signal clicked
width: 40 width: 40
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = notificationArea.containsMouse || root.isActive ? Theme.primaryPressed : Theme.secondaryHover; const baseColor = notificationArea.containsMouse
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); || root.isActive ? Theme.primaryPressed : Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
DankIcon {
anchors.centerIn: parent
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
size: Theme.iconSize - 6
color: SessionData.doNotDisturb ? Theme.error : (notificationArea.containsMouse
|| root.isActive ? Theme.primary : Theme.surfaceText)
}
Rectangle {
width: 8
height: 8
radius: 4
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
visible: root.hasUnread
}
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX,
Theme.barHeight + Theme.spacingXS,
width, section, currentScreen)
}
root.clicked()
} }
}
DankIcon { Behavior on color {
anchors.centerIn: parent ColorAnimation {
name: SessionData.doNotDisturb ? "notifications_off" : "notifications" duration: Theme.shortDuration
size: Theme.iconSize - 6 easing.type: Theme.standardEasing
color: SessionData.doNotDisturb ? Theme.error : (notificationArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText)
} }
}
Rectangle {
width: 8
height: 8
radius: 4
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
visible: root.hasUnread
}
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0);
var currentScreen = parentScreen || Screen;
var screenX = currentScreen.x || 0;
var relativeX = globalPos.x - screenX;
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen);
}
root.clicked();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }

View File

@@ -5,169 +5,166 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property string section: "right" property string section: "right"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
readonly property bool hasActivePrivacy: PrivacyService.anyPrivacyActive readonly property bool hasActivePrivacy: PrivacyService.anyPrivacyActive
readonly property int activeCount: (PrivacyService.microphoneActive ? 1 : 0) + (PrivacyService.cameraActive ? 1 : 0) + (PrivacyService.screensharingActive ? 1 : 0) readonly property int activeCount: PrivacyService.microphoneActive + PrivacyService.cameraActive
+ PrivacyService.screensharingActive
width: hasActivePrivacy ? (activeCount > 1 ? 80 : 60) : 0 width: hasActivePrivacy ? (activeCount > 1 ? 80 : 60) : 0
height: hasActivePrivacy ? 30 : 0 height: hasActivePrivacy ? 30 : 0
radius: Theme.cornerRadius radius: Theme.cornerRadius
visible: hasActivePrivacy
opacity: hasActivePrivacy ? 1 : 0
color: Qt.rgba(
privacyArea.containsMouse ? Theme.errorPressed.r : Theme.errorHover.r,
privacyArea.containsMouse ? Theme.errorPressed.g : Theme.errorHover.g,
privacyArea.containsMouse ? Theme.errorPressed.b : Theme.errorHover.b,
(privacyArea.containsMouse ? Theme.errorPressed.a : Theme.errorHover.a)
* Theme.widgetTransparency)
MouseArea {
id: privacyArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: hasActivePrivacy visible: hasActivePrivacy
opacity: hasActivePrivacy ? 1 : 0
color: { Item {
const baseColor = privacyArea.containsMouse ? Theme.errorPressed : Theme.errorHover; width: 18
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); height: 18
} visible: PrivacyService.microphoneActive
anchors.verticalCenter: parent.verticalCenter
MouseArea { DankIcon {
id: privacyArea name: "mic"
size: Theme.iconSizeSmall
anchors.fill: parent color: Theme.error
hoverEnabled: true filled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
}
}
Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS }
visible: hasActivePrivacy
Item {
width: 18
height: 18
visible: PrivacyService.microphoneActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "mic"
size: Theme.iconSizeSmall
color: Theme.error
filled: true
anchors.centerIn: parent
}
}
Item {
width: 18
height: 18
visible: PrivacyService.cameraActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "camera_video"
size: Theme.iconSizeSmall
color: Theme.surfaceText
filled: true
anchors.centerIn: parent
}
Rectangle {
width: 6
height: 6
radius: 3
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -2
anchors.topMargin: -1
SequentialAnimation on opacity {
running: parent.visible
loops: Animation.Infinite
NumberAnimation {
to: 0.3
duration: Theme.longDuration
}
NumberAnimation {
to: 1.0
duration: Theme.longDuration
}
}
}
}
Item {
width: 18
height: 18
visible: PrivacyService.screensharingActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "screen_share"
size: Theme.iconSizeSmall
color: Theme.warning
filled: true
anchors.centerIn: parent
}
}
} }
Behavior on width { Item {
NumberAnimation { width: 18
duration: Theme.mediumDuration height: 18
easing.type: Theme.emphasizedEasing visible: PrivacyService.cameraActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "camera_video"
size: Theme.iconSizeSmall
color: Theme.surfaceText
filled: true
anchors.centerIn: parent
}
Rectangle {
width: 6
height: 6
radius: 3
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -2
anchors.topMargin: -1
SequentialAnimation on opacity {
running: parent.visible && hasActivePrivacy
loops: Animation.Infinite
NumberAnimation {
to: 0.3
duration: Theme.longDuration
}
NumberAnimation {
to: 1.0
duration: Theme.longDuration
}
} }
}
}
Item {
width: 18
height: 18
visible: PrivacyService.screensharingActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "screen_share"
size: Theme.iconSizeSmall
color: Theme.warning
filled: true
anchors.centerIn: parent
}
}
}
Behavior on width {
enabled: hasActivePrivacy
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Rectangle {
id: tooltip
width: tooltipText.contentWidth + Theme.spacingM * 2
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.popupBackground()
border.color: Theme.outlineMedium
border.width: 1
visible: false
opacity: privacyArea.containsMouse && hasActivePrivacy ? 1 : 0
z: 100
x: (parent.width - width) / 2
y: -height - Theme.spacingXS
StyledText {
id: tooltipText
anchors.centerIn: parent
text: PrivacyService.getPrivacySummary()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
} }
Behavior on opacity { Behavior on opacity {
enabled: !hasActivePrivacy enabled: hasActivePrivacy
NumberAnimation { NumberAnimation {
duration: Theme.mediumDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
Rectangle { Rectangle {
id: tooltip width: 8
height: 8
width: tooltipText.contentWidth + Theme.spacingM * 2 color: parent.color
height: tooltipText.contentHeight + Theme.spacingS * 2 border.color: parent.border.color
radius: Theme.cornerRadius border.width: parent.border.width
color: Theme.popupBackground() rotation: 45
border.color: Theme.outlineMedium anchors.horizontalCenter: parent.horizontalCenter
border.width: 1 anchors.top: parent.bottom
visible: privacyArea.containsMouse && hasActivePrivacy anchors.topMargin: -4
opacity: visible ? 1 : 0
z: 100
x: (parent.width - width) / 2
y: -height - Theme.spacingXS
StyledText {
id: tooltipText
anchors.centerIn: parent
text: PrivacyService.getPrivacySummary()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
width: 8
height: 8
color: parent.color
border.color: parent.border.color
border.width: parent.border.width
rotation: 45
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom
anchors.topMargin: -4
}
} }
}
} }

View File

@@ -5,81 +5,84 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool showPercentage: true property bool showPercentage: true
property bool showIcon: true property bool showIcon: true
property var toggleProcessList property var toggleProcessList
property string section: "right" property string section: "right"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
width: 55 width: 55
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = ramArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover; const baseColor = ramArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
Component.onCompleted: {
SysMonitorService.addRef()
}
Component.onDestruction: {
SysMonitorService.removeRef()
}
MouseArea {
id: ramArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX,
Theme.barHeight + Theme.spacingXS,
width, section, currentScreen)
}
SysMonitorService.setSortBy("memory")
if (root.toggleProcessList)
root.toggleProcessList()
} }
Component.onCompleted: { }
SysMonitorService.addRef();
} Row {
Component.onDestruction: { anchors.centerIn: parent
SysMonitorService.removeRef(); spacing: 3
DankIcon {
name: "developer_board"
size: Theme.iconSize - 8
color: {
if (SysMonitorService.memoryUsage > 90)
return Theme.tempDanger
if (SysMonitorService.memoryUsage > 75)
return Theme.tempWarning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
} }
MouseArea { StyledText {
id: ramArea text: {
if (SysMonitorService.memoryUsage === undefined
anchors.fill: parent || SysMonitorService.memoryUsage === null
hoverEnabled: true || SysMonitorService.memoryUsage === 0) {
cursorShape: Qt.PointingHandCursor return "--%"
onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0);
var currentScreen = parentScreen || Screen;
var screenX = currentScreen.x || 0;
var relativeX = globalPos.x - screenX;
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen);
}
SysMonitorService.setSortBy("memory");
if (root.toggleProcessList)
root.toggleProcessList();
} }
return SysMonitorService.memoryUsage.toFixed(0) + "%"
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
}
Row {
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "developer_board"
size: Theme.iconSize - 8
color: {
if (SysMonitorService.memoryUsage > 90)
return Theme.tempDanger;
if (SysMonitorService.memoryUsage > 75)
return Theme.tempWarning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (SysMonitorService.memoryUsage === undefined || SysMonitorService.memoryUsage === null || SysMonitorService.memoryUsage === 0) {
return "--%";
}
return SysMonitorService.memoryUsage.toFixed(0) + "%";
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
} }

View File

@@ -4,114 +4,117 @@ import Quickshell.Services.SystemTray
import qs.Common import qs.Common
Rectangle { Rectangle {
id: root id: root
property var parentWindow: null property var parentWindow: null
property var parentScreen: null property var parentScreen: null
readonly property int calculatedWidth: SystemTray.items.values.length > 0 ? SystemTray.items.values.length * 24 + (SystemTray.items.values.length - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0 readonly property int calculatedWidth: SystemTray.items.values.length
> 0 ? SystemTray.items.values.length * 24
+ (SystemTray.items.values.length - 1)
* Theme.spacingXS + Theme.spacingS * 2 : 0
width: calculatedWidth
height: 30
radius: Theme.cornerRadius
color: {
if (SystemTray.items.values.length === 0)
return "transparent"
width: calculatedWidth const baseColor = Theme.secondaryHover
height: 30 return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
radius: Theme.cornerRadius baseColor.a * Theme.widgetTransparency)
color: { }
if (SystemTray.items.values.length === 0) visible: SystemTray.items.values.length > 0
return "transparent";
const baseColor = Theme.secondaryHover; Row {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); id: systemTrayRow
}
visible: SystemTray.items.values.length > 0
Row { anchors.centerIn: parent
id: systemTrayRow spacing: Theme.spacingXS
anchors.centerIn: parent Repeater {
spacing: Theme.spacingXS model: SystemTray.items.values
Repeater {
model: SystemTray.items.values
delegate: Item {
property var trayItem: modelData
property string iconSource: {
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
return "";
}
width: 24
height: 24
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
Behavior on color {
enabled: trayItemArea.containsMouse !== undefined
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Image {
anchors.centerIn: parent
width: 18
height: 18
source: parent.iconSource
asynchronous: true
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem)
return
if (trayItem.hasMenu) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
menuAnchor.menu = trayItem.menu
menuAnchor.anchor.window = parentWindow
menuAnchor.anchor.rect = Qt.rect(relativeX, Theme.barHeight + Theme.spacingS, parent.width, 1)
menuAnchor.open()
} else if (mouse.button === Qt.LeftButton) {
trayItem.activate()
}
}
}
delegate: Item {
property var trayItem: modelData
property string iconSource: {
let icon = trayItem && trayItem.icon
if (typeof icon === 'string' || icon instanceof String) {
if (icon.includes("?path=")) {
const split = icon.split("?path=")
if (split.length !== 2)
return icon
const name = split[0]
const path = split[1]
const fileName = name.substring(name.lastIndexOf("/") + 1)
return `file://${path}/${fileName}`
} }
return icon
}
return ""
} }
} width: 24
height: 24
QsMenuAnchor { Rectangle {
id: menuAnchor anchors.fill: parent
} radius: Theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Theme.primaryHover : "transparent"
Behavior on color {
enabled: trayItemArea.containsMouse !== undefined
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Image {
anchors.centerIn: parent
width: 18
height: 18
source: parent.iconSource
asynchronous: true
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: mouse => {
if (!trayItem)
return
if (trayItem.hasMenu) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
menuAnchor.menu = trayItem.menu
menuAnchor.anchor.window = parentWindow
menuAnchor.anchor.rect = Qt.rect(
relativeX, Theme.barHeight + Theme.spacingS,
parent.width, 1)
menuAnchor.open()
} else if (mouse.button === Qt.LeftButton) {
trayItem.activate()
}
}
}
}
}
}
QsMenuAnchor {
id: menuAnchor
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,87 +4,87 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property string section: "center" property string section: "center"
property var popupTarget: null property var popupTarget: null
property var parentScreen: null property var parentScreen: null
signal clicked() signal clicked
visible: SettingsData.weatherEnabled visible: SettingsData.weatherEnabled
width: visible ? Math.min(100, weatherRow.implicitWidth + Theme.spacingS * 2) : 0 width: visible ? Math.min(100,
height: 30 weatherRow.implicitWidth + Theme.spacingS * 2) : 0
radius: Theme.cornerRadius height: 30
color: { radius: Theme.cornerRadius
const baseColor = weatherArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover; color: {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency); const baseColor = weatherArea.containsMouse ? Theme.primaryHover : Theme.surfaceTextHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
Ref {
service: WeatherService
}
Row {
id: weatherRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
} }
Ref { StyledText {
service: WeatherService text: {
} var temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp
if (temp === undefined || temp === null || temp === 0) {
Row { return "--°" + (SettingsData.useFahrenheit ? "F" : "C")
id: weatherRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
} }
return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C")
StyledText { }
text: { font.pixelSize: Theme.fontSizeSmall
var temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp; color: Theme.surfaceText
if (temp === undefined || temp === null || temp === 0) { anchors.verticalCenter: parent.verticalCenter
return "--°" + (SettingsData.useFahrenheit ? "F" : "C");
}
return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C");
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
} }
}
MouseArea { MouseArea {
id: weatherArea id: weatherArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (popupTarget && popupTarget.setTriggerPosition) { if (popupTarget && popupTarget.setTriggerPosition) {
var globalPos = mapToGlobal(0, 0); var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen; var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0; var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX; var relativeX = globalPos.x - screenX
popupTarget.setTriggerPosition(relativeX, Theme.barHeight + Theme.spacingXS, width, section, currentScreen); popupTarget.setTriggerPosition(relativeX,
} Theme.barHeight + Theme.spacingXS,
root.clicked(); width, section, currentScreen)
} }
root.clicked()
} }
}
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
}
} }
}
Behavior on width { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
}
} }
}
} }

View File

@@ -6,155 +6,164 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property string screenName: "" property string screenName: ""
property int currentWorkspace: getDisplayActiveWorkspace() property int currentWorkspace: getDisplayActiveWorkspace()
property var workspaceList: { property var workspaceList: {
var baseList = getDisplayWorkspaces(); var baseList = getDisplayWorkspaces()
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList; return SettingsData.showWorkspacePadding ? padWorkspaces(
baseList) : baseList
}
function padWorkspaces(list) {
var padded = list.slice()
while (padded.length < 3)
padded.push(-1) // Use -1 as a placeholder
return padded
}
function getDisplayWorkspaces() {
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0)
return [1, 2]
if (!root.screenName)
return NiriService.getCurrentOutputWorkspaceNumbers()
var displayWorkspaces = []
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i]
if (ws.output === root.screenName)
displayWorkspaces.push(ws.idx + 1)
}
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
}
function getDisplayActiveWorkspace() {
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0)
return 1
if (!root.screenName)
return NiriService.getCurrentWorkspaceNumber()
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i]
if (ws.output === root.screenName && ws.is_active)
return ws.idx + 1
}
return 1
}
width: SettingsData.showWorkspacePadding ? Math.max(
120,
workspaceRow.implicitWidth + Theme.spacingL
* 2) : workspaceRow.implicitWidth
+ Theme.spacingL * 2
height: 30
radius: Theme.cornerRadiusLarge
color: {
const baseColor = Theme.surfaceTextHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
}
visible: NiriService.niriAvailable
Connections {
function onAllWorkspacesChanged() {
root.workspaceList
= SettingsData.showWorkspacePadding ? root.padWorkspaces(
root.getDisplayWorkspaces(
)) : root.getDisplayWorkspaces()
root.currentWorkspace = root.getDisplayActiveWorkspace()
} }
function padWorkspaces(list) { function onFocusedWorkspaceIndexChanged() {
var padded = list.slice(); root.currentWorkspace = root.getDisplayActiveWorkspace()
while (padded.length < 3)padded.push(-1) // Use -1 as a placeholder
return padded;
} }
function getDisplayWorkspaces() { function onNiriAvailableChanged() {
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0) if (NiriService.niriAvailable) {
return [1, 2]; root.workspaceList
= SettingsData.showWorkspacePadding ? root.padWorkspaces(
if (!root.screenName) root.getDisplayWorkspaces(
return NiriService.getCurrentOutputWorkspaceNumbers(); )) : root.getDisplayWorkspaces()
root.currentWorkspace = root.getDisplayActiveWorkspace()
var displayWorkspaces = []; }
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
var ws = NiriService.allWorkspaces[i];
if (ws.output === root.screenName)
displayWorkspaces.push(ws.idx + 1);
}
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2];
} }
function getDisplayActiveWorkspace() { target: NiriService
if (!NiriService.niriAvailable || NiriService.allWorkspaces.length === 0) }
return 1;
if (!root.screenName) Connections {
return NiriService.getCurrentWorkspaceNumber(); function onShowWorkspacePaddingChanged() {
var baseList = root.getDisplayWorkspaces()
for (var i = 0; i < NiriService.allWorkspaces.length; i++) { root.workspaceList = SettingsData.showWorkspacePadding ? root.padWorkspaces(
var ws = NiriService.allWorkspaces[i]; baseList) : baseList
if (ws.output === root.screenName && ws.is_active)
return ws.idx + 1;
}
return 1;
} }
width: SettingsData.showWorkspacePadding ? Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2) : workspaceRow.implicitWidth + Theme.spacingL * 2 target: SettingsData
height: 30 }
radius: Theme.cornerRadiusLarge
color: {
const baseColor = Theme.surfaceTextHover;
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency);
}
visible: NiriService.niriAvailable
Connections { Row {
function onAllWorkspacesChanged() { id: workspaceRow
root.workspaceList = SettingsData.showWorkspacePadding ? root.padWorkspaces(root.getDisplayWorkspaces()) : root.getDisplayWorkspaces();
root.currentWorkspace = root.getDisplayActiveWorkspace(); anchors.centerIn: parent
spacing: Theme.spacingS
Repeater {
model: root.workspaceList
Rectangle {
property bool isActive: modelData === root.currentWorkspace
property bool isPlaceholder: modelData === -1
property bool isHovered: mouseArea.containsMouse
property int sequentialNumber: index + 1
width: isActive ? Theme.spacingXL + Theme.spacingM : Theme.spacingL + Theme.spacingXS
height: Theme.spacingL
radius: height / 2
color: isActive ? Theme.primary : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: !isPlaceholder
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !isPlaceholder
onClicked: {
if (!isPlaceholder)
NiriService.switchToWorkspace(modelData - 1)
}
} }
function onFocusedWorkspaceIndexChanged() { StyledText {
root.currentWorkspace = root.getDisplayActiveWorkspace(); visible: SettingsData.showWorkspaceIndex
anchors.centerIn: parent
text: isPlaceholder ? sequentialNumber : sequentialNumber
color: isActive ? Qt.rgba(
Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b,
0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.fontSizeSmall
font.weight: isActive && !isPlaceholder ? Font.DemiBold : Font.Normal
} }
function onNiriAvailableChanged() { Behavior on width {
if (NiriService.niriAvailable) { NumberAnimation {
root.workspaceList = SettingsData.showWorkspacePadding ? root.padWorkspaces(root.getDisplayWorkspaces()) : root.getDisplayWorkspaces(); duration: Theme.mediumDuration
root.currentWorkspace = root.getDisplayActiveWorkspace(); easing.type: Theme.emphasizedEasing
} }
} }
target: NiriService Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
} }
}
Connections {
function onShowWorkspacePaddingChanged() {
var baseList = root.getDisplayWorkspaces();
root.workspaceList = SettingsData.showWorkspacePadding ? root.padWorkspaces(baseList) : baseList;
}
target: SettingsData
}
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
Repeater {
model: root.workspaceList
Rectangle {
property bool isActive: modelData === root.currentWorkspace
property bool isPlaceholder: modelData === -1
property bool isHovered: mouseArea.containsMouse
property int sequentialNumber: index + 1
width: isActive ? Theme.spacingXL + Theme.spacingM : Theme.spacingL + Theme.spacingXS
height: Theme.spacingL
radius: height / 2
color: isActive ? Theme.primary : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: !isPlaceholder
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !isPlaceholder
onClicked: {
if (!isPlaceholder)
NiriService.switchToWorkspace(modelData - 1);
}
}
StyledText {
visible: SettingsData.showWorkspaceIndex
anchors.centerIn: parent
text: isPlaceholder ? sequentialNumber : sequentialNumber
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.fontSizeSmall
font.weight: isActive && !isPlaceholder ? Font.DemiBold : Font.Normal
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
} }

View File

@@ -8,212 +8,204 @@ import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
property var modelData property var modelData
property bool volumePopupVisible: false property bool volumePopupVisible: false
function show() { function show() {
root.volumePopupVisible = true; root.volumePopupVisible = true
hideTimer.restart(); hideTimer.restart()
}
function resetHideTimer() {
if (root.volumePopupVisible)
hideTimer.restart()
}
screen: modelData
visible: volumePopupVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Timer {
id: hideTimer
interval: 3000
repeat: false
onTriggered: {
if (!volumePopup.containsMouse)
root.volumePopupVisible = false
else
hideTimer.restart()
}
}
Connections {
function onVolumeChanged() {
root.show()
} }
function resetHideTimer() { function onSinkChanged() {
if (root.volumePopupVisible) if (root.volumePopupVisible)
hideTimer.restart(); root.show()
} }
screen: modelData target: AudioService
visible: volumePopupVisible }
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors { Rectangle {
top: true id: volumePopup
left: true
right: true
bottom: true
}
Timer { property bool containsMouse: popupMouseArea.containsMouse
id: hideTimer
interval: 3000 width: Math.min(260, Screen.width - Theme.spacingM * 2)
repeat: false height: volumeContent.height + Theme.spacingS * 2
onTriggered: { anchors.horizontalCenter: parent.horizontalCenter
if (!volumePopup.containsMouse) anchors.bottom: parent.bottom
root.volumePopupVisible = false; anchors.bottomMargin: Theme.spacingM
else color: Theme.popupBackground()
hideTimer.restart(); radius: Theme.cornerRadiusLarge
} border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
} Theme.outline.b, 0.08)
border.width: 1
opacity: root.volumePopupVisible ? 1 : 0
scale: root.volumePopupVisible ? 1 : 0.9
layer.enabled: true
Connections { Column {
function onVolumeChanged() { id: volumeContent
root.show();
}
function onSinkChanged() { anchors.centerIn: parent
if (root.volumePopupVisible) width: parent.width - Theme.spacingS * 2
root.show(); spacing: Theme.spacingXS
} Item {
property int gap: Theme.spacingS
target: AudioService width: parent.width
} height: 40
Rectangle { Rectangle {
id: volumePopup width: Theme.iconSize
height: Theme.iconSize
property bool containsMouse: popupMouseArea.containsMouse radius: Theme.iconSize / 2
color: "transparent"
width: Math.min(260, Screen.width - Theme.spacingM * 2) x: parent.gap
height: volumeContent.height + Theme.spacingS * 2 anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: root.volumePopupVisible ? 1 : 0
scale: root.volumePopupVisible ? 1 : 0.9
layer.enabled: true
Column {
id: volumeContent
DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2 name: AudioService.sink && AudioService.sink.audio
spacing: Theme.spacingXS && AudioService.sink.audio.muted ? "volume_off" : "volume_up"
size: Theme.iconSize
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
}
Item { MouseArea {
property int gap: Theme.spacingS id: muteButton
width: parent.width
height: 40
Rectangle {
width: Theme.iconSize
height: Theme.iconSize
radius: Theme.iconSize / 2
color: "transparent"
x: parent.gap
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted ? "volume_off" : "volume_up"
size: Theme.iconSize
color: muteButton.containsMouse ? Theme.primary : Theme.surfaceText
}
MouseArea {
id: muteButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
AudioService.toggleMute();
root.resetHideTimer();
}
}
}
DankSlider {
id: volumeSlider
width: parent.width - Theme.iconSize - parent.gap * 3
height: 40
x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 100
enabled: AudioService.sink && AudioService.sink.audio
showValue: true
unit: "%"
Component.onCompleted: {
if (AudioService.sink && AudioService.sink.audio)
value = Math.round(AudioService.sink.audio.volume * 100);
}
onSliderValueChanged: function(newValue) {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.volume = newValue / 100;
root.resetHideTimer();
}
}
Connections {
function onVolumeChanged() {
volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100);
}
target: AudioService.sink && AudioService.sink.audio ? AudioService.sink.audio : null
}
}
}
}
MouseArea {
id: popupMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.NoButton cursorShape: Qt.PointingHandCursor
propagateComposedEvents: true onClicked: {
z: -1 AudioService.toggleMute()
root.resetHideTimer()
}
}
} }
layer.effect: MultiEffect { DankSlider {
shadowEnabled: true id: volumeSlider
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
transform: Translate { width: parent.width - Theme.iconSize - parent.gap * 3
y: root.volumePopupVisible ? 0 : 20 height: 40
} x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 100
enabled: AudioService.sink && AudioService.sink.audio
showValue: true
unit: "%"
Component.onCompleted: {
if (AudioService.sink && AudioService.sink.audio)
value = Math.round(AudioService.sink.audio.volume * 100)
}
onSliderValueChanged: function (newValue) {
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.volume = newValue / 100
root.resetHideTimer()
}
}
Behavior on opacity { Connections {
NumberAnimation { function onVolumeChanged() {
duration: Theme.mediumDuration volumeSlider.value = Math.round(
easing.type: Theme.emphasizedEasing AudioService.sink.audio.volume * 100)
} }
target: AudioService.sink
&& AudioService.sink.audio ? AudioService.sink.audio : null
}
} }
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on transform {
PropertyAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
} }
mask: Region { MouseArea {
item: volumePopup id: popupMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
z: -1
} }
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
transform: Translate {
y: root.volumePopupVisible ? 0 : 20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on transform {
PropertyAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
mask: Region {
item: volumePopup
}
} }

View File

@@ -6,132 +6,132 @@ import qs.Common
import qs.Widgets import qs.Widgets
LazyLoader { LazyLoader {
active: SessionData.wallpaperPath !== "" active: SessionData.wallpaperPath !== ""
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens
PanelWindow { PanelWindow {
id: wallpaperWindow id: wallpaperWindow
required property var modelData required property var modelData
screen: modelData screen: modelData
WlrLayershell.layer: WlrLayer.Background WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
anchors.top: true anchors.top: true
anchors.bottom: true anchors.bottom: true
anchors.left: true anchors.left: true
anchors.right: true anchors.right: true
color: "black" color: "black"
Item { Item {
id: root id: root
anchors.fill: parent anchors.fill: parent
property string source: SessionData.wallpaperPath || "" property string source: SessionData.wallpaperPath || ""
property Image current: one property Image current: one
onSourceChanged: { onSourceChanged: {
if (!source) if (!source)
current = null; current = null
else if (current === one) else if (current === one)
two.update(); two.update()
else else
one.update(); one.update()
}
Loader {
anchors.fill: parent
active: !root.source
asynchronous: true
sourceComponent: Rectangle {
color: Theme.surface
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: "sentiment_stressed"
color: Theme.surfaceVariantText
size: Theme.iconSize * 5
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
text: "Wallpaper missing?"
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeXLarge * 2
font.weight: Font.Bold
}
StyledText {
text: "Set wallpaper in Settings"
color: Theme.primary
font.pixelSize: Theme.fontSizeLarge
}
}
}
}
}
Img {
id: one
}
Img {
id: two
}
component Img: Image {
id: img
function update(): void {
source = "";
source = root.source;
}
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
smooth: true
asynchronous: true
cache: true
opacity: 0
onStatusChanged: {
if (status === Image.Ready)
root.current = this;
}
states: State {
name: "visible"
when: root.current === img
PropertyChanges {
img.opacity: 1
}
}
transitions: Transition {
NumberAnimation {
target: img
properties: "opacity"
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
}
}
}
}
} }
Loader {
anchors.fill: parent
active: !root.source
asynchronous: true
sourceComponent: Rectangle {
color: Theme.surface
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: "sentiment_stressed"
color: Theme.surfaceVariantText
size: Theme.iconSize * 5
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
StyledText {
text: "Wallpaper missing?"
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeXLarge * 2
font.weight: Font.Bold
}
StyledText {
text: "Set wallpaper in Settings"
color: Theme.primary
font.pixelSize: Theme.fontSizeLarge
}
}
}
}
}
Img {
id: one
}
Img {
id: two
}
component Img: Image {
id: img
function update(): void {
source = ""
source = root.source
}
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
smooth: true
asynchronous: true
cache: true
opacity: 0
onStatusChanged: {
if (status === Image.Ready)
root.current = this
}
states: State {
name: "visible"
when: root.current === img
PropertyChanges {
img.opacity: 1
}
}
transitions: Transition {
NumberAnimation {
target: img
properties: "opacity"
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
}
}
}
}
} }
}
} }

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