1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 16:02:51 -05:00

qmlfmt with 4 space

This commit is contained in:
bbedward
2025-08-20 00:05:14 -04:00
parent 6e0977c719
commit b688bbfe83
154 changed files with 28809 additions and 27639 deletions

View File

@@ -6,21 +6,21 @@ 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: [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 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]
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

@@ -9,123 +9,124 @@ import Quickshell.Io
Singleton { Singleton {
id: root id: root
property var appUsageRanking: { property var appUsageRanking: {
}
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() { Component.onCompleted: {
settingsFile.setText(JSON.stringify({ loadSettings()
"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()
}
} }
appUsageRanking = currentRanking function loadSettings() {
saveSettings() parseSettings(settingsFile.text())
}
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) { function parseSettings(content) {
if (a.usageCount !== b.usageCount) try {
return b.usageCount - a.usageCount if (content && content.trim()) {
return a.name.localeCompare(b.name) var settings = JSON.parse(content)
}) 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
}
} }
if (hasChanges) { function saveSettings() {
appUsageRanking = currentRanking settingsFile.setText(JSON.stringify({
saveSettings() "appUsageRanking": appUsageRanking
}, null, 2))
} }
}
FileView { function addAppUsage(app) {
id: settingsFile if (!app)
return
path: StandardPaths.writableLocation( var appId = app.id || (app.execString || app.exec || "")
StandardPaths.GenericStateLocation) + "/DankMaterialShell/appusage.json" if (!appId)
blockLoading: true return
blockWrites: true
watchChanges: true var currentRanking = Object.assign({}, appUsageRanking)
onLoaded: {
parseSettings(settingsFile.text()) 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()
}
}
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

@@ -6,62 +6,62 @@ 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 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] / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 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] 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

@@ -2,43 +2,44 @@ import Quickshell
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.mkdir(Paths.imagecache) 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( Quickshell.execDetached(
["find", Paths.stringify( ["find", Paths.stringify(
Paths.imagecache), "-name", "*.png", "-mmin", `+${ageInMinutes}`, "-delete"]) 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( Quickshell.execDetached(
["find", Paths.stringify( ["find", Paths.stringify(
Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"]) Paths.imagecache), "-name", `*@${size}x${size}.png`, "-delete"])
} }
// Get cache size in MB // Get cache size in MB
function getCacheSize(callback) { function getCacheSize(callback) {
var process = Qt.createQmlObject(` var process = Qt.createQmlObject(`
import Quickshell.Io import Quickshell.Io
Process { Process {
command: ["du", "-sm", "${Paths.stringify( command: ["du", "-sm", "${Paths.stringify(
Paths.imagecache)}"] Paths.imagecache)}"]
running: true running: true
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var sizeMB = parseInt(text.split("\\t")[0]) || 0 var sizeMB = parseInt(text.split("\\t")[0]) || 0
callback(sizeMB) callback(sizeMB)
} }
} }
} }
`, root) `, root)
} }
} }

View File

@@ -9,435 +9,448 @@ import Quickshell.Io
import qs.Services import qs.Services
Singleton { Singleton {
id: root id: root
readonly property string _homeUrl: StandardPaths.writableLocation( readonly property string _homeUrl: StandardPaths.writableLocation(
StandardPaths.HomeLocation) StandardPaths.HomeLocation)
readonly property string homeDir: _homeUrl.startsWith( readonly property string homeDir: _homeUrl.startsWith(
"file://") ? _homeUrl.substring( "file://") ? _homeUrl.substring(
7) : _homeUrl 7) : _homeUrl
readonly property string _configUrl: StandardPaths.writableLocation( readonly property string _configUrl: StandardPaths.writableLocation(
StandardPaths.ConfigLocation) StandardPaths.ConfigLocation)
readonly property string configDir: _configUrl.startsWith( readonly property string configDir: _configUrl.startsWith(
"file://") ? _configUrl.substring( "file://") ? _configUrl.substring(
7) : _configUrl 7) : _configUrl
readonly property string shellDir: Qt.resolvedUrl(".").toString().replace( readonly property string shellDir: Qt.resolvedUrl(".").toString().replace(
"file://", "").replace("/Common/", "") "file://",
readonly property string wallpaperPath: SessionData.wallpaperPath "").replace("/Common/", "")
property bool matugenAvailable: false readonly property string wallpaperPath: SessionData.wallpaperPath
property bool gtkThemingEnabled: false property bool matugenAvailable: false
property bool qtThemingEnabled: false property bool gtkThemingEnabled: false
property bool systemThemeGenerationInProgress: false property bool qtThemingEnabled: false
property var matugenColors: ({}) property bool systemThemeGenerationInProgress: false
property bool extractionRequested: false property var matugenColors: ({})
property int colorUpdateTrigger: 0 property bool extractionRequested: false
property string lastWallpaperTimestamp: "" property int colorUpdateTrigger: 0
property color primary: getMatugenColor("primary", "#42a5f5") property string lastWallpaperTimestamp: ""
property color secondary: getMatugenColor("secondary", "#8ab4f8") property color primary: getMatugenColor("primary", "#42a5f5")
property color tertiary: getMatugenColor("tertiary", "#bb86fc") property color secondary: getMatugenColor("secondary", "#8ab4f8")
property color tertiaryContainer: getMatugenColor("tertiary_container", property color tertiary: getMatugenColor("tertiary", "#bb86fc")
"#3700b3") property color tertiaryContainer: getMatugenColor("tertiary_container",
property color error: getMatugenColor("error", "#cf6679") "#3700b3")
property color inversePrimary: getMatugenColor("inverse_primary", "#6200ea") property color error: getMatugenColor("error", "#cf6679")
property color bg: getMatugenColor("background", "#1a1c1e") property color inversePrimary: getMatugenColor("inverse_primary", "#6200ea")
property color surface: getMatugenColor("surface", "#1a1c1e") property color bg: getMatugenColor("background", "#1a1c1e")
property color surfaceContainer: getMatugenColor("surface_container", property color surface: getMatugenColor("surface", "#1a1c1e")
"#1e2023") property color surfaceContainer: getMatugenColor("surface_container",
property color surfaceContainerHigh: getMatugenColor( "#1e2023")
"surface_container_high", "#292b2f") property color surfaceContainerHigh: getMatugenColor(
property color surfaceVariant: getMatugenColor("surface_variant", "#44464f") "surface_container_high",
property color surfaceText: getMatugenColor("on_background", "#e3e8ef") "#292b2f")
property color primaryText: getMatugenColor("on_primary", "#ffffff") property color surfaceVariant: getMatugenColor("surface_variant", "#44464f")
property color surfaceVariantText: getMatugenColor("on_surface_variant", property color surfaceText: getMatugenColor("on_background", "#e3e8ef")
"#c4c7c5") property color primaryText: getMatugenColor("on_primary", "#ffffff")
property color primaryContainer: getMatugenColor("primary_container", property color surfaceVariantText: getMatugenColor("on_surface_variant",
"#1976d2") "#c4c7c5")
property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8") property color primaryContainer: getMatugenColor("primary_container",
property color outline: getMatugenColor("outline", "#8e918f") "#1976d2")
property color accentHi: primary property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8")
property color accentLo: secondary 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", "image", wallpaperPath, "--json", "hex"]
stdout: StdioCollector {
id: matugenCollector
onStreamFinished: {
if (!matugenCollector.text) {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper Processing Failed: Empty JSON extracted from matugen output.")
return
} }
const extractedJson = extractJsonFromText(matugenCollector.text) }
if (!extractedJson) {
ToastService.wallpaperErrorStatus = "error" function extractColors() {
ToastService.showError("Wallpaper Processing Failed: Invalid JSON extracted from matugen output.") extractionRequested = true
console.log("Raw matugen output:", matugenCollector.text) if (matugenAvailable)
return 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]
} }
try { return cur || fallback
root.matugenColors = JSON.parse(extractedJson) }
root.colorsUpdated()
generateAppConfigs() function isColorDark(c) {
ToastService.clearWallpaperError() return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
} catch (e) { }
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed (JSON parse error after extraction)") 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
}
} }
}
} }
onExited: code => { Process {
if (code !== 0) { id: fileChecker
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Matugen command failed with exit code " + code)
}
}
}
function generateAppConfigs() { command: ["test", "-r", wallpaperPath]
if (!matugenColors || !matugenColors.colors) { onExited: code => {
return if (code === 0) {
} matugenProcess.running = true
} else {
generateNiriConfig() ToastService.wallpaperErrorStatus = "error"
generateGhosttyConfig() ToastService.showError("Wallpaper processing failed")
}
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 {
active-color "${primary}"
inactive-color "${secondary}"
}
focus-ring {
active-color "${inverse}"
}
background-color "${bg}"
}`
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
}
if (!matugenAvailable) {
return
}
if (!wallpaperPath || wallpaperPath === "") {
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"
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
}
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
}
// Returns the first complete JSON substring (object or array) or null.
function extractJsonFromText(text) {
if (!text) return null;
const start = text.search(/[{\[]/);
if (start === -1) return null;
const open = text[start];
const pairs = { '{': '}', '[': ']' };
const close = pairs[open];
if (!close) return null;
let inString = false;
let escape = false;
const stack = [open];
for (let i = start + 1; i < text.length; i++) {
const ch = text[i];
if (inString) {
if (escape) {
escape = false;
} else if (ch === '\\') {
escape = true;
} else if (ch === '"') {
inString = false;
} }
continue; }
}
if (ch === '"') { Process {
inString = true; id: matugenProcess
continue;
} command: ["matugen", "image", wallpaperPath, "--json", "hex"]
if (ch === '{' || ch === '[') {
stack.push(ch); stdout: StdioCollector {
continue; id: matugenCollector
}
if (ch === '}' || ch === ']') { onStreamFinished: {
const last = stack.pop(); if (!matugenCollector.text) {
if (!last || pairs[last] !== ch) { ToastService.wallpaperErrorStatus = "error"
return null; ToastService.showError(
"Wallpaper Processing Failed: Empty JSON extracted from matugen output.")
return
}
const extractedJson = extractJsonFromText(matugenCollector.text)
if (!extractedJson) {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError(
"Wallpaper Processing Failed: Invalid JSON extracted from matugen output.")
console.log("Raw matugen output:", matugenCollector.text)
return
}
try {
root.matugenColors = JSON.parse(extractedJson)
root.colorsUpdated()
generateAppConfigs()
ToastService.clearWallpaperError()
} catch (e) {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError(
"Wallpaper processing failed (JSON parse error after extraction)")
}
}
} }
if (stack.length === 0) {
return text.slice(start, i + 1); onExited: code => {
if (code !== 0) {
ToastService.wallpaperErrorStatus = "error"
ToastService.showError(
"Matugen command failed with exit code " + code)
}
} }
}
} }
return null; function generateAppConfigs() {
} if (!matugenColors || !matugenColors.colors) {
return
}
Process { generateNiriConfig()
id: gtkAvailabilityChecker generateGhosttyConfig()
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)
}
}
Process { if (gtkThemingEnabled && typeof SettingsData !== "undefined"
id: qtAvailabilityChecker && SettingsData.gtkThemingEnabled) {
command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"] generateSystemThemes()
running: false } else if (qtThemingEnabled && typeof SettingsData !== "undefined"
onExited: exitCode => { && SettingsData.qtThemingEnabled) {
qtThemingEnabled = (exitCode === 0) generateSystemThemes()
} }
}
Process {
id: systemThemeGenerator
running: false
stdout: StdioCollector {
id: systemThemeStdout
} }
stderr: StdioCollector { function generateNiriConfig() {
id: systemThemeStderr 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 {
active-color "${primary}"
inactive-color "${secondary}"
}
focus-ring {
active-color "${inverse}"
}
background-color "${bg}"
}`
Quickshell.execDetached(
["bash", "-c", `echo '${content}' > niri-colors.generated.kdl`])
} }
onExited: exitCode => { function generateGhosttyConfig() {
systemThemeGenerationInProgress = false var dark = matugenColors.colors.dark
var light = matugenColors.colors.light
if (!dark || !light)
return
if (exitCode !== 0) { var bg = dark.background || "#1a1c1e"
ToastService.showError( var fg = dark.on_background || "#e3e8ef"
"Failed to generate system themes: " + systemThemeStderr.text) 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"
Process { var bg_b = light.background || "#fef7ff"
id: systemThemeRestoreProcess var fg_b = light.on_background || "#1d1b20"
running: false 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"
stdout: StdioCollector { var content = `background = ${bg}
id: restoreThemeStdout 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}'`])
} }
stderr: StdioCollector { function checkGtkThemingAvailability() {
id: restoreThemeStderr gtkAvailabilityChecker.running = true
} }
onExited: exitCode => { function checkQtThemingAvailability() {
if (exitCode === 0) { qtAvailabilityChecker.running = true
ToastService.showInfo("System themes restored to default") }
} else {
ToastService.showWarning( function generateSystemThemes() {
"Failed to restore system themes: " + restoreThemeStderr.text) if (systemThemeGenerationInProgress) {
} return
}
if (!matugenAvailable) {
return
}
if (!wallpaperPath || wallpaperPath === "") {
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"
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
}
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
}
// Returns the first complete JSON substring (object or array) or null.
function extractJsonFromText(text) {
if (!text)
return null
const start = text.search(/[{\[]/)
if (start === -1)
return null
const open = text[start]
const pairs = {
"{": '}',
"[": ']'
}
const close = pairs[open]
if (!close)
return null
let inString = false
let escape = false
const stack = [open]
for (var i = start + 1; i < text.length; i++) {
const ch = text[i]
if (inString) {
if (escape) {
escape = false
} else if (ch === '\\') {
escape = true
} else if (ch === '"') {
inString = false
}
continue
}
if (ch === '"') {
inString = true
continue
}
if (ch === '{' || ch === '[') {
stack.push(ch)
continue
}
if (ch === '}' || ch === ']') {
const last = stack.pop()
if (!last || pairs[last] !== ch) {
return null
}
if (stack.length === 0) {
return text.slice(start, i + 1)
}
}
}
return null
}
Process {
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)
}
}
Process {
id: qtAvailabilityChecker
command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"]
running: false
onExited: exitCode => {
qtThemingEnabled = (exitCode === 0)
}
}
Process {
id: systemThemeGenerator
running: false
stdout: StdioCollector {
id: systemThemeStdout
}
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

@@ -3,12 +3,12 @@ pragma Singleton
QtObject { QtObject {
id: modalManager id: modalManager
signal closeAllModalsExcept(var excludedModal) signal closeAllModalsExcept(var excludedModal)
function openModal(modal) { function openModal(modal) {
if (!modal.allowStacking) { if (!modal.allowStacking) {
closeAllModalsExcept(modal) closeAllModalsExcept(modal)
} }
} }
} }

View File

@@ -4,45 +4,45 @@ import Quickshell
import QtCore import QtCore
Singleton { Singleton {
id: root id: root
readonly property url home: StandardPaths.standardLocations( readonly property url home: StandardPaths.standardLocations(
StandardPaths.HomeLocation)[0] StandardPaths.HomeLocation)[0]
readonly property url pictures: StandardPaths.standardLocations( readonly property url pictures: StandardPaths.standardLocations(
StandardPaths.PicturesLocation)[0] StandardPaths.PicturesLocation)[0]
readonly property url data: `${StandardPaths.standardLocations( readonly property url data: `${StandardPaths.standardLocations(
StandardPaths.GenericDataLocation)[0]}/DankMaterialShell` StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
readonly property url state: `${StandardPaths.standardLocations( readonly property url state: `${StandardPaths.standardLocations(
StandardPaths.GenericStateLocation)[0]}/DankMaterialShell` StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
readonly property url cache: `${StandardPaths.standardLocations( readonly property url cache: `${StandardPaths.standardLocations(
StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell` StandardPaths.GenericCacheLocation)[0]}/DankMaterialShell`
readonly property url config: `${StandardPaths.standardLocations( readonly property url config: `${StandardPaths.standardLocations(
StandardPaths.GenericConfigLocation)[0]}/DankMaterialShell` 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

@@ -10,290 +10,302 @@ import qs.Services
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 bool nightModeEnabled: false property bool nightModeEnabled: false
property int nightModeTemperature: 4500 property int nightModeTemperature: 4500
property var pinnedApps: [] property var pinnedApps: []
property int selectedGpuIndex: 0 property int selectedGpuIndex: 0
property bool nvidiaGpuTempEnabled: false property bool nvidiaGpuTempEnabled: false
property bool nonNvidiaGpuTempEnabled: false property bool nonNvidiaGpuTempEnabled: false
property var enabledGpuPciIds: [] property var enabledGpuPciIds: []
property bool wallpaperCyclingEnabled: false property bool wallpaperCyclingEnabled: false
property string wallpaperCyclingMode: "interval" // "interval" or "time" property string wallpaperCyclingMode: "interval" // "interval" or "time"
property int wallpaperCyclingInterval: 300 // seconds (5 minutes) property int wallpaperCyclingInterval: 300 // seconds (5 minutes)
property string wallpaperCyclingTime: "06:00" // HH:mm format property string wallpaperCyclingTime: "06:00" // HH:mm format
property string lastBrightnessDevice: "" property string lastBrightnessDevice: ""
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
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
nightModeTemperature = settings.nightModeTemperature !== undefined ? settings.nightModeTemperature : 4500
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
selectedGpuIndex = settings.selectedGpuIndex !== undefined ? settings.selectedGpuIndex : 0
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled !== undefined ? settings.nvidiaGpuTempEnabled : false
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled !== undefined ? settings.nonNvidiaGpuTempEnabled : false
enabledGpuPciIds = settings.enabledGpuPciIds !== undefined ? settings.enabledGpuPciIds : []
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled !== undefined ? settings.wallpaperCyclingEnabled : false
wallpaperCyclingMode = settings.wallpaperCyclingMode !== undefined ? settings.wallpaperCyclingMode : "interval"
wallpaperCyclingInterval = settings.wallpaperCyclingInterval !== undefined ? settings.wallpaperCyclingInterval : 300
wallpaperCyclingTime = settings.wallpaperCyclingTime !== undefined ? settings.wallpaperCyclingTime : "06:00"
lastBrightnessDevice = settings.lastBrightnessDevice !== undefined ? settings.lastBrightnessDevice : ""
}
} catch (e) {
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
"isLightMode": isLightMode,
"wallpaperPath": wallpaperPath,
"wallpaperLastPath": wallpaperLastPath,
"profileLastPath": profileLastPath,
"doNotDisturb": doNotDisturb,
"nightModeEnabled": nightModeEnabled,
"nightModeTemperature": nightModeTemperature,
"pinnedApps": pinnedApps,
"selectedGpuIndex": selectedGpuIndex,
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
"enabledGpuPciIds": enabledGpuPciIds,
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
"wallpaperCyclingMode": wallpaperCyclingMode,
"wallpaperCyclingInterval": wallpaperCyclingInterval,
"wallpaperCyclingTime": wallpaperCyclingTime,
"lastBrightnessDevice": lastBrightnessDevice
}, null, 2))
}
function setLightMode(lightMode) {
isLightMode = lightMode
saveSettings()
}
function setDoNotDisturb(enabled) {
doNotDisturb = enabled
saveSettings()
}
function setNightModeEnabled(enabled) {
nightModeEnabled = enabled
saveSettings()
}
function setNightModeTemperature(temperature) {
nightModeTemperature = temperature
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
}
function setSelectedGpuIndex(index) {
selectedGpuIndex = index
saveSettings()
}
function setNvidiaGpuTempEnabled(enabled) {
nvidiaGpuTempEnabled = enabled
saveSettings()
}
function setNonNvidiaGpuTempEnabled(enabled) {
nonNvidiaGpuTempEnabled = enabled
saveSettings()
}
function setEnabledGpuPciIds(pciIds) {
enabledGpuPciIds = pciIds
saveSettings()
}
function setWallpaperCyclingEnabled(enabled) {
wallpaperCyclingEnabled = enabled
saveSettings()
}
function setWallpaperCyclingMode(mode) {
wallpaperCyclingMode = mode
saveSettings()
}
function setWallpaperCyclingInterval(interval) {
wallpaperCyclingInterval = interval
saveSettings()
}
function setWallpaperCyclingTime(time) {
wallpaperCyclingTime = time
saveSettings()
}
function setLastBrightnessDevice(device) {
lastBrightnessDevice = device
saveSettings()
}
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 { function loadSettings() {
if (!path) { parseSettings(settingsFile.text())
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 { function parseSettings(content) {
root.setWallpaper("") try {
return "SUCCESS: Wallpaper cleared" 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
nightModeEnabled = settings.nightModeEnabled
!== undefined ? settings.nightModeEnabled : false
nightModeTemperature = settings.nightModeTemperature
!== undefined ? settings.nightModeTemperature : 4500
pinnedApps = settings.pinnedApps !== undefined ? settings.pinnedApps : []
selectedGpuIndex = settings.selectedGpuIndex
!== undefined ? settings.selectedGpuIndex : 0
nvidiaGpuTempEnabled = settings.nvidiaGpuTempEnabled
!== undefined ? settings.nvidiaGpuTempEnabled : false
nonNvidiaGpuTempEnabled = settings.nonNvidiaGpuTempEnabled
!== undefined ? settings.nonNvidiaGpuTempEnabled : false
enabledGpuPciIds = settings.enabledGpuPciIds
!== undefined ? settings.enabledGpuPciIds : []
wallpaperCyclingEnabled = settings.wallpaperCyclingEnabled
!== undefined ? settings.wallpaperCyclingEnabled : false
wallpaperCyclingMode = settings.wallpaperCyclingMode
!== undefined ? settings.wallpaperCyclingMode : "interval"
wallpaperCyclingInterval = settings.wallpaperCyclingInterval
!== undefined ? settings.wallpaperCyclingInterval : 300
wallpaperCyclingTime = settings.wallpaperCyclingTime
!== undefined ? settings.wallpaperCyclingTime : "06:00"
lastBrightnessDevice = settings.lastBrightnessDevice
!== undefined ? settings.lastBrightnessDevice : ""
}
} catch (e) {
}
} }
function next(): string { function saveSettings() {
if (!root.wallpaperPath) { settingsFile.setText(JSON.stringify({
return "ERROR: No wallpaper set" "isLightMode": isLightMode,
} "wallpaperPath": wallpaperPath,
"wallpaperLastPath": wallpaperLastPath,
try { "profileLastPath": profileLastPath,
WallpaperCyclingService.cycleNextManually() "doNotDisturb": doNotDisturb,
return "SUCCESS: Cycling to next wallpaper" "nightModeEnabled": nightModeEnabled,
} catch (e) { "nightModeTemperature": nightModeTemperature,
return "ERROR: Failed to cycle wallpaper: " + e.toString() "pinnedApps": pinnedApps,
} "selectedGpuIndex": selectedGpuIndex,
"nvidiaGpuTempEnabled": nvidiaGpuTempEnabled,
"nonNvidiaGpuTempEnabled": nonNvidiaGpuTempEnabled,
"enabledGpuPciIds": enabledGpuPciIds,
"wallpaperCyclingEnabled": wallpaperCyclingEnabled,
"wallpaperCyclingMode": wallpaperCyclingMode,
"wallpaperCyclingInterval": wallpaperCyclingInterval,
"wallpaperCyclingTime": wallpaperCyclingTime,
"lastBrightnessDevice": lastBrightnessDevice
}, null, 2))
} }
function prev(): string { function setLightMode(lightMode) {
if (!root.wallpaperPath) { isLightMode = lightMode
return "ERROR: No wallpaper set" saveSettings()
}
try {
WallpaperCyclingService.cyclePrevManually()
return "SUCCESS: Cycling to previous wallpaper"
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString()
}
}
}
IpcHandler {
target: "theme"
function toggle(): string {
root.setLightMode(!root.isLightMode)
return root.isLightMode ? "light" : "dark"
} }
function light(): string { function setDoNotDisturb(enabled) {
root.setLightMode(true) doNotDisturb = enabled
return "light" saveSettings()
} }
function dark(): string { function setNightModeEnabled(enabled) {
root.setLightMode(false) nightModeEnabled = enabled
return "dark" saveSettings()
} }
function getMode(): string { function setNightModeTemperature(temperature) {
return root.isLightMode ? "light" : "dark" nightModeTemperature = temperature
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
}
function setSelectedGpuIndex(index) {
selectedGpuIndex = index
saveSettings()
}
function setNvidiaGpuTempEnabled(enabled) {
nvidiaGpuTempEnabled = enabled
saveSettings()
}
function setNonNvidiaGpuTempEnabled(enabled) {
nonNvidiaGpuTempEnabled = enabled
saveSettings()
}
function setEnabledGpuPciIds(pciIds) {
enabledGpuPciIds = pciIds
saveSettings()
}
function setWallpaperCyclingEnabled(enabled) {
wallpaperCyclingEnabled = enabled
saveSettings()
}
function setWallpaperCyclingMode(mode) {
wallpaperCyclingMode = mode
saveSettings()
}
function setWallpaperCyclingInterval(interval) {
wallpaperCyclingInterval = interval
saveSettings()
}
function setWallpaperCyclingTime(time) {
wallpaperCyclingTime = time
saveSettings()
}
function setLastBrightnessDevice(device) {
lastBrightnessDevice = device
saveSettings()
}
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"
}
function next(): string {
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set"
}
try {
WallpaperCyclingService.cycleNextManually()
return "SUCCESS: Cycling to next wallpaper"
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString()
}
}
function prev(): string {
if (!root.wallpaperPath) {
return "ERROR: No wallpaper set"
}
try {
WallpaperCyclingService.cyclePrevManually()
return "SUCCESS: Cycling to previous wallpaper"
} catch (e) {
return "ERROR: Failed to cycle wallpaper: " + e.toString()
}
}
}
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

View File

@@ -21,123 +21,124 @@ DankModal {
property Component clipboardContent property Component clipboardContent
function updateFilteredModel() { function updateFilteredModel() {
filteredClipboardModel.clear(); filteredClipboardModel.clear()
for (var i = 0; i < clipboardModel.count; i++) { for (var i = 0; i < clipboardModel.count; i++) {
const entry = clipboardModel.get(i).entry; const entry = clipboardModel.get(i).entry
if (searchText.trim().length === 0) { if (searchText.trim().length === 0) {
filteredClipboardModel.append({ filteredClipboardModel.append({
"entry": entry "entry": entry
}); })
} else { } else {
const content = getEntryPreview(entry).toLowerCase(); const content = getEntryPreview(entry).toLowerCase()
if (content.includes(searchText.toLowerCase())) if (content.includes(searchText.toLowerCase()))
filteredClipboardModel.append({ filteredClipboardModel.append({
"entry": entry "entry": entry
}); })
} }
} }
clipboardHistoryModal.totalCount = filteredClipboardModel.count; clipboardHistoryModal.totalCount = filteredClipboardModel.count
// Clamp selectedIndex to valid range // Clamp selectedIndex to valid range
if (filteredClipboardModel.count === 0) { if (filteredClipboardModel.count === 0) {
keyboardNavigationActive = false; keyboardNavigationActive = false
selectedIndex = 0; selectedIndex = 0
} else if (selectedIndex >= filteredClipboardModel.count) { } else if (selectedIndex >= filteredClipboardModel.count) {
selectedIndex = filteredClipboardModel.count - 1; selectedIndex = filteredClipboardModel.count - 1
} }
} }
function toggle() { function toggle() {
if (shouldBeVisible) if (shouldBeVisible)
hide(); hide()
else else
show(); show()
} }
function show() { function show() {
open(); open()
clipboardHistoryModal.searchText = ""; clipboardHistoryModal.searchText = ""
initializeThumbnailSystem(); initializeThumbnailSystem()
refreshClipboard(); refreshClipboard()
keyboardController.reset(); keyboardController.reset()
Qt.callLater(function() { Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.searchField) { if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""; contentLoader.item.searchField.text = ""
contentLoader.item.searchField.forceActiveFocus(); contentLoader.item.searchField.forceActiveFocus()
} }
}); })
} }
function hide() { function hide() {
close(); close()
clipboardHistoryModal.searchText = ""; clipboardHistoryModal.searchText = ""
updateFilteredModel(); updateFilteredModel()
keyboardController.reset(); keyboardController.reset()
cleanupTempFiles(); cleanupTempFiles()
} }
function initializeThumbnailSystem() { function initializeThumbnailSystem() {}
}
function cleanupTempFiles() { function cleanupTempFiles() {
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"]); Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
} }
function generateThumbnails() { function generateThumbnails() {}
}
function refreshClipboard() { function refreshClipboard() {
clipboardProcess.running = true; clipboardProcess.running = true
} }
function copyEntry(entry) { function copyEntry(entry) {
const entryId = entry.split('\t')[0]; const entryId = entry.split('\t')[0]
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`]); Quickshell.execDetached(
ToastService.showInfo("Copied to clipboard"); ["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
hide(); ToastService.showInfo("Copied to clipboard")
hide()
} }
function deleteEntry(entry) { function deleteEntry(entry) {
deleteProcess.deletedEntry = entry; deleteProcess.deletedEntry = entry
deleteProcess.command = ["sh", "-c", `echo '${entry.replace( deleteProcess.command = ["sh", "-c", `echo '${entry.replace(
/'/g, "'\\''")}' | cliphist delete`]; /'/g, "'\\''")}' | cliphist delete`]
deleteProcess.running = true; deleteProcess.running = true
} }
function clearAll() { function clearAll() {
clearProcess.running = true; clearProcess.running = true
} }
function getEntryPreview(entry) { function getEntryPreview(entry) {
let content = entry.replace(/^\s*\d+\s+/, ""); let content = entry.replace(/^\s*\d+\s+/, "")
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) { if (content.includes("image/") || content.includes("binary data")
const dimensionMatch = content.match(/(\d+)x(\d+)/); || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
const dimensionMatch = content.match(/(\d+)x(\d+)/)
if (dimensionMatch) if (dimensionMatch)
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`; return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i); const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i)
if (typeMatch) if (typeMatch)
return `Image (${typeMatch[1].toUpperCase()})`; return `Image (${typeMatch[1].toUpperCase()})`
return "Image"; return "Image"
} }
if (content.length > 100) if (content.length > 100)
return content.substring(0, 100) + "..."; return content.substring(0, 100) + "..."
return content; return content
} }
function getEntryType(entry) { function getEntryType(entry) {
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) if (entry.includes("image/") || entry.includes("binary data")
return "image"; || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry)
|| /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry))
return "image"
if (entry.length > 200) if (entry.length > 200)
return "long_text"; return "long_text"
return "text"; return "text"
} }
visible: false visible: false
@@ -149,10 +150,10 @@ DankModal {
borderWidth: 1 borderWidth: 1
enableShadow: true enableShadow: true
onBackgroundClicked: { onBackgroundClicked: {
hide(); hide()
} }
modalFocusScope.Keys.onPressed: function(event) { modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event); keyboardController.handleKey(event)
} }
content: clipboardContent content: clipboardContent
@@ -160,107 +161,116 @@ DankModal {
id: keyboardController id: keyboardController
function reset() { function reset() {
selectedIndex = 0; selectedIndex = 0
keyboardNavigationActive = false; keyboardNavigationActive = false
showKeyboardHints = false; showKeyboardHints = false
if (typeof clipboardListView !== 'undefined' && clipboardListView) if (typeof clipboardListView !== 'undefined' && clipboardListView)
clipboardListView.keyboardActive = false; clipboardListView.keyboardActive = false
} }
function selectNext() { function selectNext() {
if (filteredClipboardModel.count === 0) if (filteredClipboardModel.count === 0)
return ; return
keyboardNavigationActive = true; keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, filteredClipboardModel.count - 1); selectedIndex = Math.min(selectedIndex + 1,
filteredClipboardModel.count - 1)
} }
function selectPrevious() { function selectPrevious() {
if (filteredClipboardModel.count === 0) if (filteredClipboardModel.count === 0)
return ; return
keyboardNavigationActive = true; keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, 0); selectedIndex = Math.max(selectedIndex - 1, 0)
} }
function copySelected() { function copySelected() {
if (filteredClipboardModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredClipboardModel.count) if (filteredClipboardModel.count === 0 || selectedIndex < 0
return ; || selectedIndex >= filteredClipboardModel.count)
return
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry; var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
copyEntry(selectedEntry); copyEntry(selectedEntry)
} }
function deleteSelected() { function deleteSelected() {
if (filteredClipboardModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredClipboardModel.count) if (filteredClipboardModel.count === 0 || selectedIndex < 0
return ; || selectedIndex >= filteredClipboardModel.count)
return
var selectedEntry = filteredClipboardModel.get(selectedIndex).entry; var selectedEntry = filteredClipboardModel.get(selectedIndex).entry
deleteEntry(selectedEntry); deleteEntry(selectedEntry)
} }
function handleKey(event) { function handleKey(event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (keyboardNavigationActive) { if (keyboardNavigationActive) {
keyboardNavigationActive = false; keyboardNavigationActive = false
if (typeof clipboardListView !== 'undefined' && clipboardListView) if (typeof clipboardListView !== 'undefined'
clipboardListView.keyboardActive = false; && clipboardListView)
clipboardListView.keyboardActive = false
event.accepted = true; event.accepted = true
} else { } else {
hide(); hide()
event.accepted = true; event.accepted = true
} }
} else if (event.key === Qt.Key_Down) { } else if (event.key === Qt.Key_Down) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true; keyboardNavigationActive = true
selectedIndex = 0; selectedIndex = 0
if (typeof clipboardListView !== 'undefined' && clipboardListView) if (typeof clipboardListView !== 'undefined'
clipboardListView.keyboardActive = true; && clipboardListView)
clipboardListView.keyboardActive = true
event.accepted = true; event.accepted = true
} else { } else {
selectNext(); selectNext()
event.accepted = true; event.accepted = true
} }
} else if (event.key === Qt.Key_Up) { } else if (event.key === Qt.Key_Up) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true; keyboardNavigationActive = true
selectedIndex = 0; selectedIndex = 0
if (typeof clipboardListView !== 'undefined' && clipboardListView) if (typeof clipboardListView !== 'undefined'
clipboardListView.keyboardActive = true; && clipboardListView)
clipboardListView.keyboardActive = true
event.accepted = true; event.accepted = true
} else if (selectedIndex === 0) { } else if (selectedIndex === 0) {
keyboardNavigationActive = false; keyboardNavigationActive = false
if (typeof clipboardListView !== 'undefined' && clipboardListView) if (typeof clipboardListView !== 'undefined'
clipboardListView.keyboardActive = false; && clipboardListView)
clipboardListView.keyboardActive = false
event.accepted = true; event.accepted = true
} else { } else {
selectPrevious(); selectPrevious()
event.accepted = true; event.accepted = true
} }
} else if (event.key === Qt.Key_Delete && (event.modifiers & Qt.ShiftModifier)) { } else if (event.key === Qt.Key_Delete
clearAll(); && (event.modifiers & Qt.ShiftModifier)) {
hide(); clearAll()
event.accepted = true; hide()
event.accepted = true
} else if (keyboardNavigationActive) { } else if (keyboardNavigationActive) {
if ((event.key === Qt.Key_C && (event.modifiers & Qt.ControlModifier)) || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { if ((event.key === Qt.Key_C
copySelected(); && (event.modifiers & Qt.ControlModifier))
event.accepted = true; || event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter) {
copySelected()
event.accepted = true
} else if (event.key === Qt.Key_Delete) { } else if (event.key === Qt.Key_Delete) {
deleteSelected(); deleteSelected()
event.accepted = true; event.accepted = true
} }
} }
if (event.key === Qt.Key_F10) { if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints; showKeyboardHints = !showKeyboardHints
event.accepted = true; event.accepted = true
} }
} }
} }
DankModal { DankModal {
@@ -269,8 +279,8 @@ DankModal {
visible: showClearConfirmation visible: showClearConfirmation
width: 350 width: 350
height: 150 height: 150
onBackgroundClicked: { onBackgroundClicked: {
showClearConfirmation = false; showClearConfirmation = false
} }
content: Component { content: Component {
@@ -326,7 +336,6 @@ DankModal {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: showClearConfirmation = false onClicked: showClearConfirmation = false
} }
} }
Rectangle { Rectangle {
@@ -350,22 +359,16 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
clearAll(); clearAll()
showClearConfirmation = false; showClearConfirmation = false
hide(); hide()
} }
} }
} }
} }
} }
} }
} }
} }
ListModel { ListModel {
@@ -384,19 +387,17 @@ DankModal {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
clipboardModel.clear(); clipboardModel.clear()
const lines = text.trim().split('\n'); const lines = text.trim().split('\n')
for (const line of lines) { for (const line of lines) {
if (line.trim().length > 0) if (line.trim().length > 0)
clipboardModel.append({ clipboardModel.append({
"entry": line "entry": line
}); })
} }
updateFilteredModel(); updateFilteredModel()
} }
} }
} }
Process { Process {
@@ -405,33 +406,35 @@ DankModal {
property string deletedEntry: "" property string deletedEntry: ""
running: false running: false
onExited: (exitCode) => { onExited: exitCode => {
if (exitCode === 0) { if (exitCode === 0) {
// Just remove the item from models instead of re-fetching everything // Just remove the item from models instead of re-fetching everything
for (var i = 0; i < clipboardModel.count; i++) { for (var i = 0; i < clipboardModel.count; i++) {
if (clipboardModel.get(i).entry === deleteProcess.deletedEntry) { if (clipboardModel.get(
clipboardModel.remove(i); i).entry === deleteProcess.deletedEntry) {
break; clipboardModel.remove(i)
} break
} }
for (var j = 0; j < filteredClipboardModel.count; j++) { }
if (filteredClipboardModel.get(j).entry === deleteProcess.deletedEntry) { for (var j = 0; j < filteredClipboardModel.count; j++) {
filteredClipboardModel.remove(j); if (filteredClipboardModel.get(
break; j).entry === deleteProcess.deletedEntry) {
} filteredClipboardModel.remove(j)
} break
clipboardHistoryModal.totalCount = filteredClipboardModel.count; }
// Clamp selectedIndex to valid range }
if (filteredClipboardModel.count === 0) { clipboardHistoryModal.totalCount = filteredClipboardModel.count
keyboardNavigationActive = false; // Clamp selectedIndex to valid range
selectedIndex = 0; if (filteredClipboardModel.count === 0) {
} else if (selectedIndex >= filteredClipboardModel.count) { keyboardNavigationActive = false
selectedIndex = filteredClipboardModel.count - 1; selectedIndex = 0
} } else if (selectedIndex >= filteredClipboardModel.count) {
} else { selectedIndex = filteredClipboardModel.count - 1
console.warn("Failed to delete clipboard entry"); }
} } else {
} console.warn("Failed to delete clipboard entry")
}
}
} }
Process { Process {
@@ -439,30 +442,31 @@ DankModal {
command: ["cliphist", "wipe"] command: ["cliphist", "wipe"]
running: false running: false
onExited: (exitCode) => { onExited: exitCode => {
if (exitCode === 0) { if (exitCode === 0) {
clipboardModel.clear(); clipboardModel.clear()
filteredClipboardModel.clear(); filteredClipboardModel.clear()
totalCount = 0; totalCount = 0
} else { } else {
}
} }
}
} }
IpcHandler { IpcHandler {
function open() { function open() {
clipboardHistoryModal.show(); clipboardHistoryModal.show()
return "CLIPBOARD_OPEN_SUCCESS"; return "CLIPBOARD_OPEN_SUCCESS"
} }
function close() { function close() {
hide(); hide()
return "CLIPBOARD_CLOSE_SUCCESS"; return "CLIPBOARD_CLOSE_SUCCESS"
} }
function toggle() { function toggle() {
clipboardHistoryModal.toggle(); clipboardHistoryModal.toggle()
return "CLIPBOARD_TOGGLE_SUCCESS"; return "CLIPBOARD_TOGGLE_SUCCESS"
} }
target: "clipboard" target: "clipboard"
@@ -472,7 +476,7 @@ DankModal {
Item { Item {
id: clipboardContent id: clipboardContent
property alias searchField: searchField property alias searchField: searchField
anchors.fill: parent anchors.fill: parent
Column { Column {
@@ -504,7 +508,6 @@ DankModal {
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Row { Row {
@@ -518,7 +521,7 @@ DankModal {
iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
hoverColor: Theme.primaryHover hoverColor: Theme.primaryHover
onClicked: { onClicked: {
showKeyboardHints = !showKeyboardHints; showKeyboardHints = !showKeyboardHints
} }
} }
@@ -528,7 +531,7 @@ DankModal {
iconColor: Theme.error iconColor: Theme.error
hoverColor: Theme.errorHover hoverColor: Theme.errorHover
onClicked: { onClicked: {
showClearConfirmation = true; showClearConfirmation = true
} }
} }
@@ -539,9 +542,7 @@ DankModal {
hoverColor: Theme.errorHover hoverColor: Theme.errorHover
onClicked: hide() onClicked: hide()
} }
} }
} }
DankTextField { DankTextField {
@@ -555,25 +556,25 @@ DankModal {
ignoreLeftRightKeys: true ignoreLeftRightKeys: true
keyForwardTargets: [modalFocusScope] keyForwardTargets: [modalFocusScope]
onTextChanged: { onTextChanged: {
clipboardHistoryModal.searchText = text; clipboardHistoryModal.searchText = text
updateFilteredModel(); updateFilteredModel()
} }
Keys.onEscapePressed: function(event) { Keys.onEscapePressed: function (event) {
hide(); hide()
event.accepted = true; event.accepted = true
} }
Component.onCompleted: { Component.onCompleted: {
Qt.callLater(function() { Qt.callLater(function () {
forceActiveFocus(); forceActiveFocus()
}); })
} }
Connections { Connections {
target: clipboardHistoryModal target: clipboardHistoryModal
function onOpened() { function onOpened() {
Qt.callLater(function() { Qt.callLater(function () {
searchField.forceActiveFocus(); searchField.forceActiveFocus()
}); })
} }
} }
} }
@@ -592,15 +593,15 @@ DankModal {
function ensureVisible(index) { function ensureVisible(index) {
if (index < 0 || index >= count) if (index < 0 || index >= count)
return ; return
var itemHeight = 72 + spacing; var itemHeight = 72 + spacing
var itemY = index * itemHeight; var itemY = index * itemHeight
var itemBottom = itemY + itemHeight; var itemBottom = itemY + itemHeight
if (itemY < contentY) if (itemY < contentY)
contentY = itemY; contentY = itemY
else if (itemBottom > contentY + height) else if (itemBottom > contentY + height)
contentY = itemBottom - height; contentY = itemBottom - height
} }
anchors.fill: parent anchors.fill: parent
@@ -618,8 +619,7 @@ DankModal {
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
onCurrentIndexChanged: { onCurrentIndexChanged: {
if (keyboardNavigationActive && currentIndex >= 0) if (keyboardNavigationActive && currentIndex >= 0)
ensureVisible(currentIndex); ensureVisible(currentIndex)
} }
StyledText { StyledText {
@@ -640,7 +640,8 @@ DankModal {
delegate: Rectangle { delegate: Rectangle {
property string entryType: getEntryType(model.entry) property string entryType: getEntryType(model.entry)
property string entryPreview: getEntryPreview(model.entry) property string entryPreview: getEntryPreview(
model.entry)
property int entryIndex: index + 1 property int entryIndex: index + 1
property string entryData: model.entry property string entryData: model.entry
property alias thumbnailImageSource: thumbnailImageSource property alias thumbnailImageSource: thumbnailImageSource
@@ -649,18 +650,25 @@ DankModal {
height: 72 height: 72
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (keyboardNavigationActive && index === selectedIndex) if (keyboardNavigationActive
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2); && index === selectedIndex)
return Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground; return mouseArea.containsMouse ? Theme.primaryHover : Theme.primaryBackground
} }
border.color: { border.color: {
if (keyboardNavigationActive && index === selectedIndex) if (keyboardNavigationActive
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5); && index === selectedIndex)
return Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b, 0.5)
return Theme.outlineStrong; return Theme.outlineStrong
} }
border.width: keyboardNavigationActive && index === selectedIndex ? 1.5 : 1 border.width: keyboardNavigationActive
&& index === selectedIndex ? 1.5 : 1
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -682,7 +690,6 @@ DankModal {
font.weight: Font.Bold font.weight: Font.Bold
color: Theme.primary color: Theme.primary
} }
} }
Row { Row {
@@ -698,10 +705,12 @@ DankModal {
CachingImage { CachingImage {
id: thumbnailImageSource id: thumbnailImageSource
property string entryId: model.entry.split('\t')[0] property string entryId: model.entry.split(
'\t')[0]
anchors.fill: parent anchors.fill: parent
source: entryType === "image" && imageLoader.imageData ? `data:image/png;base64,${imageLoader.imageData}` : "" source: entryType === "image"
&& imageLoader.imageData ? `data:image/png;base64,${imageLoader.imageData}` : ""
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
smooth: true smooth: true
cache: true cache: true
@@ -718,12 +727,10 @@ DankModal {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
imageLoader.imageData = text.trim(); imageLoader.imageData = text.trim()
} }
} }
} }
} }
MultiEffect { MultiEffect {
@@ -732,7 +739,8 @@ DankModal {
source: thumbnailImageSource source: thumbnailImageSource
maskEnabled: true maskEnabled: true
maskSource: clipboardCircularMask maskSource: clipboardCircularMask
visible: entryType === "image" && thumbnailImageSource.status === Image.Ready visible: entryType === "image"
&& thumbnailImageSource.status === Image.Ready
maskThresholdMin: 0.5 maskThresholdMin: 0.5
maskSpreadAtMin: 1 maskSpreadAtMin: 1
} }
@@ -752,25 +760,25 @@ DankModal {
color: "black" color: "black"
antialiasing: true antialiasing: true
} }
} }
DankIcon { DankIcon {
visible: !(entryType === "image" && thumbnailImageSource.status === Image.Ready) visible: !(entryType === "image"
&& thumbnailImageSource.status
=== Image.Ready)
name: { name: {
if (entryType === "image") if (entryType === "image")
return "image"; return "image"
if (entryType === "long_text") if (entryType === "long_text")
return "subject"; return "subject"
return "content_copy"; return "content_copy"
} }
size: Theme.iconSize size: Theme.iconSize
color: Theme.primary color: Theme.primary
anchors.centerIn: parent anchors.centerIn: parent
} }
} }
Column { Column {
@@ -782,11 +790,11 @@ DankModal {
text: { text: {
switch (entryType) { switch (entryType) {
case "image": case "image":
return "Image • " + entryPreview; return "Image • " + entryPreview
case "long_text": case "long_text":
return "Long Text"; return "Long Text"
default: default:
return "Text"; return "Text"
} }
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -807,11 +815,8 @@ DankModal {
maximumLineCount: entryType === "long_text" ? 3 : 1 maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
} }
} }
DankActionButton { DankActionButton {
@@ -823,7 +828,7 @@ DankModal {
iconColor: Theme.error iconColor: Theme.error
hoverColor: Theme.errorHover hoverColor: Theme.errorHover
onClicked: { onClicked: {
deleteEntry(model.entry); deleteEntry(model.entry)
} }
} }
@@ -836,11 +841,8 @@ DankModal {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: copyEntry(model.entry) onClicked: copyEntry(model.entry)
} }
} }
} }
} }
Item { Item {
@@ -852,11 +854,8 @@ DankModal {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
} }
Rectangle { Rectangle {
@@ -866,7 +865,9 @@ DankModal {
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
height: 80 height: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.95)
border.color: Theme.primary border.color: Theme.primary
border.width: 2 border.width: 2
opacity: showKeyboardHints ? 1 : 0 opacity: showKeyboardHints ? 1 : 0
@@ -889,7 +890,6 @@ DankModal {
color: Theme.surfaceText color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
Behavior on opacity { Behavior on opacity {
@@ -897,13 +897,8 @@ DankModal {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
} }
} }
} }

View File

@@ -34,10 +34,10 @@ PanelWindow {
property bool allowFocusOverride: false property bool allowFocusOverride: false
property bool allowStacking: false property bool allowStacking: false
signal opened() signal opened
signal dialogClosed() signal dialogClosed
signal backgroundClicked() signal backgroundClicked
Connections { Connections {
target: ModalManager target: ModalManager
function onCloseAllModalsExcept(excludedModal) { function onCloseAllModalsExcept(excludedModal) {
@@ -49,25 +49,24 @@ PanelWindow {
function open() { function open() {
ModalManager.openModal(root) ModalManager.openModal(root)
closeTimer.stop(); closeTimer.stop()
shouldBeVisible = true; shouldBeVisible = true
visible = true; visible = true
focusScope.forceActiveFocus(); focusScope.forceActiveFocus()
} }
function close() { function close() {
shouldBeVisible = false; shouldBeVisible = false
closeTimer.restart(); closeTimer.restart()
} }
function toggle() { function toggle() {
if (shouldBeVisible) if (shouldBeVisible)
close(); close()
else else
open(); open()
} }
visible: shouldBeVisible visible: shouldBeVisible
color: "transparent" color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.layer: WlrLayershell.Overlay
@@ -75,13 +74,13 @@ PanelWindow {
WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: shouldHaveFocus ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
onVisibleChanged: { onVisibleChanged: {
if (root.visible) { if (root.visible) {
opened(); opened()
} else { } else {
if (Qt.inputMethod) { if (Qt.inputMethod) {
Qt.inputMethod.hide(); Qt.inputMethod.hide()
Qt.inputMethod.reset(); Qt.inputMethod.reset()
} }
dialogClosed(); dialogClosed()
} }
} }
@@ -90,12 +89,10 @@ PanelWindow {
interval: animationDuration + 50 interval: animationDuration + 50
onTriggered: { onTriggered: {
visible = false; visible = false
} }
} }
anchors { anchors {
top: true top: true
left: true left: true
@@ -114,12 +111,15 @@ PanelWindow {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.closeOnBackgroundClick enabled: root.closeOnBackgroundClick
onClicked: (mouse) => { onClicked: mouse => {
var localPos = mapToItem(contentContainer, mouse.x, mouse.y); var localPos = mapToItem(contentContainer,
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) mouse.x, mouse.y)
root.backgroundClicked(); if (localPos.x < 0
|| localPos.x > contentContainer.width
} || localPos.y < 0
|| localPos.y > contentContainer.height)
root.backgroundClicked()
}
} }
Behavior on opacity { Behavior on opacity {
@@ -127,9 +127,7 @@ PanelWindow {
duration: root.animationDuration duration: root.animationDuration
easing.type: root.animationEasing easing.type: root.animationEasing
} }
} }
} }
Rectangle { Rectangle {
@@ -140,17 +138,18 @@ PanelWindow {
anchors.centerIn: positioning === "center" ? parent : undefined anchors.centerIn: positioning === "center" ? parent : undefined
x: { x: {
if (positioning === "top-right") if (positioning === "top-right")
return Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL); return Math.max(Theme.spacingL,
root.screenWidth - width - Theme.spacingL)
else if (positioning === "custom") else if (positioning === "custom")
return root.customPosition.x; return root.customPosition.x
return 0; // Will be overridden by anchors.centerIn when positioning === "center" return 0 // Will be overridden by anchors.centerIn when positioning === "center"
} }
y: { y: {
if (positioning === "top-right") if (positioning === "top-right")
return Theme.barHeight + Theme.spacingXS; return Theme.barHeight + Theme.spacingXS
else if (positioning === "custom") else if (positioning === "custom")
return root.customPosition.y; return root.customPosition.y
return 0; // Will be overridden by anchors.centerIn when positioning === "center" return 0 // Will be overridden by anchors.centerIn when positioning === "center"
} }
color: root.backgroundColor color: root.backgroundColor
radius: root.cornerRadius radius: root.cornerRadius
@@ -160,9 +159,9 @@ PanelWindow {
opacity: root.shouldBeVisible ? 1 : 0 opacity: root.shouldBeVisible ? 1 : 0
scale: { scale: {
if (root.animationType === "scale") if (root.animationType === "scale")
return root.shouldBeVisible ? 1 : 0.9; return root.shouldBeVisible ? 1 : 0.9
return 1; return 1
} }
transform: root.animationType === "slide" ? slideTransform : null transform: root.animationType === "slide" ? slideTransform : null
@@ -186,7 +185,6 @@ PanelWindow {
duration: root.animationDuration duration: root.animationDuration
easing.type: root.animationEasing easing.type: root.animationEasing
} }
} }
Behavior on scale { Behavior on scale {
@@ -196,7 +194,6 @@ PanelWindow {
duration: root.animationDuration duration: root.animationDuration
easing.type: root.animationEasing easing.type: root.animationEasing
} }
} }
layer.effect: MultiEffect { layer.effect: MultiEffect {
@@ -207,7 +204,6 @@ PanelWindow {
shadowColor: Theme.shadowStrong shadowColor: Theme.shadowStrong
shadowOpacity: 0.3 shadowOpacity: 0.3
} }
} }
FocusScope { FocusScope {
@@ -217,31 +213,35 @@ PanelWindow {
anchors.fill: parent anchors.fill: parent
visible: root.visible // Only active when the modal is visible visible: root.visible // Only active when the modal is visible
focus: root.visible focus: root.visible
Keys.onEscapePressed: (event) => { Keys.onEscapePressed: event => {
console.log("DankModal escape pressed - shouldHaveFocus:", shouldHaveFocus, "closeOnEscapeKey:", root.closeOnEscapeKey, "objectName:", root.objectName || "unnamed"); console.log(
if (root.closeOnEscapeKey && shouldHaveFocus) { "DankModal escape pressed - shouldHaveFocus:",
console.log("DankModal handling escape"); shouldHaveFocus, "closeOnEscapeKey:",
root.close(); root.closeOnEscapeKey, "objectName:",
event.accepted = true; root.objectName || "unnamed")
} if (root.closeOnEscapeKey
} && shouldHaveFocus) {
console.log("DankModal handling escape")
root.close()
event.accepted = true
}
}
onVisibleChanged: { onVisibleChanged: {
if (visible && shouldHaveFocus) if (visible && shouldHaveFocus)
Qt.callLater(function() { Qt.callLater(function () {
focusScope.forceActiveFocus(); focusScope.forceActiveFocus()
}); })
} }
Connections { Connections {
target: root target: root
function onShouldHaveFocusChanged() { function onShouldHaveFocusChanged() {
if (shouldHaveFocus && visible) { if (shouldHaveFocus && visible) {
Qt.callLater(function() { Qt.callLater(function () {
focusScope.forceActiveFocus(); focusScope.forceActiveFocus()
}); })
} }
} }
} }
} }
} }

View File

@@ -9,290 +9,291 @@ import qs.Common
import qs.Widgets import qs.Widgets
DankModal { DankModal {
id: fileBrowserModal id: fileBrowserModal
objectName: "fileBrowserModal" objectName: "fileBrowserModal"
allowStacking: true allowStacking: true
signal fileSelected(string path) signal fileSelected(string path)
property string homeDir: StandardPaths.writableLocation( property string homeDir: StandardPaths.writableLocation(
StandardPaths.HomeLocation) StandardPaths.HomeLocation)
property string currentPath: "" property string currentPath: ""
property var fileExtensions: ["*.*"] property var fileExtensions: ["*.*"]
property string browserTitle: "Select File" property string browserTitle: "Select File"
property string browserIcon: "folder_open" property string browserIcon: "folder_open"
property string browserType: "generic" // "wallpaper" or "profile" for last path memory 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
} }
if (lastPath && lastPath !== "") { function isImageFile(fileName) {
return lastPath if (!fileName)
} return false
return homeDir var ext = fileName.toLowerCase().split('.').pop()
} return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
function saveLastPath(path) {
if (browserType === "wallpaper") {
SessionData.setWallpaperLastPath(path)
} else if (browserType === "profile") {
SessionData.setProfileLastPath(path)
}
}
Component.onCompleted: {
currentPath = getLastPath()
}
width: 800
height: 600
enableShadow: true
visible: false
onBackgroundClicked: close()
onVisibleChanged: {
if (visible) {
var startPath = getLastPath()
currentPath = startPath
}
}
onCurrentPathChanged: {
}
function navigateUp() {
var path = currentPath
if (path === homeDir) {
return
} }
var lastSlash = path.lastIndexOf('/') function getLastPath() {
if (lastSlash > 0) { var lastPath = ""
var newPath = path.substring(0, lastSlash) if (browserType === "wallpaper") {
if (newPath.length < homeDir.length) { lastPath = SessionData.wallpaperLastPath
currentPath = homeDir } else if (browserType === "profile") {
saveLastPath(homeDir) lastPath = SessionData.profileLastPath
} 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
}
} }
DankActionButton { if (lastPath && lastPath !== "") {
circular: false return lastPath
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: fileBrowserModal.close()
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
} }
} return homeDir
}
Row { function saveLastPath(path) {
width: parent.width if (browserType === "wallpaper") {
spacing: Theme.spacingS SessionData.setWallpaperLastPath(path)
} else if (browserType === "profile") {
SessionData.setProfileLastPath(path)
}
}
StyledRect { Component.onCompleted: {
width: 32 currentPath = getLastPath()
height: 32 }
radius: Theme.cornerRadius
color: mouseArea.containsMouse
&& currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
opacity: currentPath !== homeDir ? 1.0 : 0.0
DankIcon { width: 800
anchors.centerIn: parent height: 600
name: "arrow_back" enableShadow: true
size: Theme.iconSizeSmall visible: false
color: Theme.surfaceText
}
MouseArea { onBackgroundClicked: close()
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
hoverEnabled: currentPath !== homeDir anchors.margins: Theme.spacingM
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor spacing: Theme.spacingS
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
}
}
DankGridView {
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
}
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: 80 width: parent.width
height: 60 height: 40
anchors.horizontalCenter: parent.horizontalCenter
CachingImage { Row {
anchors.fill: parent spacing: Theme.spacingM
imagePath: !delegateRoot.fileIsDir ? delegateRoot.filePath : "" anchors.verticalCenter: parent.verticalCenter
fillMode: Image.PreserveAspectCrop
visible: !delegateRoot.fileIsDir && isImageFile(
delegateRoot.fileName)
maxCacheSize: 80
}
DankIcon { DankIcon {
anchors.centerIn: parent name: browserIcon
name: "description" size: Theme.iconSizeLarge
size: Theme.iconSizeLarge color: Theme.primary
color: Theme.primary anchors.verticalCenter: parent.verticalCenter
visible: !delegateRoot.fileIsDir && !isImageFile( }
delegateRoot.fileName)
}
DankIcon { StyledText {
anchors.centerIn: parent text: browserTitle
name: "folder" font.pixelSize: Theme.fontSizeXLarge
size: Theme.iconSizeLarge color: Theme.surfaceText
color: Theme.primary font.weight: Font.Medium
visible: delegateRoot.fileIsDir anchors.verticalCenter: parent.verticalCenter
} }
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
hoverColor: Theme.errorHover
onClicked: fileBrowserModal.close()
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
} }
StyledText { Row {
text: delegateRoot.fileName || "" width: parent.width
font.pixelSize: Theme.fontSizeSmall spacing: Theme.spacingS
color: Theme.surfaceText
width: 120
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea { StyledRect {
id: mouseArea width: 32
anchors.fill: parent height: 32
hoverEnabled: true radius: Theme.cornerRadius
cursorShape: Qt.PointingHandCursor color: mouseArea.containsMouse
&& currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
opacity: currentPath !== homeDir ? 1.0 : 0.0
onClicked: { DankIcon {
if (delegateRoot.fileIsDir) { anchors.centerIn: parent
navigateTo(delegateRoot.filePath) name: "arrow_back"
} else { size: Theme.iconSizeSmall
fileSelected(delegateRoot.filePath) 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
}
}
DankGridView {
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
}
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,195 +8,202 @@ 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
open() open()
NetworkService.fetchNetworkInfo(ssid) NetworkService.fetchNetworkInfo(ssid)
}
function hideDialog() {
networkInfoModalVisible = false
close()
networkSSID = ""
networkData = null
networkDetails = ""
}
visible: networkInfoModalVisible
width: 600
height: 500
enableShadow: true
onBackgroundClicked: {
hideDialog()
}
onVisibleChanged: {
if (!visible) {
networkSSID = ""
networkData = null
networkDetails = ""
} }
}
content: Component { function hideDialog() {
Item { networkInfoModalVisible = false
anchors.fill: parent close()
networkSSID = ""
networkData = null
networkDetails = ""
}
Column { visible: networkInfoModalVisible
anchors.fill: parent width: 600
anchors.margins: Theme.spacingL height: 500
spacing: Theme.spacingL enableShadow: true
onBackgroundClicked: {
Row { hideDialog()
width: parent.width }
onVisibleChanged: {
Column { if (!visible) {
width: parent.width - 40 networkSSID = ""
spacing: Theme.spacingXS networkData = null
networkDetails = ""
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
}
} }
}
content: Component {
Item { Item {
width: parent.width anchors.fill: parent
height: 40
Rectangle { Column {
anchors.right: parent.right anchors.fill: parent
anchors.verticalCenter: parent.verticalCenter anchors.margins: Theme.spacingL
width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2) spacing: Theme.spacingL
height: 36
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.darker(Theme.primary,
1.1) : Theme.primary
StyledText { Row {
id: closeText width: parent.width
anchors.centerIn: parent Column {
text: "Close" width: parent.width - 40
font.pixelSize: Theme.fontSizeMedium spacing: Theme.spacingXS
color: Theme.background
font.weight: Font.Medium 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
}
}
}
}
} }
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

@@ -11,7 +11,7 @@ import qs.Widgets
DankModal { DankModal {
id: notificationModal id: notificationModal
width: 500 width: 500
height: 700 height: 700
visible: false visible: false
@@ -20,28 +20,28 @@ DankModal {
notificationModalOpen = false notificationModalOpen = false
modalKeyboardController.reset() modalKeyboardController.reset()
} }
modalFocusScope.Keys.onPressed: function(event) { modalFocusScope.Keys.onPressed: function (event) {
modalKeyboardController.handleKey(event) modalKeyboardController.handleKey(event)
} }
NotificationKeyboardController { NotificationKeyboardController {
id: modalKeyboardController id: modalKeyboardController
listView: null listView: null
isOpen: notificationModal.notificationModalOpen isOpen: notificationModal.notificationModalOpen
onClose: function() { notificationModal.hide() } onClose: function () {
notificationModal.hide()
}
} }
property bool notificationModalOpen: false property bool notificationModalOpen: false
property var notificationListRef: null property var notificationListRef: null
function show() { function show() {
notificationModalOpen = true notificationModalOpen = true
open() open()
modalKeyboardController.reset() modalKeyboardController.reset()
if (modalKeyboardController && notificationListRef) { if (modalKeyboardController && notificationListRef) {
modalKeyboardController.listView = notificationListRef modalKeyboardController.listView = notificationListRef
modalKeyboardController.rebuildFlatNavigation() modalKeyboardController.rebuildFlatNavigation()
@@ -61,7 +61,6 @@ DankModal {
show() show()
} }
IpcHandler { IpcHandler {
function open() { function open() {
notificationModal.show() notificationModal.show()
@@ -96,7 +95,7 @@ DankModal {
id: notificationHeader id: notificationHeader
keyboardController: modalKeyboardController keyboardController: modalKeyboardController
} }
NotificationSettings { NotificationSettings {
id: notificationSettings id: notificationSettings
expanded: notificationHeader.showSettings expanded: notificationHeader.showSettings
@@ -104,11 +103,11 @@ DankModal {
KeyboardNavigatedNotificationList { KeyboardNavigatedNotificationList {
id: notificationList id: notificationList
width: parent.width width: parent.width
height: parent.height - y height: parent.height - y
keyboardController: modalKeyboardController keyboardController: modalKeyboardController
Component.onCompleted: { Component.onCompleted: {
notificationModal.notificationListRef = notificationList notificationModal.notificationListRef = notificationList
if (modalKeyboardController) { if (modalKeyboardController) {
@@ -117,7 +116,6 @@ DankModal {
} }
} }
} }
} }
NotificationKeyboardHints { NotificationKeyboardHints {
@@ -128,9 +126,8 @@ DankModal {
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
showHints: modalKeyboardController.showKeyboardHints showHints: modalKeyboardController.showKeyboardHints
} }
} }
} }
content: notificationContent content: notificationContent
} }

View File

@@ -7,159 +7,160 @@ import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
id: root id: root
property string powerConfirmAction: "" property string powerConfirmAction: ""
property string powerConfirmTitle: "" property string powerConfirmTitle: ""
property string powerConfirmMessage: "" property string powerConfirmMessage: ""
function show(action, title, message) { function show(action, title, message) {
powerConfirmAction = action powerConfirmAction = action
powerConfirmTitle = title powerConfirmTitle = title
powerConfirmMessage = message powerConfirmMessage = message
open() open()
}
function executePowerAction(action) {
switch (action) {
case "logout":
NiriService.quit()
break
case "suspend":
Quickshell.execDetached(["systemctl", "suspend"])
break
case "reboot":
Quickshell.execDetached(["systemctl", "reboot"])
break
case "poweroff":
Quickshell.execDetached(["systemctl", "poweroff"])
break
} }
}
shouldBeVisible: false function executePowerAction(action) {
width: 350 switch (action) {
height: 160 case "logout":
enableShadow: false NiriService.quit()
onBackgroundClicked: { break
close() case "suspend":
} Quickshell.execDetached(["systemctl", "suspend"])
break
content: Component { case "reboot":
Item { Quickshell.execDetached(["systemctl", "reboot"])
anchors.fill: parent break
case "poweroff":
Column { Quickshell.execDetached(["systemctl", "poweroff"])
anchors.centerIn: parent break
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
} }
}
StyledText { shouldBeVisible: false
text: powerConfirmMessage width: 350
font.pixelSize: Theme.fontSizeMedium height: 160
color: Theme.surfaceText enableShadow: false
width: parent.width onBackgroundClicked: {
horizontalAlignment: Text.AlignHCenter close()
wrapMode: Text.WordWrap }
}
content: Component {
Item { Item {
height: Theme.spacingS 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
}
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: {
close()
}
}
}
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: {
close()
executePowerAction(powerConfirmAction)
}
}
}
}
}
} }
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: {
close()
}
}
}
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: {
close()
executePowerAction(powerConfirmAction)
}
}
}
}
}
} }
}
} }

View File

@@ -19,29 +19,28 @@ DankModal {
function show() { function show() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available"); console.warn("ProcessListModal: dgop is not available")
return ; return
} }
open(); open()
UserInfoService.getUptime(); UserInfoService.getUptime()
} }
function hide() { function hide() {
close(); close()
if (processContextMenu.visible) if (processContextMenu.visible)
processContextMenu.close(); processContextMenu.close()
} }
function toggle() { function toggle() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available"); console.warn("ProcessListModal: dgop is not available")
return ; return
} }
if (shouldBeVisible) if (shouldBeVisible)
hide(); hide()
else else
show(); show()
} }
width: 900 width: 900
@@ -58,23 +57,18 @@ DankModal {
ProcessesTab { ProcessesTab {
contextMenu: processContextMenu contextMenu: processContextMenu
} }
} }
Component { Component {
id: performanceTabComponent id: performanceTabComponent
PerformanceTab { PerformanceTab {}
}
} }
Component { Component {
id: systemTabComponent id: systemTabComponent
SystemTab { SystemTab {}
}
} }
ProcessContextMenu { ProcessContextMenu {
@@ -85,19 +79,19 @@ DankModal {
Item { Item {
anchors.fill: parent anchors.fill: parent
focus: true focus: true
Keys.onPressed: function(event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
processListModal.hide(); processListModal.hide()
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_1) { } else if (event.key === Qt.Key_1) {
currentTab = 0; currentTab = 0
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_2) { } else if (event.key === Qt.Key_2) {
currentTab = 1; currentTab = 1
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_3) { } else if (event.key === Qt.Key_3) {
currentTab = 2; currentTab = 2
event.accepted = true; event.accepted = true
} }
} }
@@ -139,9 +133,7 @@ DankModal {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
} }
ColumnLayout { ColumnLayout {
@@ -175,7 +167,6 @@ DankModal {
onClicked: processListModal.hide() onClicked: processListModal.hide()
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
} }
Rectangle { Rectangle {
@@ -210,17 +201,18 @@ DankModal {
name: { name: {
switch (index) { switch (index) {
case 0: case 0:
return "list_alt"; return "list_alt"
case 1: case 1:
return "analytics"; return "analytics"
case 2: case 2:
return "settings"; return "settings"
default: default:
return "tab"; return "tab"
} }
} }
size: Theme.iconSize - 2 size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText color: currentTab
=== index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7 opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -228,16 +220,15 @@ DankModal {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
} }
} }
} }
StyledText { StyledText {
text: modelData text: modelData
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText color: currentTab
=== index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1 anchors.verticalCenterOffset: -1
@@ -245,11 +236,8 @@ DankModal {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
} }
} }
} }
} }
MouseArea { MouseArea {
@@ -259,7 +247,7 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
currentTab = index; currentTab = index
} }
} }
@@ -267,22 +255,16 @@ DankModal {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
} }
} }
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
} }
} }
} }
} }
} }
} }
Rectangle { Rectangle {
@@ -308,9 +290,7 @@ DankModal {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
} }
Loader { Loader {
@@ -328,9 +308,7 @@ DankModal {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
} }
Loader { Loader {
@@ -348,17 +326,10 @@ DankModal {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
} }
} }
} }
} }
} }
} }

View File

@@ -14,21 +14,21 @@ DankModal {
property Component settingsContent property Component settingsContent
signal closingModal() signal closingModal
function show() { function show() {
open(); open()
} }
function hide() { function hide() {
close(); close()
} }
function toggle() { function toggle() {
if (shouldBeVisible) if (shouldBeVisible)
hide(); hide()
else else
show(); show()
} }
objectName: "settingsModal" objectName: "settingsModal"
@@ -40,18 +40,18 @@ DankModal {
IpcHandler { IpcHandler {
function open() { function open() {
settingsModal.show(); settingsModal.show()
return "SETTINGS_OPEN_SUCCESS"; return "SETTINGS_OPEN_SUCCESS"
} }
function close() { function close() {
settingsModal.hide(); settingsModal.hide()
return "SETTINGS_CLOSE_SUCCESS"; return "SETTINGS_CLOSE_SUCCESS"
} }
function toggle() { function toggle() {
settingsModal.toggle(); settingsModal.toggle()
return "SETTINGS_TOGGLE_SUCCESS"; return "SETTINGS_TOGGLE_SUCCESS"
} }
target: "settings" target: "settings"
@@ -103,7 +103,6 @@ DankModal {
hoverColor: Theme.errorHover hoverColor: Theme.errorHover
onClicked: settingsModal.hide() onClicked: settingsModal.hide()
} }
} }
// Main content with side navigation // Main content with side navigation
@@ -153,7 +152,8 @@ DankModal {
height: 80 height: 80
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
property bool hasImage: profileImageSource.status === Image.Ready property bool hasImage: profileImageSource.status
=== Image.Ready
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -168,10 +168,11 @@ DankModal {
id: profileImageSource id: profileImageSource
source: { source: {
if (PortalService.profileImage === "") if (PortalService.profileImage === "")
return ""; return ""
if (PortalService.profileImage.startsWith("/")) if (PortalService.profileImage.startsWith(
return "file://" + PortalService.profileImage; "/"))
return PortalService.profileImage; return "file://" + PortalService.profileImage
return PortalService.profileImage
} }
smooth: true smooth: true
asynchronous: true asynchronous: true
@@ -226,7 +227,8 @@ DankModal {
name: "warning" name: "warning"
size: Theme.iconSizeLarge size: Theme.iconSizeLarge
color: Theme.error color: Theme.error
visible: PortalService.profileImage !== "" && profileImageSource.status === Image.Error visible: PortalService.profileImage !== ""
&& profileImageSource.status === Image.Error
} }
// Hover overlay with edit and clear buttons // Hover overlay with edit and clear buttons
@@ -235,54 +237,58 @@ DankModal {
radius: width / 2 radius: width / 2
color: Qt.rgba(0, 0, 0, 0.7) color: Qt.rgba(0, 0, 0, 0.7)
visible: profileMouseArea.containsMouse visible: profileMouseArea.containsMouse
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 4 spacing: 4
Rectangle { Rectangle {
width: 28 width: 28
height: 28 height: 28
radius: 14 radius: 14
color: Qt.rgba(255, 255, 255, 0.9) color: Qt.rgba(255, 255,
255, 0.9)
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "edit" name: "edit"
size: 16 size: 16
color: "black" color: "black"
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
settingsModal.allowFocusOverride = true; settingsModal.allowFocusOverride = true
settingsModal.shouldHaveFocus = false; settingsModal.shouldHaveFocus = false
profileBrowser.open(); profileBrowser.open(
)
} }
} }
} }
Rectangle { Rectangle {
width: 28 width: 28
height: 28 height: 28
radius: 14 radius: 14
color: Qt.rgba(255, 255, 255, 0.9) color: Qt.rgba(255, 255,
255, 0.9)
visible: profileImageContainer.hasImage visible: profileImageContainer.hasImage
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "close" name: "close"
size: 16 size: 16
color: "black" color: "black"
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
PortalService.setProfileImage(""); PortalService.setProfileImage(
"")
} }
} }
} }
@@ -306,7 +312,8 @@ DankModal {
spacing: Theme.spacingXS spacing: Theme.spacingXS
StyledText { StyledText {
text: UserInfoService.fullName || "User" text: UserInfoService.fullName
|| "User"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
@@ -315,7 +322,8 @@ DankModal {
} }
StyledText { StyledText {
text: DgopService.distribution || "Linux" text: DgopService.distribution
|| "Linux"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
@@ -341,33 +349,33 @@ DankModal {
id: sidebarRepeater id: sidebarRepeater
model: [{ model: [{
"text": "Personalization", "text": "Personalization",
"icon": "person" "icon": "person"
}, { }, {
"text": "Time & Date", "text": "Time & Date",
"icon": "schedule" "icon": "schedule"
}, { }, {
"text": "Weather", "text": "Weather",
"icon": "cloud" "icon": "cloud"
}, { }, {
"text": "Top Bar", "text": "Top Bar",
"icon": "toolbar" "icon": "toolbar"
}, { }, {
"text": "Widgets", "text": "Widgets",
"icon": "widgets" "icon": "widgets"
}, { }, {
"text": "Dock", "text": "Dock",
"icon": "dock_to_bottom" "icon": "dock_to_bottom"
}, { }, {
"text": "Recent Apps", "text": "Recent Apps",
"icon": "history" "icon": "history"
}, { }, {
"text": "Theme & Colors", "text": "Theme & Colors",
"icon": "palette" "icon": "palette"
}, { }, {
"text": "About", "text": "About",
"icon": "info" "icon": "info"
}] }]
Rectangle { Rectangle {
property bool isActive: sidebarContainer.currentIndex === index property bool isActive: sidebarContainer.currentIndex === index
@@ -397,7 +405,6 @@ DankModal {
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { MouseArea {
@@ -407,7 +414,7 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
sidebarContainer.currentIndex = index; sidebarContainer.currentIndex = index
} }
} }
@@ -416,15 +423,10 @@ DankModal {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
} }
} }
} }
// Main content area // Main content area
@@ -452,9 +454,7 @@ DankModal {
PersonalizationTab { PersonalizationTab {
parentModal: settingsModal parentModal: settingsModal
} }
} }
} }
Loader { Loader {
@@ -509,11 +509,8 @@ DankModal {
asynchronous: true asynchronous: true
sourceComponent: Component { sourceComponent: Component {
DockTab { DockTab {}
}
} }
} }
Loader { Loader {
@@ -545,210 +542,224 @@ DankModal {
asynchronous: true asynchronous: true
sourceComponent: AboutTab {} sourceComponent: AboutTab {}
} }
} }
} }
} }
// Footer // Footer
Row { Row {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
// Dank logo // Dank logo
Item { Item {
width: 68 width: 68
height: 16 height: 16
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Image { Image {
anchors.fill: parent anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modals/", "") + "/assets/dank.svg" source: Qt.resolvedUrl(".").toString().replace(
"file://",
"").replace("/Modals/",
"") + "/assets/dank.svg"
sourceSize: Qt.size(68, 16) sourceSize: Qt.size(68, 16)
smooth: true smooth: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
colorization: 1 colorization: 1
colorizationColor: Theme.primary colorizationColor: Theme.primary
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/DankMaterialShell") onClicked: Qt.openUrlExternally(
"https://github.com/AvengeMedia/DankMaterialShell")
} }
} }
StyledText { StyledText {
text: "•" text: "•"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Rectangle { Rectangle {
width: Theme.spacingXS width: Theme.spacingXS
height: 1 height: 1
color: "transparent" color: "transparent"
} }
// Niri logo // Niri logo
Item { Item {
width: 24 width: 24
height: 24 height: 24
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Image { Image {
anchors.fill: parent anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modals/", "") + "/assets/niri.svg" source: Qt.resolvedUrl(".").toString().replace(
"file://",
"").replace("/Modals/",
"") + "/assets/niri.svg"
sourceSize: Qt.size(24, 24) sourceSize: Qt.size(24, 24)
smooth: true smooth: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Qt.openUrlExternally("https://github.com/YaLTeR/niri") onClicked: Qt.openUrlExternally(
"https://github.com/YaLTeR/niri")
} }
} }
Rectangle { Rectangle {
width: Theme.spacingXS width: Theme.spacingXS
height: 1 height: 1
color: "transparent" color: "transparent"
} }
StyledText { StyledText {
text: "•" text: "•"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Rectangle { Rectangle {
width: Theme.spacingM width: Theme.spacingM
height: 1 height: 1
color: "transparent" color: "transparent"
} }
// Matrix button // Matrix button
Item { Item {
width: 32 width: 32
height: 20 height: 20
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Image { Image {
anchors.fill: parent anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modals/", "") + "/assets/matrix-logo-white.svg" source: Qt.resolvedUrl(".").toString().replace(
"file://", "").replace(
"/Modals/",
"") + "/assets/matrix-logo-white.svg"
sourceSize: Qt.size(32, 20) sourceSize: Qt.size(32, 20)
smooth: true smooth: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
colorization: 1 colorization: 1
colorizationColor: Theme.surfaceText colorizationColor: Theme.surfaceText
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Qt.openUrlExternally("https://matrix.to/#/#niri:matrix.org") onClicked: Qt.openUrlExternally(
"https://matrix.to/#/#niri:matrix.org")
} }
} }
Rectangle { Rectangle {
width: Theme.spacingM width: Theme.spacingM
height: 1 height: 1
color: "transparent" color: "transparent"
} }
StyledText { StyledText {
text: "•" text: "•"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Rectangle { Rectangle {
width: Theme.spacingM width: Theme.spacingM
height: 1 height: 1
color: "transparent" color: "transparent"
} }
// Discord button // Discord button
Item { Item {
width: 16 width: 16
height: 16 height: 16
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Image { Image {
anchors.fill: parent anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modals/", "") + "/assets/discord.svg" source: Qt.resolvedUrl(".").toString().replace(
"file://",
"").replace("/Modals/",
"") + "/assets/discord.svg"
sourceSize: Qt.size(16, 16) sourceSize: Qt.size(16, 16)
smooth: true smooth: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Qt.openUrlExternally("https://discord.gg/vT8Sfjy7sx") onClicked: Qt.openUrlExternally(
"https://discord.gg/vT8Sfjy7sx")
} }
} }
Rectangle { Rectangle {
width: Theme.spacingM width: Theme.spacingM
height: 1 height: 1
color: "transparent" color: "transparent"
} }
StyledText { StyledText {
text: "•" text: "•"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Rectangle { Rectangle {
width: Theme.spacingM width: Theme.spacingM
height: 1 height: 1
color: "transparent" color: "transparent"
} }
// Reddit button // Reddit button
Item { Item {
width: 18 width: 18
height: 18 height: 18
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Image { Image {
anchors.fill: parent anchors.fill: parent
source: Qt.resolvedUrl(".").toString().replace("file://", "").replace("/Modals/", "") + "/assets/reddit.svg" source: Qt.resolvedUrl(".").toString().replace(
"file://",
"").replace("/Modals/",
"") + "/assets/reddit.svg"
sourceSize: Qt.size(18, 18) sourceSize: Qt.size(18, 18)
smooth: true smooth: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: Qt.openUrlExternally("https://reddit.com/r/niri") onClicked: Qt.openUrlExternally(
"https://reddit.com/r/niri")
} }
} }
} }
} }
} }
} }
FileBrowserModal { FileBrowserModal {
@@ -758,18 +769,17 @@ DankModal {
browserIcon: "person" browserIcon: "person"
browserType: "profile" browserType: "profile"
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => { onFileSelected: path => {
PortalService.setProfileImage(path); PortalService.setProfileImage(path)
close(); close()
} }
onDialogClosed: { onDialogClosed: {
if (settingsModal) { if (settingsModal) {
settingsModal.allowFocusOverride = false; settingsModal.allowFocusOverride = false
settingsModal.shouldHaveFocus = Qt.binding(() => { settingsModal.shouldHaveFocus = Qt.binding(() => {
return settingsModal.shouldBeVisible; return settingsModal.shouldBeVisible
}); })
} }
} }
} }
} }

View File

@@ -16,41 +16,41 @@ DankModal {
property Component spotlightContent property Component spotlightContent
function show() { function show() {
spotlightOpen = true; spotlightOpen = true
open(); open()
if (contentLoader.item && contentLoader.item.appLauncher) if (contentLoader.item && contentLoader.item.appLauncher)
contentLoader.item.appLauncher.searchQuery = ""; contentLoader.item.appLauncher.searchQuery = ""
Qt.callLater(function() { Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.searchField) if (contentLoader.item && contentLoader.item.searchField)
contentLoader.item.searchField.forceActiveFocus(); contentLoader.item.searchField.forceActiveFocus()
})
});
} }
function hide() { function hide() {
spotlightOpen = false; spotlightOpen = false
close(); close()
if (contentLoader.item && contentLoader.item.appLauncher) { if (contentLoader.item && contentLoader.item.appLauncher) {
contentLoader.item.appLauncher.searchQuery = ""; contentLoader.item.appLauncher.searchQuery = ""
contentLoader.item.appLauncher.selectedIndex = 0; contentLoader.item.appLauncher.selectedIndex = 0
contentLoader.item.appLauncher.setCategory("All"); contentLoader.item.appLauncher.setCategory("All")
} }
} }
function toggle() { function toggle() {
if (spotlightOpen) if (spotlightOpen)
hide(); hide()
else else
show(); show()
} }
shouldBeVisible: spotlightOpen shouldBeVisible: spotlightOpen
Connections { Connections {
target: ModalManager target: ModalManager
function onCloseAllModalsExcept(excludedModal) { function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) { if (excludedModal !== spotlightModal && !allowStacking
&& spotlightOpen) {
spotlightOpen = false spotlightOpen = false
} }
} }
@@ -64,43 +64,41 @@ DankModal {
enableShadow: true enableShadow: true
onVisibleChanged: { onVisibleChanged: {
if (visible && !spotlightOpen) if (visible && !spotlightOpen)
show(); show()
if (visible && contentLoader.item) if (visible && contentLoader.item)
Qt.callLater(function() { Qt.callLater(function () {
if (contentLoader.item.searchField) if (contentLoader.item.searchField)
contentLoader.item.searchField.forceActiveFocus(); contentLoader.item.searchField.forceActiveFocus()
})
});
} }
onBackgroundClicked: { onBackgroundClicked: {
hide(); hide()
} }
Component.onCompleted: { Component.onCompleted: {
} }
content: spotlightContent content: spotlightContent
IpcHandler { IpcHandler {
function open() { function open() {
spotlightModal.show(); spotlightModal.show()
return "SPOTLIGHT_OPEN_SUCCESS"; return "SPOTLIGHT_OPEN_SUCCESS"
} }
function close() { function close() {
spotlightModal.hide(); spotlightModal.hide()
return "SPOTLIGHT_CLOSE_SUCCESS"; return "SPOTLIGHT_CLOSE_SUCCESS"
} }
function toggle() { function toggle() {
spotlightModal.toggle(); spotlightModal.toggle()
return "SPOTLIGHT_TOGGLE_SUCCESS"; return "SPOTLIGHT_TOGGLE_SUCCESS"
} }
target: "spotlight" target: "spotlight"
} }
spotlightContent: Component { spotlightContent: Component {
Item { Item {
id: spotlightKeyHandler id: spotlightKeyHandler
@@ -110,29 +108,34 @@ DankModal {
anchors.fill: parent anchors.fill: parent
focus: true focus: true
Keys.onPressed: function(event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
hide(); hide()
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_Down) { } else if (event.key === Qt.Key_Down) {
appLauncher.selectNext(); appLauncher.selectNext()
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_Up) { } else if (event.key === Qt.Key_Up) {
appLauncher.selectPrevious(); appLauncher.selectPrevious()
event.accepted = true; event.accepted = true
} else if (event.key === Qt.Key_Right && appLauncher.viewMode === "grid") { } else if (event.key === Qt.Key_Right
appLauncher.selectNextInRow(); && appLauncher.viewMode === "grid") {
event.accepted = true; appLauncher.selectNextInRow()
} else if (event.key === Qt.Key_Left && appLauncher.viewMode === "grid") { event.accepted = true
appLauncher.selectPreviousInRow(); } else if (event.key === Qt.Key_Left
event.accepted = true; && appLauncher.viewMode === "grid") {
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { appLauncher.selectPreviousInRow()
appLauncher.launchSelected(); event.accepted = true
event.accepted = true; } else if (event.key === Qt.Key_Return
} else if (!searchField.activeFocus && event.text && event.text.length > 0 && event.text.match(/[a-zA-Z0-9\\s]/)) { || event.key === Qt.Key_Enter) {
searchField.forceActiveFocus(); appLauncher.launchSelected()
searchField.insertText(event.text); event.accepted = true
event.accepted = true; } else if (!searchField.activeFocus && event.text
&& event.text.length > 0 && event.text.match(
/[a-zA-Z0-9\\s]/)) {
searchField.forceActiveFocus()
searchField.insertText(event.text)
event.accepted = true
} }
} }
@@ -142,8 +145,8 @@ DankModal {
viewMode: SettingsData.spotlightModalViewMode viewMode: SettingsData.spotlightModalViewMode
gridColumns: 4 gridColumns: 4
onAppLaunched: hide() onAppLaunched: hide()
onViewModeSelected: function(mode) { onViewModeSelected: function (mode) {
SettingsData.setSpotlightModalViewMode(mode); SettingsData.setSpotlightModalViewMode(mode)
} }
} }
@@ -159,7 +162,8 @@ DankModal {
color: Theme.surfaceVariantAlpha color: Theme.surfaceVariantAlpha
border.color: Theme.outlineMedium border.color: Theme.outlineMedium
border.width: 1 border.width: 1
visible: appLauncher.categories.length > 1 || appLauncher.model.count > 0 visible: appLauncher.categories.length > 1
|| appLauncher.model.count > 0
CategorySelector { CategorySelector {
id: categorySelector id: categorySelector
@@ -169,11 +173,11 @@ DankModal {
categories: appLauncher.categories categories: appLauncher.categories
selectedCategory: appLauncher.selectedCategory selectedCategory: appLauncher.selectedCategory
compact: false compact: false
onCategorySelected: (category) => { onCategorySelected: category => {
return appLauncher.setCategory(category); return appLauncher.setCategory(
} category)
}
} }
} }
Row { Row {
@@ -183,10 +187,16 @@ DankModal {
DankTextField { DankTextField {
id: searchField id: searchField
width: parent.width - 80 - Theme.spacingM // Leave space for view toggle buttons width: parent.width - 80
- Theme.spacingM // Leave space for view toggle buttons
height: 56 height: 56
cornerRadius: Theme.cornerRadius cornerRadius: Theme.cornerRadius
backgroundColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7) backgroundColor: Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
Theme.getContentBackgroundAlpha(
) * 0.7)
normalBorderColor: Theme.outlineMedium normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary focusedBorderColor: Theme.primary
leftIconName: "search" leftIconName: "search"
@@ -202,22 +212,26 @@ DankModal {
keyForwardTargets: [spotlightKeyHandler] keyForwardTargets: [spotlightKeyHandler]
text: appLauncher.searchQuery text: appLauncher.searchQuery
onTextEdited: { onTextEdited: {
appLauncher.searchQuery = text; appLauncher.searchQuery = text
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length > 0) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0)
appLauncher.launchSelected();
else if (appLauncher.model.count > 0)
appLauncher.launchApp(appLauncher.model.get(0));
event.accepted = true;
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false;
}
} }
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide()
event.accepted = true
} else if ((event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter)
&& text.length > 0) {
if (appLauncher.keyboardNavigationActive
&& appLauncher.model.count > 0)
appLauncher.launchSelected()
else if (appLauncher.model.count > 0)
appLauncher.launchApp(
appLauncher.model.get(0))
event.accepted = true
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Up || event.key === Qt.Key_Left || event.key === Qt.Key_Right || ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && text.length === 0)) {
event.accepted = false
}
}
} }
Row { Row {
@@ -230,14 +244,16 @@ DankModal {
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent" color: appLauncher.viewMode === "list" ? Theme.primaryHover : listViewArea.containsMouse ? Theme.surfaceHover : "transparent"
border.color: appLauncher.viewMode === "list" ? Theme.primarySelected : "transparent" border.color: appLauncher.viewMode
=== "list" ? Theme.primarySelected : "transparent"
border.width: 1 border.width: 1
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "view_list" name: "view_list"
size: 18 size: 18
color: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText color: appLauncher.viewMode
=== "list" ? Theme.primary : Theme.surfaceText
} }
MouseArea { MouseArea {
@@ -247,10 +263,9 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
appLauncher.setViewMode("list"); appLauncher.setViewMode("list")
} }
} }
} }
Rectangle { Rectangle {
@@ -258,14 +273,16 @@ DankModal {
height: 36 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent" color: appLauncher.viewMode === "grid" ? Theme.primaryHover : gridViewArea.containsMouse ? Theme.surfaceHover : "transparent"
border.color: appLauncher.viewMode === "grid" ? Theme.primarySelected : "transparent" border.color: appLauncher.viewMode
=== "grid" ? Theme.primarySelected : "transparent"
border.width: 1 border.width: 1
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "grid_view" name: "grid_view"
size: 18 size: 18
color: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText color: appLauncher.viewMode
=== "grid" ? Theme.primary : Theme.surfaceText
} }
MouseArea { MouseArea {
@@ -275,14 +292,11 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
appLauncher.setViewMode("grid"); appLauncher.setViewMode("grid")
} }
} }
} }
} }
} }
Rectangle { Rectangle {
@@ -305,20 +319,20 @@ DankModal {
property bool hoverUpdatesSelection: false property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
signal keyboardNavigationReset() signal keyboardNavigationReset
signal itemClicked(int index, var modelData) signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) { function ensureVisible(index) {
if (index < 0 || index >= count) if (index < 0 || index >= count)
return ; return
var itemY = index * (itemHeight + itemSpacing); var itemY = index * (itemHeight + itemSpacing)
var itemBottom = itemY + itemHeight; var itemBottom = itemY + itemHeight
if (itemY < contentY) if (itemY < contentY)
contentY = itemY; contentY = itemY
else if (itemBottom > contentY + height) else if (itemBottom > contentY + height)
contentY = itemBottom - height; contentY = itemBottom - height
} }
anchors.fill: parent anchors.fill: parent
@@ -334,17 +348,16 @@ DankModal {
reuseItems: true reuseItems: true
onCurrentIndexChanged: { onCurrentIndexChanged: {
if (keyboardNavigationActive) if (keyboardNavigationActive)
ensureVisible(currentIndex); ensureVisible(currentIndex)
} }
onItemClicked: function(index, modelData) { onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData); appLauncher.launchApp(modelData)
} }
onItemRightClicked: function(index, modelData, mouseX, mouseY) { onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData); contextMenu.show(mouseX, mouseY, modelData)
} }
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false; appLauncher.keyboardNavigationActive = false
} }
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
@@ -377,7 +390,9 @@ DankModal {
id: listIconImg id: listIconImg
anchors.fill: parent anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : "" source: (model.icon) ? Quickshell.iconPath(
model.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true smooth: true
asynchronous: true asynchronous: true
visible: status === Image.Ready visible: status === Image.Ready
@@ -393,14 +408,16 @@ DankModal {
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" text: (model.name
&& model.name.length
> 0) ? model.name.charAt(
0).toUpperCase(
) : "A"
font.pixelSize: resultsList.iconSize * 0.4 font.pixelSize: resultsList.iconSize * 0.4
color: Theme.primary color: Theme.primary
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
} }
Column { Column {
@@ -423,11 +440,11 @@ DankModal {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
elide: Text.ElideRight elide: Text.ElideRight
visible: resultsList.showDescription && model.comment && model.comment.length > 0 visible: resultsList.showDescription
&& model.comment
&& model.comment.length > 0
} }
} }
} }
MouseArea { MouseArea {
@@ -439,25 +456,28 @@ DankModal {
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10 z: 10
onEntered: { onEntered: {
if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive) if (resultsList.hoverUpdatesSelection
resultsList.currentIndex = index; && !resultsList.keyboardNavigationActive)
resultsList.currentIndex = index
} }
onPositionChanged: { onPositionChanged: {
resultsList.keyboardNavigationReset(); resultsList.keyboardNavigationReset()
}
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
resultsList.itemClicked(index, model);
} else if (mouse.button === Qt.RightButton) {
var modalPos = mapToItem(spotlightKeyHandler, mouse.x, mouse.y);
resultsList.itemRightClicked(index, model, modalPos.x, modalPos.y);
}
} }
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
resultsList.itemClicked(
index, model)
} else if (mouse.button === Qt.RightButton) {
var modalPos = mapToItem(
spotlightKeyHandler,
mouse.x, mouse.y)
resultsList.itemRightClicked(
index, model,
modalPos.x, modalPos.y)
}
}
} }
} }
} }
DankGridView { DankGridView {
@@ -474,25 +494,30 @@ DankModal {
property int minIconSize: 32 property int minIconSize: 32
property bool hoverUpdatesSelection: false property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns property int baseCellWidth: adaptiveColumns ? Math.max(
minCellWidth,
Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20 property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns property int actualColumns: adaptiveColumns ? Math.floor(
width
/ cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth) property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset() signal keyboardNavigationReset
signal itemClicked(int index, var modelData) signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) { function ensureVisible(index) {
if (index < 0 || index >= count) if (index < 0 || index >= count)
return ; return
var itemY = Math.floor(index / actualColumns) * cellHeight; var itemY = Math.floor(
var itemBottom = itemY + cellHeight; index / actualColumns) * cellHeight
var itemBottom = itemY + cellHeight
if (itemY < contentY) if (itemY < contentY)
contentY = itemY; contentY = itemY
else if (itemBottom > contentY + height) else if (itemBottom > contentY + height)
contentY = itemBottom - height; contentY = itemBottom - height
} }
anchors.fill: parent anchors.fill: parent
@@ -510,17 +535,16 @@ DankModal {
reuseItems: true reuseItems: true
onCurrentIndexChanged: { onCurrentIndexChanged: {
if (keyboardNavigationActive) if (keyboardNavigationActive)
ensureVisible(currentIndex); ensureVisible(currentIndex)
} }
onItemClicked: function(index, modelData) { onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData); appLauncher.launchApp(modelData)
} }
onItemRightClicked: function(index, modelData, mouseX, mouseY) { onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData); contextMenu.show(mouseX, mouseY, modelData)
} }
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false; appLauncher.keyboardNavigationActive = false
} }
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
@@ -536,7 +560,8 @@ DankModal {
height: resultsGrid.cellHeight - resultsGrid.cellPadding height: resultsGrid.cellHeight - resultsGrid.cellPadding
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) color: resultsGrid.currentIndex === index ? Theme.primaryPressed : gridMouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: resultsGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium border.color: resultsGrid.currentIndex
=== index ? Theme.primarySelected : Theme.outlineMedium
border.width: resultsGrid.currentIndex === index ? 2 : 1 border.width: resultsGrid.currentIndex === index ? 2 : 1
Column { Column {
@@ -544,7 +569,12 @@ DankModal {
spacing: Theme.spacingS spacing: Theme.spacingS
Item { Item {
property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio)) property int iconSize: Math.min(
resultsGrid.maxIconSize,
Math.max(
resultsGrid.minIconSize,
resultsGrid.cellWidth
* resultsGrid.iconSizeRatio))
width: iconSize width: iconSize
height: iconSize height: iconSize
@@ -554,7 +584,9 @@ DankModal {
id: gridIconImg id: gridIconImg
anchors.fill: parent anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : "" source: (model.icon) ? Quickshell.iconPath(
model.icon,
SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true smooth: true
asynchronous: true asynchronous: true
visible: status === Image.Ready visible: status === Image.Ready
@@ -570,14 +602,18 @@ DankModal {
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A" text: (model.name
font.pixelSize: Math.min(28, parent.width * 0.5) && model.name.length
> 0) ? model.name.charAt(
0).toUpperCase(
) : "A"
font.pixelSize: Math.min(
28,
parent.width * 0.5)
color: Theme.primary color: Theme.primary
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
} }
StyledText { StyledText {
@@ -592,7 +628,6 @@ DankModal {
maximumLineCount: 2 maximumLineCount: 2
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
MouseArea { MouseArea {
@@ -604,29 +639,30 @@ DankModal {
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10 z: 10
onEntered: { onEntered: {
if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive) if (resultsGrid.hoverUpdatesSelection
resultsGrid.currentIndex = index; && !resultsGrid.keyboardNavigationActive)
resultsGrid.currentIndex = index
} }
onPositionChanged: { onPositionChanged: {
resultsGrid.keyboardNavigationReset(); resultsGrid.keyboardNavigationReset()
}
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
resultsGrid.itemClicked(index, model);
} else if (mouse.button === Qt.RightButton) {
var modalPos = mapToItem(spotlightKeyHandler, mouse.x, mouse.y);
resultsGrid.itemRightClicked(index, model, modalPos.x, modalPos.y);
}
} }
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
resultsGrid.itemClicked(
index, model)
} else if (mouse.button === Qt.RightButton) {
var modalPos = mapToItem(
spotlightKeyHandler,
mouse.x, mouse.y)
resultsGrid.itemRightClicked(
index, model,
modalPos.x, modalPos.y)
}
}
} }
} }
} }
} }
} }
Rectangle { Rectangle {
@@ -637,23 +673,29 @@ DankModal {
function show(x, y, app) { function show(x, y, app) {
currentApp = app currentApp = app
const menuWidth = 180 const menuWidth = 180
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2 const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x + 8 let finalX = x + 8
let finalY = y + 8 let finalY = y + 8
if (finalX + menuWidth > spotlightKeyHandler.width) { if (finalX + menuWidth > spotlightKeyHandler.width) {
finalX = x - menuWidth - 8 finalX = x - menuWidth - 8
} }
if (finalY + menuHeight > spotlightKeyHandler.height) { if (finalY + menuHeight > spotlightKeyHandler.height) {
finalY = y - menuHeight - 8 finalY = y - menuHeight - 8
} }
finalX = Math.max(8, Math.min(finalX, spotlightKeyHandler.width - menuWidth - 8)) finalX = Math.max(
finalY = Math.max(8, Math.min(finalY, spotlightKeyHandler.height - menuHeight - 8)) 8, Math.min(
finalX,
spotlightKeyHandler.width - menuWidth - 8))
finalY = Math.max(
8, Math.min(
finalY,
spotlightKeyHandler.height - menuHeight - 8))
contextMenu.x = finalX contextMenu.x = finalX
contextMenu.y = finalY contextMenu.y = finalY
contextMenu.visible = true contextMenu.visible = true
@@ -663,8 +705,8 @@ DankModal {
function close() { function close() {
contextMenu.menuVisible = false contextMenu.menuVisible = false
Qt.callLater(() => { Qt.callLater(() => {
contextMenu.visible = false contextMenu.visible = false
}) })
} }
visible: false visible: false
@@ -672,7 +714,8 @@ DankModal {
height: menuColumn.implicitHeight + Theme.spacingS * 2 height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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
@@ -700,7 +743,11 @@ DankModal {
width: parent.width width: parent.width
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: pinMouseArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -710,11 +757,15 @@ DankModal {
DankIcon { DankIcon {
name: { name: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) if (!contextMenu.currentApp
|| !contextMenu.currentApp.desktopEntry)
return "push_pin" return "push_pin"
var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "" var appId = contextMenu.currentApp.desktopEntry.id
return SessionData.isPinnedApp(appId) ? "keep_off" : "push_pin" || contextMenu.currentApp.desktopEntry.execString
|| ""
return SessionData.isPinnedApp(
appId) ? "keep_off" : "push_pin"
} }
size: Theme.iconSize - 2 size: Theme.iconSize - 2
color: Theme.surfaceText color: Theme.surfaceText
@@ -724,11 +775,15 @@ DankModal {
StyledText { StyledText {
text: { text: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) if (!contextMenu.currentApp
|| !contextMenu.currentApp.desktopEntry)
return "Pin to Dock" return "Pin to Dock"
var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "" var appId = contextMenu.currentApp.desktopEntry.id
return SessionData.isPinnedApp(appId) ? "Unpin from Dock" : "Pin to Dock" || contextMenu.currentApp.desktopEntry.execString
|| ""
return SessionData.isPinnedApp(
appId) ? "Unpin from Dock" : "Pin to Dock"
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
@@ -744,10 +799,13 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (!contextMenu.currentApp || !contextMenu.currentApp.desktopEntry) if (!contextMenu.currentApp
|| !contextMenu.currentApp.desktopEntry)
return return
var appId = contextMenu.currentApp.desktopEntry.id || contextMenu.currentApp.desktopEntry.execString || "" var appId = contextMenu.currentApp.desktopEntry.id
|| contextMenu.currentApp.desktopEntry.execString
|| ""
if (SessionData.isPinnedApp(appId)) if (SessionData.isPinnedApp(appId))
SessionData.removePinnedApp(appId) SessionData.removePinnedApp(appId)
else else
@@ -767,7 +825,8 @@ DankModal {
anchors.centerIn: parent anchors.centerIn: parent
width: parent.width width: parent.width
height: 1 height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
} }
} }
@@ -775,7 +834,11 @@ DankModal {
width: parent.width width: parent.width
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: launchMouseArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -808,7 +871,8 @@ DankModal {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (contextMenu.currentApp) if (contextMenu.currentApp)
appLauncher.launchApp(contextMenu.currentApp) appLauncher.launchApp(
contextMenu.currentApp)
contextMenu.close() contextMenu.close()
} }
@@ -845,13 +909,11 @@ DankModal {
width: contextMenu.width width: contextMenu.width
height: contextMenu.height height: contextMenu.height
onClicked: { onClicked: {
// Prevent closing when clicking on the menu itself // Prevent closing when clicking on the menu itself
} }
} }
} }
} }
} }
} }

View File

@@ -5,300 +5,306 @@ import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
id: root id: root
property string wifiPasswordSSID: "" property string wifiPasswordSSID: ""
property string wifiPasswordInput: "" property string wifiPasswordInput: ""
function show(ssid) { function show(ssid) {
wifiPasswordSSID = ssid wifiPasswordSSID = ssid
wifiPasswordInput = ""
open()
Qt.callLater(function() {
if (contentLoader.item && contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
})
}
shouldBeVisible: false
width: 420
height: 230
onShouldBeVisibleChanged: {
if (!shouldBeVisible)
wifiPasswordInput = ""
}
onOpened: {
Qt.callLater(function() {
if (contentLoader.item && contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
})
}
onBackgroundClicked: {
close()
wifiPasswordInput = ""
}
Connections {
function onPasswordDialogShouldReopenChanged() {
if (NetworkService.passwordDialogShouldReopen
&& NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID
wifiPasswordInput = "" wifiPasswordInput = ""
open() open()
NetworkService.passwordDialogShouldReopen = false Qt.callLater(function () {
} if (contentLoader.item && contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
})
} }
target: NetworkService shouldBeVisible: false
} width: 420
height: 230
content: Component { onShouldBeVisibleChanged: {
FocusScope { if (!shouldBeVisible)
id: wifiContent wifiPasswordInput = ""
property alias passwordInput: passwordInput }
onOpened: {
anchors.fill: parent Qt.callLater(function () {
focus: true if (contentLoader.item && contentLoader.item.passwordInput) {
Keys.onEscapePressed: function(event) { contentLoader.item.passwordInput.forceActiveFocus()
}
})
}
onBackgroundClicked: {
close() close()
wifiPasswordInput = "" wifiPasswordInput = ""
event.accepted = 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: {
close()
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
MouseArea {
anchors.fill: parent
onClicked: {
passwordInput.forceActiveFocus()
}
}
DankTextField {
id: passwordInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: ""
backgroundColor: "transparent"
focus: true
enabled: root.shouldBeVisible
onTextEdited: {
wifiPasswordInput = text
}
onAccepted: {
NetworkService.connectToWifiWithPassword(wifiPasswordSSID,
passwordInput.text)
close()
wifiPasswordInput = ""
passwordInput.text = ""
}
Component.onCompleted: {
if (root.shouldBeVisible) {
focusDelayTimer.start()
}
}
Timer {
id: focusDelayTimer
interval: 100
repeat: false
onTriggered: {
if (root.shouldBeVisible) {
passwordInput.forceActiveFocus()
}
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
focusDelayTimer.start()
}
}
}
}
}
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: {
close()
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)
close()
wifiPasswordInput = ""
passwordInput.text = ""
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
} }
}
Connections {
function onPasswordDialogShouldReopenChanged() {
if (NetworkService.passwordDialogShouldReopen
&& NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID
wifiPasswordInput = ""
open()
NetworkService.passwordDialogShouldReopen = false
}
}
target: NetworkService
}
content: Component {
FocusScope {
id: wifiContent
property alias passwordInput: passwordInput
anchors.fill: parent
focus: true
Keys.onEscapePressed: function (event) {
close()
wifiPasswordInput = ""
event.accepted = 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: {
close()
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
MouseArea {
anchors.fill: parent
onClicked: {
passwordInput.forceActiveFocus()
}
}
DankTextField {
id: passwordInput
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: wifiPasswordInput
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
placeholderText: ""
backgroundColor: "transparent"
focus: true
enabled: root.shouldBeVisible
onTextEdited: {
wifiPasswordInput = text
}
onAccepted: {
NetworkService.connectToWifiWithPassword(
wifiPasswordSSID, passwordInput.text)
close()
wifiPasswordInput = ""
passwordInput.text = ""
}
Component.onCompleted: {
if (root.shouldBeVisible) {
focusDelayTimer.start()
}
}
Timer {
id: focusDelayTimer
interval: 100
repeat: false
onTriggered: {
if (root.shouldBeVisible) {
passwordInput.forceActiveFocus()
}
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
focusDelayTimer.start()
}
}
}
}
}
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: {
close()
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)
close()
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,185 +6,190 @@ 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" return cat !== "Education"
&& cat !== "Science" && cat !== "Science"
}) })
var result = ["All"] var result = ["All"]
return result.concat(allCategories.filter(cat => { return result.concat(allCategories.filter(cat => {
return cat !== "All" return cat !== "All"
})) }))
} }
property var categoryIcons: categories.map(category => { property var categoryIcons: categories.map(category => {
return AppSearchService.getCategoryIcon( return AppSearchService.getCategoryIcon(
category) category)
}) })
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {} property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
property alias model: filteredModel property alias model: filteredModel
property var _watchApplications: AppSearchService.applications 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.getAppsInCategory("All") // HACK: Use function call instead of property apps = AppSearchService.getAppsInCategory(
} else { "All") // HACK: Use function call instead of property
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory) } else {
apps = categoryApps.slice(0, maxResults) var categoryApps = AppSearchService.getAppsInCategory(
} selectedCategory)
} else { apps = categoryApps.slice(0, maxResults)
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 {
apps = [] 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 {
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
return (a.name || "").localeCompare(b.name || "") function selectNext() {
}) if (filteredModel.count > 0) {
keyboardNavigationActive = true
apps.forEach(app => { if (viewMode === "grid") {
if (app) var newIndex = Math.min(selectedIndex + gridColumns,
filteredModel.append({ filteredModel.count - 1)
"name": app.name || "", selectedIndex = newIndex
"exec": app.execString || "", } else {
"icon": app.icon selectedIndex = Math.min(selectedIndex + 1,
|| "application-x-executable", filteredModel.count - 1)
"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 = Math.min(selectedIndex + 1, filteredModel.count - 1)
}
} }
}
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 = Math.max(selectedIndex - 1, 0) selectedIndex = Math.max(selectedIndex - 1, 0)
} }
}
} }
}
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 if (filteredModel.count > 0 && selectedIndex >= 0
&& selectedIndex < filteredModel.count) { && selectedIndex < filteredModel.count) {
var selectedApp = filteredModel.get(selectedIndex) var selectedApp = filteredModel.get(selectedIndex)
launchApp(selectedApp) 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 interval: root.debounceInterval
repeat: false repeat: false
onTriggered: updateFilteredModel() onTriggered: updateFilteredModel()
} }
} }

View File

@@ -4,145 +4,154 @@ 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.cornerRadius
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 {
property var firstRowCategories: categories.slice(0, Math.min(4, categories.length)) visible: compact
width: parent.width
width: parent.width spacing: Theme.spacingS
spacing: Theme.spacingS
Repeater { Repeater {
model: parent.firstRowCategories model: categories.slice(0, Math.min(categories.length,
8)) // Limit for space
Rectangle { Rectangle {
height: 36 height: 36
width: (parent.width - (parent.firstRowCategories.length - 1) * Theme.spacingS) / parent.firstRowCategories.length width: (parent.width - (Math.min(
radius: Theme.cornerRadius categories.length,
color: selectedCategory === modelData ? Theme.primary : "transparent" 8) - 1) * Theme.spacingS) / Math.min(
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba( categories.length, 8)
Theme.outline.r, radius: Theme.cornerRadius
Theme.outline.g, color: selectedCategory === modelData ? Theme.primary : "transparent"
Theme.outline.b, 0.3) border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(
Theme.outline.r,
Theme.outline.g,
Theme.outline.b,
0.3)
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: modelData text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight elide: Text.ElideRight
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
selectedCategory = modelData selectedCategory = modelData
categorySelected(modelData) categorySelected(modelData)
}
}
} }
}
} }
}
} }
Row { Column {
property var secondRowCategories: categories.slice(4, categories.length) visible: !compact
width: parent.width
width: parent.width spacing: Theme.spacingS
spacing: Theme.spacingS
visible: secondRowCategories.length > 0
Repeater { Row {
model: parent.secondRowCategories property var firstRowCategories: categories.slice(
0, Math.min(4,
categories.length))
Rectangle { width: parent.width
height: 36 spacing: Theme.spacingS
width: (parent.width - (parent.secondRowCategories.length - 1) * Theme.spacingS) / parent.secondRowCategories.length
radius: Theme.cornerRadius
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 { Repeater {
anchors.centerIn: parent model: parent.firstRowCategories
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 { Rectangle {
anchors.fill: parent height: 36
hoverEnabled: true width: (parent.width - (parent.firstRowCategories.length - 1)
cursorShape: Qt.PointingHandCursor * Theme.spacingS) / parent.firstRowCategories.length
onClicked: { radius: Theme.cornerRadius
selectedCategory = modelData color: selectedCategory === modelData ? Theme.primary : "transparent"
categorySelected(modelData) 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)
}
}
}
}
}
Row {
property var secondRowCategories: categories.slice(
4, categories.length)
width: parent.width
spacing: Theme.spacingS
visible: secondRowCategories.length > 0
Repeater {
model: parent.secondRowCategories
Rectangle {
height: 36
width: (parent.width - (parent.secondRowCategories.length - 1)
* Theme.spacingS) / parent.secondRowCategories.length
radius: Theme.cornerRadius
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,220 +8,224 @@ 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.lastIpcDevice) BrightnessService.setBrightnessInternal(
pendingValue, BrightnessService.lastIpcDevice)
}
} }
}
function show() { function show() {
root.brightnessPopupVisible = true root.brightnessPopupVisible = true
// Update slider to current device brightness when showing // Update slider to current device brightness when showing
if (BrightnessService.brightnessAvailable) { if (BrightnessService.brightnessAvailable) {
brightnessSlider.value = BrightnessService.brightnessLevel brightnessSlider.value = BrightnessService.brightnessLevel
} }
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() hideTimer.restart()
} }
}
Connections { function resetHideTimer() {
function onBrightnessChanged() { if (root.brightnessPopupVisible)
root.show() hideTimer.restart()
} }
target: BrightnessService screen: modelData
} visible: brightnessPopupVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
Rectangle { anchors {
id: brightnessPopup top: true
left: true
right: true
bottom: true
}
property bool containsMouse: popupMouseArea.containsMouse Timer {
id: hideTimer
width: Math.min(260, Screen.width - Theme.spacingM * 2) interval: 3000
height: brightnessContent.height + Theme.spacingS * 2 repeat: false
anchors.horizontalCenter: parent.horizontalCenter onTriggered: {
anchors.bottom: parent.bottom if (!brightnessPopup.containsMouse)
anchors.bottomMargin: Theme.spacingM root.brightnessPopupVisible = false
color: Theme.popupBackground() else
radius: Theme.cornerRadius hideTimer.restart()
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 { Connections {
id: brightnessContent function onBrightnessChanged() {
root.show()
}
anchors.centerIn: parent target: BrightnessService
width: parent.width - Theme.spacingS * 2 }
spacing: Theme.spacingXS
Item { Rectangle {
property int gap: Theme.spacingS id: brightnessPopup
width: parent.width property bool containsMouse: popupMouseArea.containsMouse
height: 40
Rectangle { width: Math.min(260, Screen.width - Theme.spacingM * 2)
width: Theme.iconSize height: brightnessContent.height + Theme.spacingS * 2
height: Theme.iconSize anchors.horizontalCenter: parent.horizontalCenter
radius: Theme.iconSize / 2 anchors.bottom: parent.bottom
color: "transparent" anchors.bottomMargin: Theme.spacingM
x: parent.gap color: Theme.popupBackground()
anchors.verticalCenter: parent.verticalCenter radius: Theme.cornerRadius
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
name: { width: parent.width - Theme.spacingS * 2
const deviceInfo = BrightnessService.getCurrentDeviceInfo(); spacing: Theme.spacingXS
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") { Item {
return "brightness_medium"; property int gap: Theme.spacingS
} else if (deviceInfo.name.includes("kbd")) {
return "keyboard"; width: parent.width
} else { height: 40
return "lightbulb";
} 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: {
const deviceInfo = BrightnessService.getCurrentDeviceInfo()
if (!deviceInfo || deviceInfo.class === "backlight"
|| deviceInfo.class === "ddc") {
return "brightness_medium"
} else if (deviceInfo.name.includes("kbd")) {
return "keyboard"
} else {
return "lightbulb"
}
}
size: Theme.iconSize
color: Theme.primary
}
}
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,
BrightnessService.lastIpcDevice)
}
}
Connections {
function onBrightnessChanged() {
brightnessSlider.value = BrightnessService.brightnessLevel
}
function onDeviceSwitched() {
brightnessSlider.value = BrightnessService.brightnessLevel
}
target: BrightnessService
}
}
} }
size: Theme.iconSize
color: Theme.primary
}
} }
DankSlider { MouseArea {
id: brightnessSlider id: popupMouseArea
width: parent.width - Theme.iconSize - parent.gap * 3 anchors.fill: parent
height: 40 hoverEnabled: true
x: parent.gap * 2 + Theme.iconSize acceptedButtons: Qt.NoButton
anchors.verticalCenter: parent.verticalCenter propagateComposedEvents: true
minimum: 1 z: -1
maximum: 100 }
enabled: BrightnessService.brightnessAvailable
showValue: true layer.effect: MultiEffect {
unit: "%" shadowEnabled: true
Component.onCompleted: { shadowHorizontalOffset: 0
if (BrightnessService.brightnessAvailable) shadowVerticalOffset: 4
value = BrightnessService.brightnessLevel shadowBlur: 0.8
} shadowColor: Qt.rgba(0, 0, 0, 0.3)
onSliderValueChanged: function (newValue) { shadowOpacity: 0.3
if (BrightnessService.brightnessAvailable) { }
brightnessDebounceTimer.pendingValue = newValue
brightnessDebounceTimer.restart() transform: Translate {
root.resetHideTimer() y: root.brightnessPopupVisible ? 0 : 20
} }
}
onSliderDragFinished: function (finalValue) { Behavior on opacity {
if (BrightnessService.brightnessAvailable) { NumberAnimation {
brightnessDebounceTimer.stop() duration: Theme.mediumDuration
BrightnessService.setBrightnessInternal(finalValue, BrightnessService.lastIpcDevice) easing.type: Theme.emphasizedEasing
} }
} }
Connections { Behavior on scale {
function onBrightnessChanged() { NumberAnimation {
brightnessSlider.value = BrightnessService.brightnessLevel duration: Theme.mediumDuration
} easing.type: Theme.emphasizedEasing
}
function onDeviceSwitched() { }
brightnessSlider.value = BrightnessService.brightnessLevel
} Behavior on transform {
PropertyAnimation {
target: BrightnessService duration: Theme.mediumDuration
} easing.type: Theme.emphasizedEasing
}
} }
}
} }
MouseArea { mask: Region {
id: popupMouseArea item: brightnessPopup
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,267 +6,271 @@ 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(), let firstDay = new Date(displayDate.getFullYear(),
displayDate.getMonth(), 1) displayDate.getMonth(), 1)
let dayOfWeek = firstDay.getDay() let dayOfWeek = firstDay.getDay()
let startDate = new Date(firstDay) let startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - dayOfWeek - 7) // Extra week padding startDate.setDate(startDate.getDate(
let lastDay = new Date(displayDate.getFullYear(), ) - dayOfWeek - 7) // Extra week padding
displayDate.getMonth() + 1, 0) let lastDay = new Date(displayDate.getFullYear(),
let endDate = new Date(lastDay) displayDate.getMonth() + 1, 0)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay( let endDate = new Date(lastDay)
)) + 7) // Extra week padding endDate.setDate(endDate.getDate() + (6 - lastDay.getDay(
CalendarService.loadEvents(startDate, endDate) )) + 7) // Extra week padding
} CalendarService.loadEvents(startDate, endDate)
}
spacing: Theme.spacingM spacing: Theme.spacingM
onDisplayDateChanged: { onDisplayDateChanged: {
loadEventsForMonth() loadEventsForMonth()
} }
Component.onCompleted: { Component.onCompleted: {
loadEventsForMonth()
}
Connections {
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable)
loadEventsForMonth() loadEventsForMonth()
} }
target: CalendarService Connections {
enabled: CalendarService !== null function onKhalAvailableChanged() {
} if (CalendarService && CalendarService.khalAvailable)
loadEventsForMonth()
Row {
width: parent.width
height: 40
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
} }
StyledText { Row {
width: parent.width - 80 width: parent.width
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 {
width: 40
height: 40
radius: Theme.cornerRadius
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_right"
size: Theme.iconSize
color: Theme.primary
}
MouseArea {
id: nextMonthArea
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 {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
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 {
anchors.centerIn: parent width: 40
width: parent.width - 4 height: 40
height: parent.height - 4 radius: Theme.cornerRadius
color: isSelected ? Theme.primary : isToday ? Qt.rgba( color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.r, Theme.primary.g,
Theme.primary.g, Theme.primary.b,
Theme.primary.b, 0.12) : "transparent"
0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
clip: true
StyledText { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
text: dayDate.getDate() name: "chevron_left"
font.pixelSize: Theme.fontSizeMedium size: Theme.iconSize
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4) color: Theme.primary
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 { MouseArea {
GradientStop { id: prevMonthArea
position: 0.89
color: "transparent"
}
GradientStop { anchors.fill: parent
position: 0.9 hoverEnabled: true
color: { cursorShape: Qt.PointingHandCursor
if (isSelected) onClicked: {
return Qt.lighter(Theme.primary, 1.3) let newDate = new Date(displayDate)
else if (isToday) newDate.setMonth(newDate.getMonth() - 1)
return Theme.primary displayDate = newDate
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 { StyledText {
id: dayArea width: parent.width - 80
height: 40
anchors.fill: parent text: Qt.formatDate(displayDate, "MMMM yyyy")
hoverEnabled: true font.pixelSize: Theme.fontSizeLarge
cursorShape: Qt.PointingHandCursor color: Theme.surfaceText
onClicked: { font.weight: Font.Medium
selectedDate = dayDate horizontalAlignment: Text.AlignHCenter
} verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_right"
size: Theme.iconSize
color: Theme.primary
}
MouseArea {
id: nextMonthArea
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 {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
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.cornerRadius
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
}
}
}
} }
}
} }
}
} }

View File

@@ -10,315 +10,317 @@ 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 shouldBeVisible: false property bool shouldBeVisible: false
property real triggerX: (Screen.width - 480) / 2 property real triggerX: (Screen.width - 480) / 2
property real triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + 4 property real triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + 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: calendarVisible || closeTimer.running
screen: triggerScreen
onCalendarVisibleChanged: {
if (calendarVisible) {
closeTimer.stop()
shouldBeVisible = true
visible = true
Qt.callLater(() => {
calendarGrid.loadEventsForMonth()
})
} else {
shouldBeVisible = false
closeTimer.restart()
}
}
Timer {
id: closeTimer
interval: Theme.mediumDuration + 50
onTriggered: {
if (!shouldBeVisible) {
visible = false
}
}
}
onVisibleChanged: {
if (visible && calendarGrid)
calendarGrid.loadEventsForMonth()
}
implicitWidth: 480
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : 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)
} }
function calculateHeight() { visible: calendarVisible || closeTimer.running
let contentHeight = Theme.spacingM * 2 screen: triggerScreen
// margins onCalendarVisibleChanged: {
let widgetHeight = 160 if (calendarVisible) {
widgetHeight += 140 + Theme.spacingM closeTimer.stop()
let calendarHeight = 300 shouldBeVisible = true
let mainRowHeight = Math.max(widgetHeight, calendarHeight) visible = true
contentHeight += mainRowHeight + Theme.spacingM Qt.callLater(() => {
if (CalendarService && CalendarService.khalAvailable) { calendarGrid.loadEventsForMonth()
let hasEvents = events.selectedDateEvents })
&& events.selectedDateEvents.length > 0 } else {
let eventsHeight = hasEvents ? Math.min( shouldBeVisible = false
300, closeTimer.restart()
80 + events.selectedDateEvents.length * 60) : 120 }
contentHeight += eventsHeight
} else {
contentHeight -= Theme.spacingM
}
return Math.min(contentHeight, parent.height * 0.9)
} }
readonly property real calculatedX: { Timer {
var screenWidth = root.screen ? root.screen.width : Screen.width id: closeTimer
if (root.triggerSection === "center") { interval: Theme.mediumDuration + 50
return (screenWidth - targetWidth) / 2 onTriggered: {
} if (!shouldBeVisible) {
visible = false
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
} }
onVisibleChanged: {
width: targetWidth if (visible && calendarGrid)
height: calculateHeight() calendarGrid.loadEventsForMonth()
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
opacity: shouldBeVisible ? 1 : 0
scale: shouldBeVisible ? 1 : 0.9
x: calculatedX
y: root.triggerY
onOpacityChanged: {
if (opacity === 1)
Qt.callLater(() => {
height = calculateHeight()
})
} }
implicitWidth: 480
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
Connections { anchors {
function onEventsByDateChanged() { top: true
if (mainContainer.opacity === 1) left: true
mainContainer.height = mainContainer.calculateHeight() right: true
} bottom: true
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 {
anchors.fill: parent id: mainContainer
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity { readonly property real targetWidth: Math.min(
running: shouldBeVisible (root.screen ? root.screen.width : Screen.width)
loops: Animation.Infinite * 0.9, 600)
NumberAnimation { function calculateWidth() {
to: 0.08 let baseWidth = 320
duration: Theme.extraLongDuration if (leftWidgets.hasAnyWidgets)
easing.type: Theme.standardEasing return Math.min(parent.width * 0.9, 600)
return Math.min(parent.width * 0.7, 400)
} }
NumberAnimation { function calculateHeight() {
to: 0.02 let contentHeight = Theme.spacingM * 2
duration: Theme.extraLongDuration // margins
easing.type: Theme.standardEasing let widgetHeight = 160
widgetHeight += 140 + Theme.spacingM
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)
} }
}
}
Column { readonly property real calculatedX: {
anchors.fill: parent var screenWidth = root.screen ? root.screen.width : Screen.width
anchors.margins: Theme.spacingM if (root.triggerSection === "center") {
spacing: Theme.spacingM return (screenWidth - targetWidth) / 2
focus: true }
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { var centerX = root.triggerX + (root.triggerWidth / 2) - (targetWidth / 2)
calendarVisible = false
event.accepted = true if (centerX >= Theme.spacingM
} else { && centerX + targetWidth <= screenWidth - Theme.spacingM) {
// Don't handle other keys - let them bubble up to modals return centerX
event.accepted = false }
if (centerX < Theme.spacingM) {
return Theme.spacingM
}
if (centerX + targetWidth > screenWidth - Theme.spacingM) {
return screenWidth - targetWidth - Theme.spacingM
}
return centerX
} }
}
Row { width: targetWidth
width: parent.width height: calculateHeight()
height: { color: Theme.surfaceContainer
let widgetHeight = 160 radius: Theme.cornerRadius
// Media widget border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
widgetHeight += 140 + Theme.spacingM // Weather/SystemInfo widget with spacing Theme.outline.b, 0.08)
let calendarHeight = 300 border.width: 1
// Calendar layer.enabled: true
return Math.max(widgetHeight, calendarHeight) opacity: shouldBeVisible ? 1 : 0
scale: shouldBeVisible ? 1 : 0.9
x: calculatedX
y: root.triggerY
onOpacityChanged: {
if (opacity === 1)
Qt.callLater(() => {
height = calculateHeight()
})
} }
spacing: Theme.spacingM
Column { Connections {
id: leftWidgets function onEventsByDateChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight()
}
property bool hasAnyWidgets: true function onKhalAvailableChanged() {
if (mainContainer.opacity === 1)
mainContainer.height = mainContainer.calculateHeight()
}
width: hasAnyWidgets ? parent.width * 0.42 : 0 // Slightly narrower for better proportions target: CalendarService
height: childrenRect.height enabled: CalendarService !== null
spacing: Theme.spacingM }
visible: hasAnyWidgets
anchors.top: parent.top
MediaPlayer { Connections {
width: parent.width function onSelectedDateEventsChanged() {
height: 160 if (mainContainer.opacity === 1)
} mainContainer.height = mainContainer.calculateHeight()
}
Weather { target: events
width: parent.width enabled: events !== null
height: 140
visible: SettingsData.weatherEnabled
}
SystemInfo {
width: parent.width
height: 140
visible: !SettingsData.weatherEnabled
}
} }
Rectangle { Rectangle {
width: leftWidgets.hasAnyWidgets ? parent.width - leftWidgets.width
- Theme.spacingM : parent.width
height: parent.height
radius: Theme.cornerRadius
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
anchors.margins: Theme.spacingS color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g,
} Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: shouldBeVisible
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
}
}
} }
}
Events { Column {
id: events anchors.fill: parent
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
}
}
width: parent.width Row {
selectedDate: calendarGrid.selectedDate 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.cornerRadius
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
}
} }
Behavior on opacity { MouseArea {
NumberAnimation { anchors.fill: parent
duration: Anims.durMed z: -1
easing.type: Easing.BezierSpline enabled: shouldBeVisible
easing.bezierCurve: Anims.emphasized 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
}
} }
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: shouldBeVisible
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,340 +6,348 @@ 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.cornerRadius
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()
} }
function onKhalAvailableChanged() { onSelectedDateEventsChanged: {
updateSelectedDateEvents() eventsList.model = selectedDateEvents
}
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(
300,
80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadius
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()
} }
target: CalendarService Connections {
enabled: CalendarService !== null function onEventsByDateChanged() {
} updateSelectedDateEvents()
}
Row { function onKhalAvailableChanged() {
id: headerRow updateSelectedDateEvents()
}
anchors.top: parent.top target: CalendarService
anchors.left: parent.left enabled: CalendarService !== null
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
} }
StyledText { Row {
text: hasEvents ? (Qt.formatDate( id: headerRow
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.verticalCenter: parent.verticalCenter anchors.margins: Theme.spacingL
anchors.leftMargin: Theme.spacingL + 4 spacing: Theme.spacingS
anchors.rightMargin: Theme.spacingM
spacing: 6 DankIcon {
name: "event"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText { StyledText {
width: parent.width text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • "
text: modelData.title + (selectedDateEvents.length
font.pixelSize: Theme.fontSizeMedium === 1 ? "1 event" : selectedDateEvents.length
color: Theme.surfaceText + " events")) : Qt.formatDate(
font.weight: Font.Medium selectedDate, "MMM d")
elide: Text.ElideRight font.pixelSize: Theme.fontSizeMedium
wrapMode: Text.Wrap color: Theme.surfaceText
maximumLineCount: 2 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
} }
Item { StyledText {
width: parent.width text: "No events"
height: Math.max(timeRow.height, locationRow.height) 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
}
}
Row { DankListView {
id: timeRow id: eventsList
spacing: 4 anchors.top: headerRow.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter 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
DankIcon { // Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
name: "schedule" interactive: true
size: Theme.fontSizeSmall flickDeceleration: 1500
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, maximumFlickVelocity: 2000
Theme.surfaceText.b, 0.7) boundsMovement: Flickable.FollowBoundsBehavior
anchors.verticalCenter: parent.verticalCenter 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
} }
StyledText { Column {
text: { id: eventContent
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 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
} }
}
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 { Item {
id: locationRow width: parent.width
height: Math.max(timeRow.height, locationRow.height)
spacing: 4 Row {
anchors.right: parent.right id: timeRow
anchors.verticalCenter: parent.verticalCenter
visible: modelData.location !== ""
DankIcon { spacing: 4
name: "location_on" anchors.left: parent.left
size: Theme.fontSizeSmall anchors.verticalCenter: parent.verticalCenter
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7) DankIcon {
anchors.verticalCenter: parent.verticalCenter 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)
}
}
}
} }
StyledText { MouseArea {
text: modelData.location id: eventMouseArea
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, anchors.fill: parent
Theme.surfaceText.b, 0.7) hoverEnabled: true
elide: Text.ElideRight cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
anchors.verticalCenter: parent.verticalCenter enabled: modelData.url !== ""
maximumLineCount: 1 onClicked: {
width: Math.min(implicitWidth, 200) if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false)
console.warn("Failed to open URL: " + modelData.url)
}
}
} }
}
}
}
MouseArea { Behavior on color {
id: eventMouseArea ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
anchors.fill: parent Behavior on border.color {
hoverEnabled: true ColorAnimation {
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor duration: Theme.shortDuration
enabled: modelData.url !== "" easing.type: Theme.standardEasing
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 { Behavior on height {
NumberAnimation { NumberAnimation {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
}
} }
}
} }

View File

@@ -8,439 +8,464 @@ 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: activePlayer && activePlayer.positionSupported ? activePlayer.position : 0 property real currentPosition: activePlayer
&& activePlayer.positionSupported ? activePlayer.position : 0
function ratio() { function ratio() {
if (!activePlayer || activePlayer.length <= 0) { if (!activePlayer || activePlayer.length <= 0) {
return 0 return 0
} }
let calculatedRatio = currentPosition / activePlayer.length let calculatedRatio = currentPosition / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio)) return Math.max(0, Math.min(1, calculatedRatio))
}
onActivePlayerChanged: {
if (activePlayer && activePlayer.positionSupported) {
activePlayer.positionChanged()
}
}
width: parent.width
height: parent.height
radius: Theme.cornerRadius
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: positionTimer
interval: 500
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
&& !progressMouseArea.isSeeking
repeat: true
onTriggered: {
if (activePlayer && activePlayer.positionSupported) {
activePlayer.positionChanged()
}
}
}
Timer {
id: cleanupTimer
interval: 2000
running: !activePlayer
onTriggered: {
lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
currentPosition = 0
stop()
}
}
Connections {
function onTrackChanged() {
if (activePlayer && activePlayer.positionSupported) {
activePlayer.positionChanged()
}
} }
target: activePlayer onActivePlayerChanged: {
} if (activePlayer && activePlayer.positionSupported) {
activePlayer.positionChanged()
}
}
width: parent.width
height: parent.height
radius: Theme.cornerRadius
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
Item { Timer {
anchors.fill: parent id: positionTimer
anchors.margins: Theme.spacingS
Column { interval: 500
anchors.centerIn: parent running: activePlayer
spacing: Theme.spacingS && activePlayer.playbackState === MprisPlaybackState.Playing
visible: (!activePlayer && !lastValidTitle) && !progressMouseArea.isSeeking
|| (activePlayer && activePlayer.trackTitle === "" repeat: true
&& lastValidTitle === "") onTriggered: {
if (activePlayer && activePlayer.positionSupported) {
DankIcon { activePlayer.positionChanged()
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 { Timer {
anchors.fill: parent id: cleanupTimer
spacing: Theme.spacingS
visible: activePlayer && activePlayer.trackTitle !== ""
|| lastValidTitle !== ""
Row { interval: 2000
width: parent.width running: !activePlayer
height: 60 onTriggered: {
spacing: Theme.spacingM lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
currentPosition = 0
stop()
}
}
Rectangle { Connections {
width: 60 function onTrackChanged() {
height: 60 if (activePlayer && activePlayer.positionSupported) {
radius: Theme.cornerRadius activePlayer.positionChanged()
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, }
Theme.surfaceVariant.b, 0.3) }
Item { target: activePlayer
anchors.fill: parent }
clip: true
Image { Item {
id: albumArt anchors.fill: parent
anchors.margins: Theme.spacingS
anchors.fill: parent Column {
source: activePlayer && activePlayer.trackArtUrl anchors.centerIn: parent
|| lastValidArtUrl || "" spacing: Theme.spacingS
onSourceChanged: { visible: (!activePlayer && !lastValidTitle)
if (activePlayer && activePlayer.trackArtUrl) || (activePlayer && activePlayer.trackTitle === ""
lastValidArtUrl = activePlayer.trackArtUrl && lastValidTitle === "")
}
fillMode: Image.PreserveAspectCrop DankIcon {
smooth: true name: "music_note"
cache: true size: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
} }
Rectangle { StyledText {
anchors.fill: parent text: "No Media Playing"
visible: albumArt.status !== Image.Ready font.pixelSize: Theme.fontSizeMedium
color: "transparent" color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
DankIcon { anchors.horizontalCenter: parent.horizontalCenter
anchors.centerIn: parent
name: "album"
size: 28
color: Theme.surfaceVariantText
}
} }
}
} }
Column { Column {
width: parent.width - 60 - Theme.spacingM anchors.fill: parent
height: parent.height spacing: Theme.spacingS
spacing: Theme.spacingXS visible: activePlayer && activePlayer.trackTitle !== ""
|| lastValidTitle !== ""
StyledText { Row {
text: activePlayer && activePlayer.trackTitle || lastValidTitle width: parent.width
|| "Unknown Track" height: 60
onTextChanged: { spacing: Theme.spacingM
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 { Rectangle {
text: activePlayer && activePlayer.trackArtist || lastValidArtist width: 60
|| "Unknown Artist" height: 60
onTextChanged: { radius: Theme.cornerRadius
if (activePlayer && activePlayer.trackArtist) color: Qt.rgba(Theme.surfaceVariant.r,
lastValidArtist = activePlayer.trackArtist Theme.surfaceVariant.g,
} Theme.surfaceVariant.b, 0.3)
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 { Item {
text: activePlayer && activePlayer.trackAlbum anchors.fill: parent
|| lastValidAlbum || "" clip: true
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 { Image {
id: progressBarContainer id: albumArt
width: parent.width anchors.fill: parent
height: 24 source: activePlayer && activePlayer.trackArtUrl
|| lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer && activePlayer.trackArtUrl)
lastValidArtUrl = activePlayer.trackArtUrl
}
fillMode: Image.PreserveAspectCrop
smooth: true
cache: true
}
Rectangle { Rectangle {
id: progressBarBackground anchors.fill: parent
visible: albumArt.status !== Image.Ready
color: "transparent"
width: parent.width DankIcon {
height: 6 anchors.centerIn: parent
radius: 3 name: "album"
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, size: 28
Theme.surfaceVariant.b, 0.3) color: Theme.surfaceVariantText
visible: activePlayer !== null }
anchors.verticalCenter: parent.verticalCenter }
}
Rectangle {
id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: Math.max(0, Math.min(parent.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 && activePlayer.canSeek) {
let ratio = Math.max(0, Math.min(
1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
}
}
onReleased: {
isSeeking = false
}
onPositionChanged: function (mouse) {
if (pressed && isSeeking && activePlayer
&& activePlayer.length > 0 && activePlayer.canSeek) {
let ratio = Math.max(0, Math.min(
1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
}
}
onClicked: function (mouse) {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
let ratio = Math.max(0, Math.min(
1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = 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 && activePlayer.canSeek) {
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
}
}
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 (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
} }
}
}
}
Rectangle { Column {
width: 32 width: parent.width - 60 - Theme.spacingM
height: 32 height: parent.height
radius: 16 spacing: Theme.spacingXS
color: Theme.primary
DankIcon { StyledText {
anchors.centerIn: parent text: activePlayer && activePlayer.trackTitle
name: activePlayer && activePlayer.playbackState || lastValidTitle || "Unknown Track"
=== MprisPlaybackState.Playing ? "pause" : "play_arrow" onTextChanged: {
size: 20 if (activePlayer && activePlayer.trackTitle)
color: Theme.background 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
}
}
} }
MouseArea { Item {
anchors.fill: parent id: progressBarContainer
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
}
Rectangle { width: parent.width
width: 28 height: 24
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.12) : "transparent"
DankIcon { Rectangle {
anchors.centerIn: parent id: progressBarBackground
name: "skip_next"
size: 16 width: parent.width
color: Theme.surfaceText 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: Math.max(0, Math.min(parent.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
&& activePlayer.canSeek) {
let ratio = Math.max(
0, Math.min(
1,
mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
}
}
onReleased: {
isSeeking = false
}
onPositionChanged: function (mouse) {
if (pressed && isSeeking && activePlayer
&& activePlayer.length > 0
&& activePlayer.canSeek) {
let ratio = Math.max(
0, Math.min(
1,
mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
}
}
onClicked: function (mouse) {
if (activePlayer && activePlayer.length > 0
&& activePlayer.canSeek) {
let ratio = Math.max(
0, Math.min(
1,
mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = 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
&& activePlayer.canSeek) {
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
}
}
onReleased: {
progressMouseArea.isSeeking = false
}
}
} }
MouseArea { Item {
id: nextBtnArea width: parent.width
height: 32
visible: activePlayer !== null
anchors.fill: parent Row {
hoverEnabled: true anchors.horizontalCenter: parent.horizontalCenter
cursorShape: Qt.PointingHandCursor spacing: Theme.spacingM
onClicked: activePlayer && activePlayer.next() 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 (activePlayer.position > 8
&& activePlayer.canSeek) {
activePlayer.position = 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 { 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

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

View File

@@ -6,225 +6,231 @@ 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.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.4) Theme.surfaceContainer.b, 0.4)
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,
border.width: 1 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
} }
StyledText { Column {
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
DankIcon {
id: refreshButton
name: "refresh"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -Theme.spacingS
anchors.topMargin: -Theme.spacingS
property bool isRefreshing: false
enabled: !isRefreshing
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
onClicked: {
refreshButton.isRefreshing = true
WeatherService.forceRefresh()
refreshTimer.restart()
}
enabled: parent.enabled
}
Timer {
id: refreshTimer
interval: 2000
onTriggered: refreshButton.isRefreshing = false
}
NumberAnimation on rotation {
running: refreshButton.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingL spacing: Theme.spacingS
visible: !WeatherService.weather.available
|| WeatherService.weather.temp === 0
DankIcon { DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode) name: "cloud_off"
size: Theme.iconSize + 8 size: Theme.iconSize + 8
color: Theme.primary color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
anchors.verticalCenter: parent.verticalCenter Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
} }
Column { StyledText {
spacing: Theme.spacingXS text: "No Weather Data"
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 font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7) Theme.surfaceText.b, 0.7)
visible: text.length > 0 anchors.horizontalCenter: parent.horizontalCenter
}
} }
}
} }
Grid { Column {
columns: 2 anchors.fill: parent
spacing: Theme.spacingM anchors.margins: Theme.spacingL
anchors.horizontalCenter: parent.horizontalCenter spacing: Theme.spacingS
visible: WeatherService.weather.available
&& WeatherService.weather.temp !== 0
Row { Item {
spacing: Theme.spacingXS width: parent.width
height: 60
DankIcon { DankIcon {
name: "humidity_low" id: refreshButton
size: Theme.fontSizeSmall name: "refresh"
color: Theme.surfaceText size: Theme.iconSize - 6
anchors.verticalCenter: parent.verticalCenter color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -Theme.spacingS
anchors.topMargin: -Theme.spacingS
property bool isRefreshing: false
enabled: !isRefreshing
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
onClicked: {
refreshButton.isRefreshing = true
WeatherService.forceRefresh()
refreshTimer.restart()
}
enabled: parent.enabled
}
Timer {
id: refreshTimer
interval: 2000
onTriggered: refreshButton.isRefreshing = false
}
NumberAnimation on rotation {
running: refreshButton.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
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
}
}
}
} }
StyledText { Grid {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--" columns: 2
font.pixelSize: Theme.fontSizeSmall spacing: Theme.spacingM
color: Theme.surfaceText anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
Row {
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
}
}
} }
}
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
}
}
} }
}
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,140 +9,142 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName( property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(
AudioService.sink) : "" 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
height: 35 spacing: Theme.spacingM
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
Row { StyledText {
anchors.left: parent.left text: "Output Device"
anchors.leftMargin: Theme.spacingM font.pixelSize: Theme.fontSizeLarge
anchors.verticalCenter: parent.verticalCenter color: Theme.surfaceText
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: 50 height: 35
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: deviceArea.containsMouse ? Qt.rgba( color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
Theme.primary.r, Theme.primary.g, border.color: 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)) Theme.primary.b, 0.3)
border.color: modelData === AudioService.sink ? Theme.primary : "transparent" border.width: 1
border.width: 1 visible: AudioService.sink !== null
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.spacingS
DankIcon { DankIcon {
name: { name: "check_circle"
if (modelData.name.includes("bluez")) size: Theme.iconSize - 4
return "headset" color: Theme.primary
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 { StyledText {
id: deviceArea text: "Current: " + (root.currentSinkDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
anchors.fill: parent color: Theme.primary
hoverEnabled: true font.weight: Font.Medium
cursorShape: Qt.PointingHandCursor }
onClicked: { }
if (modelData) }
Pipewire.preferredDefaultAudioSink = modelData
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 {
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,139 +9,141 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property string currentSourceDisplayName: AudioService.source ? AudioService.displayName( property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(
AudioService.source) : "" 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
height: 35 spacing: Theme.spacingM
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
Row { StyledText {
anchors.left: parent.left text: "Input Device"
anchors.leftMargin: Theme.spacingM font.pixelSize: Theme.fontSizeLarge
anchors.verticalCenter: parent.verticalCenter color: Theme.surfaceText
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: 50 height: 35
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: sourceArea.containsMouse ? Qt.rgba( color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
Theme.primary.r, Theme.primary.g, border.color: 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)) Theme.primary.b, 0.3)
border.color: modelData === AudioService.source ? Theme.primary : "transparent" border.width: 1
border.width: 1 visible: AudioService.source !== null
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.spacingS
DankIcon { DankIcon {
name: { name: "check_circle"
if (modelData.name.includes("bluez")) size: Theme.iconSize - 4
return "headset_mic" color: Theme.primary
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 { StyledText {
id: sourceArea text: "Current: " + (root.currentSourceDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
anchors.fill: parent color: Theme.primary
hoverEnabled: true font.weight: Font.Medium
cursorShape: Qt.PointingHandCursor }
onClicked: { }
if (modelData) }
Pipewire.preferredDefaultAudioSource = modelData
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 {
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,222 +8,232 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property real micLevel: Math.min( property real micLevel: Math.min(100,
100, (AudioService.source
(AudioService.source && AudioService.source.audio && AudioService.source.audio
&& AudioService.source.audio.volume * 100) || 0) && AudioService.source.audio.volume * 100)
property bool micMuted: (AudioService.source && AudioService.source.audio || 0)
&& AudioService.source.audio.muted) || false 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
DankIcon { StyledText {
name: root.micMuted ? "mic_off" : "mic" text: "Microphone Level"
size: Theme.iconSize font.pixelSize: Theme.fontSizeLarge
color: root.micMuted ? Theme.error : Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter font.weight: Font.Medium
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (AudioService.source && AudioService.source.audio)
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
} }
Item { Row {
id: micSliderContainer
width: parent.width - 80
height: 32
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: micSliderTrack
width: parent.width width: parent.width
height: 8 spacing: Theme.spacingM
radius: 4
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
Rectangle { DankIcon {
id: micSliderFill name: root.micMuted ? "mic_off" : "mic"
size: Theme.iconSize
color: root.micMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: parent.width * (root.micLevel / 100) MouseArea {
height: parent.height anchors.fill: parent
radius: parent.radius hoverEnabled: true
color: Theme.primary cursorShape: Qt.PointingHandCursor
onClicked: {
Behavior on width { if (AudioService.source && AudioService.source.audio)
NumberAnimation { AudioService.source.audio.muted = !AudioService.source.audio.muted
duration: Anims.durShort }
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
} }
}
} }
Rectangle { Item {
id: micHandle id: micSliderContainer
width: 18 width: parent.width - 80
height: 18 height: 32
radius: 9 anchors.verticalCenter: parent.verticalCenter
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: micTooltip id: micSliderTrack
width: tooltipText.contentWidth + Theme.spacingS * 2 width: parent.width
height: tooltipText.contentHeight + Theme.spacingXS * 2 height: 8
radius: Theme.cornerRadius radius: 4
color: Theme.surfaceContainer color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
border.color: Theme.outline Theme.surfaceVariant.b, 0.3)
border.width: 1 anchors.verticalCenter: parent.verticalCenter
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: (micMouseArea.containsMouse && !root.micMuted)
|| micMouseArea.isDragging
opacity: visible ? 1 : 0
StyledText { Rectangle {
id: tooltipText id: micSliderFill
text: Math.round(root.micLevel) + "%" width: parent.width * (root.micLevel / 100)
font.pixelSize: Theme.fontSizeSmall height: parent.height
color: Theme.surfaceText radius: parent.radius
font.weight: Font.Medium color: Theme.primary
anchors.centerIn: parent
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.cornerRadius
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
}
}
}
} }
Behavior on opacity { MouseArea {
NumberAnimation { id: micMouseArea
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on scale { property bool isDragging: false
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
}
MouseArea { anchors.fill: parent
id: micMouseArea hoverEnabled: true
cursorShape: Qt.PointingHandCursor
property bool isDragging: false preventStealing: true
onPressed: mouse => {
anchors.fill: parent isDragging = true
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( let ratio = Math.max(
0, Math.min(1, mouse.x / micSliderTrack.width)) 0, Math.min(1,
let newMicLevel = Math.max( mouse.x / micSliderTrack.width))
0, Math.min(100, Math.round(ratio * 100))) let newMicLevel = Math.round(ratio * 100)
if (AudioService.source if (AudioService.source
&& AudioService.source.audio) { && AudioService.source.audio) {
AudioService.source.audio.muted = false AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100 AudioService.source.audio.volume = newMicLevel / 100
} }
}
} }
onClicked: mouse => { onReleased: {
let ratio = Math.max(0, Math.min( isDragging = false
1, mouse.x / micSliderTrack.width)) }
let newMicLevel = Math.round(ratio * 100) onPositionChanged: mouse => {
if (AudioService.source && AudioService.source.audio) { if (pressed && isDragging) {
AudioService.source.audio.muted = false let ratio = Math.max(
AudioService.source.audio.volume = newMicLevel / 100 0, Math.min(
} 1,
} mouse.x / micSliderTrack.width))
} let newMicLevel = Math.max(
0, Math.min(100, Math.round(
MouseArea { ratio * 100)))
id: micGlobalMouseArea if (AudioService.source
&& AudioService.source.audio) {
x: 0 AudioService.source.audio.muted = false
y: 0 AudioService.source.audio.volume = newMicLevel / 100
width: root.parent ? root.parent.width : 0 }
height: root.parent ? root.parent.height : 0 }
enabled: micMouseArea.isDragging }
visible: false onClicked: mouse => {
preventStealing: true
onPositionChanged: mouse => {
if (micMouseArea.isDragging) {
let globalPos = mapToItem(micSliderTrack,
mouse.x, mouse.y)
let ratio = Math.max( let ratio = Math.max(
0, 0, Math.min(1,
Math.min(1, mouse.x / micSliderTrack.width))
globalPos.x / micSliderTrack.width)) let newMicLevel = Math.round(ratio * 100)
let newMicLevel = Math.max(
0, Math.min(100, Math.round(ratio * 100)))
if (AudioService.source if (AudioService.source
&& AudioService.source.audio) { && AudioService.source.audio) {
AudioService.source.audio.muted = false AudioService.source.audio.muted = false
AudioService.source.audio.volume = newMicLevel / 100 AudioService.source.audio.volume = newMicLevel / 100
} }
}
} }
onReleased: { }
micMouseArea.isDragging = false
}
}
}
DankIcon { MouseArea {
name: "mic" id: micGlobalMouseArea
size: Theme.iconSize
color: Theme.surfaceText x: 0
anchors.verticalCenter: parent.verticalCenter 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,63 +4,66 @@ import qs.Services
import qs.Widgets import qs.Widgets
Column { Column {
id: root id: root
property real volumeLevel: Math.min( property real volumeLevel: Math.min(
100, 100,
(AudioService.sink && AudioService.sink.audio (AudioService.sink && AudioService.sink.audio
&& AudioService.sink.audio.volume * 100) || 0) && AudioService.sink.audio.volume * 100)
property bool volumeMuted: (AudioService.sink && AudioService.sink.audio || 0)
&& AudioService.sink.audio.muted) || false 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
minimum: 0 spacing: Theme.spacingM
maximum: 100
leftIcon: root.volumeMuted ? "volume_off" : "volume_down"
rightIcon: "volume_up"
enabled: !root.volumeMuted
showValue: true
unit: "%"
Connections { StyledText {
target: AudioService.sink text: "Volume"
&& AudioService.sink.audio ? AudioService.sink.audio : null font.pixelSize: Theme.fontSizeLarge
function onVolumeChanged() { color: Theme.surfaceText
volumeSlider.value = Math.round(AudioService.sink.audio.volume * 100) font.weight: Font.Medium
}
} }
Component.onCompleted: { DankSlider {
if (AudioService.sink && AudioService.sink.audio) { id: volumeSlider
value = Math.round(AudioService.sink.audio.volume * 100)
}
let leftIconItem = volumeSlider.children[0].children[0] width: parent.width
if (leftIconItem) { minimum: 0
let mouseArea = Qt.createQmlObject( maximum: 100
'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; } }', leftIcon: root.volumeMuted ? "volume_off" : "volume_down"
leftIconItem, "dynamicMouseArea") rightIcon: "volume_up"
} 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,119 +10,119 @@ 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
} }
}
// Single Loader that switches between Output and Input
Loader {
width: parent.width
height: parent.height - 48
asynchronous: true
sourceComponent: audioTab.audioSubTab === 0 ? outputTabComponent : inputTabComponent
}
}
// Output Tab Component
Component {
id: outputTabComponent
DankFlickable {
clip: true
contentHeight: outputColumn.height
contentWidth: width
Column {
id: outputColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: volumeComponent
} }
// Single Loader that switches between Output and Input
Loader { Loader {
width: parent.width width: parent.width
sourceComponent: outputDevicesComponent height: parent.height - 48
asynchronous: true
sourceComponent: audioTab.audioSubTab === 0 ? outputTabComponent : inputTabComponent
} }
}
} }
}
// Input Tab Component // Output Tab Component
Component { Component {
id: inputTabComponent id: outputTabComponent
DankFlickable { DankFlickable {
clip: true clip: true
contentHeight: inputColumn.height contentHeight: outputColumn.height
contentWidth: width contentWidth: width
Column { Column {
id: inputColumn id: outputColumn
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
Loader { Loader {
width: parent.width width: parent.width
sourceComponent: microphoneComponent sourceComponent: volumeComponent
}
Loader {
width: parent.width
sourceComponent: outputDevicesComponent
}
}
} }
}
Loader { // Input Tab Component
width: parent.width Component {
sourceComponent: inputDevicesComponent id: inputTabComponent
DankFlickable {
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 // Volume Control Component
Component { Component {
id: volumeComponent id: volumeComponent
VolumeControl { VolumeControl {
width: parent.width width: parent.width
}
} }
}
// Microphone Control Component // Microphone Control Component
Component { Component {
id: microphoneComponent id: microphoneComponent
MicrophoneControl { MicrophoneControl {
width: parent.width width: parent.width
}
} }
}
// Output Devices Component // Output Devices Component
Component { Component {
id: outputDevicesComponent id: outputDevicesComponent
AudioDevicesList { AudioDevicesList {
width: parent.width width: parent.width
}
} }
}
// Input Devices Component // Input Devices Component
Component { Component {
id: inputDevicesComponent id: inputDevicesComponent
AudioInputDevicesList { AudioInputDevicesList {
width: parent.width width: parent.width
}
} }
}
} }

View File

@@ -9,427 +9,444 @@ 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
StyledText {
text: "Available Devices"
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
}
}
}
}
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.cornerRadius
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 { Row {
anchors.horizontalCenter: parent.horizontalCenter width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { StyledText {
name: "sync" text: "Available Devices"
size: Theme.iconSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.primary color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
} }
}
StyledText { Item {
text: "Scanning for devices..." width: parent.width - scanButton.width - parent.spacing
font.pixelSize: Theme.fontSizeLarge - 150 // Spacer to push button right
color: Theme.surfaceText height: 1
font.weight: Font.Medium }
anchors.verticalCenter: parent.verticalCenter
} 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
}
}
}
}
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.cornerRadius
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 {
text: "Make sure your device is in pairing mode"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
} }
StyledText { StyledText {
text: "Make sure your device is in pairing mode" text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7) Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter visible: {
} if (!BluetoothService.adapter || !Bluetooth.devices)
} return true
StyledText { var availableCount = Bluetooth.devices.values.filter(dev => {
text: "No devices found. Put your device in pairing mode and click Start Scanning." return dev
font.pixelSize: Theme.fontSizeMedium && !dev.paired
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, && !dev.pairing
Theme.surfaceText.b, 0.7) && !dev.blocked
visible: { && (dev.signalStrength
if (!BluetoothService.adapter || !Bluetooth.devices) === undefined
return true || dev.signalStrength > 0)
}).length
var availableCount = Bluetooth.devices.values.filter(dev => { return availableCount === 0 && !BluetoothService.adapter.discovering
return dev }
&& !dev.paired wrapMode: Text.WordWrap
&& !dev.pairing width: parent.width
&& !dev.blocked horizontalAlignment: Text.AlignHCenter
&& (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,199 +9,203 @@ 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.cornerRadius radius: Theme.cornerRadius
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,
border.width: 1 Theme.outline.b, 0.08)
z: 1000 border.width: 1
opacity: menuVisible ? 1 : 0 z: 1000
scale: menuVisible ? 1 : 0.85 opacity: menuVisible ? 1 : 0
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 {
width: parent.width anchors.fill: parent
height: 32 anchors.topMargin: 4
radius: Theme.cornerRadius anchors.leftMargin: 2
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, anchors.rightMargin: -2
Theme.primary.g, anchors.bottomMargin: -4
Theme.primary.b, radius: parent.radius
0.12) : "transparent" color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Row { Column {
anchors.left: parent.left id: menuColumn
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
hoverEnabled: true anchors.margins: Theme.spacingS
cursorShape: Qt.PointingHandCursor spacing: 1
onClicked: {
if (root.deviceData) {
if (root.deviceData.connected)
root.deviceData.disconnect()
else
BluetoothService.connectDeviceWithTrust(root.deviceData)
}
root.hide()
}
}
Behavior on color { Rectangle {
ColorAnimation { width: parent.width
duration: Theme.shortDuration height: 32
easing.type: Theme.standardEasing radius: Theme.cornerRadius
color: connectArea.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: 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 {
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: 32
radius: Theme.cornerRadius
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
}
}
} }
}
} }
Rectangle { Behavior on opacity {
width: parent.width - Theme.spacingS * 2 NumberAnimation {
height: 5 duration: Theme.mediumDuration
anchors.horizontalCenter: parent.horizontalCenter easing.type: Theme.emphasizedEasing
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 { Behavior on scale {
width: parent.width NumberAnimation {
height: 32 duration: Theme.mediumDuration
radius: Theme.cornerRadius easing.type: Theme.emphasizedEasing
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,64 +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( color: bluetoothToggle.containsMouse ? Qt.rgba(
Theme.primary.r, Theme.primary.g, Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12) : (BluetoothService.adapter 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)) && 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 border.color: BluetoothService.adapter
&& BluetoothService.adapter.enabled ? Theme.primary : "transparent" && BluetoothService.adapter.enabled ? Theme.primary : "transparent"
border.width: 2 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 { DankIcon {
name: "bluetooth" name: "bluetooth"
size: Theme.iconSizeLarge size: Theme.iconSizeLarge
color: BluetoothService.adapter color: BluetoothService.adapter
&& BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter 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)
}
}
} }
Column { MouseArea {
spacing: 2 id: bluetoothToggle
anchors.verticalCenter: parent.verticalCenter
StyledText { anchors.fill: parent
text: "Bluetooth" hoverEnabled: true
font.pixelSize: Theme.fontSizeLarge cursorShape: Qt.PointingHandCursor
color: BluetoothService.adapter onClicked: {
&& BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText if (BluetoothService.adapter)
font.weight: Font.Medium BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
} }
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,173 +9,182 @@ 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 model: BluetoothService.adapter
&& BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter( && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter(
dev => { dev => {
return dev return dev
&& (dev.paired && (dev.paired
|| dev.trusted) || 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( color: btDeviceArea.containsMouse ? Qt.rgba(
Theme.primary.r, Theme.primary.g, Theme.primary.r,
Theme.primary.b, Theme.primary.g,
0.08) : (modelData.connected ? Qt.rgba( Theme.primary.b,
Theme.primary.r, 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))
Theme.primary.g, border.color: modelData.connected ? Theme.primary : "transparent"
Theme.primary.b, border.width: 1
0.12) : Qt.rgba(
Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Row {
0.08)) anchors.left: parent.left
border.color: modelData.connected ? Theme.primary : "transparent" anchors.leftMargin: Theme.spacingM
border.width: 1 anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Row {
anchors.left: parent.left DankIcon {
anchors.leftMargin: Theme.spacingM name: BluetoothService.getDeviceIcon(modelData)
anchors.verticalCenter: parent.verticalCenter size: Theme.iconSize
spacing: Theme.spacingM color: modelData.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
DankIcon { }
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize Column {
color: modelData.connected ? Theme.primary : Theme.surfaceText spacing: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
}
StyledText {
Column { text: modelData.name || modelData.deviceName
spacing: 2 font.pixelSize: Theme.fontSizeMedium
anchors.verticalCenter: parent.verticalCenter color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
StyledText { }
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium Row {
color: modelData.connected ? Theme.primary : Theme.surfaceText spacing: Theme.spacingXS
font.weight: modelData.connected ? Font.Medium : Font.Normal
} StyledText {
text: BluetoothDeviceState.toString(modelData.state)
Row { font.pixelSize: Theme.fontSizeSmall
spacing: Theme.spacingXS color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
StyledText { Theme.surfaceText.b, 0.7)
text: BluetoothDeviceState.toString(modelData.state) }
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, StyledText {
Theme.surfaceText.b, 0.7) text: {
} if (modelData.batteryAvailable
&& modelData.battery > 0)
StyledText { return "• " + Math.round(
text: { modelData.battery * 100) + "%"
if (modelData.batteryAvailable && modelData.battery > 0)
return "• " + Math.round(modelData.battery * 100) + "%" var btBattery = BatteryService.bluetoothDevices.find(
dev => {
var btBattery = BatteryService.bluetoothDevices.find(dev => { return dev.name === (modelData.name
return dev.name === (modelData.name || modelData.deviceName) || dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || (modelData.name || modelData.deviceName).toLowerCase( || modelData.deviceName)
).includes( || dev.name.toLowerCase(
dev.name.toLowerCase( ).includes(
)) (modelData.name
}) || modelData.deviceName).toLowerCase(
return btBattery ? "• " + btBattery.percentage + "%" : "" ))
} || (modelData.name
font.pixelSize: Theme.fontSizeSmall || modelData.deviceName).toLowerCase(
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, ).includes(
Theme.surfaceText.b, 0.7) dev.name.toLowerCase())
visible: text.length > 0 })
} return btBattery ? "• " + btBattery.percentage + "%" : ""
} }
} font.pixelSize: Theme.fontSizeSmall
} color: Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Rectangle { Theme.surfaceText.b, 0.7)
id: btMenuButton visible: text.length > 0
}
width: 32 }
height: 32 }
radius: Theme.cornerRadius }
color: btMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g, Rectangle {
Theme.surfaceText.b, id: btMenuButton
0.08) : "transparent"
anchors.right: parent.right width: 32
anchors.rightMargin: Theme.spacingM height: 32
anchors.verticalCenter: parent.verticalCenter radius: Theme.cornerRadius
color: btMenuButtonArea.containsMouse ? Qt.rgba(
DankIcon { Theme.surfaceText.r,
name: "more_vert" Theme.surfaceText.g,
size: Theme.iconSize Theme.surfaceText.b,
color: Theme.surfaceText 0.08) : "transparent"
opacity: 0.6 anchors.right: parent.right
anchors.centerIn: parent anchors.rightMargin: Theme.spacingM
} anchors.verticalCenter: parent.verticalCenter
MouseArea { DankIcon {
id: btMenuButtonArea name: "more_vert"
size: Theme.iconSize
anchors.fill: parent color: Theme.surfaceText
hoverEnabled: true opacity: 0.6
cursorShape: Qt.PointingHandCursor anchors.centerIn: parent
onClicked: { }
var contextMenu = root.findBluetoothContextMenu()
if (contextMenu) { MouseArea {
contextMenu.deviceData = modelData id: btMenuButtonArea
let localPos = btMenuButtonArea.mapToItem(
contextMenu.parentItem, btMenuButtonArea.width / 2, anchors.fill: parent
btMenuButtonArea.height) hoverEnabled: true
contextMenu.show(localPos.x, localPos.y) cursorShape: Qt.PointingHandCursor
} onClicked: {
} var contextMenu = root.findBluetoothContextMenu()
} if (contextMenu) {
contextMenu.deviceData = modelData
Behavior on color { let localPos = btMenuButtonArea.mapToItem(
ColorAnimation { contextMenu.parentItem,
duration: Theme.shortDuration btMenuButtonArea.width / 2,
} btMenuButtonArea.height)
} contextMenu.show(localPos.x, localPos.y)
} }
}
MouseArea { }
id: btDeviceArea
Behavior on color {
anchors.fill: parent ColorAnimation {
anchors.rightMargin: 40 duration: Theme.shortDuration
hoverEnabled: true }
enabled: !BluetoothService.isDeviceBusy(modelData) }
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor }
onClicked: {
if (modelData.connected) MouseArea {
modelData.disconnect() id: btDeviceArea
else
BluetoothService.connectDeviceWithTrust(modelData) 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,79 +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 {
x: bluetoothContextMenuWindow.x anchors.fill: parent
y: bluetoothContextMenuWindow.y visible: bluetoothContextMenuWindow.visible
width: bluetoothContextMenuWindow.width onClicked: {
height: bluetoothContextMenuWindow.height bluetoothContextMenuWindow.hide()
onClicked: { }
} MouseArea {
} x: bluetoothContextMenuWindow.x
} y: bluetoothContextMenuWindow.y
width: bluetoothContextMenuWindow.width
height: bluetoothContextMenuWindow.height
onClicked: {
Component { }
id: toggleComponent }
BluetoothToggle {
width: parent.width
} }
}
Component { Component {
id: pairedComponent id: toggleComponent
PairedDevicesList { BluetoothToggle {
width: parent.width width: parent.width
}
} }
}
Component { Component {
id: availableComponent id: pairedComponent
AvailableDevicesList { PairedDevicesList {
width: parent.width width: parent.width
}
}
Component {
id: availableComponent
AvailableDevicesList {
width: parent.width
}
} }
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -34,12 +34,9 @@ Item {
width: parent.width width: parent.width
sourceComponent: settingsComponent sourceComponent: settingsComponent
} }
} }
} }
Component { Component {
id: brightnessComponent id: brightnessComponent
@@ -62,57 +59,62 @@ Item {
visible: BrightnessService.devices.length > 1 visible: BrightnessService.devices.length > 1
text: "Device" text: "Device"
description: { description: {
const deviceInfo = BrightnessService.getCurrentDeviceInfo(); const deviceInfo = BrightnessService.getCurrentDeviceInfo()
if (deviceInfo && deviceInfo.class === "ddc") { if (deviceInfo && deviceInfo.class === "ddc") {
return "DDC changes can be slow and unreliable"; return "DDC changes can be slow and unreliable"
} }
return ""; return ""
} }
currentValue: BrightnessService.currentDevice currentValue: BrightnessService.currentDevice
options: BrightnessService.devices.map(function(d) { options: BrightnessService.devices.map(function (d) {
return d.name; return d.name
}) })
optionIcons: BrightnessService.devices.map(function(d) { optionIcons: BrightnessService.devices.map(function (d) {
if (d.class === "backlight") if (d.class === "backlight")
return "desktop_windows"; return "desktop_windows"
if (d.class === "ddc") if (d.class === "ddc")
return "tv"; return "tv"
if (d.name.includes("kbd")) if (d.name.includes("kbd"))
return "keyboard"; return "keyboard"
return "lightbulb"; return "lightbulb"
}) })
onValueChanged: function(value) { onValueChanged: function (value) {
BrightnessService.setCurrentDevice(value, true); BrightnessService.setCurrentDevice(value, true)
} }
Connections { Connections {
target: BrightnessService target: BrightnessService
function onDevicesChanged() { function onDevicesChanged() {
if (BrightnessService.currentDevice) { if (BrightnessService.currentDevice) {
deviceDropdown.currentValue = BrightnessService.currentDevice; deviceDropdown.currentValue = BrightnessService.currentDevice
} }
// Check if saved device is now available // Check if saved device is now available
const lastDevice = SessionData.lastBrightnessDevice || ""; const lastDevice = SessionData.lastBrightnessDevice
|| ""
if (lastDevice) { if (lastDevice) {
const deviceExists = BrightnessService.devices.some(d => d.name === lastDevice); const deviceExists = BrightnessService.devices.some(
if (deviceExists && (!BrightnessService.currentDevice || BrightnessService.currentDevice !== lastDevice)) { d => d.name === lastDevice)
BrightnessService.setCurrentDevice(lastDevice, false); if (deviceExists
&& (!BrightnessService.currentDevice
|| BrightnessService.currentDevice !== lastDevice)) {
BrightnessService.setCurrentDevice(lastDevice,
false)
} }
} }
} }
function onDeviceSwitched() { function onDeviceSwitched() {
// Force update the description when device switches // Force update the description when device switches
deviceDropdown.description = Qt.binding(function() { deviceDropdown.description = Qt.binding(function () {
const deviceInfo = BrightnessService.getCurrentDeviceInfo(); const deviceInfo = BrightnessService.getCurrentDeviceInfo()
if (deviceInfo && deviceInfo.class === "ddc") { if (deviceInfo && deviceInfo.class === "ddc") {
return "DDC changes can be slow and unreliable"; return "DDC changes can be slow and unreliable"
} }
return ""; return ""
}); })
} }
} }
} }
@@ -123,31 +125,31 @@ Item {
value: BrightnessService.brightnessLevel value: BrightnessService.brightnessLevel
leftIcon: "brightness_low" leftIcon: "brightness_low"
rightIcon: "brightness_high" rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable && BrightnessService.isCurrentDeviceReady() enabled: BrightnessService.brightnessAvailable
&& BrightnessService.isCurrentDeviceReady()
opacity: BrightnessService.isCurrentDeviceReady() ? 1.0 : 0.5 opacity: BrightnessService.isCurrentDeviceReady() ? 1.0 : 0.5
onSliderValueChanged: function(newValue) { onSliderValueChanged: function (newValue) {
brightnessDebounceTimer.pendingValue = newValue; brightnessDebounceTimer.pendingValue = newValue
brightnessDebounceTimer.restart(); brightnessDebounceTimer.restart()
} }
onSliderDragFinished: function(finalValue) { onSliderDragFinished: function (finalValue) {
brightnessDebounceTimer.stop(); brightnessDebounceTimer.stop()
BrightnessService.setBrightnessInternal(finalValue, BrightnessService.currentDevice); BrightnessService.setBrightnessInternal(
finalValue, BrightnessService.currentDevice)
} }
Connections { Connections {
target: BrightnessService target: BrightnessService
function onBrightnessChanged() { function onBrightnessChanged() {
brightnessSlider.value = BrightnessService.brightnessLevel; brightnessSlider.value = BrightnessService.brightnessLevel
} }
function onDeviceSwitched() { function onDeviceSwitched() {
brightnessSlider.value = BrightnessService.brightnessLevel; brightnessSlider.value = BrightnessService.brightnessLevel
} }
} }
} }
} }
} }
Component { Component {
@@ -172,7 +174,11 @@ Item {
width: (parent.width - Theme.spacingM) / 2 width: (parent.width - Theme.spacingM) / 2
height: 80 height: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: BrightnessService.nightModeActive ? 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)) color: BrightnessService.nightModeActive ? 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: BrightnessService.nightModeActive ? Theme.primary : "transparent" border.color: BrightnessService.nightModeActive ? Theme.primary : "transparent"
border.width: BrightnessService.nightModeActive ? 1 : 0 border.width: BrightnessService.nightModeActive ? 1 : 0
@@ -194,7 +200,6 @@ Item {
font.weight: Font.Medium font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
MouseArea { MouseArea {
@@ -204,17 +209,20 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
BrightnessService.toggleNightMode(); BrightnessService.toggleNightMode()
} }
} }
} }
Rectangle { Rectangle {
width: (parent.width - Theme.spacingM) / 2 width: (parent.width - Theme.spacingM) / 2
height: 80 height: 80
radius: Theme.cornerRadius 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)) 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.color: Theme.isLightMode ? Theme.primary : "transparent"
border.width: Theme.isLightMode ? 1 : 0 border.width: Theme.isLightMode ? 1 : 0
@@ -236,7 +244,6 @@ Item {
font.weight: Font.Medium font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
MouseArea { MouseArea {
@@ -246,7 +253,7 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Theme.toggleLightMode(); Theme.toggleLightMode()
} }
} }
@@ -255,15 +262,10 @@ Item {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
} }
} }
} }
brightnessDebounceTimer: Timer { brightnessDebounceTimer: Timer {
@@ -271,13 +273,13 @@ Item {
interval: { interval: {
// Use longer interval for DDC devices since ddcutil is slow // Use longer interval for DDC devices since ddcutil is slow
const deviceInfo = BrightnessService.getCurrentDeviceInfo(); const deviceInfo = BrightnessService.getCurrentDeviceInfo()
return (deviceInfo && deviceInfo.class === "ddc") ? 100 : 50; return (deviceInfo && deviceInfo.class === "ddc") ? 100 : 50
} }
repeat: false repeat: false
onTriggered: { onTriggered: {
BrightnessService.setBrightnessInternal(pendingValue, BrightnessService.currentDevice); BrightnessService.setBrightnessInternal(
pendingValue, BrightnessService.currentDevice)
} }
} }
} }

View File

@@ -8,117 +8,120 @@ 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 if (ethernetPreferenceArea.containsMouse
&& NetworkService.wifiEnabled && NetworkService.ethernetConnected
&& NetworkService.networkStatus !== "ethernet") && NetworkService.wifiEnabled
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, && NetworkService.networkStatus !== "ethernet")
Theme.surfaceContainer.b, 0.8) return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.8)
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5) Theme.surfaceContainer.b, 0.5)
} }
border.color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Qt.rgba( border.color: NetworkService.networkStatus
Theme.outline.r, === "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r,
Theme.outline.g, Theme.outline.g,
Theme.outline.b, Theme.outline.b,
0.12) 0.12)
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1 border.width: NetworkService.networkStatus === "ethernet" ? 2 : 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
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon {
name: "lan"
size: Theme.iconSize
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: "Ethernet"
font.pixelSize: Theme.fontSizeMedium
color: NetworkService.networkStatus
=== "ethernet" ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
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)
elide: Text.ElideRight
}
}
}
DankIcon { DankIcon {
name: "lan" id: ethernetLoadingSpinner
size: Theme.iconSize
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText name: "refresh"
anchors.verticalCenter: parent.verticalCenter size: Theme.iconSize - 4
color: Theme.primary
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference
&& NetworkService.targetPreference === "ethernet"
z: 10
RotationAnimation {
target: ethernetLoadingSpinner
property: "rotation"
running: ethernetLoadingSpinner.visible
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
} }
Column { MouseArea {
anchors.verticalCenter: parent.verticalCenter id: ethernetPreferenceArea
spacing: 2
StyledText { anchors.fill: parent
text: "Ethernet" hoverEnabled: true
font.pixelSize: Theme.fontSizeMedium cursorShape: (NetworkService.ethernetConnected
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText && NetworkService.wifiEnabled
font.weight: Font.Medium && NetworkService.networkStatus
elide: Text.ElideRight !== "ethernet") ? Qt.PointingHandCursor : Qt.ArrowCursor
} enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "ethernet"
&& !NetworkService.changingNetworkPreference
onClicked: {
if (NetworkService.ethernetConnected
&& NetworkService.wifiEnabled) {
StyledText { if (NetworkService.networkStatus !== "ethernet")
text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP NetworkService.setNetworkPreference("ethernet")
|| "Connected") : "Disconnected" else
font.pixelSize: Theme.fontSizeSmall NetworkService.setNetworkPreference("auto")
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, }
Theme.surfaceText.b, 0.7) }
elide: Text.ElideRight
}
} }
}
DankIcon { Behavior on color {
id: ethernetLoadingSpinner ColorAnimation {
duration: Theme.shortDuration
name: "refresh" easing.type: Theme.standardEasing
size: Theme.iconSize - 4 }
color: Theme.primary
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference
&& NetworkService.targetPreference === "ethernet"
z: 10
RotationAnimation {
target: ethernetLoadingSpinner
property: "rotation"
running: ethernetLoadingSpinner.visible
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
} }
}
MouseArea {
id: ethernetPreferenceArea
anchors.fill: parent
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,159 +8,163 @@ import qs.Services
import qs.Widgets import qs.Widgets
Rectangle { Rectangle {
id: wifiCard id: wifiCard
property var refreshTimer property var refreshTimer
width: parent.width width: parent.width
height: 80 height: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected
&& NetworkService.wifiEnabled && NetworkService.wifiEnabled
&& NetworkService.networkStatus !== "wifi") && NetworkService.networkStatus !== "wifi")
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.8) Theme.surfaceContainer.b, 0.8)
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5) Theme.surfaceContainer.b, 0.5)
} }
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba( border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(
Theme.outline.r, Theme.outline.r,
Theme.outline.g, Theme.outline.g,
Theme.outline.b, Theme.outline.b,
0.12) 0.12)
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1 border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
visible: NetworkService.wifiAvailable visible: NetworkService.wifiAvailable
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
anchors.right: wifiToggle.left anchors.right: wifiToggle.left
anchors.rightMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon {
name: NetworkService.wifiSignalIcon
size: Theme.iconSize
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
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
elide: Text.ElideRight
}
StyledText {
text: {
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)
elide: Text.ElideRight
}
}
}
DankIcon { DankIcon {
name: NetworkService.wifiSignalIcon id: wifiLoadingSpinner
size: Theme.iconSize
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column { name: "refresh"
anchors.verticalCenter: parent.verticalCenter size: Theme.iconSize - 4
spacing: 2 color: Theme.primary
anchors.right: wifiToggle.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.changingPreference
&& NetworkService.targetPreference === "wifi"
z: 10
StyledText { RotationAnimation {
text: { target: wifiLoadingSpinner
if (!NetworkService.wifiEnabled) property: "rotation"
return "WiFi is off" running: wifiLoadingSpinner.visible
else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID) from: 0
return NetworkService.currentWifiSSID || "Connected" to: 360
else duration: 1000
return "Not Connected" loops: Animation.Infinite
} }
font.pixelSize: Theme.fontSizeMedium }
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText { DankToggle {
text: { id: wifiToggle
if (!NetworkService.wifiEnabled)
return "Turn on WiFi to see networks" checked: NetworkService.wifiEnabled
else if (NetworkService.wifiEnabled && NetworkService.currentWifiSSID) enabled: true
return NetworkService.wifiIP || "Connected" toggling: NetworkService.wifiToggling
else anchors.right: parent.right
return "Select a network below" anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
onClicked: {
if (NetworkService.wifiEnabled) {
NetworkService.currentWifiSSID = ""
NetworkService.wifiSignalStrength = 100
NetworkService.wifiNetworks = []
NetworkService.savedWifiNetworks = []
NetworkService.connectionStatus = ""
NetworkService.connectingSSID = ""
NetworkService.isScanning = false
NetworkService.refreshNetworkStatus()
}
NetworkService.toggleWifiRadio()
if (refreshTimer)
refreshTimer.triggered = true
} }
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
}
} }
}
DankIcon { MouseArea {
id: wifiLoadingSpinner id: wifiPreferenceArea
name: "refresh" anchors.fill: parent
size: Theme.iconSize - 4 anchors.rightMargin: 60 // Exclude toggle area
color: Theme.primary hoverEnabled: true
anchors.right: wifiToggle.left cursorShape: (NetworkService.ethernetConnected
anchors.rightMargin: Theme.spacingS && NetworkService.wifiEnabled
anchors.verticalCenter: parent.verticalCenter && NetworkService.networkStatus
visible: NetworkService.changingPreference !== "wifi") ? Qt.PointingHandCursor : Qt.ArrowCursor
&& NetworkService.targetPreference === "wifi" enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
z: 10 && NetworkService.networkStatus !== "wifi"
&& !NetworkService.changingNetworkPreference
onClicked: {
if (NetworkService.ethernetConnected
&& NetworkService.wifiEnabled) {
RotationAnimation { if (NetworkService.networkStatus !== "wifi")
target: wifiLoadingSpinner NetworkService.setNetworkPreference("wifi")
property: "rotation" else
running: wifiLoadingSpinner.visible NetworkService.setNetworkPreference("auto")
from: 0 }
to: 360 }
duration: 1000
loops: Animation.Infinite
} }
}
DankToggle { Behavior on color {
id: wifiToggle ColorAnimation {
duration: Theme.shortDuration
checked: NetworkService.wifiEnabled easing.type: Theme.standardEasing
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 = 100
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,289 +8,293 @@ 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( finalX = Math.max(
Theme.spacingS, Theme.spacingS, Math.min(
Math.min(finalX, finalX,
parentItem.width - menuWidth - Theme.spacingS)) parentItem.width - menuWidth - Theme.spacingS))
finalY = Math.max( finalY = Math.max(
Theme.spacingS, Theme.spacingS, Math.min(
Math.min(finalY, finalY,
parentItem.height - menuHeight - Theme.spacingS)) parentItem.height - menuHeight - Theme.spacingS))
if (finalY + menuHeight > parentItem.height - Theme.spacingS) { if (finalY + menuHeight > parentItem.height - Theme.spacingS) {
finalY = y - menuHeight - 4 finalY = y - menuHeight - 4
finalY = Math.max(Theme.spacingS, finalY) finalY = Math.max(Theme.spacingS, finalY)
} }
wifiContextMenuWindow.x = finalX wifiContextMenuWindow.x = finalX
wifiContextMenuWindow.y = finalY wifiContextMenuWindow.y = finalY
wifiContextMenuWindow.menuVisible = true wifiContextMenuWindow.menuVisible = true
}) })
} }
function hide() { function hide() {
wifiContextMenuWindow.menuVisible = false wifiContextMenuWindow.menuVisible = false
Qt.callLater(() => { Qt.callLater(() => {
wifiContextMenuWindow.visible = false wifiContextMenuWindow.visible = false
}) })
} }
visible: false visible: false
width: 160 width: 160
height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2 height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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,
border.width: 1 Theme.outline.b, 0.08)
z: 1000 border.width: 1
opacity: menuVisible ? 1 : 0 z: 1000
scale: menuVisible ? 1 : 0.85 opacity: menuVisible ? 1 : 0
Component.onCompleted: { scale: menuVisible ? 1 : 0.85
menuVisible = false Component.onCompleted: {
visible = false 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 { Rectangle {
width: parent.width anchors.fill: parent
height: 32 anchors.topMargin: 4
radius: Theme.cornerRadius anchors.leftMargin: 2
color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r, anchors.rightMargin: -2
Theme.primary.g, anchors.bottomMargin: -4
Theme.primary.b, radius: parent.radius
0.12) : "transparent" color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Row { Column {
anchors.left: parent.left id: wifiMenuColumn
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 anchors.fill: parent
hoverEnabled: true anchors.margins: Theme.spacingS
cursorShape: Qt.PointingHandCursor spacing: 1
onClicked: {
if (wifiContextMenuWindow.networkData) { Rectangle {
if (wifiContextMenuWindow.networkData.connected) { width: parent.width
NetworkService.disconnectWifi() height: 32
} else { radius: Theme.cornerRadius
if (wifiContextMenuWindow.networkData.saved) { color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r,
NetworkService.connectToWifi( Theme.primary.g,
wifiContextMenuWindow.networkData.ssid) Theme.primary.b,
} else if (wifiContextMenuWindow.networkData.secured) { 0.12) : "transparent"
if (wifiPasswordModalRef) {
wifiPasswordModalRef.show(wifiContextMenuWindow.networkData.ssid) 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
} }
} else {
NetworkService.connectToWifi(
wifiContextMenuWindow.networkData.ssid)
}
} }
}
wifiContextMenuWindow.hide()
}
}
Behavior on color { MouseArea {
ColorAnimation { id: connectWifiArea
duration: Theme.shortDuration
easing.type: Theme.standardEasing 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.show(
wifiContextMenuWindow.networkData.ssid)
}
} else {
NetworkService.connectToWifi(
wifiContextMenuWindow.networkData.ssid)
}
}
}
wifiContextMenuWindow.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
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: 32
radius: Theme.cornerRadius
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.cornerRadius
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
}
}
} }
}
} }
Rectangle { Behavior on opacity {
width: parent.width - Theme.spacingS * 2 NumberAnimation {
height: 5 duration: Theme.mediumDuration
anchors.horizontalCenter: parent.horizontalCenter easing.type: Theme.emphasizedEasing
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 { Behavior on scale {
width: parent.width NumberAnimation {
height: 32 duration: Theme.mediumDuration
radius: Theme.cornerRadius easing.type: Theme.emphasizedEasing
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.cornerRadius
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 {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
} }

View File

@@ -8,299 +8,303 @@ 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
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
StyledText { Row {
text: "Available Networks" width: parent.width
font.pixelSize: Theme.fontSizeMedium spacing: Theme.spacingS
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { StyledText {
width: parent.width - 170 text: "Available Networks"
height: 1 font.pixelSize: Theme.fontSizeMedium
} color: Theme.surfaceText
font.weight: Font.Medium
Rectangle { anchors.verticalCenter: parent.verticalCenter
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
} }
Behavior on rotation { Item {
RotationAnimation { width: parent.width - 170
duration: 200 height: 1
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: spanningNetworksColumn.width width: 28
height: 38 height: 28
radius: Theme.cornerRadius radius: 14
color: networkArea2.containsMouse ? Qt.rgba( color: refreshAreaSpan.containsMouse ? Qt.rgba(
Theme.primary.r, Theme.primary.r,
Theme.primary.g, Theme.primary.g,
Theme.primary.b, Theme.primary.b,
0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" 0.12) : NetworkService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "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: signalIcon2 id: refreshIconSpan
anchors.left: parent.left anchors.centerIn: parent
anchors.verticalCenter: parent.verticalCenter name: "refresh"
name: NetworkService.wifiSignalIcon size: Theme.iconSize - 6
size: Theme.iconSize - 2 color: refreshAreaSpan.containsMouse ? Theme.primary : Theme.surfaceText
color: modelData.connected ? 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
}
}
} }
Column { MouseArea {
anchors.left: signalIcon2.right id: refreshAreaSpan
anchors.leftMargin: Theme.spacingXS
anchors.right: rightIcons2.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText { anchors.fill: parent
width: parent.width hoverEnabled: true
text: modelData.ssid cursorShape: Qt.PointingHandCursor
font.pixelSize: Theme.fontSizeSmall onClicked: {
color: modelData.connected ? Theme.primary : Theme.surfaceText if (!NetworkService.isScanning) {
font.weight: modelData.connected ? Font.Medium : Font.Normal refreshIconSpan.rotation += 30
elide: Text.ElideRight NetworkService.scanWifi()
} }
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.show(modelData.ssid)
}
} else {
NetworkService.connectToWifi(modelData.ssid)
}
}
}
} }
}
} }
ScrollBar.vertical: ScrollBar { Flickable {
policy: ScrollBar.AsNeeded 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 {
width: spanningNetworksColumn.width
height: 38
radius: Theme.cornerRadius
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: NetworkService.wifiSignalIcon
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.show(modelData.ssid)
}
} else {
NetworkService.connectToWifi(modelData.ssid)
}
}
}
}
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
} }
}
} }

View File

@@ -9,251 +9,256 @@ import qs.Widgets
import qs.Modules.ControlCenter.Network import qs.Modules.ControlCenter.Network
Item { Item {
id: networkTab id: networkTab
property var wifiPasswordModalRef: { property var wifiPasswordModalRef: {
wifiPasswordModalLoader.active = true wifiPasswordModalLoader.active = true
return wifiPasswordModalLoader.item return wifiPasswordModalLoader.item
} }
property var networkInfoModalRef: { property var networkInfoModalRef: {
networkInfoModalLoader.active = true networkInfoModalLoader.active = true
return networkInfoModalLoader.item return networkInfoModalLoader.item
}
property var sortedWifiNetworks: {
if (!NetworkService.wifiAvailable || !NetworkService.wifiEnabled) {
return []
} }
var allNetworks = NetworkService.wifiNetworks property var sortedWifiNetworks: {
var savedNetworks = NetworkService.savedWifiNetworks if (!NetworkService.wifiAvailable || !NetworkService.wifiEnabled) {
var currentSSID = NetworkService.currentWifiSSID return []
var signalStrength = NetworkService.wifiSignalStrengthStr }
var refreshTrigger = forceRefresh
// Force recalculation var allNetworks = NetworkService.wifiNetworks
var networks = [...allNetworks] var savedNetworks = NetworkService.savedWifiNetworks
var currentSSID = NetworkService.currentWifiSSID
var signalStrength = NetworkService.wifiSignalStrengthStr
var refreshTrigger = forceRefresh
networks.forEach(function (network) { // Force recalculation
network.connected = (network.ssid === currentSSID) var networks = [...allNetworks]
network.saved = savedNetworks.some(function (saved) {
return saved.ssid === network.ssid
})
if (network.connected && signalStrength) {
network.signalStrength = signalStrength
}
})
networks.sort(function (a, b) { networks.forEach(function (network) {
if (a.connected && !b.connected) network.connected = (network.ssid === currentSSID)
return -1 network.saved = savedNetworks.some(function (saved) {
if (!a.connected && b.connected) return saved.ssid === network.ssid
return 1 })
return b.signal - a.signal if (network.connected && signalStrength) {
}) network.signalStrength = signalStrength
}
})
return networks networks.sort(function (a, b) {
} if (a.connected && !b.connected)
return -1
if (!a.connected && b.connected)
return 1
return b.signal - a.signal
})
property int forceRefresh: 0 return networks
Connections {
target: NetworkService
function onNetworksUpdated() {
forceRefresh++
} }
}
Component.onCompleted: { property int forceRefresh: 0
NetworkService.addRef()
if (NetworkService.wifiEnabled)
NetworkService.scanWifi()
}
Component.onDestruction: { Connections {
NetworkService.removeRef() target: NetworkService
} function onNetworksUpdated() {
forceRefresh++
}
}
Row { Component.onCompleted: {
anchors.fill: parent NetworkService.addRef()
spacing: Theme.spacingM if (NetworkService.wifiEnabled)
NetworkService.scanWifi()
}
Column { Component.onDestruction: {
width: (parent.width - Theme.spacingM) / 2 NetworkService.removeRef()
height: parent.height }
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Flickable { Row {
width: parent.width anchors.fill: parent
height: parent.height - 30 spacing: Theme.spacingM
clip: true
contentWidth: width
contentHeight: wifiContent.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 Column {
WheelHandler { width: (parent.width - Theme.spacingM) / 2
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad height: parent.height
onWheel: event => { spacing: Theme.spacingS
let delta = event.pixelDelta.y anchors.verticalCenter: parent.verticalCenter
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
let newY = parent.contentY - delta Flickable {
newY = Math.max( width: parent.width
0, Math.min(parent.contentHeight - parent.height, newY)) height: parent.height - 30
parent.contentY = newY clip: true
event.accepted = true contentWidth: width
} contentHeight: wifiContent.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: wifiContent
width: parent.width
spacing: Theme.spacingM
WiFiCard {}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
} }
Column { Column {
id: wifiContent width: (parent.width - Theme.spacingM) / 2
width: parent.width height: parent.height
spacing: Theme.spacingM spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
WiFiCard {} Flickable {
} width: parent.width
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
ScrollBar.vertical: ScrollBar { // Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
policy: ScrollBar.AsNeeded 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
}
}
} }
}
} }
Column { Rectangle {
width: (parent.width - Theme.spacingM) / 2 anchors.top: parent.top
height: parent.height anchors.topMargin: 100
spacing: Theme.spacingS anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter 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 {
id: ethernetContent anchors.centerIn: parent
width: parent.width spacing: Theme.spacingM
spacing: Theme.spacingM
EthernetCard {} DankIcon {
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)
}
} }
}
ScrollBar.vertical: ScrollBar { WiFiNetworksList {
policy: ScrollBar.AsNeeded wifiContextMenuWindow: wifiContextMenuWindow
sortedWifiNetworks: networkTab.sortedWifiNetworks
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
}
Connections {
target: NetworkService
function onWifiEnabledChanged() {
if (NetworkService.wifiEnabled && visible) {
// Trigger a scan when WiFi is enabled
NetworkService.scanWifi()
}
} }
}
} }
}
Rectangle { onVisibleChanged: {
anchors.top: parent.top if (visible && NetworkService.wifiEnabled
anchors.topMargin: 100 && NetworkService.wifiNetworks.length === 0) {
anchors.left: parent.left // Scan when tab becomes visible if we don't have networks cached
anchors.right: parent.right NetworkService.scanWifi()
anchors.bottom: parent.bottom }
color: "transparent"
visible: !NetworkService.wifiEnabled
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
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 { WiFiContextMenu {
wifiContextMenuWindow: wifiContextMenuWindow id: wifiContextMenuWindow
sortedWifiNetworks: networkTab.sortedWifiNetworks parentItem: networkTab
wifiPasswordModalRef: networkTab.wifiPasswordModalRef wifiPasswordModalRef: networkTab.wifiPasswordModalRef
} networkInfoModalRef: networkTab.networkInfoModalRef
Connections {
target: NetworkService
function onWifiEnabledChanged() {
if (NetworkService.wifiEnabled && visible) {
// Trigger a scan when WiFi is enabled
NetworkService.scanWifi()
}
}
}
onVisibleChanged: {
if (visible && NetworkService.wifiEnabled && NetworkService.wifiNetworks.length === 0) {
// Scan when tab becomes visible if we don't have networks cached
NetworkService.scanWifi()
}
}
WiFiContextMenu {
id: wifiContextMenuWindow
parentItem: networkTab
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
networkInfoModalRef: networkTab.networkInfoModalRef
}
MouseArea {
anchors.fill: parent
visible: wifiContextMenuWindow.visible
onClicked: {
wifiContextMenuWindow.hide()
} }
MouseArea { MouseArea {
x: wifiContextMenuWindow.x anchors.fill: parent
y: wifiContextMenuWindow.y visible: wifiContextMenuWindow.visible
width: wifiContextMenuWindow.width onClicked: {
height: wifiContextMenuWindow.height wifiContextMenuWindow.hide()
onClicked: { }
} MouseArea {
x: wifiContextMenuWindow.x
y: wifiContextMenuWindow.y
width: wifiContextMenuWindow.width
height: wifiContextMenuWindow.height
onClicked: {
}
}
} }
}
} }

View File

@@ -8,292 +8,305 @@ import qs.Common
import qs.Widgets import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
property bool powerMenuVisible: false property bool powerMenuVisible: false
signal powerActionRequested(string action, string title, string message) signal powerActionRequested(string action, string title, string message)
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.cornerRadius
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
anchors.fill: parent onClicked: {
onClicked: {
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: "Power Options"
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 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
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.powerActionRequested("logout", "Log Out", "Are you sure you want to log out?")
}
}
}
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.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend the system?")
}
}
}
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.powerActionRequested("reboot", "Reboot", "Are you sure you want to reboot the system?")
}
}
}
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.powerActionRequested("poweroff", "Power Off", "Are you sure you want to power off the system?")
}
}
}
}
} }
Behavior on opacity { Rectangle {
NumberAnimation { width: Math.min(320, parent.width - Theme.spacingL * 2)
duration: Theme.mediumDuration height: 320 // Fixed height to prevent cropping
easing.type: Theme.emphasizedEasing x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
} y: Theme.barHeight + Theme.spacingXS
} color: Theme.popupBackground()
radius: Theme.cornerRadius
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
Behavior on scale { MouseArea {
NumberAnimation {
duration: Theme.mediumDuration anchors.fill: parent
easing.type: Theme.emphasizedEasing onClicked: {
}
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: "Power Options"
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 {
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
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.powerActionRequested(
"logout", "Log Out",
"Are you sure you want to log out?")
}
}
}
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.powerActionRequested(
"suspend", "Suspend",
"Are you sure you want to suspend the system?")
}
}
}
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.powerActionRequested(
"reboot", "Reboot",
"Are you sure you want to reboot the system?")
}
}
}
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.powerActionRequested(
"poweroff", "Power Off",
"Are you sure you want to power off the system?")
}
}
}
}
}
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,194 +8,196 @@ 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 property bool contextMenuOpen: (contextMenu && contextMenu.visible
&& contextMenu.screen === modelData) && contextMenu.screen === modelData)
|| (windowsMenu && windowsMenu.visible || (windowsMenu && windowsMenu.visible
&& windowsMenu.screen === modelData) && windowsMenu.screen === modelData)
property bool windowIsFullscreen: { property bool windowIsFullscreen: {
if (!NiriService.focusedWindowId || !NiriService.niriAvailable) if (!NiriService.focusedWindowId || !NiriService.niriAvailable)
return false return false
var focusedWindow = NiriService.windows.find( var focusedWindow = NiriService.windows.find(
w => w.id === NiriService.focusedWindowId) w => w.id === NiriService.focusedWindowId)
if (!focusedWindow) if (!focusedWindow)
return false return false
var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"] var fullscreenApps = ["vlc", "mpv", "kodi", "steam", "lutris", "wine", "dosbox"]
return fullscreenApps.some(app => focusedWindow.app_id return fullscreenApps.some(app => focusedWindow.app_id
&& focusedWindow.app_id.toLowerCase( && focusedWindow.app_id.toLowerCase(
).includes(app)) ).includes(app))
}
property bool reveal: (!autoHide || dockMouseArea.containsMouse
|| dockApps.requestDockShow || contextMenuOpen)
&& !windowIsFullscreen
Connections {
target: SettingsData
function onDockTransparencyChanged() {
dock.backgroundTransparency = SettingsData.dockTransparency
} }
} property bool reveal: (!autoHide || dockMouseArea.containsMouse
|| dockApps.requestDockShow || contextMenuOpen)
&& !windowIsFullscreen
screen: modelData Connections {
visible: SettingsData.showDock target: SettingsData
color: "transparent" function onDockTransparencyChanged() {
dock.backgroundTransparency = SettingsData.dockTransparency
}
}
anchors { screen: modelData
bottom: true visible: SettingsData.showDock
left: true color: "transparent"
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: parent.bottom bottom: true
horizontalCenter: parent.horizontalCenter left: true
} right: true
implicitWidth: dock.reveal ? dockBackground.width + 32 : (dockBackground.width + 32)
hoverEnabled: true
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
} }
Item { margins {
id: dockContainer left: 0
anchors.fill: parent right: 0
}
transform: Translate { implicitHeight: 100
id: dockSlide exclusiveZone: autoHide ? -1 : 65 - 16
y: dock.reveal ? 0 : 60
Behavior on y { mask: Region {
NumberAnimation { item: dockMouseArea
duration: 200 }
easing.type: Easing.OutCubic
}
}
}
Rectangle { MouseArea {
id: dockBackground id: dockMouseArea
objectName: "dockBackground" height: dock.reveal ? 65 : 12
anchors { anchors {
top: parent.top bottom: parent.bottom
bottom: parent.bottom horizontalCenter: parent.horizontalCenter
horizontalCenter: parent.horizontalCenter
} }
implicitWidth: dock.reveal ? dockBackground.width + 32 : (dockBackground.width + 32)
hoverEnabled: true
width: dockApps.implicitWidth + 12 Behavior on height {
height: parent.height - 8 NumberAnimation {
duration: 200
anchors.topMargin: 4 easing.type: Easing.OutCubic
anchors.bottomMargin: 1
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, backgroundTransparency)
radius: Theme.cornerRadius
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 Item {
for (var i = 0; i < repeater.count; i++) { id: dockContainer
var item = repeater.itemAt(i) anchors.fill: parent
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton transform: Translate {
id: dockSlide
y: dock.reveal ? 0 : 60
Behavior on y {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
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.cornerRadius
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
}
} }
}
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

@@ -26,33 +26,34 @@ Item {
property bool showTooltip: mouseArea.containsMouse && !dragging property bool showTooltip: mouseArea.containsMouse && !dragging
property string tooltipText: { property string tooltipText: {
if (!appData) if (!appData)
return ""; return ""
// For window type, show app name + window title // For window type, show app name + window title
if (appData.type === "window" && showWindowTitle) { if (appData.type === "window" && showWindowTitle) {
var desktopEntry = DesktopEntries.byId(appData.appId); var desktopEntry = DesktopEntries.byId(appData.appId)
var appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId; var appName = desktopEntry
return appName + (windowTitle ? " • " + windowTitle : ""); && desktopEntry.name ? desktopEntry.name : appData.appId
return appName + (windowTitle ? " • " + windowTitle : "")
} }
// For pinned apps, just show app name // For pinned apps, just show app name
if (!appData.appId) if (!appData.appId)
return ""; return ""
var desktopEntry = DesktopEntries.byId(appData.appId); var desktopEntry = DesktopEntries.byId(appData.appId)
return desktopEntry && desktopEntry.name ? desktopEntry.name : appData.appId; return desktopEntry
&& desktopEntry.name ? desktopEntry.name : appData.appId
} }
width: 40 width: 40
height: 40 height: 40
onIsHoveredChanged: { onIsHoveredChanged: {
if (isHovered) { if (isHovered) {
exitAnimation.stop(); exitAnimation.stop()
if (!bounceAnimation.running) if (!bounceAnimation.running)
bounceAnimation.restart(); bounceAnimation.restart()
} else { } else {
bounceAnimation.stop(); bounceAnimation.stop()
exitAnimation.restart(); exitAnimation.restart()
} }
} }
@@ -78,7 +79,6 @@ Item {
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel easing.bezierCurve: Anims.emphasizedDecel
} }
} }
NumberAnimation { NumberAnimation {
@@ -110,8 +110,7 @@ Item {
repeat: false repeat: false
onTriggered: { onTriggered: {
if (appData && appData.isPinned) if (appData && appData.isPinned)
longPressing = true; longPressing = true
} }
} }
@@ -123,97 +122,116 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (mouse) => { onPressed: mouse => {
if (mouse.button === Qt.LeftButton && appData && appData.isPinned) { if (mouse.button === Qt.LeftButton && appData
dragStartPos = Qt.point(mouse.x, mouse.y); && appData.isPinned) {
longPressTimer.start(); dragStartPos = Qt.point(mouse.x, mouse.y)
} longPressTimer.start()
} }
onReleased: (mouse) => { }
longPressTimer.stop(); onReleased: mouse => {
if (longPressing) { longPressTimer.stop()
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps) if (longPressing) {
dockApps.movePinnedApp(originalIndex, targetIndex); if (dragging && targetIndex >= 0
&& targetIndex !== originalIndex && dockApps)
dockApps.movePinnedApp(originalIndex, targetIndex)
longPressing = false; longPressing = false
dragging = false; dragging = false
dragOffset = Qt.point(0, 0); dragOffset = Qt.point(0, 0)
targetIndex = -1; targetIndex = -1
originalIndex = -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) {
targetIndex = newTargetIndex;
dragStartPos = Qt.point(mouse.x, mouse.y);
} }
} onPositionChanged: mouse => {
} if (longPressing && !dragging) {
} var distance = Math.sqrt(
onClicked: (mouse) => { Math.pow(mouse.x - dragStartPos.x,
if (!appData || longPressing) 2) + Math.pow(
return ; 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) {
targetIndex = newTargetIndex
dragStartPos = Qt.point(mouse.x,
mouse.y)
}
}
}
}
onClicked: mouse => {
if (!appData || longPressing)
return
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
// Handle based on type // Handle based on type
if (appData.type === "pinned") { if (appData.type === "pinned") {
// Launch the pinned app // Launch the pinned app
if (appData && appData.appId) { if (appData && appData.appId) {
var desktopEntry = DesktopEntries.byId(appData.appId); var desktopEntry = DesktopEntries.byId(
if (desktopEntry) appData.appId)
AppUsageHistoryData.addAppUsage({ if (desktopEntry)
"id": appData.appId, AppUsageHistoryData.addAppUsage({
"name": desktopEntry.name || appData.appId, "id": appData.appId,
"icon": desktopEntry.icon || "", "name": desktopEntry.name
"exec": desktopEntry.exec || "", || appData.appId,
"comment": desktopEntry.comment || "" "icon": desktopEntry.icon
}); || "",
"exec": desktopEntry.exec
|| "",
"comment": desktopEntry.comment || ""
})
Quickshell.execDetached(["gtk-launch", appData.appId]); Quickshell.execDetached(
} ["gtk-launch", appData.appId])
} else if (appData.type === "window") { }
// Focus the specific window } else if (appData.type === "window") {
if (appData.windowId) // Focus the specific window
NiriService.focusWindow(appData.windowId); if (appData.windowId)
NiriService.focusWindow(appData.windowId)
}
} 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(
} else if (mouse.button === Qt.MiddleButton) { ["gtk-launch", appData.appId])
if (appData && appData.appId) { }
var desktopEntry = DesktopEntries.byId(appData.appId); } else if (mouse.button === Qt.RightButton) {
if (desktopEntry) if (contextMenu)
AppUsageHistoryData.addAppUsage({ contextMenu.showForButton(root, appData, 40)
"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);
}
}
} }
IconImage { IconImage {
@@ -224,14 +242,16 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
source: { source: {
if (!appData || !appData.appId) if (!appData || !appData.appId)
return ""; return ""
var desktopEntry = DesktopEntries.byId(appData.appId); var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.icon) { if (desktopEntry && desktopEntry.icon) {
var iconPath = Quickshell.iconPath(desktopEntry.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme); var iconPath = Quickshell.iconPath(
return iconPath; desktopEntry.icon, SettingsData.iconTheme
=== "System Default" ? "" : SettingsData.iconTheme)
return iconPath
} }
return ""; return ""
} }
smooth: true smooth: true
mipmap: true mipmap: true
@@ -254,19 +274,18 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
text: { text: {
if (!appData || !appData.appId) if (!appData || !appData.appId)
return "?"; return "?"
var desktopEntry = DesktopEntries.byId(appData.appId); var desktopEntry = DesktopEntries.byId(appData.appId)
if (desktopEntry && desktopEntry.name) if (desktopEntry && desktopEntry.name)
return desktopEntry.name.charAt(0).toUpperCase(); return desktopEntry.name.charAt(0).toUpperCase()
return appData.appId.charAt(0).toUpperCase(); return appData.appId.charAt(0).toUpperCase()
} }
font.pixelSize: 14 font.pixelSize: 14
color: Theme.primary color: Theme.primary
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
// Indicator for running/focused state // Indicator for running/focused state
@@ -280,17 +299,18 @@ Item {
visible: appData && (appData.isRunning || appData.type === "window") visible: appData && (appData.isRunning || appData.type === "window")
color: { color: {
if (!appData) if (!appData)
return "transparent"; return "transparent"
// For window type, check if focused // For window type, check if focused
if (appData.type === "window" && appData.isFocused) if (appData.type === "window" && appData.isFocused)
return Theme.primary; return Theme.primary
// For running apps, show dimmer indicator // For running apps, show dimmer indicator
if (appData.isRunning || appData.type === "window") if (appData.isRunning || appData.type === "window")
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6); return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.6)
return "transparent"; return "transparent"
} }
} }
@@ -299,5 +319,4 @@ Item {
y: 0 y: 0
} }
} }

View File

@@ -6,166 +6,175 @@ 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) if (fromIndex === toIndex)
return return
var currentPinned = [...(SessionData.pinnedApps || [])] var currentPinned = [...(SessionData.pinnedApps || [])]
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0
|| toIndex >= currentPinned.length) || toIndex >= currentPinned.length)
return 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 pinnedApps = [...(SessionData.pinnedApps || [])]
// First section: Pinned apps (always visible, not representing running windows)
pinnedApps.forEach(appId => {
items.push({
"type": "pinned",
"appId": appId,
"windowId": -1, // Use -1 instead of null to avoid ListModel warnings
"windowTitle": "",
"workspaceId": -1, // Use -1 instead of null
"isPinned": true,
"isRunning": false,
"isFocused": false
})
})
root.pinnedAppCount = pinnedApps.length
// Add separator between pinned and running if both exist
if (pinnedApps.length > 0 && NiriService.windows.length > 0) {
items.push({
"type": "separator",
"appId": "__SEPARATOR__",
"windowId": -1, // Use -1 instead of null
"windowTitle": "",
"workspaceId": -1, // Use -1 instead of null
"isPinned": false,
"isRunning": false,
"isFocused": false
})
}
// Second section: Running windows (sorted by display->workspace->position)
// NiriService.windows is already sorted by sortWindowsByLayout
NiriService.windows.forEach(window => {
// Limit window title length for tooltip
var title = window.title || "(Unnamed)"
if (title.length > 50) {
title = title.substring(0, 47) + "..."
}
// Check if this window is focused - compare as numbers
var isFocused = window.id == NiriService.focusedWindowId
items.push({
"type": "window",
"appId": window.app_id || "",
"windowId": window.id || -1,
"windowTitle": title,
"workspaceId": window.workspace_id || -1,
"isPinned": false,
"isRunning": true,
"isFocused": isFocused
})
})
items.forEach(item => {
append(item)
})
}
}
delegate: Item {
id: delegateItem
property alias dockButton: button
width: model.type === "separator" ? 16 : 40
height: 40 height: 40
Rectangle { Repeater {
visible: model.type === "separator" id: repeater
width: 2 model: ListModel {
height: 20 id: dockModel
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
radius: 1 Component.onCompleted: updateModel()
anchors.centerIn: parent
function updateModel() {
clear()
var items = []
var pinnedApps = [...(SessionData.pinnedApps || [])]
// First section: Pinned apps (always visible, not representing running windows)
pinnedApps.forEach(appId => {
items.push({
"type": "pinned",
"appId": appId,
"windowId": -1,
"windowTitle"// Use -1 instead of null to avoid ListModel warnings
: "",
"workspaceId": -1,
"isPinned"// Use -1 instead of null
: true,
"isRunning": false,
"isFocused": false
})
})
root.pinnedAppCount = pinnedApps.length
// Add separator between pinned and running if both exist
if (pinnedApps.length > 0
&& NiriService.windows.length > 0) {
items.push({
"type": "separator",
"appId": "__SEPARATOR__",
"windowId": -1,
"windowTitle"// Use -1 instead of null
: "",
"workspaceId": -1,
"isPinned"// Use -1 instead of null
: false,
"isRunning": false,
"isFocused": false
})
}
// Second section: Running windows (sorted by display->workspace->position)
// NiriService.windows is already sorted by sortWindowsByLayout
NiriService.windows.forEach(window => {
// Limit window title length for tooltip
var title = window.title
|| "(Unnamed)"
if (title.length > 50) {
title = title.substring(
0, 47) + "..."
}
// Check if this window is focused - compare as numbers
var isFocused = window.id
== NiriService.focusedWindowId
items.push({
"type": "window",
"appId": window.app_id
|| "",
"windowId": window.id || -1,
"windowTitle": title,
"workspaceId": window.workspace_id || -1,
"isPinned": false,
"isRunning": true,
"isFocused": isFocused
})
})
items.forEach(item => {
append(item)
})
}
}
delegate: Item {
id: delegateItem
property alias dockButton: button
width: model.type === "separator" ? 16 : 40
height: 40
Rectangle {
visible: model.type === "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.type !== "separator"
anchors.centerIn: parent
width: 40
height: 40
appData: model
contextMenu: root.contextMenu
windowsMenu: root.windowsMenu
dockApps: root
index: model.index
// Override tooltip for windows to show window title
showWindowTitle: model.type === "window"
windowTitle: model.windowTitle || ""
}
}
} }
}
DockAppButton { Connections {
id: button target: NiriService
visible: model.type !== "separator" function onWindowsChanged() {
anchors.centerIn: parent dockModel.updateModel()
}
width: 40 function onWindowOpenedOrChanged() {
height: 40 dockModel.updateModel()
}
appData: model function onFocusedWindowIdChanged() {
contextMenu: root.contextMenu dockModel.updateModel()
windowsMenu: root.windowsMenu
dockApps: root
index: model.index
// Override tooltip for windows to show window title
showWindowTitle: model.type === "window"
windowTitle: model.windowTitle || ""
} }
}
} }
}
Connections { Connections {
target: NiriService target: SessionData
function onWindowsChanged() { function onPinnedAppsChanged() {
dockModel.updateModel() dockModel.updateModel()
}
} }
function onWindowOpenedOrChanged() {
dockModel.updateModel()
}
function onFocusedWindowIdChanged() {
dockModel.updateModel()
}
}
Connections {
target: SessionData
function onPinnedAppsChanged() {
dockModel.updateModel()
}
}
} }

View File

@@ -8,301 +8,308 @@ 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
} }
showContextMenu = true screen: Quickshell.screens[0]
}
function close() {
showContextMenu = false
}
screen: Quickshell.screens[0] visible: showContextMenu
WlrLayershell.layer: WlrLayershell.Overlay
visible: showContextMenu WlrLayershell.exclusiveZone: -1
WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.exclusiveZone: -1 color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None anchors {
color: "transparent" top: true
anchors { left: true
top: true right: true
left: true bottom: true
right: true
bottom: true
}
property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
onAnchorItemChanged: updatePosition()
onVisibleChanged: if (visible)
updatePosition()
function updatePosition() {
if (!anchorItem) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
} }
var dockWindow = anchorItem.Window.window property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
if (!dockWindow) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100) onAnchorItemChanged: updatePosition()
return onVisibleChanged: if (visible)
updatePosition()
function updatePosition() {
if (!anchorItem) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
}
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)
} }
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.cornerRadius
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 {
anchors.fill: parent id: menuContainer
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 { width: Math.min(400,
id: menuColumn Math.max(200,
width: parent.width - Theme.spacingS * 2 menuColumn.implicitWidth + Theme.spacingS * 2))
anchors.horizontalCenter: parent.horizontalCenter height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
Rectangle { x: {
width: parent.width var left = 10
height: 28 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.cornerRadius radius: Theme.cornerRadius
color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.primary.g, Theme.outline.b, 0.08)
Theme.primary.b, border.width: 1
0.12) : "transparent" opacity: showContextMenu ? 1 : 0
scale: showContextMenu ? 1 : 0.85
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 { Rectangle {
required property var model
width: menuColumn.width
height: 28
radius: Theme.cornerRadius
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
hoverEnabled: true anchors.topMargin: 4
cursorShape: Qt.PointingHandCursor anchors.leftMargin: 2
onClicked: { anchors.rightMargin: -2
NiriService.focusWindow(model.id) anchors.bottomMargin: -4
root.close() radius: parent.radius
} 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.cornerRadius
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 { Column {
id: closeAllArea id: menuColumn
anchors.fill: parent width: parent.width - Theme.spacingS * 2
hoverEnabled: true anchors.horizontalCenter: parent.horizontalCenter
cursorShape: Qt.PointingHandCursor anchors.top: parent.top
onClicked: { anchors.topMargin: Theme.spacingS
if (!root.appData || !root.appData.windows) spacing: 1
return
for (var i = 0; i < root.appData.windows.count; i++) { Rectangle {
var window = root.appData.windows.get(i) width: parent.width
NiriService.closeWindow(window.id) height: 28
radius: Theme.cornerRadius
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.cornerRadius
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.cornerRadius
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,207 +8,208 @@ 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
} }
showWindowsMenu = true function close() {
} showWindowsMenu = false
function close() {
showWindowsMenu = false
}
screen: Quickshell.screens[0]
visible: showWindowsMenu
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)
onAnchorItemChanged: updatePosition()
onVisibleChanged: if (visible)
updatePosition()
function updatePosition() {
if (!anchorItem) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
} }
var dockWindow = anchorItem.Window.window screen: Quickshell.screens[0]
if (!dockWindow) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100) visible: showWindowsMenu
return WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
} }
var buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0) property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
var actualDockHeight = root.dockVisibleHeight // fallback onAnchorItemChanged: updatePosition()
onVisibleChanged: if (visible)
updatePosition()
function findDockBackground(item) { function updatePosition() {
if (item.objectName === "dockBackground") { if (!anchorItem) {
return item anchorPos = Qt.point(screen.width / 2, screen.height - 100)
} return
for (var i = 0; i < item.children.length; i++) { }
var found = findDockBackground(item.children[i])
if (found) var dockWindow = anchorItem.Window.window
return found if (!dockWindow) {
} anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return null 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 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.cornerRadius
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 {
anchors.fill: parent id: menuContainer
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 { width: Math.min(600, Math.max(
id: windowColumn 250,
width: parent.width - Theme.spacingS * 2 windowColumn.implicitWidth + Theme.spacingS * 2))
anchors.horizontalCenter: parent.horizontalCenter height: Math.max(60, windowColumn.implicitHeight + Theme.spacingS * 2)
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
Repeater { x: {
model: root.appData var left = 10
&& root.appData.windows ? root.appData.windows : null 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.cornerRadius
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 {
required property var model
width: windowColumn.width
height: 32
radius: Theme.cornerRadius
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
hoverEnabled: true anchors.topMargin: 4
cursorShape: Qt.PointingHandCursor anchors.leftMargin: 2
onClicked: { anchors.rightMargin: -2
NiriService.focusWindow(model.id) anchors.bottomMargin: -4
root.close() radius: parent.radius
} 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.cornerRadius
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
}
} }
}
} }
Behavior on opacity { MouseArea {
NumberAnimation { anchors.fill: parent
duration: Theme.shortDuration z: -1
easing.type: Theme.emphasizedEasing hoverEnabled: false
} 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

@@ -114,4 +114,4 @@ PanelWindow {
mask: Region { mask: Region {
item: inhibitorPopup item: inhibitorPopup
} }
} }

View File

@@ -8,148 +8,152 @@ import qs.Common
import qs.Services import qs.Services
Item { Item {
id: root id: root
property string sid: Quickshell.env("XDG_SESSION_ID") || "self" property string sid: Quickshell.env("XDG_SESSION_ID") || "self"
property string sessionPath: "" property string sessionPath: ""
function activate() { function activate() {
loader.activeAsync = true loader.activeAsync = true
} }
Component.onCompleted: { Component.onCompleted: {
getSessionPath.running = true getSessionPath.running = true
} }
Process { Process {
id: getSessionPath id: getSessionPath
command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", sid] command: ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", "/org/freedesktop/login1", "--method", "org.freedesktop.login1.Manager.GetSession", sid]
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const match = text.match(/objectpath '([^']+)'/) const match = text.match(/objectpath '([^']+)'/)
if (match) { if (match) {
root.sessionPath = match[1] root.sessionPath = match[1]
console.log("Found session path:", root.sessionPath) console.log("Found session path:", root.sessionPath)
checkCurrentLockState.running = true checkCurrentLockState.running = true
lockStateMonitor.running = true lockStateMonitor.running = true
} else { } else {
console.warn("Could not determine session path") console.warn("Could not determine session path")
}
}
} }
}
}
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("Failed to get session path, exit code:", exitCode) console.warn("Failed to get session path, exit code:", exitCode)
} }
}
}
Process {
id: checkCurrentLockState
command: root.sessionPath ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.login1.Session", "LockedHint"] : []
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.includes("true")) {
console.log("Session is locked on startup, activating lock screen")
LockScreenService.resetState();
loader.activeAsync = true
} }
}
} }
onExited: (exitCode, exitStatus) => { Process {
if (exitCode !== 0) { id: checkCurrentLockState
console.warn("Failed to check initial lock state, exit code:", exitCode) command: root.sessionPath ? ["gdbus", "call", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath, "--method", "org.freedesktop.DBus.Properties.Get", "org.freedesktop.login1.Session", "LockedHint"] : []
} running: false
}
}
Process { stdout: StdioCollector {
id: lockStateMonitor onStreamFinished: {
command: root.sessionPath ? ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath] : [] if (text.includes("true")) {
running: false console.log(
"Session is locked on startup, activating lock screen")
stdout: SplitParser { LockScreenService.resetState()
splitMarker: "\n" loader.activeAsync = true
}
onRead: (line) => { }
if (line.includes("org.freedesktop.login1.Session.Lock")) {
console.log("login1: Lock signal received -> show lock")
LockScreenService.resetState();
loader.activeAsync = true
} else if (line.includes("org.freedesktop.login1.Session.Unlock")) {
console.log("login1: Unlock signal received -> hide lock")
loader.active = false
} else if (line.includes("LockedHint") && line.includes("true")) {
console.log("login1: LockedHint=true -> show lock")
LockScreenService.resetState();
loader.activeAsync = true
} else if (line.includes("LockedHint") && line.includes("false")) {
console.log("login1: LockedHint=false -> hide lock")
loader.active = false
} }
}
}
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.warn("gdbus monitor failed, exit code:", exitCode) console.warn("Failed to check initial lock state, exit code:",
} exitCode)
} }
}
LazyLoader {
id: loader
WlSessionLock {
id: sessionLock
property bool unlocked: false
property string sharedPasswordBuffer: ""
locked: true
onLockedChanged: {
if (!locked)
loader.active = false
}
LockSurface {
id: lockSurface
lock: sessionLock
sharedPasswordBuffer: sessionLock.sharedPasswordBuffer
onPasswordChanged: newPassword => {
sessionLock.sharedPasswordBuffer = newPassword
} }
}
}
}
LockScreenDemo {
id: demoWindow
}
IpcHandler {
target: "lock"
function lock(): void {
console.log("Lock screen requested via IPC")
LockScreenService.resetState();
loader.activeAsync = true
} }
function demo(): void { Process {
console.log("Lock screen DEMO mode requested via IPC") id: lockStateMonitor
demoWindow.showDemo() command: root.sessionPath ? ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.login1", "--object-path", root.sessionPath] : []
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.includes("org.freedesktop.login1.Session.Lock")) {
console.log("login1: Lock signal received -> show lock")
LockScreenService.resetState()
loader.activeAsync = true
} else if (line.includes(
"org.freedesktop.login1.Session.Unlock")) {
console.log("login1: Unlock signal received -> hide lock")
loader.active = false
} else if (line.includes("LockedHint") && line.includes(
"true")) {
console.log("login1: LockedHint=true -> show lock")
LockScreenService.resetState()
loader.activeAsync = true
} else if (line.includes("LockedHint") && line.includes(
"false")) {
console.log("login1: LockedHint=false -> hide lock")
loader.active = false
}
}
}
onExited: (exitCode, exitStatus) => {
if (exitCode !== 0) {
console.warn("gdbus monitor failed, exit code:", exitCode)
}
}
} }
function isLocked(): bool { LazyLoader {
return loader.active id: loader
WlSessionLock {
id: sessionLock
property bool unlocked: false
property string sharedPasswordBuffer: ""
locked: true
onLockedChanged: {
if (!locked)
loader.active = false
}
LockSurface {
id: lockSurface
lock: sessionLock
sharedPasswordBuffer: sessionLock.sharedPasswordBuffer
onPasswordChanged: newPassword => {
sessionLock.sharedPasswordBuffer = newPassword
}
}
}
}
LockScreenDemo {
id: demoWindow
}
IpcHandler {
target: "lock"
function lock(): void {
console.log("Lock screen requested via IPC")
LockScreenService.resetState()
loader.activeAsync = true
}
function demo(): void {
console.log("Lock screen DEMO mode requested via IPC")
demoWindow.showDemo()
}
function isLocked(): bool {
return loader.active
}
} }
}
} }

View File

@@ -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

@@ -7,60 +7,60 @@ 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 && !lock.unlocked readonly property bool locked: thisLocked && lock && !lock.unlocked
function unlock(): void { function unlock(): void {
console.log("LockSurface.unlock() called") console.log("LockSurface.unlock() called")
if (lock) { if (lock) {
lock.unlocked = true lock.unlocked = true
animDelay.start() animDelay.start()
} }
} }
Component.onCompleted: { Component.onCompleted: {
thisLocked = true thisLocked = true
} }
Component.onDestruction: { Component.onDestruction: {
animDelay.stop() animDelay.stop()
} }
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: { onTriggered: {
if (root.lock) { if (root.lock) {
root.lock.locked = false root.lock.locked = false
} }
} }
} }
PowerConfirmModal { PowerConfirmModal {
id: powerConfirmModal id: powerConfirmModal
} }
Loader { Loader {
anchors.fill: parent anchors.fill: parent
sourceComponent: LockScreenContent { sourceComponent: LockScreenContent {
demoMode: false demoMode: false
powerModal: powerConfirmModal powerModal: powerConfirmModal
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,112 +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()
} }
target: AudioService screen: modelData
} visible: micPopupVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
Rectangle { anchors {
id: micPopup top: true
left: true
width: Theme.iconSize + Theme.spacingS * 2 right: true
height: Theme.iconSize + Theme.spacingS * 2 bottom: true
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
color: Theme.popupBackground()
radius: Theme.cornerRadius
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 { Timer {
shadowEnabled: true id: hideTimer
shadowHorizontalOffset: 0
shadowVerticalOffset: 4 interval: 2000
shadowBlur: 0.8 repeat: false
shadowColor: Qt.rgba(0, 0, 0, 0.3) onTriggered: {
shadowOpacity: 0.3 root.micPopupVisible = false
}
} }
transform: Translate { Connections {
y: root.micPopupVisible ? 0 : 20 function onMicMuteChanged() {
root.show()
}
target: AudioService
} }
Behavior on opacity { Rectangle {
NumberAnimation { id: micPopup
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing width: Theme.iconSize + Theme.spacingS * 2
} height: Theme.iconSize + Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingM
color: Theme.popupBackground()
radius: Theme.cornerRadius
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
}
}
} }
Behavior on scale { mask: Region {
NumberAnimation { item: micPopup
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
} }
Behavior on transform {
PropertyAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
mask: Region {
item: micPopup
}
} }

View File

@@ -5,97 +5,109 @@ import qs.Widgets
DankListView { DankListView {
id: listView id: listView
property var keyboardController: null property var keyboardController: null
property bool keyboardActive: false property bool keyboardActive: false
property bool autoScrollDisabled: false property bool autoScrollDisabled: false
onIsUserScrollingChanged: { onIsUserScrollingChanged: {
if (isUserScrolling && keyboardController && keyboardController.keyboardNavigationActive) { if (isUserScrolling && keyboardController
&& keyboardController.keyboardNavigationActive) {
autoScrollDisabled = true autoScrollDisabled = true
} }
} }
function enableAutoScroll() { function enableAutoScroll() {
autoScrollDisabled = false autoScrollDisabled = false
} }
property alias count: listView.count property alias count: listView.count
property alias listContentHeight: listView.contentHeight property alias listContentHeight: listView.contentHeight
clip: true clip: true
model: NotificationService.groupedNotifications model: NotificationService.groupedNotifications
spacing: Theme.spacingL spacing: Theme.spacingL
Timer { Timer {
id: positionPreservationTimer id: positionPreservationTimer
interval: 200 interval: 200
running: keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled running: keyboardController
&& keyboardController.keyboardNavigationActive
&& !autoScrollDisabled
repeat: true repeat: true
onTriggered: { onTriggered: {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) { if (keyboardController
&& keyboardController.keyboardNavigationActive
&& !autoScrollDisabled) {
keyboardController.ensureVisible() keyboardController.ensureVisible()
} }
} }
} }
NotificationEmptyState { NotificationEmptyState {
visible: listView.count === 0 visible: listView.count === 0
anchors.centerIn: parent anchors.centerIn: parent
} }
onModelChanged: { onModelChanged: {
if (keyboardController && keyboardController.keyboardNavigationActive) { if (keyboardController && keyboardController.keyboardNavigationActive) {
keyboardController.rebuildFlatNavigation() keyboardController.rebuildFlatNavigation()
Qt.callLater(function() { Qt.callLater(function () {
if (keyboardController && keyboardController.keyboardNavigationActive && !autoScrollDisabled) { if (keyboardController
&& keyboardController.keyboardNavigationActive
&& !autoScrollDisabled) {
keyboardController.ensureVisible() keyboardController.ensureVisible()
} }
}) })
} }
} }
delegate: Item { delegate: Item {
required property var modelData required property var modelData
required property int index required property int index
readonly property bool isExpanded: NotificationService.expandedGroups[modelData?.key] || false readonly property bool isExpanded: NotificationService.expandedGroups[modelData?.key]
|| false
width: ListView.view.width width: ListView.view.width
height: notificationCardWrapper.height height: notificationCardWrapper.height
Item { Item {
id: notificationCardWrapper id: notificationCardWrapper
width: parent.width width: parent.width
height: notificationCard.height height: notificationCard.height
NotificationCard { NotificationCard {
id: notificationCard id: notificationCard
width: parent.width width: parent.width
notificationGroup: modelData notificationGroup: modelData
isGroupSelected: { isGroupSelected: {
if (!keyboardController || !keyboardController.keyboardNavigationActive) return false if (!keyboardController
|| !keyboardController.keyboardNavigationActive)
return false
keyboardController.selectionVersion keyboardController.selectionVersion
if (!listView.keyboardActive) return false if (!listView.keyboardActive)
return false
const selection = keyboardController.getCurrentSelection() const selection = keyboardController.getCurrentSelection()
return selection.type === "group" && selection.groupIndex === index return selection.type === "group"
&& selection.groupIndex === index
} }
selectedNotificationIndex: { selectedNotificationIndex: {
if (!keyboardController || !keyboardController.keyboardNavigationActive) return -1 if (!keyboardController
|| !keyboardController.keyboardNavigationActive)
return -1
keyboardController.selectionVersion keyboardController.selectionVersion
if (!listView.keyboardActive) return -1 if (!listView.keyboardActive)
return -1
const selection = keyboardController.getCurrentSelection() const selection = keyboardController.getCurrentSelection()
return (selection.type === "notification" && selection.groupIndex === index) return (selection.type === "notification"
? selection.notificationIndex : -1 && selection.groupIndex === index) ? selection.notificationIndex : -1
} }
keyboardNavigationActive: listView.keyboardActive keyboardNavigationActive: listView.keyboardActive
} }
} }
} }
Connections { Connections {
function onGroupedNotificationsChanged() { function onGroupedNotificationsChanged() {
@@ -104,11 +116,11 @@ DankListView {
keyboardController.rebuildFlatNavigation() keyboardController.rebuildFlatNavigation()
return return
} }
keyboardController.rebuildFlatNavigation() keyboardController.rebuildFlatNavigation()
if (keyboardController.keyboardNavigationActive) { if (keyboardController.keyboardNavigationActive) {
Qt.callLater(function() { Qt.callLater(function () {
if (!autoScrollDisabled) { if (!autoScrollDisabled) {
keyboardController.ensureVisible() keyboardController.ensureVisible()
} }
@@ -116,28 +128,29 @@ DankListView {
} }
} }
} }
function onExpandedGroupsChanged() { function onExpandedGroupsChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) { if (keyboardController
Qt.callLater(function() { && keyboardController.keyboardNavigationActive) {
Qt.callLater(function () {
if (!autoScrollDisabled) { if (!autoScrollDisabled) {
keyboardController.ensureVisible() keyboardController.ensureVisible()
} }
}) })
} }
} }
function onExpandedMessagesChanged() { function onExpandedMessagesChanged() {
if (keyboardController && keyboardController.keyboardNavigationActive) { if (keyboardController
Qt.callLater(function() { && keyboardController.keyboardNavigationActive) {
Qt.callLater(function () {
if (!autoScrollDisabled) { if (!autoScrollDisabled) {
keyboardController.ensureVisible() keyboardController.ensureVisible()
} }
}) })
} }
} }
target: NotificationService target: NotificationService
} }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,193 +10,200 @@ import qs.Widgets
import qs.Modules.Notifications.Center import qs.Modules.Notifications.Center
DankPopout { DankPopout {
id: root id: root
property bool notificationHistoryVisible: false property bool notificationHistoryVisible: false
property string triggerSection: "right" property string triggerSection: "right"
property var triggerScreen: null property var triggerScreen: null
NotificationKeyboardController {
id: keyboardController
listView: null
isOpen: notificationHistoryVisible
onClose: function() { notificationHistoryVisible = false }
}
function setTriggerPosition(x, y, width, section, screen) { NotificationKeyboardController {
triggerX = x id: keyboardController
triggerY = y listView: null
triggerWidth = width isOpen: notificationHistoryVisible
triggerSection = section onClose: function () {
triggerScreen = screen notificationHistoryVisible = false
}
popupWidth: 400
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
triggerX: Screen.width - 400 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
triggerWidth: 40
positioning: "center"
WlrLayershell.namespace: "quickshell-notifications"
screen: triggerScreen
shouldBeVisible: notificationHistoryVisible
visible: shouldBeVisible
onNotificationHistoryVisibleChanged: {
if (notificationHistoryVisible) {
open()
} else {
close()
}
}
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
NotificationService.disablePopups(true)
// Set up keyboard controller when content is loaded
Qt.callLater(function() {
if (contentLoader.item) {
contentLoader.item.externalKeyboardController = keyboardController
// Find the notification list and set up the connection
var notificationList = findChild(contentLoader.item, "notificationList")
var notificationHeader = findChild(contentLoader.item, "notificationHeader")
if (notificationList) {
keyboardController.listView = notificationList
notificationList.keyboardController = keyboardController
}
if (notificationHeader) {
notificationHeader.keyboardController = keyboardController
}
keyboardController.reset()
keyboardController.rebuildFlatNavigation()
} }
})
} else {
NotificationService.disablePopups(false)
// Reset keyboard state when closing
keyboardController.keyboardNavigationActive = false
} }
}
function setTriggerPosition(x, y, width, section, screen) {
function findChild(parent, objectName) { triggerX = x
if (parent.objectName === objectName) { triggerY = y
return parent triggerWidth = width
triggerSection = section
triggerScreen = screen
} }
for (var i = 0; i < parent.children.length; i++) {
var child = parent.children[i]
var result = findChild(child, objectName)
if (result) {
return result
}
}
return null
}
content: Component { popupWidth: 400
Rectangle { popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
id: notificationContent triggerX: Screen.width - 400 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
property var externalKeyboardController: null triggerWidth: 40
property real cachedHeaderHeight: 32 positioning: "center"
WlrLayershell.namespace: "quickshell-notifications"
implicitHeight: { screen: triggerScreen
let baseHeight = Theme.spacingL * 2 shouldBeVisible: notificationHistoryVisible
baseHeight += cachedHeaderHeight visible: shouldBeVisible
baseHeight += (notificationSettings.expanded ? notificationSettings.contentHeight : 0)
baseHeight += Theme.spacingM * 2
let listHeight = notificationList.listContentHeight
if (NotificationService.groupedNotifications.length === 0)
listHeight = 200
baseHeight += Math.min(listHeight, 600)
return Math.max(300, Math.min(baseHeight, root.screen ? root.screen.height * 0.8 : Screen.height * 0.8))
}
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
focus: true
Component.onCompleted: { onNotificationHistoryVisibleChanged: {
if (root.shouldBeVisible) if (notificationHistoryVisible) {
forceActiveFocus() open()
} } else {
close()
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
} else if (externalKeyboardController) {
externalKeyboardController.handleKey(event)
} }
} }
Connections { onShouldBeVisibleChanged: {
function onShouldBeVisibleChanged() { if (shouldBeVisible) {
if (root.shouldBeVisible) NotificationService.disablePopups(true)
// Set up keyboard controller when content is loaded
Qt.callLater(function () { Qt.callLater(function () {
notificationContent.forceActiveFocus() if (contentLoader.item) {
contentLoader.item.externalKeyboardController = keyboardController
// Find the notification list and set up the connection
var notificationList = findChild(contentLoader.item,
"notificationList")
var notificationHeader = findChild(contentLoader.item,
"notificationHeader")
if (notificationList) {
keyboardController.listView = notificationList
notificationList.keyboardController = keyboardController
}
if (notificationHeader) {
notificationHeader.keyboardController = keyboardController
}
keyboardController.reset()
keyboardController.rebuildFlatNavigation()
}
}) })
else } else {
notificationContent.focus = false NotificationService.disablePopups(false)
// Reset keyboard state when closing
keyboardController.keyboardNavigationActive = false
} }
target: root
}
FocusScope {
id: contentColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
focus: true
Column {
id: contentColumnInner
anchors.fill: parent
spacing: Theme.spacingM
NotificationHeader {
id: notificationHeader
objectName: "notificationHeader"
onHeightChanged: notificationContent.cachedHeaderHeight = height
}
NotificationSettings {
id: notificationSettings
expanded: notificationHeader.showSettings
}
KeyboardNavigatedNotificationList {
id: notificationList
objectName: "notificationList"
width: parent.width
height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2
}
}
}
NotificationKeyboardHints {
id: keyboardHints
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
showHints: externalKeyboardController ? externalKeyboardController.showKeyboardHints : false
z: 200
}
Behavior on implicitHeight {
NumberAnimation {
duration: 180
easing.type: Easing.OutQuart
}
}
} }
}
} function findChild(parent, objectName) {
if (parent.objectName === objectName) {
return parent
}
for (var i = 0; i < parent.children.length; i++) {
var child = parent.children[i]
var result = findChild(child, objectName)
if (result) {
return result
}
}
return null
}
content: Component {
Rectangle {
id: notificationContent
property var externalKeyboardController: null
property real cachedHeaderHeight: 32
implicitHeight: {
let baseHeight = Theme.spacingL * 2
baseHeight += cachedHeaderHeight
baseHeight += (notificationSettings.expanded ? notificationSettings.contentHeight : 0)
baseHeight += Theme.spacingM * 2
let listHeight = notificationList.listContentHeight
if (NotificationService.groupedNotifications.length === 0)
listHeight = 200
baseHeight += Math.min(listHeight, 600)
return Math.max(
300, Math.min(
baseHeight,
root.screen ? root.screen.height * 0.8 : Screen.height * 0.8))
}
color: Theme.popupBackground()
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 1
focus: true
Component.onCompleted: {
if (root.shouldBeVisible)
forceActiveFocus()
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.close()
event.accepted = true
} else if (externalKeyboardController) {
externalKeyboardController.handleKey(event)
}
}
Connections {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible)
Qt.callLater(function () {
notificationContent.forceActiveFocus()
})
else
notificationContent.focus = false
}
target: root
}
FocusScope {
id: contentColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
focus: true
Column {
id: contentColumnInner
anchors.fill: parent
spacing: Theme.spacingM
NotificationHeader {
id: notificationHeader
objectName: "notificationHeader"
onHeightChanged: notificationContent.cachedHeaderHeight = height
}
NotificationSettings {
id: notificationSettings
expanded: notificationHeader.showSettings
}
KeyboardNavigatedNotificationList {
id: notificationList
objectName: "notificationList"
width: parent.width
height: parent.height - notificationContent.cachedHeaderHeight
- notificationSettings.height - contentColumnInner.spacing * 2
}
}
}
NotificationKeyboardHints {
id: keyboardHints
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
showHints: externalKeyboardController ? externalKeyboardController.showKeyboardHints : false
z: 200
}
Behavior on implicitHeight {
NumberAnimation {
duration: 180
easing.type: Easing.OutQuart
}
}
}
}
}

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 { DankIcon {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
name: "notifications_none" name: "notifications_none"
size: Theme.iconSizeLarge + 16 size: Theme.iconSizeLarge + 16
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3) 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
}
} }
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,161 +5,165 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: root id: root
property var keyboardController: null
property bool showSettings: false
width: parent.width property var keyboardController: null
height: 32 property bool showSettings: false
Row { width: parent.width
anchors.left: parent.left height: 32
anchors.verticalCenter: parent.verticalCenter
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.cornerRadius
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
}
}
}
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
// Keyboard help button
DankActionButton {
id: helpButton
iconName: "info"
iconColor: keyboardController && keyboardController.showKeyboardHints ? Theme.primary : Theme.surfaceText
buttonSize: 28
visible: keyboardController !== null
anchors.verticalCenter: parent.verticalCenter
onClicked: {
if (keyboardController) {
keyboardController.showKeyboardHints = !keyboardController.showKeyboardHints
}
}
}
// Settings button
DankActionButton {
id: settingsButton
iconName: "settings"
iconColor: root.showSettings ? Theme.primary : Theme.surfaceText
buttonSize: 28
anchors.verticalCenter: parent.verticalCenter
onClicked: root.showSettings = !root.showSettings
}
Rectangle {
id: clearAllButton
width: 120
height: 28
radius: Theme.cornerRadius
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 { Row {
anchors.centerIn: parent anchors.left: parent.left
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
} spacing: Theme.spacingXS
StyledText { StyledText {
text: "Clear All" text: "Notifications"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeLarge
color: clearArea.containsMouse ? Theme.primary : Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium 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.cornerRadius
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
}
}
}
}
}
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} spacing: Theme.spacingXS
}
MouseArea { // Keyboard help button
id: clearArea DankActionButton {
id: helpButton
iconName: "info"
iconColor: keyboardController
&& keyboardController.showKeyboardHints ? Theme.primary : Theme.surfaceText
buttonSize: 28
visible: keyboardController !== null
anchors.verticalCenter: parent.verticalCenter
onClicked: {
if (keyboardController) {
keyboardController.showKeyboardHints = !keyboardController.showKeyboardHints
}
}
}
anchors.fill: parent // Settings button
hoverEnabled: true DankActionButton {
cursorShape: Qt.PointingHandCursor id: settingsButton
onClicked: NotificationService.clearAllNotifications() iconName: "settings"
} iconColor: root.showSettings ? Theme.primary : Theme.surfaceText
buttonSize: 28
anchors.verticalCenter: parent.verticalCenter
onClicked: root.showSettings = !root.showSettings
}
Behavior on color { Rectangle {
ColorAnimation { id: clearAllButton
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color { width: 120
ColorAnimation { height: 28
duration: Theme.shortDuration radius: Theme.cornerRadius
easing.type: Theme.standardEasing 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
}
}
}
} }
}
}
} }

View File

@@ -4,114 +4,124 @@ import qs.Services
QtObject { QtObject {
id: controller id: controller
property var listView: null property var listView: null
property bool isOpen: false property bool isOpen: false
property var onClose: null property var onClose: null
property int selectionVersion: 0 property int selectionVersion: 0
property bool keyboardNavigationActive: false property bool keyboardNavigationActive: false
property int selectedFlatIndex: 0 property int selectedFlatIndex: 0
property var flatNavigation: [] property var flatNavigation: []
property bool showKeyboardHints: false property bool showKeyboardHints: false
property string selectedNotificationId: "" property string selectedNotificationId: ""
property string selectedGroupKey: "" property string selectedGroupKey: ""
property string selectedItemType: "" property string selectedItemType: ""
property bool isTogglingGroup: false property bool isTogglingGroup: false
property bool isRebuilding: false property bool isRebuilding: false
function rebuildFlatNavigation() { function rebuildFlatNavigation() {
isRebuilding = true isRebuilding = true
var nav = [] var nav = []
var groups = NotificationService.groupedNotifications var groups = NotificationService.groupedNotifications
for (var i = 0; i < groups.length; i++) { for (var i = 0; i < groups.length; i++) {
var group = groups[i] var group = groups[i]
var isExpanded = NotificationService.expandedGroups[group.key] || false var isExpanded = NotificationService.expandedGroups[group.key]
|| false
// Add the group itself // Add the group itself
nav.push({ nav.push({
type: "group", "type": "group",
groupIndex: i, "groupIndex": i,
notificationIndex: -1, "notificationIndex": -1,
groupKey: group.key, "groupKey": group.key,
notificationId: "" "notificationId": ""
}) })
// If expanded, add individual notifications // If expanded, add individual notifications
if (isExpanded) { if (isExpanded) {
var notifications = group.notifications || [] var notifications = group.notifications || []
for (var j = 0; j < notifications.length; j++) { for (var j = 0; j < notifications.length; j++) {
var notifId = String(notifications[j]?.notification?.id || "") var notifId = String(
notifications[j]?.notification?.id || "")
nav.push({ nav.push({
type: "notification", "type": "notification",
groupIndex: i, "groupIndex": i,
notificationIndex: j, "notificationIndex": j,
groupKey: group.key, "groupKey": group.key,
notificationId: notifId "notificationId": notifId
}) })
} }
} }
} }
flatNavigation = nav flatNavigation = nav
updateSelectedIndexFromId() updateSelectedIndexFromId()
isRebuilding = false isRebuilding = false
} }
function updateSelectedIndexFromId() { function updateSelectedIndexFromId() {
if (!keyboardNavigationActive) return if (!keyboardNavigationActive)
return
// Find the index that matches our selected ID/key // Find the index that matches our selected ID/key
for (var i = 0; i < flatNavigation.length; i++) { for (var i = 0; i < flatNavigation.length; i++) {
var item = flatNavigation[i] var item = flatNavigation[i]
if (selectedItemType === "group" && item.type === "group" && item.groupKey === selectedGroupKey) { if (selectedItemType === "group" && item.type === "group"
&& item.groupKey === selectedGroupKey) {
selectedFlatIndex = i selectedFlatIndex = i
selectionVersion++ // Trigger UI update selectionVersion++ // Trigger UI update
return return
} else if (selectedItemType === "notification" && item.type === "notification" && String(item.notificationId) === String(selectedNotificationId)) { } else if (selectedItemType === "notification"
&& item.type === "notification" && String(
item.notificationId) === String(
selectedNotificationId)) {
selectedFlatIndex = i selectedFlatIndex = i
selectionVersion++ // Trigger UI update selectionVersion++ // Trigger UI update
return return
} }
} }
// If not found, try to find the same group but select the group header instead // If not found, try to find the same group but select the group header instead
if (selectedItemType === "notification") { if (selectedItemType === "notification") {
for (var j = 0; j < flatNavigation.length; j++) { for (var j = 0; j < flatNavigation.length; j++) {
var groupItem = flatNavigation[j] var groupItem = flatNavigation[j]
if (groupItem.type === "group" && groupItem.groupKey === selectedGroupKey) { if (groupItem.type === "group"
&& groupItem.groupKey === selectedGroupKey) {
selectedFlatIndex = j selectedFlatIndex = j
selectedItemType = "group" selectedItemType = "group"
selectedNotificationId = "" selectedNotificationId = ""
selectionVersion++ // Trigger UI update selectionVersion++ // Trigger UI update
return return
} }
} }
} }
// If still not found, clamp to valid range and update // If still not found, clamp to valid range and update
if (flatNavigation.length > 0) { if (flatNavigation.length > 0) {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1) selectedFlatIndex = Math.min(selectedFlatIndex,
flatNavigation.length - 1)
selectedFlatIndex = Math.max(selectedFlatIndex, 0) selectedFlatIndex = Math.max(selectedFlatIndex, 0)
updateSelectedIdFromIndex() updateSelectedIdFromIndex()
selectionVersion++ // Trigger UI update selectionVersion++ // Trigger UI update
} }
} }
function updateSelectedIdFromIndex() { function updateSelectedIdFromIndex() {
if (selectedFlatIndex >= 0 && selectedFlatIndex < flatNavigation.length) { if (selectedFlatIndex >= 0
&& selectedFlatIndex < flatNavigation.length) {
var item = flatNavigation[selectedFlatIndex] var item = flatNavigation[selectedFlatIndex]
selectedItemType = item.type selectedItemType = item.type
selectedGroupKey = item.groupKey selectedGroupKey = item.groupKey
selectedNotificationId = item.notificationId selectedNotificationId = item.notificationId
} }
} }
function reset() { function reset() {
selectedFlatIndex = 0 selectedFlatIndex = 0
keyboardNavigationActive = false keyboardNavigationActive = false
@@ -122,87 +132,100 @@ QtObject {
} }
rebuildFlatNavigation() rebuildFlatNavigation()
} }
function selectNext() { function selectNext() {
keyboardNavigationActive = true keyboardNavigationActive = true
if (flatNavigation.length === 0) return if (flatNavigation.length === 0)
return
// Re-enable auto-scrolling when arrow keys are used // Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) { if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll() listView.enableAutoScroll()
} }
selectedFlatIndex = Math.min(selectedFlatIndex + 1, flatNavigation.length - 1) selectedFlatIndex = Math.min(selectedFlatIndex + 1,
flatNavigation.length - 1)
updateSelectedIdFromIndex() updateSelectedIdFromIndex()
selectionVersion++ selectionVersion++
ensureVisible() ensureVisible()
} }
function selectPrevious() { function selectPrevious() {
keyboardNavigationActive = true keyboardNavigationActive = true
if (flatNavigation.length === 0) return if (flatNavigation.length === 0)
return
// Re-enable auto-scrolling when arrow keys are used // Re-enable auto-scrolling when arrow keys are used
if (listView && listView.enableAutoScroll) { if (listView && listView.enableAutoScroll) {
listView.enableAutoScroll() listView.enableAutoScroll()
} }
selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0) selectedFlatIndex = Math.max(selectedFlatIndex - 1, 0)
updateSelectedIdFromIndex() updateSelectedIdFromIndex()
selectionVersion++ selectionVersion++
ensureVisible() ensureVisible()
} }
function toggleGroupExpanded() { function toggleGroupExpanded() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) return if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex] const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex] const group = groups[currentItem.groupIndex]
if (!group) return if (!group)
return
// Prevent expanding groups with < 2 notifications // Prevent expanding groups with < 2 notifications
const notificationCount = group.notifications ? group.notifications.length : 0 const notificationCount = group.notifications ? group.notifications.length : 0
if (notificationCount < 2) return if (notificationCount < 2)
return
const wasExpanded = NotificationService.expandedGroups[group.key] || false
const wasExpanded = NotificationService.expandedGroups[group.key]
|| false
const groupIndex = currentItem.groupIndex const groupIndex = currentItem.groupIndex
isTogglingGroup = true isTogglingGroup = true
NotificationService.toggleGroupExpansion(group.key) NotificationService.toggleGroupExpansion(group.key)
rebuildFlatNavigation() rebuildFlatNavigation()
// Smart selection after toggle // Smart selection after toggle
if (!wasExpanded) { if (!wasExpanded) {
// Just expanded - move to first notification in the group // Just expanded - move to first notification in the group
for (let i = 0; i < flatNavigation.length; i++) { for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "notification" && flatNavigation[i].groupIndex === groupIndex) { if (flatNavigation[i].type === "notification"
&& flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i selectedFlatIndex = i
break break
} }
} }
} else { } else {
// Just collapsed - stay on the group header // Just collapsed - stay on the group header
for (let i = 0; i < flatNavigation.length; i++) { for (var i = 0; i < flatNavigation.length; i++) {
if (flatNavigation[i].type === "group" && flatNavigation[i].groupIndex === groupIndex) { if (flatNavigation[i].type === "group"
&& flatNavigation[i].groupIndex === groupIndex) {
selectedFlatIndex = i selectedFlatIndex = i
break break
} }
} }
} }
isTogglingGroup = false isTogglingGroup = false
ensureVisible() ensureVisible()
} }
function handleEnterKey() { function handleEnterKey() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) return if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex] const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex] const group = groups[currentItem.groupIndex]
if (!group) return if (!group)
return
if (currentItem.type === "group") { if (currentItem.type === "group") {
const notificationCount = group.notifications ? group.notifications.length : 0 const notificationCount = group.notifications ? group.notifications.length : 0
if (notificationCount >= 2) { if (notificationCount >= 2) {
@@ -214,100 +237,117 @@ QtObject {
executeAction(0) executeAction(0)
} }
} }
function toggleTextExpanded() { function toggleTextExpanded() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) return if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex] const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex] const group = groups[currentItem.groupIndex]
if (!group) return if (!group)
return
var messageId = "" var messageId = ""
if (currentItem.type === "group") { if (currentItem.type === "group") {
messageId = group.latestNotification?.notification?.id + "_desc" messageId = group.latestNotification?.notification?.id + "_desc"
} else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) { } else if (currentItem.type === "notification"
messageId = group.notifications[currentItem.notificationIndex]?.notification?.id + "_desc" && currentItem.notificationIndex >= 0
&& currentItem.notificationIndex < group.notifications.length) {
messageId = group.notifications[currentItem.notificationIndex]?.notification?.id
+ "_desc"
} }
if (messageId) { if (messageId) {
NotificationService.toggleMessageExpansion(messageId) NotificationService.toggleMessageExpansion(messageId)
} }
} }
function executeAction(actionIndex) { function executeAction(actionIndex) {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) return if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex] const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex] const group = groups[currentItem.groupIndex]
if (!group) return if (!group)
return
var actions = [] var actions = []
if (currentItem.type === "group") { if (currentItem.type === "group") {
actions = group.latestNotification?.actions || [] actions = group.latestNotification?.actions || []
} else if (currentItem.type === "notification" && currentItem.notificationIndex >= 0 && currentItem.notificationIndex < group.notifications.length) { } else if (currentItem.type === "notification"
actions = group.notifications[currentItem.notificationIndex]?.actions || [] && currentItem.notificationIndex >= 0
&& currentItem.notificationIndex < group.notifications.length) {
actions = group.notifications[currentItem.notificationIndex]?.actions
|| []
} }
if (actionIndex >= 0 && actionIndex < actions.length) { if (actionIndex >= 0 && actionIndex < actions.length) {
const action = actions[actionIndex] const action = actions[actionIndex]
if (action.invoke) { if (action.invoke) {
action.invoke() action.invoke()
if (onClose) onClose() if (onClose)
onClose()
} }
} }
} }
function clearSelected() { function clearSelected() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length) return if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length)
return
const currentItem = flatNavigation[selectedFlatIndex] const currentItem = flatNavigation[selectedFlatIndex]
const groups = NotificationService.groupedNotifications const groups = NotificationService.groupedNotifications
const group = groups[currentItem.groupIndex] const group = groups[currentItem.groupIndex]
if (!group) return if (!group)
return
// Save current state for smart navigation // Save current state for smart navigation
const currentGroupKey = group.key const currentGroupKey = group.key
const isNotification = currentItem.type === "notification" const isNotification = currentItem.type === "notification"
const notificationIndex = currentItem.notificationIndex const notificationIndex = currentItem.notificationIndex
const totalNotificationsInGroup = group.notifications ? group.notifications.length : 0 const totalNotificationsInGroup = group.notifications ? group.notifications.length : 0
const isLastNotificationInGroup = isNotification && totalNotificationsInGroup === 1 const isLastNotificationInGroup = isNotification
const isLastNotificationInList = isNotification && notificationIndex === totalNotificationsInGroup - 1 && totalNotificationsInGroup === 1
const isLastNotificationInList = isNotification
&& notificationIndex === totalNotificationsInGroup - 1
// Store what to select next BEFORE clearing // Store what to select next BEFORE clearing
let nextTargetType = "" let nextTargetType = ""
let nextTargetGroupKey = "" let nextTargetGroupKey = ""
let nextTargetNotificationIndex = -1 let nextTargetNotificationIndex = -1
if (currentItem.type === "group") { if (currentItem.type === "group") {
NotificationService.dismissGroup(group.key) NotificationService.dismissGroup(group.key)
// Look for next group // Look for next group
for (let i = currentItem.groupIndex + 1; i < groups.length; i++) { for (var i = currentItem.groupIndex + 1; i < groups.length; i++) {
nextTargetType = "group" nextTargetType = "group"
nextTargetGroupKey = groups[i].key nextTargetGroupKey = groups[i].key
break break
} }
if (!nextTargetGroupKey && currentItem.groupIndex > 0) { if (!nextTargetGroupKey && currentItem.groupIndex > 0) {
nextTargetType = "group" nextTargetType = "group"
nextTargetGroupKey = groups[currentItem.groupIndex - 1].key nextTargetGroupKey = groups[currentItem.groupIndex - 1].key
} }
} else if (isNotification) { } else if (isNotification) {
const notification = group.notifications[notificationIndex] const notification = group.notifications[notificationIndex]
NotificationService.dismissNotification(notification) NotificationService.dismissNotification(notification)
if (isLastNotificationInGroup) { if (isLastNotificationInGroup) {
for (let i = currentItem.groupIndex + 1; i < groups.length; i++) { for (var i = currentItem.groupIndex + 1; i < groups.length; i++) {
nextTargetType = "group" nextTargetType = "group"
nextTargetGroupKey = groups[i].key nextTargetGroupKey = groups[i].key
break break
} }
if (!nextTargetGroupKey && currentItem.groupIndex > 0) { if (!nextTargetGroupKey && currentItem.groupIndex > 0) {
nextTargetType = "group" nextTargetType = "group"
nextTargetGroupKey = groups[currentItem.groupIndex - 1].key nextTargetGroupKey = groups[currentItem.groupIndex - 1].key
@@ -331,66 +371,77 @@ QtObject {
nextTargetNotificationIndex = notificationIndex nextTargetNotificationIndex = notificationIndex
} }
} }
rebuildFlatNavigation() rebuildFlatNavigation()
// Find and select the target we identified // Find and select the target we identified
if (flatNavigation.length === 0) { if (flatNavigation.length === 0) {
selectedFlatIndex = 0 selectedFlatIndex = 0
updateSelectedIdFromIndex() updateSelectedIdFromIndex()
} else if (nextTargetGroupKey) { } else if (nextTargetGroupKey) {
let found = false let found = false
for (let i = 0; i < flatNavigation.length; i++) { for (var i = 0; i < flatNavigation.length; i++) {
const item = flatNavigation[i] const item = flatNavigation[i]
if (nextTargetType === "group" && item.type === "group" && item.groupKey === nextTargetGroupKey) { if (nextTargetType === "group" && item.type === "group"
&& item.groupKey === nextTargetGroupKey) {
selectedFlatIndex = i selectedFlatIndex = i
found = true found = true
break break
} else if (nextTargetType === "notification" && item.type === "notification" && } else if (nextTargetType === "notification"
item.groupKey === nextTargetGroupKey && item.notificationIndex === nextTargetNotificationIndex) { && item.type === "notification"
&& item.groupKey === nextTargetGroupKey
&& item.notificationIndex === nextTargetNotificationIndex) {
selectedFlatIndex = i selectedFlatIndex = i
found = true found = true
break break
} }
} }
if (!found) { if (!found) {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1) selectedFlatIndex = Math.min(selectedFlatIndex,
flatNavigation.length - 1)
} }
updateSelectedIdFromIndex() updateSelectedIdFromIndex()
} else { } else {
selectedFlatIndex = Math.min(selectedFlatIndex, flatNavigation.length - 1) selectedFlatIndex = Math.min(selectedFlatIndex,
flatNavigation.length - 1)
updateSelectedIdFromIndex() updateSelectedIdFromIndex()
} }
ensureVisible() ensureVisible()
} }
function ensureVisible() { function ensureVisible() {
if (flatNavigation.length === 0 || selectedFlatIndex >= flatNavigation.length || !listView) return if (flatNavigation.length === 0
|| selectedFlatIndex >= flatNavigation.length || !listView)
return
const currentItem = flatNavigation[selectedFlatIndex] const currentItem = flatNavigation[selectedFlatIndex]
if (keyboardNavigationActive && currentItem && currentItem.groupIndex >= 0) { if (keyboardNavigationActive && currentItem
&& currentItem.groupIndex >= 0) {
// Always center the selected item for better visibility // Always center the selected item for better visibility
// This ensures the selected item stays in view even when new notifications arrive // This ensures the selected item stays in view even when new notifications arrive
if (currentItem.type === "notification") { if (currentItem.type === "notification") {
// For individual notifications, center on the group but bias towards the notification // For individual notifications, center on the group but bias towards the notification
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Center) listView.positionViewAtIndex(currentItem.groupIndex,
ListView.Center)
} else { } else {
// For group headers, center on the group // For group headers, center on the group
listView.positionViewAtIndex(currentItem.groupIndex, ListView.Center) listView.positionViewAtIndex(currentItem.groupIndex,
ListView.Center)
} }
// Force immediate update // Force immediate update
listView.forceLayout() listView.forceLayout()
} }
} }
function handleKey(event) { function handleKey(event) {
if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) && (event.modifiers & Qt.ShiftModifier)) { if ((event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace)
&& (event.modifiers & Qt.ShiftModifier)) {
NotificationService.clearAllNotifications() NotificationService.clearAllNotifications()
rebuildFlatNavigation() rebuildFlatNavigation()
if (flatNavigation.length === 0) { if (flatNavigation.length === 0) {
@@ -406,19 +457,20 @@ QtObject {
event.accepted = true event.accepted = true
return return
} }
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (keyboardNavigationActive) { if (keyboardNavigationActive) {
keyboardNavigationActive = false keyboardNavigationActive = false
event.accepted = true event.accepted = true
} else { } else {
if (onClose) onClose() if (onClose)
onClose()
event.accepted = true event.accepted = true
} }
} else if (event.key === Qt.Key_Down || event.key === 16777237) { } else if (event.key === Qt.Key_Down || event.key === 16777237) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true keyboardNavigationActive = true
rebuildFlatNavigation() // Ensure we have fresh navigation data rebuildFlatNavigation() // Ensure we have fresh navigation data
selectedFlatIndex = 0 selectedFlatIndex = 0
updateSelectedIdFromIndex() updateSelectedIdFromIndex()
// Set keyboardActive on listView to show highlight // Set keyboardActive on listView to show highlight
@@ -435,7 +487,7 @@ QtObject {
} else if (event.key === Qt.Key_Up || event.key === 16777235) { } else if (event.key === Qt.Key_Up || event.key === 16777235) {
if (!keyboardNavigationActive) { if (!keyboardNavigationActive) {
keyboardNavigationActive = true keyboardNavigationActive = true
rebuildFlatNavigation() // Ensure we have fresh navigation data rebuildFlatNavigation() // Ensure we have fresh navigation data
selectedFlatIndex = 0 selectedFlatIndex = 0
updateSelectedIdFromIndex() updateSelectedIdFromIndex()
// Set keyboardActive on listView to show highlight // Set keyboardActive on listView to show highlight
@@ -462,13 +514,15 @@ QtObject {
if (event.key === Qt.Key_Space) { if (event.key === Qt.Key_Space) {
toggleGroupExpanded() toggleGroupExpanded()
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { } else if (event.key === Qt.Key_Return
|| event.key === Qt.Key_Enter) {
handleEnterKey() handleEnterKey()
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_E) { } else if (event.key === Qt.Key_E) {
toggleTextExpanded() toggleTextExpanded()
event.accepted = true event.accepted = true
} else if (event.key === Qt.Key_Delete || event.key === Qt.Key_Backspace) { } else if (event.key === Qt.Key_Delete
|| event.key === Qt.Key_Backspace) {
clearSelected() clearSelected()
event.accepted = true event.accepted = true
} else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) { } else if (event.key >= Qt.Key_1 && event.key <= Qt.Key_9) {
@@ -477,19 +531,28 @@ QtObject {
event.accepted = true event.accepted = true
} }
} }
if (event.key === Qt.Key_F10) { if (event.key === Qt.Key_F10) {
showKeyboardHints = !showKeyboardHints showKeyboardHints = !showKeyboardHints
event.accepted = true event.accepted = true
} }
} }
// Get current selection info for UI // Get current selection info for UI
function getCurrentSelection() { function getCurrentSelection() {
if (!keyboardNavigationActive || selectedFlatIndex < 0 || selectedFlatIndex >= flatNavigation.length) { if (!keyboardNavigationActive || selectedFlatIndex < 0
return { type: "", groupIndex: -1, notificationIndex: -1 } || selectedFlatIndex >= flatNavigation.length) {
return {
"type": "",
"groupIndex": -1,
"notificationIndex": -1
}
}
const result = flatNavigation[selectedFlatIndex] || {
"type": "",
"groupIndex": -1,
"notificationIndex": -1
} }
const result = flatNavigation[selectedFlatIndex] || { type: "", groupIndex: -1, notificationIndex: -1 }
return result return result
} }
} }

View File

@@ -9,7 +9,8 @@ Rectangle {
height: 80 height: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.95)
border.color: Theme.primary border.color: Theme.primary
border.width: 2 border.width: 2
opacity: showHints ? 1 : 0 opacity: showHints ? 1 : 0
@@ -39,7 +40,6 @@ Rectangle {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
} }
Behavior on opacity { Behavior on opacity {
@@ -47,7 +47,5 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }

View File

@@ -6,19 +6,21 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
property bool expanded: false property bool expanded: false
readonly property real contentHeight: contentColumn.height + Theme.spacingL * 2 readonly property real contentHeight: contentColumn.height + Theme.spacingL * 2
width: parent.width width: parent.width
height: expanded ? Math.min(contentHeight, 400) : 0 height: expanded ? Math.min(contentHeight, 400) : 0
visible: expanded visible: expanded
clip: true clip: true
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.3) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) Theme.surfaceContainer.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
border.width: 1 border.width: 1
Behavior on height { Behavior on height {
NumberAnimation { NumberAnimation {
duration: Anims.durShort duration: Anims.durShort
@@ -26,48 +28,74 @@ Rectangle {
easing.bezierCurve: Anims.emphasized easing.bezierCurve: Anims.emphasized
} }
} }
// Ensure smooth opacity transition // Ensure smooth opacity transition
opacity: expanded ? 1 : 0 opacity: expanded ? 1 : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Anims.durShort duration: Anims.durShort
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized easing.bezierCurve: Anims.emphasized
} }
} }
readonly property var timeoutOptions: [ readonly property var timeoutOptions: [{
{ text: "Never", value: 0 }, "text": "Never",
{ text: "1 second", value: 1000 }, "value": 0
{ text: "3 seconds", value: 3000 }, }, {
{ text: "5 seconds", value: 5000 }, "text": "1 second",
{ text: "8 seconds", value: 8000 }, "value": 1000
{ text: "10 seconds", value: 10000 }, }, {
{ text: "15 seconds", value: 15000 }, "text": "3 seconds",
{ text: "30 seconds", value: 30000 }, "value": 3000
{ text: "1 minute", value: 60000 }, }, {
{ text: "2 minutes", value: 120000 }, "text": "5 seconds",
{ text: "5 minutes", value: 300000 }, "value": 5000
{ text: "10 minutes", value: 600000 } }, {
] "text": "8 seconds",
"value": 8000
}, {
"text": "10 seconds",
"value": 10000
}, {
"text": "15 seconds",
"value": 15000
}, {
"text": "30 seconds",
"value": 30000
}, {
"text": "1 minute",
"value": 60000
}, {
"text": "2 minutes",
"value": 120000
}, {
"text": "5 minutes",
"value": 300000
}, {
"text": "10 minutes",
"value": 600000
}]
function getTimeoutText(value) { function getTimeoutText(value) {
if (value === undefined || value === null || isNaN(value)) { if (value === undefined || value === null || isNaN(value)) {
return "5 seconds" return "5 seconds"
} }
for (let i = 0; i < timeoutOptions.length; i++) { for (var i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].value === value) { if (timeoutOptions[i].value === value) {
return timeoutOptions[i].text return timeoutOptions[i].text
} }
} }
if (value === 0) return "Never" if (value === 0)
if (value < 1000) return value + "ms" return "Never"
if (value < 60000) return Math.round(value / 1000) + " seconds" if (value < 1000)
return value + "ms"
if (value < 60000)
return Math.round(value / 1000) + " seconds"
return Math.round(value / 60000) + " minutes" return Math.round(value / 60000) + " minutes"
} }
Column { Column {
id: contentColumn id: contentColumn
anchors.top: parent.top anchors.top: parent.top
@@ -75,30 +103,30 @@ Rectangle {
anchors.right: parent.right anchors.right: parent.right
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
spacing: Theme.spacingM spacing: Theme.spacingM
StyledText { StyledText {
text: "Notification Settings" text: "Notification Settings"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold font.weight: Font.Bold
color: Theme.surfaceText color: Theme.surfaceText
} }
Item { Item {
width: parent.width width: parent.width
height: 36 height: 36
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { DankIcon {
name: SessionData.doNotDisturb ? "notifications_off" : "notifications" name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
size: Theme.iconSizeSmall size: Theme.iconSizeSmall
color: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText color: SessionData.doNotDisturb ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
StyledText { StyledText {
text: "Do Not Disturb" text: "Do Not Disturb"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -106,28 +134,30 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
DankToggle { DankToggle {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: SessionData.doNotDisturb checked: SessionData.doNotDisturb
onToggled: SessionData.setDoNotDisturb(!SessionData.doNotDisturb) onToggled: SessionData.setDoNotDisturb(
!SessionData.doNotDisturb)
} }
} }
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 1 height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
} }
StyledText { StyledText {
text: "Notification Timeouts" text: "Notification Timeouts"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
DankDropdown { DankDropdown {
width: parent.width width: parent.width
text: "Low Priority" text: "Low Priority"
@@ -135,15 +165,16 @@ Rectangle {
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow) currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
options: timeoutOptions.map(opt => opt.text) options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => { onValueChanged: value => {
for (let i = 0; i < timeoutOptions.length; i++) { for (var i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) { if (timeoutOptions[i].text === value) {
SettingsData.setNotificationTimeoutLow(timeoutOptions[i].value) SettingsData.setNotificationTimeoutLow(
break timeoutOptions[i].value)
} break
} }
} }
}
} }
DankDropdown { DankDropdown {
width: parent.width width: parent.width
text: "Normal Priority" text: "Normal Priority"
@@ -151,63 +182,67 @@ Rectangle {
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal) currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
options: timeoutOptions.map(opt => opt.text) options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => { onValueChanged: value => {
for (let i = 0; i < timeoutOptions.length; i++) { for (var i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) { if (timeoutOptions[i].text === value) {
SettingsData.setNotificationTimeoutNormal(timeoutOptions[i].value) SettingsData.setNotificationTimeoutNormal(
break timeoutOptions[i].value)
} break
} }
} }
}
} }
DankDropdown { DankDropdown {
width: parent.width width: parent.width
text: "Critical Priority" text: "Critical Priority"
description: "Timeout for critical priority notifications" description: "Timeout for critical priority notifications"
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical) currentValue: getTimeoutText(
SettingsData.notificationTimeoutCritical)
options: timeoutOptions.map(opt => opt.text) options: timeoutOptions.map(opt => opt.text)
onValueChanged: value => { onValueChanged: value => {
for (let i = 0; i < timeoutOptions.length; i++) { for (var i = 0; i < timeoutOptions.length; i++) {
if (timeoutOptions[i].text === value) { if (timeoutOptions[i].text === value) {
SettingsData.setNotificationTimeoutCritical(timeoutOptions[i].value) SettingsData.setNotificationTimeoutCritical(
break timeoutOptions[i].value)
} break
} }
} }
}
} }
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 1 height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
} }
Item { Item {
width: parent.width width: parent.width
height: 36 height: 36
Row { Row {
anchors.left: parent.left anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM spacing: Theme.spacingM
DankIcon { DankIcon {
name: "notifications_active" name: "notifications_active"
size: Theme.iconSizeSmall size: Theme.iconSizeSmall
color: SettingsData.notificationOverlayEnabled ? Theme.primary : Theme.surfaceText color: SettingsData.notificationOverlayEnabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Column { Column {
spacing: 2 spacing: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
text: "Notification Overlay" text: "Notification Overlay"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
} }
StyledText { StyledText {
text: "Display all priorities over fullscreen apps" text: "Display all priorities over fullscreen apps"
font.pixelSize: Theme.fontSizeSmall - 1 font.pixelSize: Theme.fontSizeSmall - 1
@@ -215,13 +250,14 @@ Rectangle {
} }
} }
} }
DankToggle { DankToggle {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.notificationOverlayEnabled checked: SettingsData.notificationOverlayEnabled
onToggled: (toggled) => SettingsData.setNotificationOverlayEnabled(toggled) onToggled: toggled => SettingsData.setNotificationOverlayEnabled(
toggled)
} }
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,280 +4,290 @@ 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)
}
target: NotificationService
}
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) property Connections notificationConnections
if (p.forceExit) {
p.forceExit()
} else if (p.destroy) {
try {
p.destroy()
} catch (e) {
notificationConnections: Connections {
function onVisibleNotificationsChanged() {
manager._sync(NotificationService.visibleNotifications)
}
target: NotificationService
}
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 (var k = 0; k < survivors.length; ++k) {
survivors[k].screenY = topMargin + k * baseNotificationHeight
}
}
if (popupWindows.length === 0)
sweeper.stop()
} }
} }
if (toRemove.length > 0) {
for (let zombie of toRemove) { function _hasWindowFor(w) {
const i = popupWindows.indexOf(zombie) return popupWindows.some(p => {
if (i !== -1) return p && p.notificationData === w
&& !p._isDestroying
&& p.status !== Component.Null
})
}
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()) {
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 expiredCandidates = activeWindows.filter(p => {
if (!p.notificationData
|| !p.notificationData.notification)
return false
if (p.notificationData.notification.urgency === 2)
return false
const timeoutMs = p.notificationData.timer ? p.notificationData.timer.interval : 5000
if (timeoutMs === 0)
return false
return !p.notificationData.timer.running
}).sort(
(a, b) => b.screenY - a.screenY)
if (expiredCandidates.length > 0) {
const toRemove = expiredCandidates[0]
if (toRemove && !toRemove.exiting) {
toRemove.notificationData.removedByLimit = true
toRemove.notificationData.popup = false
}
return
}
const timeoutCandidates = activeWindows.filter(p => {
if (!p.notificationData
|| !p.notificationData.notification)
return false
if (p.notificationData.notification.urgency === 2)
return false
const timeoutMs = p.notificationData.timer ? p.notificationData.timer.interval : 5000
return timeoutMs > 0
}).sort((a, b) => {
const aTimeout = a.notificationData.timer ? a.notificationData.timer.interval : 5000
const bTimeout = b.notificationData.timer ? b.notificationData.timer.interval : 5000
if (aTimeout !== bTimeout)
return aTimeout - bTimeout
return b.screenY - a.screenY
})
if (timeoutCandidates.length > 0) {
const toRemove = timeoutCandidates[0]
if (toRemove && !toRemove.exiting) {
toRemove.notificationData.removedByLimit = true
toRemove.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.splice(i, 1)
popupWindows = popupWindows.slice()
} }
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 (var 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) }
function cleanupAllWindows() {
sweeper.stop() sweeper.stop()
} for (let p of popupWindows.slice()) {
} if (p) {
try {
function _hasWindowFor(w) { if (p.forceExit)
return popupWindows.some(p => { p.forceExit()
return p && p.notificationData === w else if (p.destroy)
&& !p._isDestroying p.destroy()
&& p.status !== Component.Null } catch (e) {
})
}
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()) {
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 expiredCandidates = activeWindows.filter(p => {
if (!p.notificationData || !p.notificationData.notification) return false
if (p.notificationData.notification.urgency === 2) return false
const timeoutMs = p.notificationData.timer ? p.notificationData.timer.interval : 5000
if (timeoutMs === 0) return false
return !p.notificationData.timer.running
}).sort((a, b) => b.screenY - a.screenY)
if (expiredCandidates.length > 0) {
const toRemove = expiredCandidates[0]
if (toRemove && !toRemove.exiting) {
toRemove.notificationData.removedByLimit = true
toRemove.notificationData.popup = false
}
return
}
const timeoutCandidates = activeWindows.filter(p => {
if (!p.notificationData || !p.notificationData.notification) return false
if (p.notificationData.notification.urgency === 2) return false
const timeoutMs = p.notificationData.timer ? p.notificationData.timer.interval : 5000
return timeoutMs > 0
}).sort((a, b) => {
const aTimeout = a.notificationData.timer ? a.notificationData.timer.interval : 5000
const bTimeout = b.notificationData.timer ? b.notificationData.timer.interval : 5000
if (aTimeout !== bTimeout) return aTimeout - bTimeout
return b.screenY - a.screenY
})
if (timeoutCandidates.length > 0) {
const toRemove = timeoutCandidates[0]
if (toRemove && !toRemove.exiting) {
toRemove.notificationData.removedByLimit = true
toRemove.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,474 +5,479 @@ 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: {
DgopService.addRef(["cpu", "memory", "network", "disk"]) DgopService.addRef(["cpu", "memory", "network", "disk"])
} }
Component.onDestruction: { Component.onDestruction: {
DgopService.removeRef(["cpu", "memory", "network", "disk"]) DgopService.removeRef(["cpu", "memory", "network", "disk"])
} }
Rectangle { Rectangle {
width: parent.width
height: 200
radius: Theme.cornerRadius
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: 32 height: 200
spacing: Theme.spacingM radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
StyledText { Theme.surfaceVariant.b, 0.04)
text: "CPU" border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
font.pixelSize: Theme.fontSizeLarge Theme.outline.b, 0.06)
font.weight: Font.Bold border.width: 1
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 80
height: 24
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: DgopService.cpuUsage.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: DgopService.cpuCores + " 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 { Column {
width: parent.width anchors.fill: parent
spacing: 6 anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Repeater {
model: DgopService.perCoreCpuUsage
Row { Row {
width: parent.width width: parent.width
height: 20 height: 32
spacing: Theme.spacingS spacing: Theme.spacingM
StyledText { StyledText {
text: "C" + index text: "CPU"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceVariantText font.weight: Font.Bold
width: 24 color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter 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 { Rectangle {
width: parent.width * Math.min(1, modelData / 100) width: 80
height: parent.height height: 24
radius: parent.radius radius: Theme.cornerRadius
color: { color: Qt.rgba(Theme.primary.r, Theme.primary.g,
const usage = modelData Theme.primary.b, 0.12)
if (usage > 80) anchors.verticalCenter: parent.verticalCenter
return Theme.error
if (usage > 60) StyledText {
return Theme.warning text: DgopService.cpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall
return Theme.primary font.weight: Font.Bold
} color: Theme.primary
anchors.centerIn: parent
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
} }
}
} }
}
StyledText { Item {
text: modelData ? modelData.toFixed(0) + "%" : "0%" width: parent.width - 280
font.pixelSize: Theme.fontSizeSmall height: 1
font.weight: Font.Medium }
color: Theme.surfaceText
width: 32 StyledText {
horizontalAlignment: Text.AlignRight text: DgopService.cpuCores + " cores"
anchors.verticalCenter: parent.verticalCenter font.pixelSize: Theme.fontSizeSmall
} color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
} }
}
}
}
}
}
Rectangle { ScrollView {
width: parent.width width: parent.width
height: 80 height: parent.height - 40
radius: Theme.cornerRadius clip: true
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, ScrollBar.vertical.policy: ScrollBar.AsNeeded
Theme.surfaceVariant.b, 0.04) ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.06) Column {
border.width: 1 width: parent.width
spacing: 6
Repeater {
model: DgopService.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.cornerRadius
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: DgopService.formatSystemMemory(
DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(
DgopService.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: DgopService.totalMemoryKB
> 0 ? parent.width * (DgopService.usedMemoryKB
/ DgopService.totalMemoryKB) : 0
height: parent.height
radius: parent.radius
color: {
const usage = DgopService.totalMemoryKB
> 0 ? (DgopService.usedMemoryKB
/ DgopService.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: DgopService.totalMemoryKB
> 0 ? ((DgopService.usedMemoryKB
/ DgopService.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: DgopService.totalSwapKB
> 0 ? DgopService.formatSystemMemory(
DgopService.usedSwapKB) + " / "
+ DgopService.formatSystemMemory(
DgopService.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: DgopService.totalSwapKB
> 0 ? parent.width * (DgopService.usedSwapKB
/ DgopService.totalSwapKB) : 0
height: parent.height
radius: parent.radius
color: {
if (!DgopService.totalSwapKB)
return Qt.rgba(Theme.surfaceText.r,
Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
const usage = DgopService.usedSwapKB / DgopService.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: DgopService.totalSwapKB
> 0 ? ((DgopService.usedSwapKB / DgopService.totalSwapKB) * 100).toFixed(
1) + "% used" : "Not available"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
Row { Row {
anchors.centerIn: parent width: parent.width
anchors.margins: Theme.spacingM height: 80
spacing: 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: DgopService.formatSystemMemory(
DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(
DgopService.totalMemoryKB)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item {
width: Theme.spacingL
height: 1
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle { Rectangle {
width: parent.width width: (parent.width - Theme.spacingM) / 2
height: 16 height: 80
radius: 8 radius: Theme.cornerRadius
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) 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
Rectangle { Column {
width: DgopService.totalMemoryKB anchors.centerIn: parent
> 0 ? parent.width * (DgopService.usedMemoryKB spacing: Theme.spacingXS
/ DgopService.totalMemoryKB) : 0
height: parent.height
radius: parent.radius
color: {
const usage = DgopService.totalMemoryKB
> 0 ? (DgopService.usedMemoryKB
/ DgopService.totalMemoryKB) : 0
if (usage > 0.9)
return Theme.error
if (usage > 0.7) StyledText {
return Theme.warning text: "Network"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
return Theme.secondary Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
StyledText {
text: "↓"
font.pixelSize: Theme.fontSizeSmall
color: Theme.info
}
StyledText {
text: DgopService.networkRxRate
> 0 ? formatNetworkSpeed(
DgopService.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: DgopService.networkTxRate
> 0 ? formatNetworkSpeed(
DgopService.networkTxRate) : "0 B/s"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
} }
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
} }
StyledText {
text: DgopService.totalMemoryKB
> 0 ? ((DgopService.usedMemoryKB
/ DgopService.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: DgopService.totalSwapKB
> 0 ? DgopService.formatSystemMemory(
DgopService.usedSwapKB) + " / "
+ DgopService.formatSystemMemory(
DgopService.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 { Rectangle {
width: parent.width width: (parent.width - Theme.spacingM) / 2
height: 16 height: 80
radius: 8 radius: Theme.cornerRadius
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) 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
Rectangle { Column {
width: DgopService.totalSwapKB anchors.centerIn: parent
> 0 ? parent.width * (DgopService.usedSwapKB spacing: Theme.spacingXS
/ DgopService.totalSwapKB) : 0
height: parent.height
radius: parent.radius
color: {
if (!DgopService.totalSwapKB)
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
const usage = DgopService.usedSwapKB / DgopService.totalSwapKB StyledText {
if (usage > 0.9) text: "Disk"
return Theme.error font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
if (usage > 0.7) Row {
return Theme.warning spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
return Theme.info Row {
spacing: 4
StyledText {
text: "R"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
}
StyledText {
text: formatDiskSpeed(DgopService.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(DgopService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
} }
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
}
}
}
} }
StyledText {
text: DgopService.totalSwapKB
> 0 ? ((DgopService.usedSwapKB
/ DgopService.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.cornerRadius
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: DgopService.networkRxRate
> 0 ? formatNetworkSpeed(
DgopService.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: DgopService.networkTxRate
> 0 ? formatNetworkSpeed(
DgopService.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.cornerRadius
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(DgopService.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(DgopService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
}
}
} }

View File

@@ -7,238 +7,246 @@ 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" if (!processContextMenu.parent && typeof Overlay !== "undefined"
&& Overlay.overlay) && Overlay.overlay)
processContextMenu.parent = 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
} }
}
background: Rectangle { width: 180
color: "transparent" height: menuColumn.implicitHeight + Theme.spacingS * 2
} padding: 0
modal: false
closePolicy: Popup.CloseOnEscape
onClosed: {
closePolicy = Popup.CloseOnEscape
}
onOpened: {
outsideClickTimer.start()
}
contentItem: Rectangle { Timer {
id: menuContent id: outsideClickTimer
color: Theme.popupBackground() interval: 100
radius: Theme.cornerRadius onTriggered: {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, processContextMenu.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
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.cornerRadius
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 { background: Rectangle {
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.cornerRadius
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"
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.cornerRadius
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.cornerRadius
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()
}
}
}
} }
}
contentItem: Rectangle {
id: menuContent
color: Theme.popupBackground()
radius: Theme.cornerRadius
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.cornerRadius
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.cornerRadius
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.cornerRadius
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.cornerRadius
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,227 +4,229 @@ 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.cornerRadius radius: Theme.cornerRadius
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g, Theme.primary.g,
Theme.primary.b, 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 {
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: DgopService.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: DgopService.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: DgopService.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" 0.08) : "transparent"
anchors.right: parent.right border.color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r,
anchors.verticalCenter: parent.verticalCenter Theme.primary.g,
Theme.primary.b,
0.12) : "transparent"
border.width: 1
DankIcon { MouseArea {
name: "more_vert" id: processMouseArea
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
onClicked: { acceptedButtons: Qt.LeftButton | Qt.RightButton
if (process && process.pid > 0 && contextMenu) { onClicked: mouse => {
contextMenu.processData = process if (mouse.button === Qt.RightButton) {
let globalPos = menuButtonArea.mapToGlobal( if (process && process.pid > 0 && contextMenu) {
menuButtonArea.width / 2, menuButtonArea.height) contextMenu.processData = process
let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal( let globalPos = processMouseArea.mapToGlobal(
globalPos.x, mouse.x, mouse.y)
globalPos.y) : globalPos let localPos = contextMenu.parent ? contextMenu.parent.mapFromGlobal(
contextMenu.show(localPos.x, localPos.y) 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: DgopService.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: DgopService.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: DgopService.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
}
}
} }
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
} }
}
} }

View File

@@ -12,125 +12,127 @@ import qs.Services
import qs.Widgets import qs.Widgets
DankPopout { DankPopout {
id: processListPopout id: processListPopout
property var parentWidget: null property var parentWidget: null
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() {
close()
if (processContextMenu.visible)
processContextMenu.close()
}
function show() {
open()
}
popupWidth: 600
popupHeight: 600
triggerX: Screen.width - 600 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
triggerWidth: 55
positioning: "center"
WlrLayershell.namespace: "quickshell-processlist"
screen: triggerScreen
visible: shouldBeVisible
shouldBeVisible: false
Ref {
service: DgopService
}
content: Component {
Rectangle {
id: processListContent
radius: Theme.cornerRadius
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.shouldBeVisible)
forceActiveFocus()
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
processListPopout.close()
event.accepted = true
}
}
Connections {
function onShouldBeVisibleChanged() {
if (processListPopout.shouldBeVisible)
Qt.callLater(function () {
processListContent.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.cornerRadius
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.cornerRadius
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 { function hide() {
id: processContextMenu close()
} if (processContextMenu.visible)
} processContextMenu.close()
}
function show() {
open()
}
popupWidth: 600
popupHeight: 600
triggerX: Screen.width - 600 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.topBarSpacing + Theme.spacingXS
triggerWidth: 55
positioning: "center"
WlrLayershell.namespace: "quickshell-processlist"
screen: triggerScreen
visible: shouldBeVisible
shouldBeVisible: false
Ref {
service: DgopService
}
content: Component {
Rectangle {
id: processListContent
radius: Theme.cornerRadius
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.shouldBeVisible)
forceActiveFocus()
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
processListPopout.close()
event.accepted = true
}
}
Connections {
function onShouldBeVisibleChanged() {
if (processListPopout.shouldBeVisible)
Qt.callLater(function () {
processListContent.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.cornerRadius
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.cornerRadius
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,257 +5,266 @@ 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: {
DgopService.addRef(["processes"]) DgopService.addRef(["processes"])
} }
Component.onDestruction: { Component.onDestruction: {
DgopService.removeRef(["processes"]) DgopService.removeRef(["processes"])
} }
Item { Item {
id: columnHeaders id: columnHeaders
width: parent.width width: parent.width
anchors.leftMargin: 8 anchors.leftMargin: 8
height: 24 height: 24
Rectangle { Rectangle {
width: 60 width: 60
height: 20 height: 20
color: { color: {
if (DgopService.currentSort === "name") { if (DgopService.currentSort === "name") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
}
return 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: DgopService.currentSort === "name" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "name" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: processHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("name")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
} }
return processHeaderArea.containsMouse ? Qt.rgba(Theme.surfaceText.r,
Rectangle {
width: 80
height: 20
color: {
if (DgopService.currentSort === "cpu") {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
}
return cpuHeaderArea.containsMouse ? Qt.rgba(
Theme.surfaceText.r,
Theme.surfaceText.g, Theme.surfaceText.g,
Theme.surfaceText.b, Theme.surfaceText.b,
0.08) : "transparent" 0.08) : "transparent"
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
anchors.left: parent.left anchors.right: parent.right
anchors.leftMargin: 0 anchors.rightMargin: 200
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
text: "Process" text: "CPU"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
font.weight: DgopService.currentSort === "name" ? Font.Bold : Font.Medium font.weight: DgopService.currentSort === "cpu" ? Font.Bold : Font.Medium
color: Theme.surfaceText color: Theme.surfaceText
opacity: DgopService.currentSort === "name" ? 1 : 0.7 opacity: DgopService.currentSort === "cpu" ? 1 : 0.7
anchors.centerIn: parent anchors.centerIn: parent
} }
MouseArea { MouseArea {
id: processHeaderArea id: cpuHeaderArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
DgopService.setSortBy("name") DgopService.setSortBy("cpu")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
} }
}
Behavior on color { Rectangle {
ColorAnimation { width: 80
duration: Theme.shortDuration height: 20
color: {
if (DgopService.currentSort === "memory") {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
}
return 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: DgopService.currentSort === "memory" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "memory" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: memoryHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("memory")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 50
height: 20
color: {
if (DgopService.currentSort === "pid") {
return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
}
return 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: DgopService.currentSort === "pid" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "pid" ? 1 : 0.7
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
MouseArea {
id: pidHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.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: DgopService.sortDescending ? "↓" : "↑"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: sortOrderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// ! TODO - we lost this with dgop
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
} }
}
} }
Rectangle { DankListView {
width: 80 id: processListView
height: 20
color: { property string keyRoleName: "pid"
if (DgopService.currentSort === "cpu") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) width: parent.width
height: parent.height - columnHeaders.height
clip: true
spacing: 4
model: DgopService.processes
delegate: ProcessListItem {
process: modelData
contextMenu: root.contextMenu
} }
return 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: DgopService.currentSort === "cpu" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "cpu" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: cpuHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("cpu")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
} }
Rectangle {
width: 80
height: 20
color: {
if (DgopService.currentSort === "memory") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
}
return 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: DgopService.currentSort === "memory" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "memory" ? 1 : 0.7
anchors.centerIn: parent
}
MouseArea {
id: memoryHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.setSortBy("memory")
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
Rectangle {
width: 50
height: 20
color: {
if (DgopService.currentSort === "pid") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
}
return 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: DgopService.currentSort === "pid" ? Font.Bold : Font.Medium
color: Theme.surfaceText
opacity: DgopService.currentSort === "pid" ? 1 : 0.7
horizontalAlignment: Text.AlignHCenter
anchors.centerIn: parent
}
MouseArea {
id: pidHeaderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
DgopService.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: DgopService.sortDescending ? "↓" : "↑"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: sortOrderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
// ! TODO - we lost this with dgop
}
}
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: DgopService.processes
delegate: ProcessListItem {
process: modelData
contextMenu: root.contextMenu
}
}
} }

View File

@@ -5,24 +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 { ProcessContextMenu {
id: localContextMenu id: localContextMenu
} }
} }

View File

@@ -8,10 +8,10 @@ Row {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
Component.onCompleted: { Component.onCompleted: {
DgopService.addRef(["cpu", "memory", "system"]); DgopService.addRef(["cpu", "memory", "system"])
} }
Component.onDestruction: { Component.onDestruction: {
DgopService.removeRef(["cpu", "memory", "system"]); DgopService.removeRef(["cpu", "memory", "system"])
} }
Rectangle { Rectangle {
@@ -20,13 +20,22 @@ Row {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (DgopService.sortBy === "cpu") if (DgopService.sortBy === "cpu")
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16); return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.16)
else if (cpuCardMouseArea.containsMouse) else if (cpuCardMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12); return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.12)
else else
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08); return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.08)
} }
border.color: DgopService.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.color: DgopService.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: DgopService.sortBy === "cpu" ? 2 : 1 border.width: DgopService.sortBy === "cpu" ? 2 : 1
MouseArea { MouseArea {
@@ -57,7 +66,8 @@ Row {
StyledText { StyledText {
text: { text: {
if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null) if (DgopService.cpuUsage === undefined
|| DgopService.cpuUsage === null)
return "--%" return "--%"
return DgopService.cpuUsage.toFixed(1) + "%" return DgopService.cpuUsage.toFixed(1) + "%"
} }
@@ -71,32 +81,34 @@ Row {
Rectangle { Rectangle {
width: 1 width: 1
height: 20 height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
StyledText { StyledText {
text: { text: {
if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature <= 0) if (DgopService.cpuTemperature === undefined
return "--°"; || DgopService.cpuTemperature === null
|| DgopService.cpuTemperature <= 0)
return "--°"
return Math.round(DgopService.cpuTemperature) + "°"; return Math.round(DgopService.cpuTemperature) + "°"
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
font.weight: Font.Medium font.weight: Font.Medium
color: { color: {
if (DgopService.cpuTemperature > 80) if (DgopService.cpuTemperature > 80)
return Theme.error; return Theme.error
if (DgopService.cpuTemperature > 60) if (DgopService.cpuTemperature > 60)
return Theme.warning; return Theme.warning
return Theme.surfaceText; return Theme.surfaceText
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
StyledText { StyledText {
@@ -106,23 +118,19 @@ Row {
color: Theme.surfaceText color: Theme.surfaceText
opacity: 0.7 opacity: 0.7
} }
} }
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
} }
} }
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
} }
} }
} }
Rectangle { Rectangle {
@@ -131,13 +139,24 @@ Row {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (DgopService.sortBy === "memory") if (DgopService.sortBy === "memory")
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16); return Qt.rgba(Theme.primary.r, Theme.primary.g,
Theme.primary.b, 0.16)
else if (memoryCardMouseArea.containsMouse) else if (memoryCardMouseArea.containsMouse)
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12); return Qt.rgba(Theme.secondary.r, Theme.secondary.g,
Theme.secondary.b, 0.12)
else else
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08); return Qt.rgba(Theme.secondary.r, Theme.secondary.g,
Theme.secondary.b, 0.08)
} }
border.color: DgopService.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.color: DgopService.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: DgopService.sortBy === "memory" ? 2 : 1 border.width: DgopService.sortBy === "memory" ? 2 : 1
MouseArea { MouseArea {
@@ -167,7 +186,8 @@ Row {
spacing: Theme.spacingS spacing: Theme.spacingS
StyledText { StyledText {
text: DgopService.formatSystemMemory(DgopService.usedMemoryKB) text: DgopService.formatSystemMemory(
DgopService.usedMemoryKB)
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
font.weight: Font.Bold font.weight: Font.Bold
@@ -178,13 +198,15 @@ Row {
Rectangle { Rectangle {
width: 1 width: 1
height: 20 height: 20
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
Theme.surfaceText.b, 0.3)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: DgopService.totalSwapKB > 0 visible: DgopService.totalSwapKB > 0
} }
StyledText { StyledText {
text: DgopService.totalSwapKB > 0 ? DgopService.formatSystemMemory(DgopService.usedSwapKB) : "" text: DgopService.totalSwapKB > 0 ? DgopService.formatSystemMemory(
DgopService.usedSwapKB) : ""
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
font.weight: Font.Medium font.weight: Font.Medium
@@ -192,38 +214,35 @@ Row {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: DgopService.totalSwapKB > 0 visible: DgopService.totalSwapKB > 0
} }
} }
StyledText { StyledText {
text: { text: {
if (DgopService.totalSwapKB > 0) if (DgopService.totalSwapKB > 0)
return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB) + " + swap"; return "of " + DgopService.formatSystemMemory(
DgopService.totalMemoryKB) + " + swap"
return "of " + DgopService.formatSystemMemory(DgopService.totalMemoryKB); return "of " + DgopService.formatSystemMemory(
DgopService.totalMemoryKB)
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
color: Theme.surfaceText color: Theme.surfaceText
opacity: 0.7 opacity: 0.7
} }
} }
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
} }
} }
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
} }
} }
} }
Rectangle { Rectangle {
@@ -231,48 +250,74 @@ Row {
height: 80 height: 80
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { if (!DgopService.availableGpus
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) || DgopService.availableGpus.length === 0) {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16); if (gpuCardMouseArea.containsMouse
&& DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.16)
else else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); return Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
} }
var gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; var gpu = DgopService.availableGpus[Math.min(
var vendor = gpu.vendor.toLowerCase(); SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
var vendor = gpu.vendor.toLowerCase()
if (vendor.includes("nvidia")) { if (vendor.includes("nvidia")) {
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) if (gpuCardMouseArea.containsMouse
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.2); && DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.success.r, Theme.success.g,
Theme.success.b, 0.2)
else else
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12); return Qt.rgba(Theme.success.r, Theme.success.g,
Theme.success.b, 0.12)
} else if (vendor.includes("amd")) { } else if (vendor.includes("amd")) {
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) if (gpuCardMouseArea.containsMouse
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2); && DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.2)
else else
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); return Qt.rgba(Theme.error.r, Theme.error.g,
Theme.error.b, 0.12)
} else if (vendor.includes("intel")) { } else if (vendor.includes("intel")) {
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) if (gpuCardMouseArea.containsMouse
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.2); && DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.info.r, Theme.info.g,
Theme.info.b, 0.2)
else else
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.12); return Qt.rgba(Theme.info.r, Theme.info.g,
Theme.info.b, 0.12)
} }
if (gpuCardMouseArea.containsMouse && DgopService.availableGpus.length > 1) if (gpuCardMouseArea.containsMouse
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.16); && DgopService.availableGpus.length > 1)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.16)
else else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08); return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.08)
} }
border.color: { border.color: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) if (!DgopService.availableGpus
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2); || DgopService.availableGpus.length === 0)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
var gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; var gpu = DgopService.availableGpus[Math.min(
var vendor = gpu.vendor.toLowerCase(); SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
var vendor = gpu.vendor.toLowerCase()
if (vendor.includes("nvidia")) if (vendor.includes("nvidia"))
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3); return Qt.rgba(Theme.success.r, Theme.success.g,
Theme.success.b, 0.3)
else if (vendor.includes("amd")) else if (vendor.includes("amd"))
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3); return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3)
else if (vendor.includes("intel")) else if (vendor.includes("intel"))
return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3); return Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.3)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2); return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
} }
border.width: 1 border.width: 1
@@ -282,17 +327,19 @@ Row {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: DgopService.availableGpus.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: DgopService.availableGpus.length
onClicked: (mouse) => { > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
if (mouse.button === Qt.LeftButton) { onClicked: mouse => {
if (DgopService.availableGpus.length > 1) { if (mouse.button === Qt.LeftButton) {
var nextIndex = (SessionData.selectedGpuIndex + 1) % DgopService.availableGpus.length; if (DgopService.availableGpus.length > 1) {
SessionData.setSelectedGpuIndex(nextIndex); var nextIndex = (SessionData.selectedGpuIndex + 1)
} % DgopService.availableGpus.length
} else if (mouse.button === Qt.RightButton) { SessionData.setSelectedGpuIndex(nextIndex)
gpuContextMenu.popup(); }
} } else if (mouse.button === Qt.RightButton) {
} gpuContextMenu.popup()
}
}
} }
Column { Column {
@@ -311,52 +358,69 @@ Row {
StyledText { StyledText {
text: { text: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) if (!DgopService.availableGpus
return "No GPU"; || DgopService.availableGpus.length === 0)
return "No GPU"
var gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; var gpu = DgopService.availableGpus[Math.min(
SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
// Check if temperature monitoring is enabled for this GPU // Check if temperature monitoring is enabled for this GPU
var tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1; var tempEnabled = SessionData.enabledGpuPciIds
var temp = gpu.temperature; && SessionData.enabledGpuPciIds.indexOf(
var hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0; gpu.pciId) !== -1
var temp = gpu.temperature
var hasTemp = tempEnabled && temp !== undefined
&& temp !== null && temp !== 0
if (hasTemp) if (hasTemp)
return Math.round(temp) + "°"; return Math.round(temp) + "°"
else else
return gpu.vendor; return gpu.vendor
} }
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
font.weight: Font.Bold font.weight: Font.Bold
color: { color: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) if (!DgopService.availableGpus
return Theme.surfaceText; || DgopService.availableGpus.length === 0)
return Theme.surfaceText
var gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; var gpu = DgopService.availableGpus[Math.min(
var tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1; SessionData.selectedGpuIndex,
var temp = gpu.temperature || 0; DgopService.availableGpus.length - 1)]
var tempEnabled = SessionData.enabledGpuPciIds
&& SessionData.enabledGpuPciIds.indexOf(
gpu.pciId) !== -1
var temp = gpu.temperature || 0
if (tempEnabled && temp > 80) if (tempEnabled && temp > 80)
return Theme.error; return Theme.error
if (tempEnabled && temp > 60) if (tempEnabled && temp > 60)
return Theme.warning; return Theme.warning
return Theme.surfaceText; return Theme.surfaceText
} }
} }
StyledText { StyledText {
text: { text: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) if (!DgopService.availableGpus
return "No GPUs detected"; || DgopService.availableGpus.length === 0)
return "No GPUs detected"
var gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)]; var gpu = DgopService.availableGpus[Math.min(
var tempEnabled = SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1; SessionData.selectedGpuIndex,
var temp = gpu.temperature; DgopService.availableGpus.length - 1)]
var hasTemp = tempEnabled && temp !== undefined && temp !== null && temp !== 0; var tempEnabled = SessionData.enabledGpuPciIds
&& SessionData.enabledGpuPciIds.indexOf(
gpu.pciId) !== -1
var temp = gpu.temperature
var hasTemp = tempEnabled && temp !== undefined
&& temp !== null && temp !== 0
if (hasTemp) if (hasTemp)
return gpu.vendor + " " + gpu.displayName; return gpu.vendor + " " + gpu.displayName
else else
return gpu.displayName; return gpu.displayName
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.family: SettingsData.monoFontFamily font.family: SettingsData.monoFontFamily
@@ -366,7 +430,6 @@ Row {
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1
} }
} }
Menu { Menu {
@@ -376,26 +439,36 @@ Row {
text: "Enable GPU Temperature" text: "Enable GPU Temperature"
checkable: true checkable: true
checked: { checked: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return false return false
} }
var gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)] var gpu = DgopService.availableGpus[Math.min(
if (!gpu.pciId) return false SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(gpu.pciId) !== -1 : false if (!gpu.pciId)
return false
return SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.indexOf(
gpu.pciId) !== -1 : false
} }
onTriggered: { onTriggered: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) { if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0) {
return return
} }
var gpu = DgopService.availableGpus[Math.min(SessionData.selectedGpuIndex, DgopService.availableGpus.length - 1)] var gpu = DgopService.availableGpus[Math.min(
if (!gpu.pciId) return SessionData.selectedGpuIndex,
DgopService.availableGpus.length - 1)]
var enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice() : [] if (!gpu.pciId)
return
var enabledIds = SessionData.enabledGpuPciIds ? SessionData.enabledGpuPciIds.slice(
) : []
var index = enabledIds.indexOf(gpu.pciId) var index = enabledIds.indexOf(gpu.pciId)
if (checked && index === -1) { if (checked && index === -1) {
enabledIds.push(gpu.pciId) enabledIds.push(gpu.pciId)
DgopService.addGpuPciId(gpu.pciId) DgopService.addGpuPciId(gpu.pciId)
@@ -403,7 +476,7 @@ Row {
enabledIds.splice(index, 1) enabledIds.splice(index, 1)
DgopService.removeGpuPciId(gpu.pciId) DgopService.removeGpuPciId(gpu.pciId)
} }
SessionData.setEnabledGpuPciIds(enabledIds) SessionData.setEnabledGpuPciIds(enabledIds)
} }
} }
@@ -413,9 +486,6 @@ Row {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
} }
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -24,8 +24,10 @@ Item {
width: parent.width width: parent.width
height: asciiSection.implicitHeight + Theme.spacingL * 2 height: asciiSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -41,7 +43,7 @@ Item {
StyledText { StyledText {
id: asciiText id: asciiText
text: "██████╗ █████╗ ███╗ ██╗██╗ ██╗\n██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝\n██║ ██║███████║██╔██╗ ██║█████╔╝ \n██║ ██║██╔══██║██║╚██╗██║██╔═██╗ \n██████╔╝██║ ██║██║ ╚████║██║ ██╗\n╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝" text: "██████╗ █████╗ ███╗ ██╗██╗ ██╗\n██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝\n██║ ██║███████║██╔██╗ ██║█████╔╝ \n██║ ██║██╔══██║██║╚██╗██║██╔═██╗ \n██████╔╝██║ ██║██║ ╚████║██║ ██╗\n╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝"
isMonospace: true isMonospace: true
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -63,7 +65,7 @@ Item {
text: "A desktop shell built with <a href=\"https://quickshell.org\">Quickshell</a>" text: "A desktop shell built with <a href=\"https://quickshell.org\">Quickshell</a>"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
linkColor: Theme.primary linkColor: Theme.primary
onLinkActivated: (url) => Qt.openUrlExternally(url) onLinkActivated: url => Qt.openUrlExternally(url)
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
width: parent.width width: parent.width
@@ -76,9 +78,7 @@ Item {
propagateComposedEvents: true propagateComposedEvents: true
} }
} }
} }
} }
// Project Information // Project Information
@@ -86,8 +86,10 @@ Item {
width: parent.width width: parent.width
height: projectSection.implicitHeight + Theme.spacingL * 2 height: projectSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -115,16 +117,15 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
StyledText { StyledText {
text: `DankMaterialShell is a modern desktop inspired by <a href="https://m3.material.io/">MUI 3</a>. text: `DankMaterialShell is a modern desktop inspired by <a href="https://m3.material.io/">MUI 3</a>.
<br /><br/>The goal is to provide a high level of functionality and customization so that it can be a suitable replacement for complete desktop environments like Gnome, KDE, or Cosmic. <br /><br/>The goal is to provide a high level of functionality and customization so that it can be a suitable replacement for complete desktop environments like Gnome, KDE, or Cosmic.
` `
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
linkColor: Theme.primary linkColor: Theme.primary
onLinkActivated: (url) => Qt.openUrlExternally(url) onLinkActivated: url => Qt.openUrlExternally(url)
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
width: parent.width width: parent.width
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -137,7 +138,6 @@ Item {
} }
} }
} }
} }
// Technical Details // Technical Details
@@ -145,8 +145,10 @@ Item {
width: parent.width width: parent.width
height: techSection.implicitHeight + Theme.spacingL * 2 height: techSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -174,7 +176,6 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Grid { Grid {
@@ -234,7 +235,7 @@ Item {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
linkColor: Theme.primary linkColor: Theme.primary
onLinkActivated: (url) => Qt.openUrlExternally(url) onLinkActivated: url => Qt.openUrlExternally(url)
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@@ -256,7 +257,7 @@ Item {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
linkColor: Theme.primary linkColor: Theme.primary
onLinkActivated: (url) => Qt.openUrlExternally(url) onLinkActivated: url => Qt.openUrlExternally(url)
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@@ -265,15 +266,9 @@ Item {
propagateComposedEvents: true propagateComposedEvents: true
} }
} }
} }
} }
} }
} }
} }
}
}

View File

@@ -5,228 +5,219 @@ import qs.Common
import qs.Widgets import qs.Widgets
Item { Item {
id: dockTab id: dockTab
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Theme.spacingL anchors.topMargin: Theme.spacingL
clip: true clip: true
contentHeight: mainColumn.height contentHeight: mainColumn.height
contentWidth: width contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Enable Dock
StyledRect {
width: parent.width
height: enableDockSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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 { Column {
id: enableDockSection id: mainColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingXL
DankIcon { // Enable Dock
name: "dock_to_bottom" StyledRect {
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - enableToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Show Dock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Display a dock at the bottom of the screen with pinned and running applications"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width width: parent.width
} height: enableDockSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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: enableDockSection
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
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- enableToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Show Dock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Display a dock at the bottom of the screen with pinned and running applications"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: enableToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.showDock
onToggled: checked => {
SettingsData.setShowDock(checked)
}
}
}
}
} }
DankToggle { // Auto-hide Dock
id: enableToggle StyledRect {
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.showDock
onToggled: checked => {
SettingsData.setShowDock(checked)
}
}
}
}
}
// Auto-hide Dock
StyledRect {
width: parent.width
height: autoHideSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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
visible: SettingsData.showDock
opacity: visible ? 1 : 0
Column {
id: autoHideSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "visibility_off"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM - autoHideToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Auto-hide Dock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Hide the dock when not in use and reveal it when hovering near the bottom of the screen"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width width: parent.width
} height: autoHideSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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
visible: SettingsData.showDock
opacity: visible ? 1 : 0
Column {
id: autoHideSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "visibility_off"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: parent.width - Theme.iconSize - Theme.spacingM
- autoHideToggle.width - Theme.spacingM
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: "Auto-hide Dock"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: "Hide the dock when not in use and reveal it when hovering near the bottom of the screen"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: parent.width
}
}
DankToggle {
id: autoHideToggle
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.dockAutoHide
onToggled: checked => {
SettingsData.setDockAutoHide(checked)
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
} }
DankToggle { // Dock Transparency Section
id: autoHideToggle StyledRect {
width: parent.width
height: transparencySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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
visible: SettingsData.showDock
opacity: visible ? 1 : 0
anchors.verticalCenter: parent.verticalCenter Column {
checked: SettingsData.dockAutoHide id: transparencySection
onToggled: checked => {
SettingsData.setDockAutoHide(checked) anchors.fill: parent
} anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "opacity"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Dock Transparency"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankSlider {
width: parent.width
height: 32
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
}
}
} }
}
} }
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Dock Transparency Section
StyledRect {
width: parent.width
height: transparencySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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
visible: SettingsData.showDock
opacity: visible ? 1 : 0
Column {
id: transparencySection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "opacity"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Dock Transparency"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankSlider {
width: parent.width
height: 32
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
}
}
}
} }
}
} }

View File

@@ -17,56 +17,75 @@ Item {
property bool fontsEnumerated: false property bool fontsEnumerated: false
function enumerateFonts() { function enumerateFonts() {
var fonts = ["Default"]; var fonts = ["Default"]
var availableFonts = Qt.fontFamilies(); var availableFonts = Qt.fontFamilies()
var rootFamilies = []; var rootFamilies = []
var seenFamilies = new Set(); var seenFamilies = new Set()
for (var i = 0; i < availableFonts.length; i++) { for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i]; var fontName = availableFonts[i]
if (fontName.startsWith(".")) if (fontName.startsWith("."))
continue; continue
if (fontName === SettingsData.defaultFontFamily) if (fontName === SettingsData.defaultFontFamily)
continue; continue
var rootName = fontName.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, function(match, suffix) { var rootName = fontName.replace(
return match; / (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
}).trim(); "").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i,
function (match, suffix) {
return match
}).trim()
if (!seenFamilies.has(rootName) && rootName !== "") { if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName); seenFamilies.add(rootName)
rootFamilies.push(rootName); rootFamilies.push(rootName)
} }
} }
cachedFontFamilies = fonts.concat(rootFamilies.sort()); cachedFontFamilies = fonts.concat(rootFamilies.sort())
var monoFonts = ["Default"]; var monoFonts = ["Default"]
var monoFamilies = []; var monoFamilies = []
var seenMonoFamilies = new Set(); var seenMonoFamilies = new Set()
for (var j = 0; j < availableFonts.length; j++) { for (var j = 0; j < availableFonts.length; j++) {
var fontName2 = availableFonts[j]; var fontName2 = availableFonts[j]
if (fontName2.startsWith(".")) if (fontName2.startsWith("."))
continue; continue
if (fontName2 === SettingsData.defaultMonoFontFamily) if (fontName2 === SettingsData.defaultMonoFontFamily)
continue; continue
var lowerName = fontName2.toLowerCase(); var lowerName = fontName2.toLowerCase()
if (lowerName.includes("mono") || lowerName.includes("code") || lowerName.includes("console") || lowerName.includes("terminal") || lowerName.includes("courier") || lowerName.includes("dejavu sans mono") || lowerName.includes("jetbrains") || lowerName.includes("fira") || lowerName.includes("hack") || lowerName.includes("source code") || lowerName.includes("ubuntu mono") || lowerName.includes("cascadia")) { if (lowerName.includes("mono") || lowerName.includes(
var rootName2 = fontName2.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").trim(); "code") || lowerName.includes(
"console") || lowerName.includes(
"terminal") || lowerName.includes(
"courier") || lowerName.includes(
"dejavu sans mono") || lowerName.includes(
"jetbrains") || lowerName.includes(
"fira") || lowerName.includes(
"hack") || lowerName.includes(
"source code") || lowerName.includes(
"ubuntu mono") || lowerName.includes("cascadia")) {
var rootName2 = fontName2.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").trim()
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") { if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
seenMonoFamilies.add(rootName2); seenMonoFamilies.add(rootName2)
monoFamilies.push(rootName2); monoFamilies.push(rootName2)
} }
} }
} }
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort()); cachedMonoFamilies = monoFonts.concat(monoFamilies.sort())
} }
Component.onCompleted: { Component.onCompleted: {
// Access WallpaperCyclingService to ensure it's initialized // Access WallpaperCyclingService to ensure it's initialized
WallpaperCyclingService.cyclingActive; WallpaperCyclingService.cyclingActive
if (!fontsEnumerated) { if (!fontsEnumerated) {
enumerateFonts(); enumerateFonts()
fontsEnumerated = true; fontsEnumerated = true
} }
} }
@@ -83,14 +102,15 @@ Item {
width: parent.width width: parent.width
spacing: Theme.spacingXL spacing: Theme.spacingXL
// Wallpaper Section // Wallpaper Section
StyledRect { StyledRect {
width: parent.width width: parent.width
height: wallpaperSection.implicitHeight + Theme.spacingL * 2 height: wallpaperSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -118,7 +138,6 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Row { Row {
@@ -148,7 +167,6 @@ Item {
maskThresholdMin: 0.5 maskThresholdMin: 0.5
maskSpreadAtMin: 1 maskSpreadAtMin: 1
} }
} }
Rectangle { Rectangle {
@@ -169,7 +187,6 @@ Item {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
visible: SessionData.wallpaperPath === "" visible: SessionData.wallpaperPath === ""
} }
} }
Column { Column {
@@ -178,7 +195,9 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
StyledText { StyledText {
text: SessionData.wallpaperPath ? SessionData.wallpaperPath.split('/').pop() : "No wallpaper selected" text: SessionData.wallpaperPath ? SessionData.wallpaperPath.split(
'/').pop(
) : "No wallpaper selected"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
elide: Text.ElideMiddle elide: Text.ElideMiddle
@@ -222,7 +241,6 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { MouseArea {
@@ -230,13 +248,12 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (parentModal) { if (parentModal) {
parentModal.allowFocusOverride = true; parentModal.allowFocusOverride = true
parentModal.shouldHaveFocus = false; parentModal.shouldHaveFocus = false
} }
wallpaperBrowser.open(); wallpaperBrowser.open()
} }
} }
} }
StyledRect { StyledRect {
@@ -263,7 +280,6 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { MouseArea {
@@ -271,16 +287,12 @@ Item {
enabled: SessionData.wallpaperPath !== "" enabled: SessionData.wallpaperPath !== ""
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: { onClicked: {
SessionData.setWallpaper(""); SessionData.setWallpaper("")
} }
} }
} }
} }
} }
} }
// Wallpaper Cycling Section - Full Width // Wallpaper Cycling Section - Full Width
@@ -309,7 +321,8 @@ Item {
} }
Column { Column {
width: parent.width - Theme.iconSize - Theme.spacingM - controlsRow.width - Theme.spacingM width: parent.width - Theme.iconSize - Theme.spacingM
- controlsRow.width - Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -326,7 +339,6 @@ Item {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
width: parent.width width: parent.width
} }
} }
Row { Row {
@@ -339,7 +351,11 @@ Item {
width: 60 width: 60
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: prevButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8) : Theme.primary color: prevButtonArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.8) : Theme.primary
opacity: SessionData.wallpaperPath ? 1 : 0.5 opacity: SessionData.wallpaperPath ? 1 : 0.5
Row { Row {
@@ -359,7 +375,6 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { MouseArea {
@@ -370,17 +385,20 @@ Item {
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
hoverEnabled: true hoverEnabled: true
onClicked: { onClicked: {
WallpaperCyclingService.cyclePrevManually(); WallpaperCyclingService.cyclePrevManually()
} }
} }
} }
StyledRect { StyledRect {
width: 60 width: 60
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: nextButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8) : Theme.primary color: nextButtonArea.containsMouse ? Qt.rgba(
Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.8) : Theme.primary
opacity: SessionData.wallpaperPath ? 1 : 0.5 opacity: SessionData.wallpaperPath ? 1 : 0.5
Row { Row {
@@ -400,7 +418,6 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { MouseArea {
@@ -411,10 +428,9 @@ Item {
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
hoverEnabled: true hoverEnabled: true
onClicked: { onClicked: {
WallpaperCyclingService.cycleNextManually(); WallpaperCyclingService.cycleNextManually()
} }
} }
} }
DankToggle { DankToggle {
@@ -422,13 +438,12 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: SessionData.wallpaperCyclingEnabled checked: SessionData.wallpaperCyclingEnabled
onToggled: (toggled) => { onToggled: toggled => {
return SessionData.setWallpaperCyclingEnabled(toggled); return SessionData.setWallpaperCyclingEnabled(
} toggled)
}
} }
} }
} }
// Cycling mode and settings // Cycling mode and settings
@@ -455,16 +470,17 @@ Item {
width: 200 width: 200
height: 32 height: 32
model: [{ model: [{
"text": "Interval" "text": "Interval"
}, { }, {
"text": "Time" "text": "Time"
}] }]
currentIndex: SessionData.wallpaperCyclingMode === "time" ? 1 : 0 currentIndex: SessionData.wallpaperCyclingMode
onTabClicked: (index) => { === "time" ? 1 : 0
SessionData.setWallpaperCyclingMode(index === 1 ? "time" : "interval"); onTabClicked: index => {
} SessionData.setWallpaperCyclingMode(
index === 1 ? "time" : "interval")
}
} }
} }
// Interval settings // Interval settings
@@ -478,16 +494,18 @@ Item {
description: "How often to change wallpaper" description: "How often to change wallpaper"
options: intervalOptions options: intervalOptions
currentValue: { currentValue: {
const currentSeconds = SessionData.wallpaperCyclingInterval; const currentSeconds = SessionData.wallpaperCyclingInterval
const index = intervalValues.indexOf(currentSeconds); const index = intervalValues.indexOf(
return index >= 0 ? intervalOptions[index] : "5 minutes"; currentSeconds)
} return index >= 0 ? intervalOptions[index] : "5 minutes"
onValueChanged: (value) => {
const index = intervalOptions.indexOf(value);
if (index >= 0)
SessionData.setWallpaperCyclingInterval(intervalValues[index]);
} }
onValueChanged: value => {
const index = intervalOptions.indexOf(
value)
if (index >= 0)
SessionData.setWallpaperCyclingInterval(
intervalValues[index])
}
} }
// Time settings // Time settings
@@ -512,25 +530,28 @@ Item {
topPadding: Theme.spacingS topPadding: Theme.spacingS
bottomPadding: Theme.spacingS bottomPadding: Theme.spacingS
onAccepted: { onAccepted: {
var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(
text)
if (isValid) if (isValid)
SessionData.setWallpaperCyclingTime(text); SessionData.setWallpaperCyclingTime(
text)
else else
text = SessionData.wallpaperCyclingTime; text = SessionData.wallpaperCyclingTime
} }
onEditingFinished: { onEditingFinished: {
var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(text); var isValid = /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/.test(
text)
if (isValid) if (isValid)
SessionData.setWallpaperCyclingTime(text); SessionData.setWallpaperCyclingTime(
text)
else else
text = SessionData.wallpaperCyclingTime; text = SessionData.wallpaperCyclingTime
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
validator: RegularExpressionValidator { validator: RegularExpressionValidator {
regularExpression: /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/ regularExpression: /^([0-1][0-9]|2[0-3]):[0-5][0-9]$/
} }
} }
StyledText { StyledText {
@@ -539,15 +560,10 @@ Item {
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
} }
} }
} }
} }
// Dynamic Theme Section // Dynamic Theme Section
@@ -555,8 +571,10 @@ Item {
width: parent.width width: parent.width
height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2 height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -578,7 +596,8 @@ Item {
} }
Column { Column {
width: parent.width - Theme.iconSize - Theme.spacingM - toggle.width - Theme.spacingM width: parent.width - Theme.iconSize - Theme.spacingM
- toggle.width - Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -596,7 +615,6 @@ Item {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width width: parent.width
} }
} }
DankToggle { DankToggle {
@@ -605,14 +623,13 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: Theme.isDynamicTheme checked: Theme.isDynamicTheme
enabled: ToastService.wallpaperErrorStatus !== "matugen_missing" enabled: ToastService.wallpaperErrorStatus !== "matugen_missing"
onToggled: (toggled) => { onToggled: toggled => {
if (toggled) if (toggled)
Theme.switchTheme(10, true); Theme.switchTheme(10, true)
else else
Theme.switchTheme(0); Theme.switchTheme(0)
} }
} }
} }
StyledText { StyledText {
@@ -623,9 +640,7 @@ Item {
width: parent.width width: parent.width
leftPadding: Theme.iconSize + Theme.spacingM leftPadding: Theme.iconSize + Theme.spacingM
} }
} }
} }
// Display Settings // Display Settings
@@ -633,8 +648,10 @@ Item {
width: parent.width width: parent.width
height: displaySection.implicitHeight + Theme.spacingL * 2 height: displaySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -662,7 +679,6 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
DankToggle { DankToggle {
@@ -672,23 +688,22 @@ Item {
text: "Night Mode" text: "Night Mode"
description: "Apply warm color temperature to reduce eye strain" description: "Apply warm color temperature to reduce eye strain"
checked: BrightnessService.nightModeActive checked: BrightnessService.nightModeActive
onToggled: (checked) => { onToggled: checked => {
if (checked !== BrightnessService.nightModeActive) { if (checked !== BrightnessService.nightModeActive) {
if (checked) if (checked)
BrightnessService.enableNightMode(); BrightnessService.enableNightMode()
else else
BrightnessService.disableNightMode(); BrightnessService.disableNightMode()
} }
} }
Connections { Connections {
function onNightModeActiveChanged() { function onNightModeActiveChanged() {
nightModeToggle.checked = BrightnessService.nightModeActive; nightModeToggle.checked = BrightnessService.nightModeActive
} }
target: BrightnessService target: BrightnessService
} }
} }
DankDropdown { DankDropdown {
@@ -699,16 +714,18 @@ Item {
opacity: !BrightnessService.nightModeActive ? 1 : 0.6 opacity: !BrightnessService.nightModeActive ? 1 : 0.6
currentValue: SessionData.nightModeTemperature + "K" currentValue: SessionData.nightModeTemperature + "K"
options: { options: {
var temps = []; var temps = []
for (var i = 2500; i <= 6000; i += 500) { for (var i = 2500; i <= 6000; i += 500) {
temps.push(i + "K"); temps.push(i + "K")
} }
return temps; return temps
}
onValueChanged: (value) => {
var temp = parseInt(value.replace("K", ""));
SessionData.setNightModeTemperature(temp);
} }
onValueChanged: value => {
var temp = parseInt(
value.replace("K", ""))
SessionData.setNightModeTemperature(
temp)
}
} }
DankToggle { DankToggle {
@@ -716,15 +733,13 @@ Item {
text: "Light Mode" text: "Light Mode"
description: "Use light theme instead of dark theme" description: "Use light theme instead of dark theme"
checked: SessionData.isLightMode checked: SessionData.isLightMode
onToggled: (checked) => { onToggled: checked => {
SessionData.setLightMode(checked); SessionData.setLightMode(checked)
Theme.isLightMode = checked; Theme.isLightMode = checked
PortalService.setLightMode(checked); PortalService.setLightMode(checked)
} }
} }
} }
} }
// Font Settings // Font Settings
@@ -732,8 +747,10 @@ Item {
width: parent.width width: parent.width
height: fontSection.implicitHeight + Theme.spacingL * 2 height: fontSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -761,7 +778,6 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
DankDropdown { DankDropdown {
@@ -770,20 +786,21 @@ Item {
description: "Select system font family" description: "Select system font family"
currentValue: { currentValue: {
if (SettingsData.fontFamily === SettingsData.defaultFontFamily) if (SettingsData.fontFamily === SettingsData.defaultFontFamily)
return "Default"; return "Default"
else else
return SettingsData.fontFamily || "Default"; return SettingsData.fontFamily || "Default"
} }
enableFuzzySearch: true enableFuzzySearch: true
popupWidthOffset: 100 popupWidthOffset: 100
maxPopupHeight: 400 maxPopupHeight: 400
options: cachedFontFamilies options: cachedFontFamilies
onValueChanged: (value) => { onValueChanged: value => {
if (value.startsWith("Default")) if (value.startsWith("Default"))
SettingsData.setFontFamily(SettingsData.defaultFontFamily); SettingsData.setFontFamily(
else SettingsData.defaultFontFamily)
SettingsData.setFontFamily(value); else
} SettingsData.setFontFamily(value)
}
} }
DankDropdown { DankDropdown {
@@ -793,64 +810,64 @@ Item {
currentValue: { currentValue: {
switch (SettingsData.fontWeight) { switch (SettingsData.fontWeight) {
case Font.Thin: case Font.Thin:
return "Thin"; return "Thin"
case Font.ExtraLight: case Font.ExtraLight:
return "Extra Light"; return "Extra Light"
case Font.Light: case Font.Light:
return "Light"; return "Light"
case Font.Normal: case Font.Normal:
return "Regular"; return "Regular"
case Font.Medium: case Font.Medium:
return "Medium"; return "Medium"
case Font.DemiBold: case Font.DemiBold:
return "Demi Bold"; return "Demi Bold"
case Font.Bold: case Font.Bold:
return "Bold"; return "Bold"
case Font.ExtraBold: case Font.ExtraBold:
return "Extra Bold"; return "Extra Bold"
case Font.Black: case Font.Black:
return "Black"; return "Black"
default: default:
return "Regular"; return "Regular"
} }
} }
options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"] options: ["Thin", "Extra Light", "Light", "Regular", "Medium", "Demi Bold", "Bold", "Extra Bold", "Black"]
onValueChanged: (value) => { onValueChanged: value => {
var weight; var weight
switch (value) { switch (value) {
case "Thin": case "Thin":
weight = Font.Thin; weight = Font.Thin
break; break
case "Extra Light": case "Extra Light":
weight = Font.ExtraLight; weight = Font.ExtraLight
break; break
case "Light": case "Light":
weight = Font.Light; weight = Font.Light
break; break
case "Regular": case "Regular":
weight = Font.Normal; weight = Font.Normal
break; break
case "Medium": case "Medium":
weight = Font.Medium; weight = Font.Medium
break; break
case "Demi Bold": case "Demi Bold":
weight = Font.DemiBold; weight = Font.DemiBold
break; break
case "Bold": case "Bold":
weight = Font.Bold; weight = Font.Bold
break; break
case "Extra Bold": case "Extra Bold":
weight = Font.ExtraBold; weight = Font.ExtraBold
break; break
case "Black": case "Black":
weight = Font.Black; weight = Font.Black
break; break
default: default:
weight = Font.Normal; weight = Font.Normal
break; break
} }
SettingsData.setFontWeight(weight); SettingsData.setFontWeight(weight)
} }
} }
DankDropdown { DankDropdown {
@@ -859,24 +876,24 @@ Item {
description: "Select monospace font for process list and technical displays" description: "Select monospace font for process list and technical displays"
currentValue: { currentValue: {
if (SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily) if (SettingsData.monoFontFamily === SettingsData.defaultMonoFontFamily)
return "Default"; return "Default"
return SettingsData.monoFontFamily || "Default"; return SettingsData.monoFontFamily || "Default"
} }
enableFuzzySearch: true enableFuzzySearch: true
popupWidthOffset: 100 popupWidthOffset: 100
maxPopupHeight: 400 maxPopupHeight: 400
options: cachedMonoFamilies options: cachedMonoFamilies
onValueChanged: (value) => { onValueChanged: value => {
if (value === "Default") if (value === "Default")
SettingsData.setMonoFontFamily(SettingsData.defaultMonoFontFamily); SettingsData.setMonoFontFamily(
else SettingsData.defaultMonoFontFamily)
SettingsData.setMonoFontFamily(value); else
} SettingsData.setMonoFontFamily(
value)
}
} }
} }
} }
// Corner Radius // Corner Radius
@@ -884,8 +901,10 @@ Item {
width: parent.width width: parent.width
height: cornerRadiusSection.implicitHeight + Theme.spacingL * 2 height: cornerRadiusSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -934,19 +953,17 @@ Item {
maximum: 32 maximum: 32
unit: "" unit: ""
showValue: true showValue: true
onSliderValueChanged: (newValue) => { onSliderValueChanged: newValue => {
SettingsData.setCornerRadius(newValue); SettingsData.setCornerRadius(
} newValue)
}
} }
} }
} }
} }
} }
} }
FileBrowserModal { FileBrowserModal {
id: wallpaperBrowser id: wallpaperBrowser
@@ -954,18 +971,17 @@ Item {
browserIcon: "wallpaper" browserIcon: "wallpaper"
browserType: "wallpaper" browserType: "wallpaper"
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => { onFileSelected: path => {
SessionData.setWallpaper(path); SessionData.setWallpaper(path)
close(); close()
} }
onDialogClosed: { onDialogClosed: {
if (parentModal) { if (parentModal) {
parentModal.allowFocusOverride = false; parentModal.allowFocusOverride = false
parentModal.shouldHaveFocus = Qt.binding(() => { parentModal.shouldHaveFocus = Qt.binding(() => {
return parentModal.shouldBeVisible; return parentModal.shouldBeVisible
}); })
} }
} }
} }
} }

View File

@@ -5,236 +5,247 @@ import qs.Common
import qs.Widgets import qs.Widgets
Item { Item {
id: recentAppsTab id: recentAppsTab
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Theme.spacingL anchors.topMargin: Theme.spacingL
clip: true clip: true
contentHeight: mainColumn.height contentHeight: mainColumn.height
contentWidth: width contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
StyledRect {
width: parent.width
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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 { Column {
id: recentlyUsedSection id: mainColumn
property var rankedAppsModel: {
var apps = []
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)
})
return apps.slice(0, 20)
}
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingXL
DankIcon { StyledRect {
name: "history" width: parent.width
size: Theme.iconSize height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
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 {
width: parent.width
text: "Apps are ordered by usage frequency, then last used, then alphabetically."
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
Column {
id: rankedAppsList
width: parent.width
spacing: Theme.spacingS
Repeater {
model: recentlyUsedSection.rankedAppsModel
delegate: Rectangle {
width: rankedAppsList.width
height: 48
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
Theme.surfaceContainer.g, Theme.surfaceVariant.b, 0.3)
Theme.surfaceContainer.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1) Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Row { Column {
anchors.left: parent.left id: recentlyUsedSection
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
StyledText { property var rankedAppsModel: {
text: (index + 1).toString() var apps = []
font.pixelSize: Theme.fontSizeSmall for (var appId in (AppUsageHistoryData.appUsageRanking
font.weight: Font.Medium || {})) {
color: Theme.primary var appData = (AppUsageHistoryData.appUsageRanking
width: 20 || {})[appId]
anchors.verticalCenter: parent.verticalCenter 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
Image { return a.name.localeCompare(b.name)
width: 24 })
height: 24 return apps.slice(0, 20)
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.fill: parent
anchors.verticalCenter: parent.verticalCenter anchors.margins: Theme.spacingL
spacing: 2 spacing: Theme.spacingM
StyledText { Row {
text: modelData.name || "Unknown App" width: parent.width
font.pixelSize: Theme.fontSizeMedium spacing: Theme.spacingM
font.weight: Font.Medium
color: Theme.surfaceText 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 {
text: { width: parent.width
if (!modelData.lastUsed) text: "Apps are ordered by usage frequency, then last used, then alphabetically."
return "Never used" font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
var date = new Date(modelData.lastUsed) wrapMode: Text.WordWrap
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 { Column {
anchors.right: parent.right id: rankedAppsList
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
width: parent.width spacing: Theme.spacingS
text: recentlyUsedSection.rankedAppsModel.length
=== 0 ? "No apps have been launched yet." : "" Repeater {
font.pixelSize: Theme.fontSizeMedium model: recentlyUsedSection.rankedAppsModel
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter delegate: Rectangle {
visible: recentlyUsedSection.rankedAppsModel.length === 0 width: rankedAppsList.width
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
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
StyledText {
text: (index + 1).toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
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 {
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()
}
}
}
}
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
}
}
}
} }
}
} }
}
} }
} }
}

View File

@@ -5,103 +5,104 @@ 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
height: headerRow.height spacing: expanded ? Theme.spacingM : 0
enabled: collapsible Component.onCompleted: {
hoverEnabled: collapsible if (!collapsible)
onClicked: { expanded = true
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 {
anchors.fill: parent width: parent.width
color: parent.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, height: 1
Theme.primary.b, color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
0.08) : "transparent" visible: expanded || !collapsible
radius: Theme.radiusS
} }
Row { Loader {
id: headerRow id: contentLoader
width: parent.width width: parent.width
spacing: Theme.spacingS active: lazyLoad ? expanded || !collapsible : true
topPadding: Theme.spacingS visible: expanded || !collapsible
bottomPadding: Theme.spacingS asynchronous: true
opacity: visible ? 1 : 0
DankIcon { Behavior on opacity {
name: root.collapsible ? (root.expanded ? "expand_less" : "expand_more") : root.iconName NumberAnimation {
size: Theme.iconSize - 2 duration: Appearance.anim.durations.normal
color: Theme.primary easing.type: Easing.BezierSpline
anchors.verticalCenter: parent.verticalCenter easing.bezierCurve: Appearance.anim.curves.standard
}
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

@@ -12,54 +12,73 @@ Item {
property bool fontsEnumerated: false property bool fontsEnumerated: false
function enumerateFonts() { function enumerateFonts() {
var fonts = ["Default"]; var fonts = ["Default"]
var availableFonts = Qt.fontFamilies(); var availableFonts = Qt.fontFamilies()
var rootFamilies = []; var rootFamilies = []
var seenFamilies = new Set(); var seenFamilies = new Set()
for (var i = 0; i < availableFonts.length; i++) { for (var i = 0; i < availableFonts.length; i++) {
var fontName = availableFonts[i]; var fontName = availableFonts[i]
if (fontName.startsWith(".")) if (fontName.startsWith("."))
continue; continue
if (fontName === SettingsData.defaultFontFamily) if (fontName === SettingsData.defaultFontFamily)
continue; continue
var rootName = fontName.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i, function(match, suffix) { var rootName = fontName.replace(
return match; / (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
}).trim(); "").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").replace(/ (UI|Display|Text|Mono|Sans|Serif)$/i,
function (match, suffix) {
return match
}).trim()
if (!seenFamilies.has(rootName) && rootName !== "") { if (!seenFamilies.has(rootName) && rootName !== "") {
seenFamilies.add(rootName); seenFamilies.add(rootName)
rootFamilies.push(rootName); rootFamilies.push(rootName)
} }
} }
cachedFontFamilies = fonts.concat(rootFamilies.sort()); cachedFontFamilies = fonts.concat(rootFamilies.sort())
var monoFonts = ["Default"]; var monoFonts = ["Default"]
var monoFamilies = []; var monoFamilies = []
var seenMonoFamilies = new Set(); var seenMonoFamilies = new Set()
for (var j = 0; j < availableFonts.length; j++) { for (var j = 0; j < availableFonts.length; j++) {
var fontName2 = availableFonts[j]; var fontName2 = availableFonts[j]
if (fontName2.startsWith(".")) if (fontName2.startsWith("."))
continue; continue
if (fontName2 === SettingsData.defaultMonoFontFamily) if (fontName2 === SettingsData.defaultMonoFontFamily)
continue; continue
var lowerName = fontName2.toLowerCase(); var lowerName = fontName2.toLowerCase()
if (lowerName.includes("mono") || lowerName.includes("code") || lowerName.includes("console") || lowerName.includes("terminal") || lowerName.includes("courier") || lowerName.includes("dejavu sans mono") || lowerName.includes("jetbrains") || lowerName.includes("fira") || lowerName.includes("hack") || lowerName.includes("source code") || lowerName.includes("ubuntu mono") || lowerName.includes("cascadia")) { if (lowerName.includes("mono") || lowerName.includes(
var rootName2 = fontName2.replace(/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i, "").replace(/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i, "").trim(); "code") || lowerName.includes(
"console") || lowerName.includes(
"terminal") || lowerName.includes(
"courier") || lowerName.includes(
"dejavu sans mono") || lowerName.includes(
"jetbrains") || lowerName.includes(
"fira") || lowerName.includes(
"hack") || lowerName.includes(
"source code") || lowerName.includes(
"ubuntu mono") || lowerName.includes("cascadia")) {
var rootName2 = fontName2.replace(
/ (Thin|Extra Light|Light|Regular|Medium|Semi Bold|Demi Bold|Bold|Extra Bold|Black|Heavy)$/i,
"").replace(
/ (Italic|Oblique|Condensed|Extended|Narrow|Wide)$/i,
"").trim()
if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") { if (!seenMonoFamilies.has(rootName2) && rootName2 !== "") {
seenMonoFamilies.add(rootName2); seenMonoFamilies.add(rootName2)
monoFamilies.push(rootName2); monoFamilies.push(rootName2)
} }
} }
} }
cachedMonoFamilies = monoFonts.concat(monoFamilies.sort()); cachedMonoFamilies = monoFonts.concat(monoFamilies.sort())
} }
Component.onCompleted: { Component.onCompleted: {
if (!fontsEnumerated) { if (!fontsEnumerated) {
enumerateFonts(); enumerateFonts()
fontsEnumerated = true; fontsEnumerated = true
} }
} }
@@ -81,8 +100,10 @@ Item {
width: parent.width width: parent.width
height: themeSection.implicitHeight + Theme.spacingL * 2 height: themeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -110,7 +131,6 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Column { Column {
@@ -128,10 +148,11 @@ Item {
StyledText { StyledText {
text: { text: {
if (Theme.isDynamicTheme) if (Theme.isDynamicTheme)
return "Wallpaper-based dynamic colors"; return "Wallpaper-based dynamic colors"
var descriptions = ["Material blue inspired by modern interfaces", "Deep blue inspired by material 3", "Rich purple tones for BB elegance", "Natural green for productivity", "Energetic orange for creativity", "Bold red for impact", "Cool cyan for tranquility", "Vibrant pink for expression", "Warm amber for comfort", "Soft coral for gentle warmth"]; var descriptions = ["Material blue inspired by modern interfaces", "Deep blue inspired by material 3", "Rich purple tones for BB elegance", "Natural green for productivity", "Energetic orange for creativity", "Bold red for impact", "Cool cyan for tranquility", "Vibrant pink for expression", "Warm amber for comfort", "Soft coral for gentle warmth"]
return descriptions[Theme.currentThemeIndex] || "Select a theme"; return descriptions[Theme.currentThemeIndex]
|| "Select a theme"
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -140,7 +161,6 @@ Item {
width: Math.min(parent.width, 400) width: Math.min(parent.width, 400)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
} }
Column { Column {
@@ -160,8 +180,10 @@ Item {
radius: 16 radius: 16
color: Theme.themes[index].primary color: Theme.themes[index].primary
border.color: Theme.outline border.color: Theme.outline
border.width: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 2 : 1 border.width: (Theme.currentThemeIndex === index
scale: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 1.1 : 1 && !Theme.isDynamicTheme) ? 2 : 1
scale: (Theme.currentThemeIndex === index
&& !Theme.isDynamicTheme) ? 1.1 : 1
Rectangle { Rectangle {
width: nameText.contentWidth + Theme.spacingS * 2 width: nameText.contentWidth + Theme.spacingS * 2
@@ -183,7 +205,6 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.centerIn: parent anchors.centerIn: parent
} }
} }
MouseArea { MouseArea {
@@ -193,7 +214,7 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Theme.switchTheme(index, false); Theme.switchTheme(index, false)
} }
} }
@@ -202,7 +223,6 @@ Item {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
Behavior on border.width { Behavior on border.width {
@@ -210,13 +230,9 @@ Item {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
} }
} }
} }
Row { Row {
@@ -248,7 +264,8 @@ Item {
anchors.bottom: parent.top anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingXS anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea2.containsMouse && themeIndex < Theme.themes.length visible: mouseArea2.containsMouse
&& themeIndex < Theme.themes.length
StyledText { StyledText {
id: nameText2 id: nameText2
@@ -258,7 +275,6 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.centerIn: parent anchors.centerIn: parent
} }
} }
MouseArea { MouseArea {
@@ -269,8 +285,7 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (themeIndex < Theme.themes.length) if (themeIndex < Theme.themes.length)
Theme.switchTheme(themeIndex); Theme.switchTheme(themeIndex)
} }
} }
@@ -279,7 +294,6 @@ Item {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
Behavior on border.width { Behavior on border.width {
@@ -287,13 +301,9 @@ Item {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
} }
} }
} }
Item { Item {
@@ -307,18 +317,26 @@ Item {
radius: 20 radius: 20
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: { color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "error"
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12); || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b, 0.12)
else else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3); return Qt.rgba(Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.3)
} }
border.color: { border.color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "error"
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5); || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b, 0.5)
else if (Theme.isDynamicTheme) else if (Theme.isDynamicTheme)
return Theme.primary; return Theme.primary
else else
return Theme.outline; return Theme.outline
} }
border.width: Theme.isDynamicTheme ? 2 : 1 border.width: Theme.isDynamicTheme ? 2 : 1
scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1) scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1)
@@ -329,17 +347,21 @@ Item {
DankIcon { DankIcon {
name: { name: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "error"
return "error"; || ToastService.wallpaperErrorStatus
=== "matugen_missing")
return "error"
else else
return "palette"; return "palette"
} }
size: 16 size: 16
color: { color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "error"
return Theme.error; || ToastService.wallpaperErrorStatus
=== "matugen_missing")
return Theme.error
else else
return Theme.surfaceText; return Theme.surfaceText
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -347,23 +369,25 @@ Item {
StyledText { StyledText {
text: { text: {
if (ToastService.wallpaperErrorStatus === "error") if (ToastService.wallpaperErrorStatus === "error")
return "Error"; return "Error"
else if (ToastService.wallpaperErrorStatus === "matugen_missing") else if (ToastService.wallpaperErrorStatus
return "No matugen"; === "matugen_missing")
return "No matugen"
else else
return "Auto"; return "Auto"
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: { color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "error"
return Theme.error; || ToastService.wallpaperErrorStatus
=== "matugen_missing")
return Theme.error
else else
return Theme.surfaceText; return Theme.surfaceText
} }
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { MouseArea {
@@ -374,11 +398,13 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "matugen_missing")
ToastService.showError("matugen not found - install matugen package for dynamic theming"); ToastService.showError(
"matugen not found - install matugen package for dynamic theming")
else if (ToastService.wallpaperErrorStatus === "error") else if (ToastService.wallpaperErrorStatus === "error")
ToastService.showError("Wallpaper processing failed - check wallpaper path"); ToastService.showError(
"Wallpaper processing failed - check wallpaper path")
else else
Theme.switchTheme(10, true); Theme.switchTheme(10, true)
} }
} }
@@ -392,25 +418,30 @@ Item {
anchors.bottom: parent.top anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: autoMouseArea.containsMouse && (!Theme.isDynamicTheme || ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") visible: autoMouseArea.containsMouse
&& (!Theme.isDynamicTheme
|| ToastService.wallpaperErrorStatus === "error"
|| ToastService.wallpaperErrorStatus
=== "matugen_missing")
StyledText { StyledText {
id: autoTooltipText id: autoTooltipText
text: { text: {
if (ToastService.wallpaperErrorStatus === "matugen_missing") if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "Install matugen package for dynamic themes"; return "Install matugen package for dynamic themes"
else else
return "Dynamic wallpaper-based colors"; return "Dynamic wallpaper-based colors"
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceText color: (ToastService.wallpaperErrorStatus === "error"
|| ToastService.wallpaperErrorStatus
=== "matugen_missing") ? Theme.error : Theme.surfaceText
anchors.centerIn: parent anchors.centerIn: parent
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: Math.min(implicitWidth, 250) width: Math.min(implicitWidth, 250)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
} }
Behavior on scale { Behavior on scale {
@@ -418,7 +449,6 @@ Item {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
Behavior on color { Behavior on color {
@@ -426,7 +456,6 @@ Item {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
Behavior on border.color { Behavior on border.color {
@@ -434,15 +463,10 @@ Item {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
} }
} }
} }
// Transparency Settings // Transparency Settings
@@ -450,8 +474,10 @@ Item {
width: parent.width width: parent.width
height: transparencySection.implicitHeight + Theme.spacingL * 2 height: transparencySection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -479,7 +505,6 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Column { Column {
@@ -496,16 +521,17 @@ Item {
DankSlider { DankSlider {
width: parent.width width: parent.width
height: 24 height: 24
value: Math.round(SettingsData.topBarTransparency * 100) value: Math.round(
SettingsData.topBarTransparency * 100)
minimum: 0 minimum: 0
maximum: 100 maximum: 100
unit: "" unit: ""
showValue: true showValue: true
onSliderValueChanged: (newValue) => { onSliderValueChanged: newValue => {
SettingsData.setTopBarTransparency(newValue / 100); SettingsData.setTopBarTransparency(
} newValue / 100)
}
} }
} }
Column { Column {
@@ -522,16 +548,17 @@ Item {
DankSlider { DankSlider {
width: parent.width width: parent.width
height: 24 height: 24
value: Math.round(SettingsData.topBarWidgetTransparency * 100) value: Math.round(
SettingsData.topBarWidgetTransparency * 100)
minimum: 0 minimum: 0
maximum: 100 maximum: 100
unit: "" unit: ""
showValue: true showValue: true
onSliderValueChanged: (newValue) => { onSliderValueChanged: newValue => {
SettingsData.setTopBarWidgetTransparency(newValue / 100); SettingsData.setTopBarWidgetTransparency(
} newValue / 100)
}
} }
} }
Column { Column {
@@ -548,20 +575,19 @@ Item {
DankSlider { DankSlider {
width: parent.width width: parent.width
height: 24 height: 24
value: Math.round(SettingsData.popupTransparency * 100) value: Math.round(
SettingsData.popupTransparency * 100)
minimum: 0 minimum: 0
maximum: 100 maximum: 100
unit: "" unit: ""
showValue: true showValue: true
onSliderValueChanged: (newValue) => { onSliderValueChanged: newValue => {
SettingsData.setPopupTransparency(newValue / 100); SettingsData.setPopupTransparency(
} newValue / 100)
}
} }
} }
} }
} }
// System Configuration Warning // System Configuration Warning
@@ -569,8 +595,10 @@ Item {
width: parent.width width: parent.width
height: warningText.implicitHeight + Theme.spacingM * 2 height: warningText.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) color: Qt.rgba(Theme.warning.r, Theme.warning.g,
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3) Theme.warning.b, 0.12)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g,
Theme.warning.b, 0.3)
border.width: 1 border.width: 1
Row { Row {
@@ -595,9 +623,7 @@ Item {
width: parent.width - Theme.iconSizeSmall - Theme.spacingM width: parent.width - Theme.iconSizeSmall - Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
} }
// Icon Theme // Icon Theme
@@ -605,8 +631,10 @@ Item {
width: parent.width width: parent.width
height: iconThemeSection.implicitHeight + Theme.spacingL * 2 height: iconThemeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -637,21 +665,20 @@ Item {
popupWidthOffset: 100 popupWidthOffset: 100
maxPopupHeight: 236 maxPopupHeight: 236
options: { options: {
SettingsData.detectAvailableIconThemes(); SettingsData.detectAvailableIconThemes()
return SettingsData.availableIconThemes; return SettingsData.availableIconThemes
}
onValueChanged: (value) => {
SettingsData.setIconTheme(value);
if (value !== "System Default" && !SettingsData.qt5ctAvailable && !SettingsData.qt6ctAvailable)
ToastService.showWarning("qt5ct or qt6ct not found - Qt app themes may not update without these tools");
} }
onValueChanged: value => {
SettingsData.setIconTheme(value)
if (value !== "System Default"
&& !SettingsData.qt5ctAvailable
&& !SettingsData.qt6ctAvailable)
ToastService.showWarning(
"qt5ct or qt6ct not found - Qt app themes may not update without these tools")
}
} }
} }
} }
} }
// System App Theming // System App Theming
@@ -659,8 +686,10 @@ Item {
width: parent.width width: parent.width
height: systemThemingSection.implicitHeight + Theme.spacingL * 2 height: systemThemingSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
visible: Theme.isDynamicTheme && Colors.matugenAvailable visible: Theme.isDynamicTheme && Colors.matugenAvailable
@@ -689,7 +718,6 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
DankToggle { DankToggle {
@@ -697,9 +725,10 @@ Item {
text: "Theme GTK Applications" text: "Theme GTK Applications"
description: Colors.gtkThemingEnabled ? "File managers, text editors, and system dialogs will match your theme" : "GTK theming not available (install gsettings)" description: Colors.gtkThemingEnabled ? "File managers, text editors, and system dialogs will match your theme" : "GTK theming not available (install gsettings)"
enabled: Colors.gtkThemingEnabled enabled: Colors.gtkThemingEnabled
checked: Colors.gtkThemingEnabled && SettingsData.gtkThemingEnabled checked: Colors.gtkThemingEnabled
onToggled: function(checked) { && SettingsData.gtkThemingEnabled
SettingsData.setGtkThemingEnabled(checked); onToggled: function (checked) {
SettingsData.setGtkThemingEnabled(checked)
} }
} }
@@ -708,18 +737,14 @@ Item {
text: "Theme Qt Applications" text: "Theme Qt Applications"
description: Colors.qtThemingEnabled ? "Qt applications will match your theme colors" : "Qt theming not available (install qt5ct or qt6ct)" description: Colors.qtThemingEnabled ? "Qt applications will match your theme colors" : "Qt theming not available (install qt5ct or qt6ct)"
enabled: Colors.qtThemingEnabled enabled: Colors.qtThemingEnabled
checked: Colors.qtThemingEnabled && SettingsData.qtThemingEnabled checked: Colors.qtThemingEnabled
onToggled: function(checked) { && SettingsData.qtThemingEnabled
SettingsData.setQtThemingEnabled(checked); onToggled: function (checked) {
SettingsData.setQtThemingEnabled(checked)
} }
} }
} }
} }
} }
} }
}
}

View File

@@ -24,8 +24,10 @@ Item {
width: parent.width width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2 height: timeSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -47,7 +49,8 @@ Item {
} }
Column { Column {
width: parent.width - Theme.iconSize - Theme.spacingM - toggle.width - Theme.spacingM width: parent.width - Theme.iconSize - Theme.spacingM
- toggle.width - Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -65,7 +68,6 @@ Item {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width width: parent.width
} }
} }
DankToggle { DankToggle {
@@ -73,15 +75,13 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.use24HourClock checked: SettingsData.use24HourClock
onToggled: (checked) => { onToggled: checked => {
return SettingsData.setClockFormat(checked); return SettingsData.setClockFormat(
} checked)
}
} }
} }
} }
} }
// Date Format Section // Date Format Section
@@ -89,8 +89,10 @@ Item {
width: parent.width width: parent.width
height: dateSection.implicitHeight + Theme.spacingL * 2 height: dateSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -118,123 +120,130 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
DankDropdown { DankDropdown {
width: parent.width width: parent.width
height: 50 height: 50
text: "Top Bar Format" text: "Top Bar Format"
description: "Preview: " + Qt.formatDate(new Date(), SettingsData.clockDateFormat) description: "Preview: " + Qt.formatDate(
new Date(),
SettingsData.clockDateFormat)
currentValue: { currentValue: {
// Find matching preset or show "Custom" // Find matching preset or show "Custom"
const presets = [{ const presets = [{
"format": "ddd d", "format": "ddd d",
"label": "Day Date" "label": "Day Date"
}, { }, {
"format": "ddd MMM d", "format": "ddd MMM d",
"label": "Day Month Date" "label": "Day Month Date"
}, { }, {
"format": "MMM d", "format": "MMM d",
"label": "Month Date" "label": "Month Date"
}, { }, {
"format": "M/d", "format": "M/d",
"label": "Numeric (M/D)" "label": "Numeric (M/D)"
}, { }, {
"format": "d/M", "format": "d/M",
"label": "Numeric (D/M)" "label": "Numeric (D/M)"
}, { }, {
"format": "ddd d MMM yyyy", "format": "ddd d MMM yyyy",
"label": "Full with Year" "label": "Full with Year"
}, { }, {
"format": "yyyy-MM-dd", "format": "yyyy-MM-dd",
"label": "ISO Date" "label": "ISO Date"
}, { }, {
"format": "dddd, MMMM d", "format": "dddd, MMMM d",
"label": "Full Day & Month" "label": "Full Day & Month"
}]; }]
const match = presets.find((p) => { const match = presets.find(p => {
return p.format === SettingsData.clockDateFormat; return p.format
}); === SettingsData.clockDateFormat
return match ? match.label : "Custom: " + SettingsData.clockDateFormat; })
return match ? match.label : "Custom: " + SettingsData.clockDateFormat
} }
options: ["Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."] options: ["Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
onValueChanged: (value) => { onValueChanged: value => {
const formatMap = { const formatMap = {
"Day Date": "ddd d", "Day Date": "ddd d",
"Day Month Date": "ddd MMM d", "Day Month Date": "ddd MMM d",
"Month Date": "MMM d", "Month Date": "MMM d",
"Numeric (M/D)": "M/d", "Numeric (M/D)": "M/d",
"Numeric (D/M)": "d/M", "Numeric (D/M)": "d/M",
"Full with Year": "ddd d MMM yyyy", "Full with Year": "ddd d MMM yyyy",
"ISO Date": "yyyy-MM-dd", "ISO Date": "yyyy-MM-dd",
"Full Day & Month": "dddd, MMMM d" "Full Day & Month": "dddd, MMMM d"
}; }
if (value === "Custom...") { if (value === "Custom...") {
customFormatInput.visible = true; customFormatInput.visible = true
} else { } else {
customFormatInput.visible = false; customFormatInput.visible = false
SettingsData.setClockDateFormat(formatMap[value]); SettingsData.setClockDateFormat(
} formatMap[value])
} }
}
} }
DankDropdown { DankDropdown {
width: parent.width width: parent.width
height: 50 height: 50
text: "Lock Screen Format" text: "Lock Screen Format"
description: "Preview: " + Qt.formatDate(new Date(), SettingsData.lockDateFormat) description: "Preview: " + Qt.formatDate(
new Date(),
SettingsData.lockDateFormat)
currentValue: { currentValue: {
// Find matching preset or show "Custom" // Find matching preset or show "Custom"
const presets = [{ const presets = [{
"format": "ddd d", "format": "ddd d",
"label": "Day Date" "label": "Day Date"
}, { }, {
"format": "ddd MMM d", "format": "ddd MMM d",
"label": "Day Month Date" "label": "Day Month Date"
}, { }, {
"format": "MMM d", "format": "MMM d",
"label": "Month Date" "label": "Month Date"
}, { }, {
"format": "M/d", "format": "M/d",
"label": "Numeric (M/D)" "label": "Numeric (M/D)"
}, { }, {
"format": "d/M", "format": "d/M",
"label": "Numeric (D/M)" "label": "Numeric (D/M)"
}, { }, {
"format": "ddd d MMM yyyy", "format": "ddd d MMM yyyy",
"label": "Full with Year" "label": "Full with Year"
}, { }, {
"format": "yyyy-MM-dd", "format": "yyyy-MM-dd",
"label": "ISO Date" "label": "ISO Date"
}, { }, {
"format": "dddd, MMMM d", "format": "dddd, MMMM d",
"label": "Full Day & Month" "label": "Full Day & Month"
}]; }]
const match = presets.find((p) => { const match = presets.find(p => {
return p.format === SettingsData.lockDateFormat; return p.format
}); === SettingsData.lockDateFormat
return match ? match.label : "Custom: " + SettingsData.lockDateFormat; })
return match ? match.label : "Custom: " + SettingsData.lockDateFormat
} }
options: ["Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."] options: ["Day Date", "Day Month Date", "Month Date", "Numeric (M/D)", "Numeric (D/M)", "Full with Year", "ISO Date", "Full Day & Month", "Custom..."]
onValueChanged: (value) => { onValueChanged: value => {
const formatMap = { const formatMap = {
"Day Date": "ddd d", "Day Date": "ddd d",
"Day Month Date": "ddd MMM d", "Day Month Date": "ddd MMM d",
"Month Date": "MMM d", "Month Date": "MMM d",
"Numeric (M/D)": "M/d", "Numeric (M/D)": "M/d",
"Numeric (D/M)": "d/M", "Numeric (D/M)": "d/M",
"Full with Year": "ddd d MMM yyyy", "Full with Year": "ddd d MMM yyyy",
"ISO Date": "yyyy-MM-dd", "ISO Date": "yyyy-MM-dd",
"Full Day & Month": "dddd, MMMM d" "Full Day & Month": "dddd, MMMM d"
}; }
if (value === "Custom...") { if (value === "Custom...") {
customLockFormatInput.visible = true; customLockFormatInput.visible = true
} else { } else {
customLockFormatInput.visible = false; customLockFormatInput.visible = false
SettingsData.setLockDateFormat(formatMap[value]); SettingsData.setLockDateFormat(
} formatMap[value])
} }
}
} }
DankTextField { DankTextField {
@@ -246,8 +255,7 @@ Item {
text: SettingsData.clockDateFormat text: SettingsData.clockDateFormat
onTextChanged: { onTextChanged: {
if (visible && text) if (visible && text)
SettingsData.setClockDateFormat(text); SettingsData.setClockDateFormat(text)
} }
} }
@@ -260,8 +268,7 @@ Item {
text: SettingsData.lockDateFormat text: SettingsData.lockDateFormat
onTextChanged: { onTextChanged: {
if (visible && text) if (visible && text)
SettingsData.setLockDateFormat(text); SettingsData.setLockDateFormat(text)
} }
} }
@@ -269,8 +276,11 @@ Item {
width: parent.width width: parent.width
height: formatHelp.implicitHeight + Theme.spacingM * 2 height: formatHelp.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.2) color: Qt.rgba(Theme.surfaceVariant.r,
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1) Theme.surfaceVariant.g,
Theme.surfaceVariant.b, 0.2)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.1)
border.width: 1 border.width: 1
Column { Column {
@@ -324,7 +334,6 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
} }
Column { Column {
@@ -360,21 +369,12 @@ Item {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
} }
} }
} }
} }
} }
} }
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -24,8 +24,10 @@ Item {
width: parent.width width: parent.width
height: enableWeatherSection.implicitHeight + Theme.spacingL * 2 height: enableWeatherSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
Column { Column {
@@ -47,7 +49,8 @@ Item {
} }
Column { Column {
width: parent.width - Theme.iconSize - Theme.spacingM - enableToggle.width - Theme.spacingM width: parent.width - Theme.iconSize - Theme.spacingM
- enableToggle.width - Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -65,7 +68,6 @@ Item {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width width: parent.width
} }
} }
DankToggle { DankToggle {
@@ -73,15 +75,13 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.weatherEnabled checked: SettingsData.weatherEnabled
onToggled: (checked) => { onToggled: checked => {
return SettingsData.setWeatherEnabled(checked); return SettingsData.setWeatherEnabled(
} checked)
}
} }
} }
} }
} }
// Temperature Unit // Temperature Unit
@@ -89,8 +89,10 @@ Item {
width: parent.width width: parent.width
height: temperatureSection.implicitHeight + Theme.spacingL * 2 height: temperatureSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
visible: SettingsData.weatherEnabled visible: SettingsData.weatherEnabled
opacity: visible ? 1 : 0 opacity: visible ? 1 : 0
@@ -114,7 +116,8 @@ Item {
} }
Column { Column {
width: parent.width - Theme.iconSize - Theme.spacingM - temperatureToggle.width - Theme.spacingM width: parent.width - Theme.iconSize - Theme.spacingM
- temperatureToggle.width - Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -132,7 +135,6 @@ Item {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width width: parent.width
} }
} }
DankToggle { DankToggle {
@@ -140,13 +142,12 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.useFahrenheit checked: SettingsData.useFahrenheit
onToggled: (checked) => { onToggled: checked => {
return SettingsData.setTemperatureUnit(checked); return SettingsData.setTemperatureUnit(
} checked)
}
} }
} }
} }
Behavior on opacity { Behavior on opacity {
@@ -155,7 +156,6 @@ Item {
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
} }
// Location Settings // Location Settings
@@ -163,8 +163,10 @@ Item {
width: parent.width width: parent.width
height: locationSection.implicitHeight + Theme.spacingL * 2 height: locationSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.2)
border.width: 1 border.width: 1
visible: SettingsData.weatherEnabled visible: SettingsData.weatherEnabled
opacity: visible ? 1 : 0 opacity: visible ? 1 : 0
@@ -188,7 +190,8 @@ Item {
} }
Column { Column {
width: parent.width - Theme.iconSize - Theme.spacingM - autoLocationToggle.width - Theme.spacingM width: parent.width - Theme.iconSize - Theme.spacingM
- autoLocationToggle.width - Theme.spacingM
spacing: Theme.spacingXS spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -206,7 +209,6 @@ Item {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width width: parent.width
} }
} }
DankToggle { DankToggle {
@@ -214,11 +216,11 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.useAutoLocation checked: SettingsData.useAutoLocation
onToggled: (checked) => { onToggled: checked => {
return SettingsData.setAutoLocation(checked); return SettingsData.setAutoLocation(
} checked)
}
} }
} }
Column { Column {
@@ -245,12 +247,12 @@ Item {
currentLocation: SettingsData.weatherLocation currentLocation: SettingsData.weatherLocation
placeholderText: "New York, NY" placeholderText: "New York, NY"
onLocationSelected: (displayName, coordinates) => { onLocationSelected: (displayName, coordinates) => {
SettingsData.setWeatherLocation(displayName, coordinates); SettingsData.setWeatherLocation(
} displayName,
coordinates)
}
} }
} }
} }
Behavior on opacity { Behavior on opacity {
@@ -259,11 +261,7 @@ Item {
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
} }
} }
} }
} }

View File

@@ -5,373 +5,391 @@ import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
id: widgetTweaksTab id: widgetTweaksTab
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Theme.spacingL anchors.topMargin: Theme.spacingL
clip: true clip: true
contentHeight: mainColumn.height contentHeight: mainColumn.height
contentWidth: width contentWidth: width
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
// Launcher Button Section
StyledRect {
width: parent.width
height: launcherButtonSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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 { Column {
id: launcherButtonSection id: mainColumn
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingXL
DankIcon { // Launcher Button Section
name: "apps" StyledRect {
size: Theme.iconSize width: parent.width
color: Theme.primary height: launcherButtonSection.implicitHeight + Theme.spacingL * 2
anchors.verticalCenter: parent.verticalCenter radius: Theme.cornerRadius
} 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
StyledText { Column {
text: "Launcher Button" id: launcherButtonSection
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
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
}
}
}
}
}
StyledRect {
width: parent.width
height: workspaceSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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: workspaceSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "view_module"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Workspace Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Workspace Index Numbers"
description: "Show workspace index numbers in the top bar workspace switcher"
checked: SettingsData.showWorkspaceIndex
onToggled: checked => {
return SettingsData.setShowWorkspaceIndex(checked)
}
}
DankToggle {
width: parent.width
text: "Workspace Padding"
description: "Always show a minimum of 3 workspaces, even if fewer are available"
checked: SettingsData.showWorkspacePadding
onToggled: checked => {
return SettingsData.setShowWorkspacePadding(checked)
}
}
}
}
StyledRect {
width: parent.width
height: workspaceIconsSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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
visible: SettingsData.hasNamedWorkspaces()
Column {
id: workspaceIconsSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "label"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Named Workspace Icons"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: "Configure icons for named workspaces. Icons take priority over numbers when both are enabled."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
wrapMode: Text.WordWrap
}
Repeater {
model: SettingsData.getNamedWorkspaces()
Rectangle {
width: parent.width
height: workspaceIconRow.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.3)
border.width: 1
Row {
id: workspaceIconRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
StyledText {
text: "\"" + modelData + "\""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: 150
elide: Text.ElideRight
}
DankIconPicker {
id: iconPicker
anchors.verticalCenter: parent.verticalCenter
Component.onCompleted: {
var iconData = SettingsData.getWorkspaceNameIcon(modelData)
if (iconData) {
setIcon(iconData.value, iconData.type)
}
}
onIconSelected: (iconName, iconType) => {
SettingsData.setWorkspaceNameIcon(modelData, {
type: iconType,
value: iconName
})
setIcon(iconName, iconType)
}
Connections {
target: SettingsData
function onWorkspaceIconsUpdated() {
var iconData = SettingsData.getWorkspaceNameIcon(modelData)
if (iconData) {
iconPicker.setIcon(iconData.value, iconData.type)
} else {
iconPicker.setIcon("", "icon")
}
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer
border.color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
border.width: 1
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "close"
size: 16
color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
anchors.centerIn: parent
}
MouseArea {
id: clearMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true anchors.margins: Theme.spacingL
cursorShape: Qt.PointingHandCursor spacing: Theme.spacingM
onClicked: {
SettingsData.removeWorkspaceNameIcon(modelData)
}
}
}
Item { Row {
width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4 width: parent.width
height: 1 spacing: Theme.spacingM
DankIcon {
name: "apps"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Launcher Button"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
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
}
}
}
}
}
StyledRect {
width: parent.width
height: workspaceSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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: workspaceSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "view_module"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Workspace Settings"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
width: parent.width
text: "Workspace Index Numbers"
description: "Show workspace index numbers in the top bar workspace switcher"
checked: SettingsData.showWorkspaceIndex
onToggled: checked => {
return SettingsData.setShowWorkspaceIndex(
checked)
}
}
DankToggle {
width: parent.width
text: "Workspace Padding"
description: "Always show a minimum of 3 workspaces, even if fewer are available"
checked: SettingsData.showWorkspacePadding
onToggled: checked => {
return SettingsData.setShowWorkspacePadding(
checked)
}
}
}
}
StyledRect {
width: parent.width
height: workspaceIconsSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
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
visible: SettingsData.hasNamedWorkspaces()
Column {
id: workspaceIconsSection
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: "label"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: "Named Workspace Icons"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
width: parent.width
text: "Configure icons for named workspaces. Icons take priority over numbers when both are enabled."
font.pixelSize: Theme.fontSizeSmall
color: Theme.outline
wrapMode: Text.WordWrap
}
Repeater {
model: SettingsData.getNamedWorkspaces()
Rectangle {
width: parent.width
height: workspaceIconRow.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r,
Theme.surfaceContainer.g,
Theme.surfaceContainer.b, 0.5)
border.color: Qt.rgba(Theme.outline.r,
Theme.outline.g,
Theme.outline.b, 0.3)
border.width: 1
Row {
id: workspaceIconRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
StyledText {
text: "\"" + modelData + "\""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
width: 150
elide: Text.ElideRight
}
DankIconPicker {
id: iconPicker
anchors.verticalCenter: parent.verticalCenter
Component.onCompleted: {
var iconData = SettingsData.getWorkspaceNameIcon(
modelData)
if (iconData) {
setIcon(iconData.value,
iconData.type)
}
}
onIconSelected: (iconName, iconType) => {
SettingsData.setWorkspaceNameIcon(
modelData, {
"type": iconType,
"value": iconName
})
setIcon(iconName,
iconType)
}
Connections {
target: SettingsData
function onWorkspaceIconsUpdated() {
var iconData = SettingsData.getWorkspaceNameIcon(
modelData)
if (iconData) {
iconPicker.setIcon(
iconData.value,
iconData.type)
} else {
iconPicker.setIcon("", "icon")
}
}
}
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: clearMouseArea.containsMouse ? Theme.errorHover : Theme.surfaceContainer
border.color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
border.width: 1
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "close"
size: 16
color: clearMouseArea.containsMouse ? Theme.error : Theme.outline
anchors.centerIn: parent
}
MouseArea {
id: clearMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
SettingsData.removeWorkspaceNameIcon(
modelData)
}
}
}
Item {
width: parent.width - 150 - 240 - 28 - Theme.spacingM * 4
height: 1
}
}
}
}
} }
}
} }
}
} }
}
} }
} }
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,256 +9,257 @@ 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
property bool expanded: false
Connections {
target: ToastService
function onResetToastState() {
toast.expanded = false
}
} }
width: ToastService.hasDetails ? 380 : messageText.implicitWidth + Theme.iconSize + Theme.spacingM * 3 + Theme.spacingL * 2 Rectangle {
height: toastContent.height + Theme.spacingL * 2 id: toast
anchors.horizontalCenter: parent.horizontalCenter
y: Theme.barHeight - 4 + SettingsData.topBarSpacing + 2
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.cornerRadius
layer.enabled: true
opacity: ToastService.toastVisible ? 0.9 : 0
Column { property bool expanded: false
id: toastContent
anchors.top: parent.top Connections {
anchors.left: parent.left target: ToastService
anchors.right: parent.right function onResetToastState() {
anchors.topMargin: Theme.spacingL toast.expanded = false
anchors.leftMargin: Theme.spacingL }
anchors.rightMargin: Theme.spacingL }
spacing: Theme.spacingS
Item { width: ToastService.hasDetails ? 380 : messageText.implicitWidth + Theme.iconSize
width: parent.width + Theme.spacingM * 3 + Theme.spacingL * 2
height: Theme.iconSize + 8 height: toastContent.height + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
DankIcon { y: Theme.barHeight - 4 + SettingsData.topBarSpacing + 2
id: statusIcon color: {
name: {
switch (ToastService.currentLevel) { switch (ToastService.currentLevel) {
case ToastService.levelError: case ToastService.levelError:
return "error" return Theme.error
case ToastService.levelWarn: case ToastService.levelWarn:
return "warning" return Theme.warning
case ToastService.levelInfo: case ToastService.levelInfo:
return "info" return Theme.primary
default: default:
return "info" return Theme.primary
} }
}
size: Theme.iconSize
color: Theme.background
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
} }
radius: Theme.cornerRadius
layer.enabled: true
opacity: ToastService.toastVisible ? 0.9 : 0
StyledText { Column {
id: messageText id: toastContent
text: ToastService.currentMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
anchors.left: statusIcon.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
wrapMode: Text.NoWrap
}
DankActionButton { anchors.top: parent.top
iconName: toast.expanded ? "expand_less" : "expand_more" anchors.left: parent.left
iconSize: Theme.iconSize anchors.right: parent.right
iconColor: Theme.background anchors.topMargin: Theme.spacingL
buttonSize: Theme.iconSize + 8 anchors.leftMargin: Theme.spacingL
anchors.right: closeButton.left anchors.rightMargin: Theme.spacingL
anchors.rightMargin: 2 spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: ToastService.hasDetails
onClicked: { Item {
toast.expanded = !toast.expanded width: parent.width
if (toast.expanded) { height: Theme.iconSize + 8
ToastService.stopTimer()
} else { DankIcon {
ToastService.restartTimer() id: statusIcon
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.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: messageText
text: ToastService.currentMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
anchors.left: statusIcon.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
wrapMode: Text.NoWrap
}
DankActionButton {
iconName: toast.expanded ? "expand_less" : "expand_more"
iconSize: Theme.iconSize
iconColor: Theme.background
buttonSize: Theme.iconSize + 8
anchors.right: closeButton.left
anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter
visible: ToastService.hasDetails
onClicked: {
toast.expanded = !toast.expanded
if (toast.expanded) {
ToastService.stopTimer()
} else {
ToastService.restartTimer()
}
}
}
DankActionButton {
id: closeButton
iconName: "close"
iconSize: Theme.iconSize
iconColor: Theme.background
buttonSize: Theme.iconSize + 8
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: ToastService.hasDetails
onClicked: {
ToastService.hideToast()
}
}
} }
}
}
DankActionButton { Rectangle {
id: closeButton width: parent.width
iconName: "close" height: detailsText.height + Theme.spacingS * 2
iconSize: Theme.iconSize color: Qt.rgba(0, 0, 0, 0.2)
iconColor: Theme.background radius: Theme.cornerRadius / 2
buttonSize: Theme.iconSize + 8 visible: toast.expanded && ToastService.hasDetails
anchors.right: parent.right anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
visible: ToastService.hasDetails
onClicked: { StyledText {
ToastService.hideToast() id: detailsText
} text: ToastService.currentDetails
} font.pixelSize: Theme.fontSizeSmall
} color: Theme.background
isMonospace: true
anchors.left: parent.left
anchors.right: copyButton.left
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingS
anchors.rightMargin: Theme.spacingS
wrapMode: Text.Wrap
}
Rectangle { DankActionButton {
width: parent.width id: copyButton
height: detailsText.height + Theme.spacingS * 2 iconName: "content_copy"
color: Qt.rgba(0, 0, 0, 0.2) iconSize: Theme.iconSizeSmall
radius: Theme.cornerRadius / 2 iconColor: Theme.background
visible: toast.expanded && ToastService.hasDetails buttonSize: Theme.iconSizeSmall + 8
anchors.horizontalCenter: parent.horizontalCenter anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS
StyledText { property bool showTooltip: false
id: detailsText
text: ToastService.currentDetails
font.pixelSize: Theme.fontSizeSmall
color: Theme.background
isMonospace: true
anchors.left: parent.left
anchors.right: copyButton.left
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingS
anchors.rightMargin: Theme.spacingS
wrapMode: Text.Wrap
}
DankActionButton { onClicked: {
id: copyButton Quickshell.execDetached(
iconName: "content_copy" ["wl-copy", ToastService.currentDetails])
iconSize: Theme.iconSizeSmall showTooltip = true
iconColor: Theme.background tooltipTimer.start()
buttonSize: Theme.iconSizeSmall + 8 }
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS
property bool showTooltip: false Timer {
id: tooltipTimer
interval: 1500
onTriggered: copyButton.showTooltip = false
}
onClicked: { Rectangle {
Quickshell.execDetached(["wl-copy", ToastService.currentDetails]) visible: copyButton.showTooltip
showTooltip = true width: tooltipLabel.implicitWidth + 16
tooltipTimer.start() height: tooltipLabel.implicitHeight + 8
} color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 4
x: -width / 2 + copyButton.width / 2
Timer { StyledText {
id: tooltipTimer id: tooltipLabel
interval: 1500 anchors.centerIn: parent
onTriggered: copyButton.showTooltip = false text: "Copied!"
} font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
Rectangle { }
visible: copyButton.showTooltip }
width: tooltipLabel.implicitWidth + 16 }
height: tooltipLabel.implicitHeight + 8
color: Theme.surfaceContainer
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
y: -height - 4
x: -width / 2 + copyButton.width / 2
StyledText {
id: tooltipLabel
anchors.centerIn: parent
text: "Copied!"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
} }
}
} }
}
MouseArea {
anchors.fill: parent
visible: !ToastService.hasDetails
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 color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on height {
enabled: ToastService.toastVisible
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.Linear
}
}
Behavior on width {
enabled: false
}
} }
MouseArea { mask: Region {
anchors.fill: parent item: toast
visible: !ToastService.hasDetails
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 color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on height {
enabled: ToastService.toastVisible
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.Linear
}
}
Behavior on width {
enabled: false
}
}
mask: Region {
item: toast
}
} }

View File

@@ -4,71 +4,72 @@ 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 readonly property bool isPlaying: hasActiveMedia && activePlayer
&& activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.playbackState === MprisPlaybackState.Playing
width: 20 width: 20
height: Theme.iconSize height: Theme.iconSize
Loader { Loader {
active: isPlaying active: isPlaying
sourceComponent: Component { sourceComponent: Component {
Ref { Ref {
service: CavaService service: CavaService
} }
} }
} }
Timer { Timer {
id: fallbackTimer id: fallbackTimer
running: !CavaService.cavaAvailable && isPlaying running: !CavaService.cavaAvailable && isPlaying
interval: 256 interval: 256
repeat: true repeat: true
onTriggered: { onTriggered: {
CavaService.values = [Math.random() * 40 + 10, Math.random( CavaService.values = [Math.random() * 40 + 10, Math.random(
) * 60 + 20, Math.random() * 50 + 15, Math.random( ) * 60 + 20, Math.random() * 50 + 15, Math.random(
) * 35 + 20, Math.random() * 45 + 15, Math.random( ) * 35 + 20, Math.random() * 45 + 15, Math.random(
) * 55 + 25] ) * 55 + 25]
} }
} }
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 1.5 spacing: 1.5
Repeater { Repeater {
model: 6 model: 6
Rectangle { Rectangle {
width: 2 width: 2
height: { height: {
if (root.isPlaying && CavaService.values.length > index) { if (root.isPlaying && CavaService.values.length > index) {
const rawLevel = CavaService.values[index] || 0 const rawLevel = CavaService.values[index] || 0
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), const scaledLevel = Math.sqrt(
100) / 100) * 100 Math.min(Math.max(rawLevel, 0),
const maxHeight = Theme.iconSize - 2 100) / 100) * 100
const minHeight = 3 const maxHeight = Theme.iconSize - 2
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight) const minHeight = 3
} return minHeight + (scaledLevel / 100) * (maxHeight - minHeight)
return 3 }
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
}
}
}
} }
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
}
}
}
} }
}
} }

View File

@@ -5,202 +5,202 @@ 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: 2
DankIcon {
name: {
if (!BatteryService.batteryAvailable)
return "power"
if (BatteryService.isCharging) {
if (BatteryService.batteryLevel >= 90)
return "battery_charging_full"
if (BatteryService.batteryLevel >= 80)
return "battery_charging_90"
if (BatteryService.batteryLevel >= 60)
return "battery_charging_80"
if (BatteryService.batteryLevel >= 50)
return "battery_charging_60"
if (BatteryService.batteryLevel >= 30)
return "battery_charging_50"
if (BatteryService.batteryLevel >= 20)
return "battery_charging_30"
return "battery_charging_20"
}
// Check if plugged in but not charging (like at 80% charge limit)
if (BatteryService.isPluggedIn) {
if (BatteryService.batteryLevel >= 90)
return "battery_charging_full"
if (BatteryService.batteryLevel >= 80)
return "battery_charging_90"
if (BatteryService.batteryLevel >= 60)
return "battery_charging_80"
if (BatteryService.batteryLevel >= 50)
return "battery_charging_60"
if (BatteryService.batteryLevel >= 30)
return "battery_charging_50"
if (BatteryService.batteryLevel >= 20)
return "battery_charging_30"
return "battery_charging_20"
}
// On battery power
if (BatteryService.batteryLevel >= 95)
return "battery_full"
if (BatteryService.batteryLevel >= 85)
return "battery_6_bar"
if (BatteryService.batteryLevel >= 70)
return "battery_5_bar"
if (BatteryService.batteryLevel >= 55)
return "battery_4_bar"
if (BatteryService.batteryLevel >= 40)
return "battery_3_bar"
if (BatteryService.batteryLevel >= 25)
return "battery_2_bar"
return "battery_1_bar"
}
size: Theme.iconSize - 6
color: {
if (!BatteryService.batteryAvailable)
return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error
if (BatteryService.isCharging || BatteryService.isPluggedIn)
return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
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
onPressed: {
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: Theme.surfaceContainer color: {
border.color: Theme.surfaceVariantAlpha const baseColor = batteryArea.containsMouse
border.width: 1 || batteryPopupVisible ? Theme.primaryPressed : Theme.secondaryHover
visible: batteryArea.containsMouse && !batteryPopupVisible return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
anchors.bottom: parent.top baseColor.a * Theme.widgetTransparency)
anchors.bottomMargin: Theme.spacingS }
anchors.horizontalCenter: parent.horizontalCenter visible: true
opacity: batteryArea.containsMouse ? 1 : 0
Column { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 2 spacing: 2
StyledText { DankIcon {
id: tooltipText name: {
if (!BatteryService.batteryAvailable)
return "power"
text: { if (BatteryService.isCharging) {
if (!BatteryService.batteryAvailable) { if (BatteryService.batteryLevel >= 90)
if (typeof PowerProfiles === "undefined") return "battery_charging_full"
return "Power Management" if (BatteryService.batteryLevel >= 80)
return "battery_charging_90"
if (BatteryService.batteryLevel >= 60)
return "battery_charging_80"
if (BatteryService.batteryLevel >= 50)
return "battery_charging_60"
if (BatteryService.batteryLevel >= 30)
return "battery_charging_50"
if (BatteryService.batteryLevel >= 20)
return "battery_charging_30"
return "battery_charging_20"
}
switch (PowerProfiles.profile) { // Check if plugged in but not charging (like at 80% charge limit)
case PowerProfile.PowerSaver: if (BatteryService.isPluggedIn) {
return "Power Profile: Power Saver" if (BatteryService.batteryLevel >= 90)
case PowerProfile.Performance: return "battery_charging_full"
return "Power Profile: Performance" if (BatteryService.batteryLevel >= 80)
default: return "battery_charging_90"
return "Power Profile: Balanced" if (BatteryService.batteryLevel >= 60)
return "battery_charging_80"
if (BatteryService.batteryLevel >= 50)
return "battery_charging_60"
if (BatteryService.batteryLevel >= 30)
return "battery_charging_50"
if (BatteryService.batteryLevel >= 20)
return "battery_charging_30"
return "battery_charging_20"
}
// On battery power
if (BatteryService.batteryLevel >= 95)
return "battery_full"
if (BatteryService.batteryLevel >= 85)
return "battery_6_bar"
if (BatteryService.batteryLevel >= 70)
return "battery_5_bar"
if (BatteryService.batteryLevel >= 55)
return "battery_4_bar"
if (BatteryService.batteryLevel >= 40)
return "battery_3_bar"
if (BatteryService.batteryLevel >= 25)
return "battery_2_bar"
return "battery_1_bar"
} }
} size: Theme.iconSize - 6
let status = BatteryService.batteryStatus color: {
let level = BatteryService.batteryLevel + "%" if (!BatteryService.batteryAvailable)
let time = BatteryService.formatTimeRemaining() return Theme.surfaceText
if (time !== "Unknown")
return status + " • " + level + " • " + time if (BatteryService.isLowBattery && !BatteryService.isCharging)
else return Theme.error
return status + " • " + level
if (BatteryService.isCharging || BatteryService.isPluggedIn)
return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
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
} }
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
} }
Behavior on opacity { MouseArea {
NumberAnimation { id: batteryArea
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on color { anchors.fill: parent
ColorAnimation { hoverEnabled: true
duration: Theme.shortDuration cursorShape: Qt.PointingHandCursor
easing.type: Theme.standardEasing onPressed: {
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
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

@@ -12,17 +12,18 @@ Rectangle {
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: { Component.onCompleted: {
root.currentDate = systemClock.date; root.currentDate = systemClock.date
} }
Row { Row {
@@ -32,7 +33,10 @@ Rectangle {
spacing: Theme.spacingS spacing: Theme.spacingS
StyledText { StyledText {
text: SettingsData.use24HourClock ? Qt.formatTime(root.currentDate, "H:mm") : Qt.formatTime(root.currentDate, "h:mm AP") text: SettingsData.use24HourClock ? Qt.formatTime(
root.currentDate,
"H:mm") : Qt.formatTime(
root.currentDate, "h:mm AP")
font.pixelSize: Theme.fontSizeMedium - 1 font.pixelSize: Theme.fontSizeMedium - 1
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -53,7 +57,6 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.clockCompactMode visible: !SettingsData.clockCompactMode
} }
} }
SystemClock { SystemClock {
@@ -71,13 +74,15 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: { onPressed: {
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,
width, section, currentScreen)
} }
root.clockClicked(); root.clockClicked()
} }
} }
@@ -86,7 +91,5 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }

View File

@@ -4,134 +4,139 @@ 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
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2) width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = controlCenterArea.containsMouse const baseColor = controlCenterArea.containsMouse
|| root.isActive ? Theme.primaryPressed : Theme.secondaryHover || root.isActive ? Theme.primaryPressed : Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency) baseColor.a * Theme.widgetTransparency)
}
Row {
id: controlIndicators
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
if (NetworkService.networkStatus === "ethernet")
return "lan"
return NetworkService.wifiSignalIcon
}
size: Theme.iconSize - 8
color: NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: true
} }
DankIcon { Row {
name: "bluetooth" id: controlIndicators
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: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)
return "volume_off"
else if (AudioService.sink.audio.volume * 100 < 33)
return "volume_down"
else
return "volume_up"
}
return "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
MouseArea { DankIcon {
id: audioWheelArea name: {
if (NetworkService.networkStatus === "ethernet")
return "lan"
return NetworkService.wifiSignalIcon
}
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: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted
|| AudioService.sink.audio.volume === 0)
return "volume_off"
else if (AudioService.sink.audio.volume * 100 < 33)
return "volume_down"
else
return "volume_up"
}
return "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
acceptedButtons: Qt.NoButton cursorShape: Qt.PointingHandCursor
onWheel: function (wheelEvent) { onPressed: {
let delta = wheelEvent.angleDelta.y if (popupTarget && popupTarget.setTriggerPosition) {
let currentVolume = (AudioService.sink && AudioService.sink.audio var globalPos = mapToGlobal(0, 0)
&& AudioService.sink.audio.volume * 100) || 0 var currentScreen = parentScreen || Screen
let newVolume var screenX = currentScreen.x || 0
if (delta > 0) var relativeX = globalPos.x - screenX
newVolume = Math.min(100, currentVolume + 5) popupTarget.setTriggerPosition(
else relativeX, Theme.barHeight + Theme.spacingXS,
newVolume = Math.max(0, currentVolume - 5) width, section, currentScreen)
if (AudioService.sink && AudioService.sink.audio) { }
AudioService.sink.audio.muted = false root.clicked()
AudioService.sink.audio.volume = newVolume / 100
}
wheelEvent.accepted = true
} }
}
} }
DankIcon { Behavior on color {
name: "mic" ColorAnimation {
size: Theme.iconSize - 8 duration: Theme.shortDuration
color: Theme.primary easing.type: Theme.standardEasing
anchors.verticalCenter: parent.verticalCenter }
visible: false // TODO: Add mic detection
} }
}
MouseArea {
id: controlCenterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
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,84 +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, return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency) baseColor.a * Theme.widgetTransparency)
}
Component.onCompleted: {
DgopService.addRef(["cpu"])
}
Component.onDestruction: {
DgopService.removeRef(["cpu"])
}
MouseArea {
id: cpuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
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)
}
DgopService.setSortBy("cpu")
if (root.toggleProcessList)
root.toggleProcessList()
} }
} Component.onCompleted: {
DgopService.addRef(["cpu"])
Row { }
anchors.centerIn: parent Component.onDestruction: {
spacing: 3 DgopService.removeRef(["cpu"])
DankIcon {
name: "memory"
size: Theme.iconSize - 8
color: {
if (DgopService.cpuUsage > 80)
return Theme.tempDanger
if (DgopService.cpuUsage > 60)
return Theme.tempWarning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
} }
StyledText { MouseArea {
text: { id: cpuArea
if (DgopService.cpuUsage === undefined
|| DgopService.cpuUsage === null anchors.fill: parent
|| DgopService.cpuUsage === 0) { hoverEnabled: true
return "--%" cursorShape: Qt.PointingHandCursor
onPressed: {
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)
}
DgopService.setSortBy("cpu")
if (root.toggleProcessList)
root.toggleProcessList()
}
}
Row {
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "memory"
size: Theme.iconSize - 8
color: {
if (DgopService.cpuUsage > 80)
return Theme.tempDanger
if (DgopService.cpuUsage > 60)
return Theme.tempWarning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (DgopService.cpuUsage === undefined
|| DgopService.cpuUsage === null
|| DgopService.cpuUsage === 0) {
return "--%"
}
return DgopService.cpuUsage.toFixed(0) + "%"
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
return DgopService.cpuUsage.toFixed(0) + "%"
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
}
} }

View File

@@ -5,91 +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, return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency) baseColor.a * Theme.widgetTransparency)
}
Component.onCompleted: {
DgopService.addRef(["cpu"])
}
Component.onDestruction: {
DgopService.removeRef(["cpu"])
}
MouseArea {
id: cpuTempArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
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)
}
DgopService.setSortBy("cpu")
if (root.toggleProcessList)
root.toggleProcessList()
} }
} Component.onCompleted: {
DgopService.addRef(["cpu"])
Row { }
anchors.centerIn: parent Component.onDestruction: {
spacing: 3 DgopService.removeRef(["cpu"])
DankIcon {
name: "memory"
size: Theme.iconSize - 8
color: {
if (DgopService.cpuTemperature > 85)
return Theme.tempDanger
if (DgopService.cpuTemperature > 69)
return Theme.tempWarning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
} }
StyledText { MouseArea {
text: { id: cpuTempArea
if (DgopService.cpuTemperature === undefined
|| DgopService.cpuTemperature === null anchors.fill: parent
|| DgopService.cpuTemperature < 0) { hoverEnabled: true
return "--°" cursorShape: Qt.PointingHandCursor
onPressed: {
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)
}
DgopService.setSortBy("cpu")
if (root.toggleProcessList)
root.toggleProcessList()
} }
return Math.round(DgopService.cpuTemperature) + "°"
}
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
}
Behavior on color { Row {
ColorAnimation { anchors.centerIn: parent
duration: Theme.shortDuration spacing: 3
easing.type: Theme.standardEasing
DankIcon {
name: "memory"
size: Theme.iconSize - 8
color: {
if (DgopService.cpuTemperature > 85)
return Theme.tempDanger
if (DgopService.cpuTemperature > 69)
return Theme.tempWarning
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (DgopService.cpuTemperature === undefined
|| DgopService.cpuTemperature === null
|| DgopService.cpuTemperature < 0) {
return "--°"
}
return Math.round(DgopService.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

@@ -13,15 +13,18 @@ Rectangle {
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,
maxCompactWidth) : Math.min(baseWidth,
maxNormalWidth)
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (!NiriService.focusedWindowTitle) if (!NiriService.focusedWindowTitle)
return "transparent"; 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 clip: true
visible: NiriService.niriAvailable && NiriService.focusedWindowTitle visible: NiriService.niriAvailable && NiriService.focusedWindowTitle
@@ -37,16 +40,17 @@ Rectangle {
text: { text: {
if (!NiriService.focusedWindowId) if (!NiriService.focusedWindowId)
return ""; return ""
var window = NiriService.windows.find((w) => { var window = NiriService.windows.find(w => {
return w.id == NiriService.focusedWindowId; return w.id == NiriService.focusedWindowId
}); })
if (!window || !window.app_id) if (!window || !window.app_id)
return ""; return ""
var desktopEntry = DesktopEntries.byId(window.app_id); var desktopEntry = DesktopEntries.byId(window.app_id)
return desktopEntry && desktopEntry.name ? desktopEntry.name : window.app_id; return desktopEntry
&& desktopEntry.name ? desktopEntry.name : window.app_id
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
@@ -70,20 +74,24 @@ Rectangle {
id: titleText id: titleText
text: { text: {
var title = NiriService.focusedWindowTitle || ""; var title = NiriService.focusedWindowTitle || ""
var appName = appText.text; var appName = appText.text
if (!title || !appName) return title; if (!title || !appName)
return title
// Remove app name from end of title if it exists there // Remove app name from end of title if it exists there
if (title.endsWith(" - " + appName)) { if (title.endsWith(" - " + appName)) {
return title.substring(0, title.length - (" - " + appName).length); return title.substring(
0, title.length - (" - " + appName).length)
} }
if (title.endsWith(appName)) { if (title.endsWith(appName)) {
return title.substring(0, title.length - appName.length).replace(/ - $/, ""); return title.substring(
0, title.length - appName.length).replace(
/ - $/, "")
} }
return title; return title
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
@@ -94,7 +102,6 @@ Rectangle {
width: Math.min(implicitWidth, compactMode ? 280 : 250) width: Math.min(implicitWidth, compactMode ? 280 : 250)
visible: text.length > 0 visible: text.length > 0
} }
} }
MouseArea { MouseArea {
@@ -109,7 +116,6 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
Behavior on width { Behavior on width {
@@ -117,7 +123,5 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }

View File

@@ -5,182 +5,185 @@ 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 property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex
!== undefined) ? widgetData.selectedGpuIndex : 0 !== 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 return (root.widgetData
&& root.widgetData.selectedGpuIndex !== undefined) ? root.widgetData.selectedGpuIndex : 0 && 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: {
DgopService.addRef(["gpu"])
console.log("GpuTemperature widget - pciId:", widgetData ? widgetData.pciId : "no widgetData", "selectedGpuIndex:", widgetData ? widgetData.selectedGpuIndex : "no widgetData")
// Add this widget's PCI ID to the service
if (widgetData && widgetData.pciId) {
console.log("Adding GPU PCI ID to service:", widgetData.pciId)
DgopService.addGpuPciId(widgetData.pciId)
} else {
console.log("No PCI ID in widget data, starting auto-detection")
// No PCI ID saved, auto-detect and save the first GPU
autoSaveTimer.running = true
}
}
Component.onDestruction: {
DgopService.removeRef(["gpu"])
// Remove this widget's PCI ID from the service
if (widgetData && widgetData.pciId) {
DgopService.removeGpuPciId(widgetData.pciId)
}
}
property real displayTemp: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0)
return 0
if (selectedGpuIndex >= 0
&& selectedGpuIndex < DgopService.availableGpus.length) {
return DgopService.availableGpus[selectedGpuIndex].temperature || 0
}
return 0
}
MouseArea {
id: gpuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
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)
}
DgopService.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 { width: 55
ColorAnimation { height: 30
duration: Theme.shortDuration radius: Theme.cornerRadius
easing.type: Theme.standardEasing color: {
const baseColor = gpuArea.containsMouse ? Theme.primaryPressed : Theme.secondaryHover
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
} }
} Component.onCompleted: {
DgopService.addRef(["gpu"])
Timer { console.log("GpuTemperature widget - pciId:",
id: autoSaveTimer widgetData ? widgetData.pciId : "no widgetData",
interval: 100 "selectedGpuIndex:",
running: false widgetData ? widgetData.selectedGpuIndex : "no widgetData")
onTriggered: { // Add this widget's PCI ID to the service
if (DgopService.availableGpus && DgopService.availableGpus.length > 0) { if (widgetData && widgetData.pciId) {
const firstGpu = DgopService.availableGpus[0] console.log("Adding GPU PCI ID to service:", widgetData.pciId)
if (firstGpu && firstGpu.pciId) { DgopService.addGpuPciId(widgetData.pciId)
// Save the first GPU's PCI ID to this widget's settings } else {
updateWidgetPciId(firstGpu.pciId) console.log("No PCI ID in widget data, starting auto-detection")
DgopService.addGpuPciId(firstGpu.pciId) // No PCI ID saved, auto-detect and save the first GPU
autoSaveTimer.running = true
} }
}
} }
} Component.onDestruction: {
DgopService.removeRef(["gpu"])
function updateWidgetPciId(pciId) { // Remove this widget's PCI ID from the service
// Find and update this widget's pciId in the settings if (widgetData && widgetData.pciId) {
var sections = ["left", "center", "right"] DgopService.removeGpuPciId(widgetData.pciId)
for (var s = 0; s < sections.length; s++) {
var sectionId = sections[s]
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i]
if (typeof widget === "object" && widget.id === "gpuTemp"
&& (!widget.pciId || widget.pciId === "")) {
widgets[i] = {
"id": widget.id,
"enabled": widget.enabled !== undefined ? widget.enabled : true,
"selectedGpuIndex": 0,
"pciId": pciId
}
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
return
} }
}
} }
}
property real displayTemp: {
if (!DgopService.availableGpus
|| DgopService.availableGpus.length === 0)
return 0
if (selectedGpuIndex >= 0
&& selectedGpuIndex < DgopService.availableGpus.length) {
return DgopService.availableGpus[selectedGpuIndex].temperature || 0
}
return 0
}
MouseArea {
id: gpuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
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)
}
DgopService.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
}
}
Timer {
id: autoSaveTimer
interval: 100
running: false
onTriggered: {
if (DgopService.availableGpus
&& DgopService.availableGpus.length > 0) {
const firstGpu = DgopService.availableGpus[0]
if (firstGpu && firstGpu.pciId) {
// Save the first GPU's PCI ID to this widget's settings
updateWidgetPciId(firstGpu.pciId)
DgopService.addGpuPciId(firstGpu.pciId)
}
}
}
}
function updateWidgetPciId(pciId) {
// Find and update this widget's pciId in the settings
var sections = ["left", "center", "right"]
for (var s = 0; s < sections.length; s++) {
var sectionId = sections[s]
var widgets = []
if (sectionId === "left")
widgets = SettingsData.topBarLeftWidgets.slice()
else if (sectionId === "center")
widgets = SettingsData.topBarCenterWidgets.slice()
else if (sectionId === "right")
widgets = SettingsData.topBarRightWidgets.slice()
for (var i = 0; i < widgets.length; i++) {
var widget = widgets[i]
if (typeof widget === "object" && widget.id === "gpuTemp"
&& (!widget.pciId || widget.pciId === "")) {
widgets[i] = {
"id": widget.id,
"enabled": widget.enabled !== undefined ? widget.enabled : true,
"selectedGpuIndex": 0,
"pciId": pciId
}
if (sectionId === "left")
SettingsData.setTopBarLeftWidgets(widgets)
else if (sectionId === "center")
SettingsData.setTopBarCenterWidgets(widgets)
else if (sectionId === "right")
SettingsData.setTopBarRightWidgets(widgets)
return
}
}
}
}
} }

View File

@@ -7,46 +7,43 @@ 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
width: 40 width: 40
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = mouseArea.containsMouse const baseColor = mouseArea.containsMouse ? Theme.primaryPressed : (IdleInhibitorService.idleInhibited ? Theme.primaryHover : Theme.secondaryHover)
? Theme.primaryPressed return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
: (IdleInhibitorService.idleInhibited ? Theme.primaryHover : Theme.secondaryHover) baseColor.a * Theme.widgetTransparency)
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b,
baseColor.a * Theme.widgetTransparency)
} }
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: IdleInhibitorService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" name: IdleInhibitorService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
size: Theme.iconSize - 6 size: Theme.iconSize - 6
color: Theme.surfaceText color: Theme.surfaceText
} }
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
IdleInhibitorService.toggleIdleInhibit() IdleInhibitorService.toggleIdleInhibit()
} }
} }
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }

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