1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

Initial qmlformat

This commit is contained in:
bbedward
2025-07-17 17:58:56 -04:00
parent cbb42df3a9
commit 023b6bc0d1
62 changed files with 7805 additions and 6606 deletions

View File

@@ -25,6 +25,10 @@ quickshell -p shell.qml
# Or use the shorthand
qs -p .
# Code formatting and linting
qmlformat -i **/*.qml # Format all QML files in place
qmllint **/*.qml # Lint all QML files for syntax errors
```
## Architecture Overview
@@ -119,13 +123,19 @@ shell.qml # Main entry point (minimal orchestration)
- `id` should be the first property
- Properties before signal handlers before child components
- Prefer property bindings over imperative code
- **IMPORTANT**: Be very conservative with comments - add comments only when absolutely necessary for understanding complex logic
2. **Naming Conventions**:
- **Services**: Use `Singleton` type with `id: root`
- **Components**: Use descriptive names (e.g., `CustomSlider`, `TopBar`)
- **Properties**: camelCase for properties, PascalCase for types
3. **Component Structure**:
3. **Null-Safe Operations**:
- **Do NOT use** `?.` operator (not supported by qmlformat)
- **Use** `object && object.property` instead of `object?.property`
- **Example**: `activePlayer && activePlayer.trackTitle` instead of `activePlayer?.trackTitle`
4. **Component Structure**:
```qml
// For regular components
Item {
@@ -230,11 +240,12 @@ The shell uses Quickshell's `Variants` pattern for multi-monitor support:
When modifying the shell:
1. **Test changes**: `qs -p .` (automatic reload on file changes)
2. **Performance**: Ensure animations remain smooth (60 FPS target)
3. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
4. **Wayland compatibility**: Test on Wayland session
5. **Multi-monitor**: Verify behavior with multiple displays
6. **Feature detection**: Test on systems with/without required tools
2. **Code quality**: Run `qmlformat -i **/*.qml` and `qmllint **/*.qml` to ensure proper formatting and syntax
3. **Performance**: Ensure animations remain smooth (60 FPS target)
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
5. **Wayland compatibility**: Test on Wayland session
6. **Multi-monitor**: Verify behavior with multiple displays
7. **Feature detection**: Test on systems with/without required tools
### Adding New Widgets

View File

@@ -1,180 +1,181 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Qt.labs.platform
import QtQuick
import Quickshell
import Quickshell.Io
import Qt.labs.platform
import qs.Services
Singleton {
// This dependency forces re-evaluation when colorUpdateTrigger changes
// Just check if matugen is available
id: root
/* ──────────────── basic state ──────────────── */
signal colorsUpdated()
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
readonly property string homeDir: _homeUrl.startsWith("file://")
? _homeUrl.substring(7)
: _homeUrl
readonly property string homeDir: _homeUrl.startsWith("file://") ? _homeUrl.substring(7) : _homeUrl
readonly property string wallpaperPath: homeDir + "/quickshell/current_wallpaper"
readonly property string notifyPath: homeDir + "/quickshell/wallpaper_changed"
property bool matugenAvailable: false
property string matugenJson: ""
property var matugenColors: ({})
property var matugenColors: ({
})
property bool extractionRequested: false
property int colorUpdateTrigger: 0 // Force property re-evaluation
Component.onCompleted: {
console.log("Colors.qml → home =", homeDir)
// Don't automatically run color extraction - only when requested
matugenCheck.running = true // Just check if matugen is available
// Connect to Theme light mode changes to update colors
if (typeof Theme !== "undefined") {
Theme.isLightModeChanged.connect(root.onLightModeChanged)
}
}
function onLightModeChanged() {
// Force color properties to update when light mode changes
if (matugenColors && Object.keys(matugenColors).length > 0) {
console.log("Light mode changed - updating dynamic colors")
colorUpdateTrigger++ // This will trigger re-evaluation of all color properties
colorsUpdated()
}
}
/* ──────────────── availability checks ──────────────── */
Process {
id: matugenCheck
command: ["which", "matugen"]
onExited: (code) => {
matugenAvailable = (code === 0)
console.log("Matugen in PATH:", matugenAvailable)
if (!matugenAvailable) {
console.warn("Matugen missing → dynamic theme disabled")
ToastService.wallpaperErrorStatus = "matugen_missing"
ToastService.showWarning("matugen not found - dynamic theming disabled")
return
}
// If extraction was requested, continue the process
if (extractionRequested) {
console.log("Continuing with color extraction")
fileChecker.running = true
}
}
}
Process {
id: fileChecker // exists & readable?
command: ["test", "-r", wallpaperPath]
onExited: (code) => {
if (code === 0) {
matugenProcess.running = true
} else {
console.error("code", code)
console.error("Wallpaper not found:", wallpaperPath)
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed")
}
}
}
/* ──────────────── matugen invocation ──────────────── */
Process {
id: matugenProcess
command: ["matugen", "-v", "image", wallpaperPath, "--json", "hex"]
/* ── grab stdout as a stream ── */
stdout: StdioCollector {
id: matugenCollector
onStreamFinished: {
const out = matugenCollector.text
if (!out.length) {
console.error("matugen produced zero bytes\nstderr:", matugenProcess.stderr)
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed")
return
}
try {
root.matugenJson = out
root.matugenColors = JSON.parse(out)
root.colorsUpdated()
ToastService.clearWallpaperError()
ToastService.showInfo("Dynamic theme colors updated")
} catch (e) {
console.error("JSON parse failed:", e)
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed")
}
}
}
/* grab stderr too, so we can print it above */
stderr: StdioCollector { id: matugenErr }
}
/* ──────────────── wallpaper change monitor ──────────────── */
// ──────────────── wallpaper change monitor ────────────────
property string lastWallpaperTimestamp: ""
/* ──────────────── public helper ──────────────── */
function extractColors() {
console.log("Colors.extractColors() called, matugenAvailable:", matugenAvailable)
extractionRequested = true
if (matugenAvailable)
fileChecker.running = true
else
matugenCheck.running = true
}
function getMatugenColor(path, fallback) {
// Include colorUpdateTrigger in the function to make properties reactive to changes
colorUpdateTrigger // This dependency forces re-evaluation when colorUpdateTrigger changes
// Use light or dark colors based on Theme.isLightMode
const colorMode = (typeof Theme !== "undefined" && Theme.isLightMode) ? "light" : "dark"
let cur = 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
}
/* ──────────────── color properties (MD3) ──────────────── */
// ──────────────── color properties (MD3) ────────────────
property color primary: getMatugenColor("primary", "#42a5f5")
property color secondary: getMatugenColor("secondary", "#8ab4f8")
property color tertiary: getMatugenColor("tertiary", "#bb86fc")
property color tertiaryContainer: getMatugenColor("tertiary_container", "#3700b3")
property color error: getMatugenColor("error", "#cf6679")
property color inversePrimary: getMatugenColor("inverse_primary", "#6200ea")
/* backgrounds */
// backgrounds
property color bg: getMatugenColor("background", "#1a1c1e")
property color surface: getMatugenColor("surface", "#1a1c1e")
property color surfaceContainer: getMatugenColor("surface_container", "#1e2023")
property color surfaceContainerHigh: getMatugenColor("surface_container_high", "#292b2f")
property color surfaceVariant: getMatugenColor("surface_variant", "#44464f")
/* text */
// text
property color surfaceText: getMatugenColor("on_background", "#e3e8ef")
property color primaryText: getMatugenColor("on_primary", "#ffffff")
property color surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5")
/* containers & misc */
// containers & misc
property color primaryContainer: getMatugenColor("primary_container", "#1976d2")
property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8")
property color outline: getMatugenColor("outline", "#8e918f")
/* legacy aliases */
// legacy aliases
property color accentHi: primary
property color accentLo: secondary
function isColorDark(c) {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5
// ──────────────── basic state ────────────────
signal colorsUpdated()
function onLightModeChanged() {
// Force color properties to update when light mode changes
if (matugenColors && Object.keys(matugenColors).length > 0) {
console.log("Light mode changed - updating dynamic colors");
colorUpdateTrigger++; // This will trigger re-evaluation of all color properties
colorsUpdated();
}
}
// ──────────────── public helper ────────────────
function extractColors() {
console.log("Colors.extractColors() called, matugenAvailable:", matugenAvailable);
extractionRequested = true;
if (matugenAvailable)
fileChecker.running = true;
else
matugenCheck.running = true;
}
function getMatugenColor(path, fallback) {
// Include colorUpdateTrigger in the function to make properties reactive to changes
colorUpdateTrigger;
// Use light or dark colors based on Theme.isLightMode
const colorMode = (typeof Theme !== "undefined" && Theme.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: {
console.log("Colors.qml → home =", homeDir);
// Don't automatically run color extraction - only when requested
matugenCheck.running = true;
// Connect to Theme light mode changes to update colors
if (typeof Theme !== "undefined")
Theme.isLightModeChanged.connect(root.onLightModeChanged);
}
// ──────────────── availability checks ────────────────
Process {
id: matugenCheck
command: ["which", "matugen"]
onExited: (code) => {
matugenAvailable = (code === 0);
console.log("Matugen in PATH:", matugenAvailable);
if (!matugenAvailable) {
console.warn("Matugen missing → dynamic theme disabled");
ToastService.wallpaperErrorStatus = "matugen_missing";
ToastService.showWarning("matugen not found - dynamic theming disabled");
return ;
}
// If extraction was requested, continue the process
if (extractionRequested) {
console.log("Continuing with color extraction");
fileChecker.running = true;
}
}
}
Process {
id: fileChecker // exists & readable?
command: ["test", "-r", wallpaperPath]
onExited: (code) => {
if (code === 0) {
matugenProcess.running = true;
} else {
console.error("code", code);
console.error("Wallpaper not found:", wallpaperPath);
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper processing failed");
}
}
}
// ──────────────── matugen invocation ────────────────
Process {
id: matugenProcess
command: ["matugen", "-v", "image", wallpaperPath, "--json", "hex"]
// ── grab stdout as a stream ──
stdout: StdioCollector {
id: matugenCollector
onStreamFinished: {
const out = matugenCollector.text;
if (!out.length) {
console.error("matugen produced zero bytes\nstderr:", matugenProcess.stderr);
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper Processing Failed");
return ;
}
try {
root.matugenJson = out;
root.matugenColors = JSON.parse(out);
root.colorsUpdated();
ToastService.clearWallpaperError();
ToastService.showInfo("Loaded Dynamic Theme Colors");
} catch (e) {
console.error("JSON parse failed:", e);
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper Processing Failed");
}
}
}
// grab stderr too, so we can print it above
stderr: StdioCollector {
id: matugenErr
}
}
}

View File

@@ -1,10 +1,15 @@
pragma Singleton
import QtQuick
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
// "auto", "wifi", "ethernet"
// Alphabetical tiebreaker
id: root
property int themeIndex: 0
@@ -13,14 +18,12 @@ Singleton {
property real topBarTransparency: 0.75
property real popupTransparency: 0.92
property var recentlyUsedApps: []
// New global preferences
property bool use24HourClock: true
property bool useFahrenheit: false
property bool nightModeEnabled: false
property string profileImage: ""
property string weatherLocationOverride: "New York, NY"
// Widget visibility preferences for TopBar
property bool showFocusedWindow: true
property bool showWeather: true
@@ -28,294 +31,284 @@ Singleton {
property bool showClipboard: true
property bool showSystemResources: true
property bool showSystemTray: true
// View mode preferences for launchers
property string appLauncherViewMode: "list"
property string spotlightLauncherViewMode: "list"
// Network preference
property string networkPreference: "auto" // "auto", "wifi", "ethernet"
Component.onCompleted: loadSettings()
// Monitor system resources preference changes to control service monitoring
onShowSystemResourcesChanged: {
console.log("Prefs: System resources monitoring", showSystemResources ? "enabled" : "disabled")
// Control SystemMonitorService based on whether system monitor widgets are visible
if (typeof SystemMonitorService !== 'undefined') {
SystemMonitorService.enableTopBarMonitoring(showSystemResources)
}
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
console.log("Settings file loaded successfully")
parseSettings(settingsFile.text())
}
onLoadFailed: (error) => {
console.log("Settings file not found, using defaults. Error:", error)
applyStoredTheme()
}
}
property string networkPreference: "auto"
function loadSettings() {
parseSettings(settingsFile.text())
parseSettings(settingsFile.text());
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content)
themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0
themeIsDynamic = settings.themeIsDynamic !== undefined ? settings.themeIsDynamic : false
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
topBarTransparency = settings.topBarTransparency !== undefined ?
(settings.topBarTransparency > 1 ? settings.topBarTransparency / 100.0 : settings.topBarTransparency) : 0.75
popupTransparency = settings.popupTransparency !== undefined ?
(settings.popupTransparency > 1 ? settings.popupTransparency / 100.0 : settings.popupTransparency) : 0.92
recentlyUsedApps = settings.recentlyUsedApps || []
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
profileImage = settings.profileImage !== undefined ? settings.profileImage : ""
weatherLocationOverride = settings.weatherLocationOverride !== undefined ? settings.weatherLocationOverride : "New York, NY"
showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true
showWeather = settings.showWeather !== undefined ? settings.showWeather : true
showMusic = settings.showMusic !== undefined ? settings.showMusic : true
showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true
showSystemResources = settings.showSystemResources !== undefined ? settings.showSystemResources : true
showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true
appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list"
spotlightLauncherViewMode = settings.spotlightLauncherViewMode !== undefined ? settings.spotlightLauncherViewMode : "list"
networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto"
console.log("Loaded settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length)
applyStoredTheme()
var settings = JSON.parse(content);
themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0;
themeIsDynamic = settings.themeIsDynamic !== undefined ? settings.themeIsDynamic : false;
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false;
topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 0.75;
popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 0.92;
recentlyUsedApps = settings.recentlyUsedApps || [];
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
profileImage = settings.profileImage !== undefined ? settings.profileImage : "";
weatherLocationOverride = settings.weatherLocationOverride !== undefined ? settings.weatherLocationOverride : "New York, NY";
showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true;
showWeather = settings.showWeather !== undefined ? settings.showWeather : true;
showMusic = settings.showMusic !== undefined ? settings.showMusic : true;
showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true;
showSystemResources = settings.showSystemResources !== undefined ? settings.showSystemResources : true;
showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true;
appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list";
spotlightLauncherViewMode = settings.spotlightLauncherViewMode !== undefined ? settings.spotlightLauncherViewMode : "list";
networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto";
console.log("Loaded settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length);
applyStoredTheme();
} else {
console.log("Settings file is empty - applying default theme")
applyStoredTheme()
console.log("Settings file is empty - applying default theme");
applyStoredTheme();
}
} catch (e) {
console.log("Could not parse settings, using defaults:", e)
applyStoredTheme()
console.log("Could not parse settings, using defaults:", e);
applyStoredTheme();
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
themeIndex,
themeIsDynamic,
isLightMode,
topBarTransparency,
popupTransparency,
recentlyUsedApps,
use24HourClock,
useFahrenheit,
nightModeEnabled,
profileImage,
weatherLocationOverride,
showFocusedWindow,
showWeather,
showMusic,
showClipboard,
showSystemResources,
showSystemTray,
appLauncherViewMode,
spotlightLauncherViewMode,
networkPreference
}, null, 2))
console.log("Saving settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length)
"themeIndex": themeIndex,
"themeIsDynamic": themeIsDynamic,
"isLightMode": isLightMode,
"topBarTransparency": topBarTransparency,
"popupTransparency": popupTransparency,
"recentlyUsedApps": recentlyUsedApps,
"use24HourClock": use24HourClock,
"useFahrenheit": useFahrenheit,
"nightModeEnabled": nightModeEnabled,
"profileImage": profileImage,
"weatherLocationOverride": weatherLocationOverride,
"showFocusedWindow": showFocusedWindow,
"showWeather": showWeather,
"showMusic": showMusic,
"showClipboard": showClipboard,
"showSystemResources": showSystemResources,
"showSystemTray": showSystemTray,
"appLauncherViewMode": appLauncherViewMode,
"spotlightLauncherViewMode": spotlightLauncherViewMode,
"networkPreference": networkPreference
}, null, 2));
console.log("Saving settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length);
}
function applyStoredTheme() {
console.log("Applying stored theme:", themeIndex, themeIsDynamic, "lightMode:", isLightMode)
console.log("Applying stored theme:", themeIndex, themeIsDynamic, "lightMode:", isLightMode);
if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode
Theme.switchTheme(themeIndex, themeIsDynamic, false)
Theme.isLightMode = isLightMode;
Theme.switchTheme(themeIndex, themeIsDynamic, false);
} else {
Qt.callLater(() => {
if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode
Theme.switchTheme(themeIndex, themeIsDynamic, false)
Theme.isLightMode = isLightMode;
Theme.switchTheme(themeIndex, themeIsDynamic, false);
}
})
});
}
}
function setTheme(index, isDynamic) {
console.log("Prefs setTheme called - themeIndex:", index, "isDynamic:", isDynamic)
themeIndex = index
themeIsDynamic = isDynamic
saveSettings()
console.log("Prefs setTheme called - themeIndex:", index, "isDynamic:", isDynamic);
themeIndex = index;
themeIsDynamic = isDynamic;
saveSettings();
}
function setLightMode(lightMode) {
console.log("Prefs setLightMode called - isLightMode:", lightMode)
isLightMode = lightMode
saveSettings()
console.log("Prefs setLightMode called - isLightMode:", lightMode);
isLightMode = lightMode;
saveSettings();
}
function setTopBarTransparency(transparency) {
console.log("Prefs setTopBarTransparency called - topBarTransparency:", transparency)
topBarTransparency = transparency
saveSettings()
console.log("Prefs setTopBarTransparency called - topBarTransparency:", transparency);
topBarTransparency = transparency;
saveSettings();
}
function setPopupTransparency(transparency) {
console.log("Prefs setPopupTransparency called - popupTransparency:", transparency)
popupTransparency = transparency
saveSettings()
console.log("Prefs setPopupTransparency called - popupTransparency:", transparency);
popupTransparency = transparency;
saveSettings();
}
function addRecentApp(app) {
if (!app) return
if (!app)
return ;
var execProp = app.execString || app.exec || ""
if (!execProp) return
var execProp = app.execString || app.exec || "";
if (!execProp)
return ;
var existingIndex = -1
var existingIndex = -1;
for (var i = 0; i < recentlyUsedApps.length; i++) {
if (recentlyUsedApps[i].exec === execProp) {
existingIndex = i
break
existingIndex = i;
break;
}
}
if (existingIndex >= 0) {
// App exists, increment usage count
recentlyUsedApps[existingIndex].usageCount = (recentlyUsedApps[existingIndex].usageCount || 1) + 1
recentlyUsedApps[existingIndex].lastUsed = Date.now()
recentlyUsedApps[existingIndex].usageCount = (recentlyUsedApps[existingIndex].usageCount || 1) + 1;
recentlyUsedApps[existingIndex].lastUsed = Date.now();
} else {
// New app, create entry
var appData = {
name: app.name || "",
exec: execProp,
icon: app.icon || "application-x-executable",
comment: app.comment || "",
usageCount: 1,
lastUsed: Date.now()
"name": app.name || "",
"exec": execProp,
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"usageCount": 1,
"lastUsed": Date.now()
};
recentlyUsedApps.push(appData);
}
recentlyUsedApps.push(appData)
}
// Sort by usage count (descending), then alphabetically by name
var sortedApps = recentlyUsedApps.sort(function(a, b) {
if (a.usageCount !== b.usageCount) {
return b.usageCount - a.usageCount // Higher usage count first
}
return a.name.localeCompare(b.name) // Alphabetical tiebreaker
})
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount;
// Higher usage count first
return a.name.localeCompare(b.name);
});
// Limit to 10 apps
if (sortedApps.length > 10) {
sortedApps = sortedApps.slice(0, 10)
}
if (sortedApps.length > 10)
sortedApps = sortedApps.slice(0, 10);
// Reassign to trigger property change signal
recentlyUsedApps = sortedApps
saveSettings()
recentlyUsedApps = sortedApps;
saveSettings();
}
function getRecentApps() {
return recentlyUsedApps
return recentlyUsedApps;
}
// New preference setters
function setClockFormat(use24Hour) {
console.log("Prefs setClockFormat called - use24HourClock:", use24Hour)
use24HourClock = use24Hour
saveSettings()
console.log("Prefs setClockFormat called - use24HourClock:", use24Hour);
use24HourClock = use24Hour;
saveSettings();
}
function setTemperatureUnit(fahrenheit) {
console.log("Prefs setTemperatureUnit called - useFahrenheit:", fahrenheit)
useFahrenheit = fahrenheit
saveSettings()
console.log("Prefs setTemperatureUnit called - useFahrenheit:", fahrenheit);
useFahrenheit = fahrenheit;
saveSettings();
}
function setNightModeEnabled(enabled) {
console.log("Prefs setNightModeEnabled called - nightModeEnabled:", enabled)
nightModeEnabled = enabled
saveSettings()
console.log("Prefs setNightModeEnabled called - nightModeEnabled:", enabled);
nightModeEnabled = enabled;
saveSettings();
}
function setProfileImage(imageUrl) {
console.log("Prefs setProfileImage called - profileImage:", imageUrl)
profileImage = imageUrl
saveSettings()
console.log("Prefs setProfileImage called - profileImage:", imageUrl);
profileImage = imageUrl;
saveSettings();
}
// Widget visibility setters
function setShowFocusedWindow(enabled) {
console.log("Prefs setShowFocusedWindow called - showFocusedWindow:", enabled)
showFocusedWindow = enabled
saveSettings()
console.log("Prefs setShowFocusedWindow called - showFocusedWindow:", enabled);
showFocusedWindow = enabled;
saveSettings();
}
function setShowWeather(enabled) {
console.log("Prefs setShowWeather called - showWeather:", enabled)
showWeather = enabled
saveSettings()
console.log("Prefs setShowWeather called - showWeather:", enabled);
showWeather = enabled;
saveSettings();
}
function setShowMusic(enabled) {
console.log("Prefs setShowMusic called - showMusic:", enabled)
showMusic = enabled
saveSettings()
console.log("Prefs setShowMusic called - showMusic:", enabled);
showMusic = enabled;
saveSettings();
}
function setShowClipboard(enabled) {
console.log("Prefs setShowClipboard called - showClipboard:", enabled)
showClipboard = enabled
saveSettings()
console.log("Prefs setShowClipboard called - showClipboard:", enabled);
showClipboard = enabled;
saveSettings();
}
function setShowSystemResources(enabled) {
console.log("Prefs setShowSystemResources called - showSystemResources:", enabled)
showSystemResources = enabled
saveSettings()
console.log("Prefs setShowSystemResources called - showSystemResources:", enabled);
showSystemResources = enabled;
saveSettings();
}
function setShowSystemTray(enabled) {
console.log("Prefs setShowSystemTray called - showSystemTray:", enabled)
showSystemTray = enabled
saveSettings()
console.log("Prefs setShowSystemTray called - showSystemTray:", enabled);
showSystemTray = enabled;
saveSettings();
}
// View mode setters
function setAppLauncherViewMode(mode) {
console.log("Prefs setAppLauncherViewMode called - appLauncherViewMode:", mode)
appLauncherViewMode = mode
saveSettings()
console.log("Prefs setAppLauncherViewMode called - appLauncherViewMode:", mode);
appLauncherViewMode = mode;
saveSettings();
}
function setSpotlightLauncherViewMode(mode) {
console.log("Prefs setSpotlightLauncherViewMode called - spotlightLauncherViewMode:", mode)
spotlightLauncherViewMode = mode
saveSettings()
console.log("Prefs setSpotlightLauncherViewMode called - spotlightLauncherViewMode:", mode);
spotlightLauncherViewMode = mode;
saveSettings();
}
// Weather location override setter
function setWeatherLocationOverride(location) {
console.log("Prefs setWeatherLocationOverride called - weatherLocationOverride:", location)
weatherLocationOverride = location
saveSettings()
console.log("Prefs setWeatherLocationOverride called - weatherLocationOverride:", location);
weatherLocationOverride = location;
saveSettings();
}
// Network preference setter
function setNetworkPreference(preference) {
console.log("Prefs setNetworkPreference called - networkPreference:", preference)
networkPreference = preference
saveSettings()
console.log("Prefs setNetworkPreference called - networkPreference:", preference);
networkPreference = preference;
saveSettings();
}
Component.onCompleted: loadSettings()
// Monitor system resources preference changes to control service monitoring
onShowSystemResourcesChanged: {
console.log("Prefs: System resources monitoring", showSystemResources ? "enabled" : "disabled");
// Control SystemMonitorService based on whether system monitor widgets are visible
if (typeof SystemMonitorService !== 'undefined')
SystemMonitorService.enableTopBarMonitoring(showSystemResources);
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
console.log("Settings file loaded successfully");
parseSettings(settingsFile.text());
}
onLoadFailed: (error) => {
console.log("Settings file not found, using defaults. Error:", error);
applyStoredTheme();
}
}
}

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Services.UPower

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Bluetooth

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell

View File

@@ -1,40 +1,21 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
import Quickshell.Widgets
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: null
property MprisPlayer _candidatePlayer: availablePlayers.find(p => p.isPlaying)
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
?? availablePlayers.find(p => p.canControl && p.canPlay)
?? null
Timer {
id: playerSwitchTimer
interval: 300
onTriggered: {
if (_candidatePlayer !== activePlayer) {
activePlayer = _candidatePlayer
}
}
}
on_CandidatePlayerChanged: {
if (_candidatePlayer === null && activePlayer !== null) {
playerSwitchTimer.restart()
} else if (_candidatePlayer !== null) {
playerSwitchTimer.stop()
activePlayer = _candidatePlayer
}
}
IpcHandler {
target: "mpris"

View File

@@ -1,9 +1,10 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root

View File

@@ -1,5 +1,6 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io

View File

@@ -1,7 +1,7 @@
pragma Singleton
pragma ComponentBehavior: Bound
import
QtQuick
import QtQuick
import Quickshell
import Quickshell.Io

View File

@@ -1,8 +1,9 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root

View File

@@ -1,9 +1,10 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root

View File

@@ -13,7 +13,7 @@ Singleton {
property var wifiNetworks: []
property var savedWifiNetworks: []
property bool isScanning: false
property string connectionStatus: "" // "connecting", "connected", "failed", ""
property string connectionStatus: "" // "cosnnecting", "connected", "failed", ""
property string connectingSSID: ""
Process {

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,42 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import Quickshell.Services.UPower
PanelWindow {
id: root
property bool batteryPopupVisible: false
visible: batteryPopupVisible
function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined")
return false;
return PowerProfiles.profile === profile;
}
function setProfile(profile) {
if (typeof PowerProfiles === "undefined") {
errorToast.show();
return ;
}
PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile)
errorToast.show();
else
console.log("Set power profile to: " + PowerProfile.toString(profile));
}
visible: batteryPopupVisible
implicitWidth: 400
implicitHeight: 300
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
@@ -34,7 +50,7 @@ PanelWindow {
MouseArea {
anchors.fill: parent
onClicked: {
batteryPopupVisible = false
batteryPopupVisible = false;
}
}
@@ -47,29 +63,15 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: batteryPopupVisible ? 1.0 : 0.0
scale: batteryPopupVisible ? 1.0 : 0.85
opacity: batteryPopupVisible ? 1 : 0
scale: batteryPopupVisible ? 1 : 0.85
// Prevent click-through to background
MouseArea {
// Consume the click to prevent it from reaching the background
anchors.fill: parent
onClicked: {
// Consume the click to prevent it from reaching the background
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
@@ -94,7 +96,10 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 200; height: 1 }
Item {
width: parent.width - 200
height: 1
}
Rectangle {
width: 32
@@ -112,14 +117,17 @@ PanelWindow {
MouseArea {
id: closeBatteryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
batteryPopupVisible = false
batteryPopupVisible = false;
}
}
}
}
Rectangle {
@@ -142,9 +150,13 @@ PanelWindow {
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
@@ -160,9 +172,13 @@ PanelWindow {
text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
font.weight: Font.Bold
}
@@ -171,29 +187,37 @@ PanelWindow {
text: BatteryService.batteryStatus
font.pixelSize: Theme.fontSizeMedium
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: {
let time = BatteryService.formatTimeRemaining()
if (time !== "Unknown") {
return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time
}
return ""
let time = BatteryService.formatTimeRemaining();
if (time !== "Unknown")
return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time;
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
// No battery info card
@@ -234,8 +258,11 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
}
// Battery details
@@ -271,11 +298,14 @@ PanelWindow {
text: BatteryService.batteryHealth
font.pixelSize: Theme.fontSizeMedium
color: {
if (BatteryService.batteryHealth === "N/A") return Theme.surfaceText
var healthNum = parseInt(BatteryService.batteryHealth)
return healthNum < 80 ? Theme.error : Theme.surfaceText
if (BatteryService.batteryHealth === "N/A")
return Theme.surfaceText;
var healthNum = parseInt(BatteryService.batteryHealth);
return healthNum < 80 ? Theme.error : Theme.surfaceText;
}
}
}
// Capacity
@@ -295,8 +325,11 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
}
}
}
// Power profiles
@@ -317,16 +350,13 @@ PanelWindow {
spacing: Theme.spacingS
Repeater {
model: (typeof PowerProfiles !== "undefined") ?
[PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) :
[PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
model: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: profileArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(batteryControlPopup.isActiveProfile(modelData) ? 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.08))
color: profileArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (batteryControlPopup.isActiveProfile(modelData) ? 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.08))
border.color: batteryControlPopup.isActiveProfile(modelData) ? Theme.primary : "transparent"
border.width: 2
@@ -360,22 +390,28 @@ PanelWindow {
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
MouseArea {
id: profileArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
batteryControlPopup.setProfile(modelData)
batteryControlPopup.setProfile(modelData);
}
}
}
}
}
}
// Degradation reason warning
@@ -418,16 +454,44 @@ PanelWindow {
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8)
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Error toast
Rectangle {
id: errorToast
function show() {
visible = true;
hideTimer.restart();
}
width: Math.min(300, parent.width - Theme.spacingL * 2)
height: 50
radius: Theme.cornerRadius
@@ -448,34 +512,11 @@ PanelWindow {
Timer {
id: hideTimer
interval: 3000
onTriggered: errorToast.visible = false
}
function show() {
visible = true
hideTimer.restart()
}
}
function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined") return false
return PowerProfiles.profile === profile
}
function setProfile(profile) {
if (typeof PowerProfiles === "undefined") {
errorToast.show()
return
}
PowerProfiles.profile = profile
if (PowerProfiles.profile !== profile) {
errorToast.show()
} else {
console.log("Set power profile to: " + PowerProfile.toString(profile))
}
}
}

View File

@@ -1,7 +1,7 @@
import QtQuick
import Quickshell.Services.UPower
import qs.Common
import qs.Services
import Quickshell.Services.UPower
Rectangle {
id: batteryWidget
@@ -13,9 +13,7 @@ Rectangle {
width: BatteryService.batteryAvailable ? 70 : 40
height: 30
radius: Theme.cornerRadius
color: batteryArea.containsMouse || batteryPopupVisible ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: batteryArea.containsMouse || batteryPopupVisible ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
visible: true
Row {
@@ -27,19 +25,37 @@ Rectangle {
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
color: {
if (!BatteryService.batteryAvailable) return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
if (!BatteryService.batteryAvailable)
return Theme.surfaceText;
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
SequentialAnimation on opacity {
running: BatteryService.isCharging
loops: Animation.Infinite
NumberAnimation { to: 0.6; duration: 1000; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 1000; easing.type: Easing.InOutQuad }
NumberAnimation {
to: 0.6
duration: 1000
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1
duration: 1000
easing.type: Easing.InOutQuad
}
}
}
Text {
@@ -47,30 +63,38 @@ Rectangle {
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
if (!BatteryService.batteryAvailable)
return Theme.surfaceText;
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
}
}
MouseArea {
id: batteryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
toggleBatteryPopup()
toggleBatteryPopup();
}
}
// Tooltip on hover
Rectangle {
id: batteryTooltip
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
@@ -78,19 +102,10 @@ Rectangle {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
visible: batteryArea.containsMouse && !batteryPopupVisible
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
opacity: batteryArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
opacity: batteryArea.containsMouse ? 1 : 0
Column {
anchors.centerIn: parent
@@ -98,31 +113,44 @@ Rectangle {
Text {
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"
}
}
if (typeof PowerProfiles === "undefined")
return "Power Management";
let status = BatteryService.batteryStatus
let level = BatteryService.batteryLevel + "%"
let time = BatteryService.formatTimeRemaining()
if (time !== "Unknown") {
return status + " • " + level + " • " + time
} else {
return status + " • " + level
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 {
@@ -130,5 +158,7 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -10,46 +10,44 @@ Column {
property date displayDate: new Date()
property date selectedDate: new Date()
spacing: Theme.spacingM
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable)
return ;
// Calculate date range with padding
let firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
let dayOfWeek = firstDay.getDay();
let startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - dayOfWeek - 7); // Extra week padding
let lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0);
let endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7); // Extra week padding
CalendarService.loadEvents(startDate, endDate);
}
spacing: Theme.spacingM
// Load events when display date changes
onDisplayDateChanged: {
loadEventsForMonth()
loadEventsForMonth();
}
Component.onCompleted: {
console.log("CalendarWidget: Component completed, CalendarService available:", !!CalendarService);
if (CalendarService)
console.log("CalendarWidget: khal available:", CalendarService.khalAvailable);
loadEventsForMonth();
}
// Load events when calendar service becomes available
Connections {
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable)
loadEventsForMonth();
}
target: CalendarService
enabled: CalendarService !== null
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable) {
loadEventsForMonth()
}
}
}
Component.onCompleted: {
console.log("CalendarWidget: Component completed, CalendarService available:", !!CalendarService)
if (CalendarService) {
console.log("CalendarWidget: khal available:", CalendarService.khalAvailable)
}
loadEventsForMonth()
}
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) return
// Calculate date range with padding
let firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
let dayOfWeek = firstDay.getDay()
let startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - dayOfWeek - 7) // Extra week padding
let lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0)
let endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7) // Extra week padding
CalendarService.loadEvents(startDate, endDate)
}
// Month navigation header
@@ -74,16 +72,17 @@ Column {
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() - 1)
displayDate = newDate
let newDate = new Date(displayDate);
newDate.setMonth(newDate.getMonth() - 1);
displayDate = newDate;
}
}
}
Text {
@@ -114,17 +113,19 @@ Column {
MouseArea {
id: nextMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() + 1)
displayDate = newDate
let newDate = new Date(displayDate);
newDate.setMonth(newDate.getMonth() + 1);
displayDate = newDate;
}
}
}
}
// Days of week header
@@ -147,61 +148,59 @@ Column {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
// Calendar grid
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
property date firstDay: {
let date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
let dayOfWeek = date.getDay()
date.setDate(date.getDate() - dayOfWeek)
return date
}
Repeater {
model: 42
Rectangle {
width: parent.width / 7
height: parent.height / 6
property date dayDate: {
let date = new Date(parent.firstDay)
date.setDate(date.getDate() + index)
return date
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()
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"
width: parent.width / 7
height: parent.height / 6
color: isSelected ? Theme.primary : isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadiusSmall
Text {
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)
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
}
// Event indicator - full-width elegant bar
Rectangle {
// Use a lighter tint of primary for selected state
id: eventIndicator
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
@@ -209,37 +208,32 @@ Column {
height: 3
radius: 1.5
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
// Dynamic color based on state with opacity
color: {
if (isSelected) {
// Use a lighter tint of primary for selected state
return Qt.lighter(Theme.primary, 1.3)
} else if (isToday) {
return Theme.primary
} else {
return Theme.primary
if (isSelected)
return Qt.lighter(Theme.primary, 1.3);
else if (isToday)
return Theme.primary;
else
return Theme.primary;
}
}
opacity: {
if (isSelected) {
return 0.9
} else if (isToday) {
return 0.8
} else {
return 0.6
if (isSelected)
return 0.9;
else if (isToday)
return 0.8;
else
return 0.6;
}
}
// Subtle animation on hover
scale: dayArea.containsMouse ? 1.05 : 1.0
scale: dayArea.containsMouse ? 1.05 : 1
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
@@ -247,6 +241,7 @@ Column {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
@@ -254,20 +249,26 @@ Column {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedDate = dayDate
selectedDate = dayDate;
}
}
}
}
}
}

View File

@@ -2,9 +2,9 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Services.Mpris
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
@@ -15,14 +15,11 @@ PanelWindow {
property bool calendarVisible: false
visible: calendarVisible
implicitWidth: 480
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
@@ -34,54 +31,45 @@ PanelWindow {
Rectangle {
id: mainContainer
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() {
let contentHeight = Theme.spacingM * 2; // margins
// Main row with widgets and calendar
let widgetHeight = 160;
// Media widget always present
widgetHeight += 140 + Theme.spacingM;
// Weather widget always present
let calendarHeight = 300;
let mainRowHeight = Math.max(widgetHeight, calendarHeight);
contentHeight += mainRowHeight + Theme.spacingM;
// Add events widget height - use calculated height instead of actual
if (CalendarService && CalendarService.khalAvailable) {
let hasEvents = eventsWidget.selectedDateEvents && eventsWidget.selectedDateEvents.length > 0;
let eventsHeight = hasEvents ? Math.min(300, 80 + eventsWidget.selectedDateEvents.length * 60) : 120;
contentHeight += eventsHeight;
}
return Math.min(contentHeight, parent.height * 0.9);
}
width: calculateWidth()
height: calculateHeight()
x: (parent.width - width) / 2
y: Theme.barHeight + 4
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() {
let contentHeight = Theme.spacingM * 2 // margins
// Main row with widgets and calendar
let widgetHeight = 160 // Media widget always present
widgetHeight += 140 + Theme.spacingM // Weather widget always present
let calendarHeight = 300
let mainRowHeight = Math.max(widgetHeight, calendarHeight)
contentHeight += mainRowHeight + Theme.spacingM
// Add events widget height - use calculated height instead of actual
if (CalendarService && CalendarService.khalAvailable) {
let hasEvents = eventsWidget.selectedDateEvents && eventsWidget.selectedDateEvents.length > 0
let eventsHeight = hasEvents ? Math.min(300, 80 + eventsWidget.selectedDateEvents.length * 60) : 120
contentHeight += eventsHeight
}
return Math.min(contentHeight, parent.height * 0.9)
}
color: Theme.surfaceContainer
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
opacity: calendarVisible ? 1 : 0
scale: calendarVisible ? 1 : 0.92
Rectangle {
anchors.fill: parent
@@ -91,62 +79,45 @@ PanelWindow {
SequentialAnimation on opacity {
running: calendarVisible
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
opacity: calendarVisible ? 1.0 : 0.0
scale: calendarVisible ? 1.0 : 0.92
}
// Update height when calendar service events change
Connections {
function onEventsByDateChanged() {
mainContainer.height = mainContainer.calculateHeight();
}
function onKhalAvailableChanged() {
mainContainer.height = mainContainer.calculateHeight();
}
target: CalendarService
enabled: CalendarService !== null
function onEventsByDateChanged() {
mainContainer.height = mainContainer.calculateHeight()
}
function onKhalAvailableChanged() {
mainContainer.height = mainContainer.calculateHeight()
}
}
// Update height when events widget's selectedDateEvents changes
Connections {
function onSelectedDateEventsChanged() {
mainContainer.height = mainContainer.calculateHeight();
}
target: eventsWidget
enabled: eventsWidget !== null
function onSelectedDateEventsChanged() {
mainContainer.height = mainContainer.calculateHeight()
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.longDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.longDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Column {
@@ -158,24 +129,25 @@ PanelWindow {
Row {
width: parent.width
height: {
let widgetHeight = 160 // Media widget always present
widgetHeight += 140 + Theme.spacingM // Weather widget always present
let calendarHeight = 300
return Math.max(widgetHeight, calendarHeight)
let widgetHeight = 160; // Media widget always present
widgetHeight += 140 + Theme.spacingM; // Weather widget always present
let calendarHeight = 300;
return Math.max(widgetHeight, calendarHeight);
}
spacing: Theme.spacingM
// Left section for widgets
Column {
id: leftWidgets
property bool hasAnyWidgets: true // Always show media widget and weather widget
width: hasAnyWidgets ? parent.width * 0.45 : 0
height: childrenRect.height
spacing: Theme.spacingM
visible: hasAnyWidgets
anchors.top: parent.top
property bool hasAnyWidgets: true // Always show media widget and weather widget
MediaPlayerWidget {
visible: true // Always visible - shows placeholder when no media
width: parent.width
@@ -187,31 +159,70 @@ PanelWindow {
width: parent.width
height: 140
}
}
// Right section for calendar
CalendarWidget {
id: calendarWidget
width: leftWidgets.hasAnyWidgets ? parent.width * 0.55 - Theme.spacingL : parent.width
height: parent.height
}
}
// Full-width events widget below
EventsWidget {
id: eventsWidget
width: parent.width
selectedDate: calendarWidget.selectedDate
}
}
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 {
NumberAnimation {
duration: Theme.longDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.longDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
calendarVisible = false
calendarVisible = false;
}
}
}

View File

@@ -11,13 +11,22 @@ Rectangle {
property date selectedDate: new Date()
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
onSelectedDateEventsChanged: {
console.log("EventsWidget: selectedDateEvents changed, count:", selectedDateEvents.length)
eventsList.model = selectedDateEvents
}
property bool shouldShow: CalendarService && CalendarService.khalAvailable
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
let events = CalendarService.getEventsForDate(selectedDate);
console.log("EventsWidget: Updating events for", Qt.formatDate(selectedDate, "yyyy-MM-dd"), "found", events.length, "events");
selectedDateEvents = events;
} else {
selectedDateEvents = [];
}
}
onSelectedDateEventsChanged: {
console.log("EventsWidget: selectedDateEvents changed, count:", selectedDateEvents.length);
eventsList.model = selectedDateEvents;
}
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadiusLarge
@@ -25,58 +34,33 @@ Rectangle {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
visible: shouldShow
// Material elevation shadow
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
Component.onCompleted: {
updateSelectedDateEvents();
}
onSelectedDateChanged: {
updateSelectedDateEvents();
}
// Update events when selected date or events change
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents();
}
function onKhalAvailableChanged() {
updateSelectedDateEvents();
}
target: CalendarService
enabled: CalendarService !== null
function onEventsByDateChanged() {
updateSelectedDateEvents()
}
function onKhalAvailableChanged() {
updateSelectedDateEvents()
}
}
Component.onCompleted: {
updateSelectedDateEvents()
}
onSelectedDateChanged: {
updateSelectedDateEvents()
}
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
let events = CalendarService.getEventsForDate(selectedDate)
console.log("EventsWidget: Updating events for", Qt.formatDate(selectedDate, "yyyy-MM-dd"), "found", events.length, "events")
selectedDateEvents = events
} else {
selectedDateEvents = []
}
}
// Header - always visible when widget is shown
Row {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
@@ -92,15 +76,13 @@ Rectangle {
}
Text {
text: hasEvents ?
(Qt.formatDate(selectedDate, "MMM d") + " • " +
(selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) :
Qt.formatDate(selectedDate, "MMM d")
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
// No events placeholder - centered in entire widget (not just content area)
@@ -124,11 +106,13 @@ Rectangle {
font.weight: Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Events list - positioned below header when there are events
ListView {
id: eventsList
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
@@ -136,7 +120,7 @@ Rectangle {
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingM
visible: opacity > 0
opacity: hasEvents ? 1.0 : 0.0
opacity: hasEvents ? 1 : 0
clip: true
spacing: Theme.spacingS
boundsMovement: Flickable.StopAtBounds
@@ -151,6 +135,7 @@ Rectangle {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
delegate: Rectangle {
@@ -158,20 +143,18 @@ Rectangle {
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)
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"
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
@@ -189,6 +172,7 @@ Rectangle {
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -213,6 +197,7 @@ Rectangle {
Row {
id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
@@ -228,15 +213,14 @@ Rectangle {
Text {
text: {
if (modelData.allDay) {
return "All day"
return "All day";
} else {
let timeFormat = Prefs.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
let timeFormat = Prefs.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
@@ -244,10 +228,12 @@ Rectangle {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: locationRow
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -270,22 +256,25 @@ Rectangle {
maximumLineCount: 1
width: Math.min(implicitWidth, 200)
}
}
}
}
MouseArea {
id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
console.warn("Couldn't open", modelData.url)
}
if (Qt.openUrlExternally(modelData.url) === false)
console.warn("Couldn't open", modelData.url);
}
}
}
@@ -295,6 +284,7 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
@@ -302,7 +292,28 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}

View File

@@ -10,87 +10,76 @@ Rectangle {
id: mediaPlayerWidget
property MprisPlayer activePlayer: MprisController.activePlayer
property string lastValidTitle: ""
property string lastValidArtist: ""
property string lastValidAlbum: ""
property string lastValidArtUrl: ""
property real currentPosition: 0
Timer {
id: clearCacheTimer
interval: 2000
onTriggered: {
if (!activePlayer) {
lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
}
}
// Simple progress ratio calculation
function ratio() {
return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0;
}
onActivePlayerChanged: {
if (!activePlayer) {
clearCacheTimer.restart()
} else {
clearCacheTimer.stop()
if (!activePlayer)
clearCacheTimer.restart();
else
clearCacheTimer.stop();
}
}
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
Timer {
id: clearCacheTimer
interval: 2000
onTriggered: {
if (!activePlayer) {
lastValidTitle = "";
lastValidArtist = "";
lastValidAlbum = "";
lastValidArtUrl = "";
}
}
property real currentPosition: 0
// Simple progress ratio calculation
function ratio() {
return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0
}
// Updates progress bar every 2 seconds when playing
Timer {
id: positionTimer
interval: 2000
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.length > 0 && !progressMouseArea.isSeeking
repeat: true
onTriggered: {
if (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking) {
currentPosition = activePlayer.position
}
if (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking)
currentPosition = activePlayer.position;
}
}
// Backend events
Connections {
target: activePlayer
function onPositionChanged() {
if (!progressMouseArea.isSeeking) {
currentPosition = activePlayer.position
}
if (!progressMouseArea.isSeeking)
currentPosition = activePlayer.position;
}
function onPostTrackChanged() {
currentPosition = activePlayer?.position || 0
currentPosition = activePlayer && activePlayer.position || 0;
}
function onTrackTitleChanged() {
currentPosition = activePlayer?.position || 0
currentPosition = activePlayer && activePlayer.position || 0;
}
target: activePlayer
}
Item {
@@ -117,6 +106,7 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Active content in a column
@@ -144,12 +134,13 @@ Rectangle {
Image {
id: albumArt
anchors.fill: parent
source: activePlayer?.trackArtUrl || lastValidArtUrl || ""
source: activePlayer && activePlayer.trackArtUrl || lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer?.trackArtUrl) {
if (activePlayer && activePlayer.trackArtUrl)
lastValidArtUrl = activePlayer.trackArtUrl;
}
}
fillMode: Image.PreserveAspectCrop
smooth: true
@@ -167,8 +158,11 @@ Rectangle {
font.pixelSize: 28
color: Theme.surfaceVariantText
}
}
}
}
// Track Info
@@ -178,11 +172,11 @@ Rectangle {
spacing: Theme.spacingXS
Text {
text: activePlayer?.trackTitle || lastValidTitle || "Unknown Track"
text: activePlayer && activePlayer.trackTitle || lastValidTitle || "Unknown Track"
onTextChanged: {
if (activePlayer?.trackTitle) {
if (activePlayer && activePlayer.trackTitle)
lastValidTitle = activePlayer.trackTitle;
}
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
@@ -192,11 +186,11 @@ Rectangle {
}
Text {
text: activePlayer?.trackArtist || lastValidArtist || "Unknown Artist"
text: activePlayer && activePlayer.trackArtist || lastValidArtist || "Unknown Artist"
onTextChanged: {
if (activePlayer?.trackArtist) {
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)
@@ -205,11 +199,11 @@ Rectangle {
}
Text {
text: activePlayer?.trackAlbum || lastValidAlbum || ""
text: activePlayer && activePlayer.trackAlbum || lastValidAlbum || ""
onTextChanged: {
if (activePlayer?.trackAlbum) {
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)
@@ -217,17 +211,21 @@ Rectangle {
elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Progress bar
Item {
id: progressBarContainer
width: parent.width
height: 24
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
@@ -237,78 +235,83 @@ Rectangle {
Rectangle {
id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: parent.width * ratio()
Behavior on width {
NumberAnimation { duration: 100 }
NumberAnimation {
duration: 100
}
}
}
// Drag handle
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))
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.0
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation { duration: 150 }
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
property bool isSeeking: false
onPressed: function(mouse) {
isSeeking = true
isSeeking = true;
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
isSeeking = false
isSeeking = false;
}
onPositionChanged: function(mouse) {
if (pressed && isSeeking && activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onClicked: function(mouse) {
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
}
@@ -316,25 +319,25 @@ Rectangle {
// Global mouse area for drag tracking
MouseArea {
id: progressGlobalMouseArea
anchors.fill: parent.parent.parent // Fill the entire media player widget
enabled: progressMouseArea.isSeeking
visible: false
preventStealing: true
onPositionChanged: function(mouse) {
if (progressMouseArea.isSeeking && activePlayer && activePlayer.length > 0) {
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
progressMouseArea.isSeeking = false;
}
}
onReleased: {
progressMouseArea.isSeeking = false
}
}
}
// Control buttons - always visible
@@ -365,21 +368,24 @@ Rectangle {
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer) return
if (!activePlayer)
return ;
// >8 s → jump to start, otherwise previous track
if (currentPosition > 8 && activePlayer.canSeek) {
activePlayer.position = 0
currentPosition = 0
activePlayer.position = 0;
currentPosition = 0;
} else {
activePlayer.previous()
activePlayer.previous();
}
}
}
}
// Play/Pause button
@@ -391,7 +397,7 @@ Rectangle {
Text {
anchors.centerIn: parent
text: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
text: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.background
@@ -401,8 +407,9 @@ Rectangle {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.togglePlaying()
onClicked: activePlayer && activePlayer.togglePlaying()
}
}
// Next button
@@ -422,14 +429,30 @@ Rectangle {
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.next()
onClicked: activePlayer && activePlayer.next()
}
}
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}

View File

@@ -7,23 +7,13 @@ import qs.Services
Rectangle {
id: weatherWidget
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
// Placeholder when no weather - centered in entire widget
Column {
@@ -45,6 +35,7 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Weather content when available - original Column structure
@@ -86,9 +77,14 @@ Rectangle {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: if (WeatherService.weather.available) Prefs.setTemperatureUnit(!Prefs.useFahrenheit)
onClicked: {
if (WeatherService.weather.available)
Prefs.setTemperatureUnit(!Prefs.useFahrenheit);
}
enabled: WeatherService.weather.available
}
}
Text {
@@ -97,8 +93,11 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
// Weather details grid
@@ -109,6 +108,7 @@ Rectangle {
Row {
spacing: Theme.spacingXS
Text {
text: "humidity_low"
font.family: Theme.iconFont
@@ -116,16 +116,19 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
Text {
text: "air"
font.family: Theme.iconFont
@@ -133,16 +136,19 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
Text {
text: "wb_twilight"
font.family: Theme.iconFont
@@ -150,16 +156,19 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: WeatherService.weather.sunrise || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
Text {
text: "bedtime"
font.family: Theme.iconFont
@@ -167,13 +176,27 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: WeatherService.weather.sunset || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
@@ -10,7 +10,6 @@ Item {
id: audioTab
property int audioSubTab: 0 // 0: Output, 1: Input
readonly property real volumeLevel: AudioService.volumeLevel
readonly property real micLevel: AudioService.micLevel
readonly property bool volumeMuted: AudioService.sinkMuted
@@ -50,6 +49,7 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: audioTab.audioSubTab = 0
}
}
Rectangle {
@@ -72,7 +72,9 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: audioTab.audioSubTab = 1
}
}
}
// Output Tab Content
@@ -115,16 +117,19 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMute()
}
}
Item {
id: volumeSliderContainer
width: parent.width - 80
height: 32
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: volumeSliderTrack
width: parent.width
height: 8
radius: 4
@@ -133,93 +138,99 @@ Item {
Rectangle {
id: volumeSliderFill
width: parent.width * (audioTab.volumeLevel / 100)
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation { duration: 100 }
NumberAnimation {
duration: 100
}
}
}
// Draggable handle
Rectangle {
id: volumeHandle
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, volumeSliderFill.width - width/2))
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1.0
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation { duration: 150 }
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
id: volumeMouseArea
property bool isDragging: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
property bool isDragging: false
onPressed: (mouse) => {
isDragging = true
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume);
}
onReleased: {
isDragging = false
isDragging = false;
}
onPositionChanged: (mouse) => {
if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume);
}
}
onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume);
}
}
// Global mouse area for drag tracking
MouseArea {
id: volumeGlobalMouseArea
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
enabled: volumeMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (volumeMouseArea.isDragging) {
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume);
}
}
onReleased: {
volumeMouseArea.isDragging = false;
}
}
onReleased: {
volumeMouseArea.isDragging = false
}
}
}
Text {
@@ -229,7 +240,9 @@ Item {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Output Devices
@@ -273,7 +286,9 @@ Item {
color: Theme.primary
font.weight: Font.Medium
}
}
}
// Real audio devices
@@ -284,8 +299,7 @@ Item {
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.active ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.active ? 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.active ? Theme.primary : "transparent"
border.width: 1
@@ -297,10 +311,14 @@ Item {
Text {
text: {
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"
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";
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
@@ -321,33 +339,39 @@ Item {
Text {
text: {
if (modelData.subtitle && modelData.subtitle !== "") {
return modelData.subtitle + (modelData.active ? " • Selected" : "")
} else {
return modelData.active ? "Selected" : ""
}
if (modelData.subtitle && modelData.subtitle !== "")
return modelData.subtitle + (modelData.active ? " • Selected" : "");
else
return modelData.active ? "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: {
AudioService.setAudioSink(modelData.name)
AudioService.setAudioSink(modelData.name);
}
}
}
}
}
}
}
// Input Tab Content
@@ -390,16 +414,19 @@ Item {
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMicMute()
}
}
Item {
id: micSliderContainer
width: parent.width - 80
height: 32
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: micSliderTrack
width: parent.width
height: 8
radius: 4
@@ -408,93 +435,99 @@ Item {
Rectangle {
id: micSliderFill
width: parent.width * (audioTab.micLevel / 100)
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation { duration: 100 }
NumberAnimation {
duration: 100
}
}
}
// Draggable handle
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))
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.0
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation { duration: 150 }
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
id: micMouseArea
property bool isDragging: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
property bool isDragging: false
onPressed: (mouse) => {
isDragging = true
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel);
}
onReleased: {
isDragging = false
isDragging = false;
}
onPositionChanged: (mouse) => {
if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel);
}
}
onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel);
}
}
// Global mouse area for drag tracking
MouseArea {
id: micGlobalMouseArea
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
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.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel);
}
}
onReleased: {
micMouseArea.isDragging = false;
}
}
onReleased: {
micMouseArea.isDragging = false
}
}
}
Text {
@@ -504,6 +537,7 @@ Item {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
@@ -549,7 +583,9 @@ Item {
color: Theme.primary
font.weight: Font.Medium
}
}
}
// Real audio input devices
@@ -560,8 +596,7 @@ Item {
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.active ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: sourceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.active ? 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.active ? Theme.primary : "transparent"
border.width: 1
@@ -573,9 +608,12 @@ Item {
Text {
text: {
if (modelData.name.includes("bluez")) return "headset_mic"
else if (modelData.name.includes("usb")) return "headset_mic"
else return "mic"
if (modelData.name.includes("bluez"))
return "headset_mic";
else if (modelData.name.includes("usb"))
return "headset_mic";
else
return "mic";
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
@@ -596,33 +634,41 @@ Item {
Text {
text: {
if (modelData.subtitle && modelData.subtitle !== "") {
return modelData.subtitle + (modelData.active ? " • Selected" : "")
} else {
return modelData.active ? "Selected" : ""
}
if (modelData.subtitle && modelData.subtitle !== "")
return modelData.subtitle + (modelData.active ? " • Selected" : "");
else
return modelData.active ? "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: {
AudioService.setAudioSource(modelData.name)
AudioService.setAudioSource(modelData.name);
}
}
}
}
}
}
}
}
}

View File

@@ -1,9 +1,9 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
@@ -13,7 +13,6 @@ Item {
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
@@ -25,8 +24,7 @@ Item {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
(BluetoothService.adapter && BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (BluetoothService.adapter && BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : "transparent"
border.width: 2
@@ -60,19 +58,22 @@ Item {
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: {
BluetoothService.toggleAdapter()
BluetoothService.toggleAdapter();
}
}
}
Column {
@@ -88,14 +89,15 @@ Item {
}
Repeater {
model: BluetoothService.adapter && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter(dev => dev && dev.paired && BluetoothService.isValidDevice(dev)) : []
model: BluetoothService.adapter && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter((dev) => {
return dev && dev.paired && BluetoothService.isValidDevice(dev);
}) : []
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: 1
@@ -135,32 +137,32 @@ Item {
Text {
text: {
if (modelData.batteryAvailable && modelData.battery > 0) {
return "• " + Math.round(modelData.battery * 100) + "%"
}
var btBattery = BatteryService.bluetoothDevices.find(dev =>
dev.name === (modelData.name || modelData.deviceName) ||
dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) ||
(modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase())
)
return btBattery ? "• " + btBattery.percentage + "%" : ""
if (modelData.batteryAvailable && modelData.battery > 0)
return "• " + Math.round(modelData.battery * 100) + "%";
var btBattery = BatteryService.bluetoothDevices.find((dev) => {
return dev.name === (modelData.name || modelData.deviceName) || dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || (modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase());
});
return btBattery ? "• " + btBattery.percentage + "%" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Rectangle {
id: btMenuButton
width: 32
height: 32
radius: Theme.cornerRadius
color: btMenuButtonArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: btMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
@@ -177,36 +179,43 @@ Item {
MouseArea {
id: btMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
bluetoothContextMenuWindow.deviceData = modelData
let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height)
bluetoothContextMenuWindow.show(localPos.x, localPos.y)
bluetoothContextMenuWindow.deviceData = modelData;
let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height);
bluetoothContextMenuWindow.show(localPos.x, localPos.y);
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
ColorAnimation {
duration: Theme.shortDuration
}
}
}
MouseArea {
id: btDeviceArea
anchors.fill: parent
anchors.rightMargin: 40
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BluetoothService.debugDevice(modelData)
BluetoothService.toggle(modelData.address)
BluetoothService.debugDevice(modelData);
BluetoothService.toggle(modelData.address);
}
}
}
}
}
Column {
@@ -226,7 +235,10 @@ Item {
anchors.verticalCenter: parent.verticalCenter
}
Item { width: 1; height: 1 }
Item {
width: 1
height: 1
}
Rectangle {
width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2)
@@ -250,29 +262,32 @@ Item {
Text {
id: scanText
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Stop Scanning" : "Start Scanning"
font.pixelSize: Theme.fontSizeMedium
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.stopScan()
} else {
BluetoothService.startScan()
}
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
BluetoothService.stopScan();
else
BluetoothService.startScan();
}
}
}
}
Repeater {
@@ -286,15 +301,25 @@ Item {
height: 70
radius: Theme.cornerRadius
color: {
if (availableDeviceArea.containsMouse) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
if (modelData.pairing) 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)
if (availableDeviceArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
if (modelData.pairing)
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)
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
@@ -309,9 +334,13 @@ Item {
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: {
if (modelData.pairing) return Theme.warning
if (modelData.blocked) return Theme.error
return Theme.surfaceText
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
@@ -324,9 +353,13 @@ Item {
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
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Theme.surfaceText;
}
font.weight: modelData.pairing ? Font.Medium : Font.Normal
}
@@ -340,16 +373,23 @@ Item {
Text {
text: {
switch (pairingStatus) {
case "pairing": return "Pairing..."
case "blocked": return "Blocked"
default: return BluetoothService.getSignalStrength(modelData)
case "pairing":
return "Pairing...";
case "blocked":
return "Blocked";
default:
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)
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);
}
}
@@ -367,9 +407,13 @@ Item {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: modelData.rssi !== undefined && modelData.rssi !== 0 && pairingStatus === "available"
}
}
}
}
}
Rectangle {
@@ -380,20 +424,28 @@ Item {
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: {
if (!canPair && !modelData.pairing) return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
if (actionButtonArea.containsMouse) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
return "transparent"
if (!canPair && !modelData.pairing)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
if (actionButtonArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
return "transparent";
}
border.color: canPair || modelData.pairing ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
opacity: canPair || modelData.pairing ? 1.0 : 0.5
opacity: canPair || modelData.pairing ? 1 : 0.5
Text {
anchors.centerIn: parent
text: {
if (modelData.pairing) return "Pairing..."
if (modelData.blocked) return "Blocked"
return "Pair"
if (modelData.pairing)
return "Pairing...";
if (modelData.blocked)
return "Blocked";
return "Pair";
}
font.pixelSize: Theme.fontSizeSmall
color: canPair || modelData.pairing ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
@@ -402,34 +454,37 @@ Item {
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canPair ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: canPair
onClicked: {
if (canPair) {
BluetoothService.pair(modelData.address)
}
if (canPair)
BluetoothService.pair(modelData.address);
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90 // Don't overlap with action button
hoverEnabled: true
cursorShape: canPair ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: canPair
onClicked: {
if (canPair) {
BluetoothService.pair(modelData.address)
}
if (canPair)
BluetoothService.pair(modelData.address);
}
}
}
}
Column {
@@ -455,6 +510,7 @@ Item {
to: 360
duration: 2000
}
}
Text {
@@ -464,6 +520,7 @@ Item {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
@@ -472,6 +529,7 @@ Item {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Text {
@@ -483,15 +541,39 @@ Item {
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
}
}
Rectangle {
id: bluetoothContextMenuWindow
property var deviceData: null
property bool menuVisible: false
function show(x, y) {
const menuWidth = 160;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
let finalX = x - menuWidth / 2;
let finalY = y;
finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth));
finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight));
bluetoothContextMenuWindow.x = finalX;
bluetoothContextMenuWindow.y = finalY;
bluetoothContextMenuWindow.visible = true;
bluetoothContextMenuWindow.menuVisible = true;
}
function hide() {
bluetoothContextMenuWindow.menuVisible = false;
Qt.callLater(() => {
bluetoothContextMenuWindow.visible = false;
});
}
visible: false
width: 160
height: menuColumn.implicitHeight + Theme.spacingS * 2
@@ -500,6 +582,8 @@ Item {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Rectangle {
anchors.fill: parent
@@ -512,25 +596,9 @@ Item {
z: parent.z - 1
}
opacity: menuVisible ? 1.0 : 0.0
scale: menuVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
@@ -563,19 +631,20 @@ Item {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow.deviceData) {
BluetoothService.toggle(bluetoothContextMenuWindow.deviceData.address)
}
bluetoothContextMenuWindow.hide()
if (bluetoothContextMenuWindow.deviceData)
BluetoothService.toggle(bluetoothContextMenuWindow.deviceData.address);
bluetoothContextMenuWindow.hide();
}
}
@@ -584,7 +653,9 @@ Item {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
@@ -599,6 +670,7 @@ Item {
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
@@ -629,19 +701,20 @@ Item {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow.deviceData) {
BluetoothService.forget(bluetoothContextMenuWindow.deviceData.address)
}
bluetoothContextMenuWindow.hide()
if (bluetoothContextMenuWindow.deviceData)
BluetoothService.forget(bluetoothContextMenuWindow.deviceData.address);
bluetoothContextMenuWindow.hide();
}
}
@@ -650,37 +723,36 @@ Item {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
function show(x, y) {
const menuWidth = 160
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x - menuWidth / 2
let finalY = y
finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth))
finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight))
bluetoothContextMenuWindow.x = finalX
bluetoothContextMenuWindow.y = finalY
bluetoothContextMenuWindow.visible = true
bluetoothContextMenuWindow.menuVisible = true
}
function hide() {
bluetoothContextMenuWindow.menuVisible = false
Qt.callLater(() => { bluetoothContextMenuWindow.visible = false })
}
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
visible: bluetoothContextMenuWindow.visible
onClicked: {
bluetoothContextMenuWindow.hide()
bluetoothContextMenuWindow.hide();
}
MouseArea {
@@ -691,5 +763,7 @@ Item {
onClicked: {
}
}
}
}

View File

@@ -2,9 +2,9 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
@@ -12,21 +12,19 @@ PanelWindow {
id: root
property bool controlCenterVisible: false
property string currentTab: "network" // "network", "audio", "bluetooth", "display"
property bool powerOptionsExpanded: false
visible: controlCenterVisible
onVisibleChanged: {
// Enable/disable WiFi auto-refresh based on control center visibility
WifiService.autoRefreshEnabled = visible && NetworkService.wifiEnabled
WifiService.autoRefreshEnabled = visible && NetworkService.wifiEnabled;
}
implicitWidth: 600
implicitHeight: 500
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
@@ -36,9 +34,6 @@ PanelWindow {
bottom: true
}
property string currentTab: "network" // "network", "audio", "bluetooth", "display"
property bool powerOptionsExpanded: false
Rectangle {
width: Math.min(600, Screen.width - Theme.spacingL * 2)
height: root.powerOptionsExpanded ? 570 : 500
@@ -48,50 +43,66 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: controlCenterVisible ? 1 : 0
// TopBar dropdown animation - optimized for performance
transform: [
Scale {
id: scaleTransform
origin.x: 600 // Use fixed width since popup is max 600px wide
origin.y: 0
xScale: controlCenterVisible ? 1.0 : 0.95
yScale: controlCenterVisible ? 1.0 : 0.8
xScale: controlCenterVisible ? 1 : 0.95
yScale: controlCenterVisible ? 1 : 0.8
},
Translate {
id: translateTransform
x: controlCenterVisible ? 0 : 15 // Slide slightly left when hidden
y: controlCenterVisible ? 0 : -30
}
]
// Single coordinated animation for better performance
states: [
State {
name: "visible"
when: controlCenterVisible
PropertyChanges { target: scaleTransform; xScale: 1.0; yScale: 1.0 }
PropertyChanges { target: translateTransform; x: 0; y: 0 }
PropertyChanges {
target: scaleTransform
xScale: 1
yScale: 1
}
PropertyChanges {
target: translateTransform
x: 0
y: 0
}
},
State {
name: "hidden"
when: !controlCenterVisible
PropertyChanges { target: scaleTransform; xScale: 0.95; yScale: 0.8 }
PropertyChanges { target: translateTransform; x: 15; y: -30 }
PropertyChanges {
target: scaleTransform
xScale: 0.95
yScale: 0.8
}
PropertyChanges {
target: translateTransform
x: 15
y: -30
}
}
]
// Power menu height animation
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration // Faster for height changes
easing.type: Theme.standardEasing
}
}
transitions: [
Transition {
from: "*"; to: "*"
from: "*"
to: "*"
ParallelAnimation {
NumberAnimation {
targets: [scaleTransform, translateTransform]
@@ -99,19 +110,12 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
opacity: controlCenterVisible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
@@ -140,11 +144,12 @@ PanelWindow {
// Profile Picture Container
Item {
id: avatarContainer
width: 64
height: 64
property bool hasImage: profileImageLoader.status === Image.Ready
width: 64
height: 64
// This rectangle provides the themed ring via its border.
Rectangle {
anchors.fill: parent
@@ -158,12 +163,15 @@ PanelWindow {
// Hidden Image loader. Its only purpose is to load the texture.
Image {
id: profileImageLoader
source: {
if (Prefs.profileImage === "") return ""
if (Prefs.profileImage.startsWith("/")) {
return "file://" + Prefs.profileImage
}
return Prefs.profileImage
if (Prefs.profileImage === "")
return "";
if (Prefs.profileImage.startsWith("/"))
return "file://" + Prefs.profileImage;
return Prefs.profileImage;
}
smooth: true
asynchronous: true
@@ -180,11 +188,12 @@ PanelWindow {
maskSource: circularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSpreadAtMin: 1
}
Item {
id: circularMask
width: 64 - 10
height: 64 - 10
layer.enabled: true
@@ -197,6 +206,7 @@ PanelWindow {
color: "black"
antialiasing: true
}
}
// Fallback for when there is no image.
@@ -213,6 +223,7 @@ PanelWindow {
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
}
}
// Error icon for when the image fails to load.
@@ -224,6 +235,7 @@ PanelWindow {
color: Theme.primaryText
visible: Prefs.profileImage !== "" && profileImageLoader.status === Image.Error
}
}
// User Info Text
@@ -231,7 +243,6 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Text {
text: UserInfoService.fullName || UserInfoService.username || "User"
font.pixelSize: Theme.fontSizeLarge
@@ -245,7 +256,9 @@ PanelWindow {
color: Theme.surfaceVariantText
font.weight: Font.Normal
}
}
}
// Action Buttons - Power and Settings
@@ -260,9 +273,7 @@ PanelWindow {
width: 40
height: 40
radius: 20
color: powerButton.containsMouse || root.powerOptionsExpanded ?
Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
color: powerButton.containsMouse || root.powerOptionsExpanded ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Rectangle {
anchors.centerIn: parent
@@ -285,31 +296,40 @@ PanelWindow {
NumberAnimation {
target: parent
property: "opacity"
to: 0.0
to: 0
duration: Theme.shortDuration / 2
easing.type: Theme.standardEasing
}
PropertyAction { target: parent; property: "text" }
PropertyAction {
target: parent
property: "text"
}
NumberAnimation {
target: parent
property: "opacity"
to: 1.0
to: 1
duration: Theme.shortDuration / 2
easing.type: Theme.standardEasing
}
}
}
}
}
MouseArea {
id: powerButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = !root.powerOptionsExpanded
root.powerOptionsExpanded = !root.powerOptionsExpanded;
}
}
@@ -318,7 +338,9 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Settings Button
@@ -326,9 +348,7 @@ PanelWindow {
width: 40
height: 40
radius: 20
color: settingsButton.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.5)
color: settingsButton.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.5)
Text {
anchors.centerIn: parent
@@ -340,13 +360,13 @@ PanelWindow {
MouseArea {
id: settingsButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
controlCenterVisible = false
settingsPopup.settingsVisible = true
controlCenterVisible = false;
settingsPopup.settingsVisible = true;
}
}
@@ -355,9 +375,13 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
// Animated Collapsible Power Options (optimized)
@@ -368,24 +392,9 @@ PanelWindow {
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: root.powerOptionsExpanded ? 1 : 0
opacity: root.powerOptionsExpanded ? 1.0 : 0.0
opacity: root.powerOptionsExpanded ? 1 : 0
clip: true
// Single coordinated animation for power options
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
@@ -396,9 +405,7 @@ PanelWindow {
width: 100
height: 34
radius: Theme.cornerRadius
color: logoutButton.containsMouse ?
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
color: logoutButton.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
@@ -419,21 +426,22 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false
root.powerOptionsExpanded = false;
if (typeof root !== "undefined" && root.powerConfirmDialog) {
root.powerConfirmDialog.powerConfirmAction = "logout"
root.powerConfirmDialog.powerConfirmTitle = "Logout"
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to logout?"
root.powerConfirmDialog.powerConfirmVisible = true
root.powerConfirmDialog.powerConfirmAction = "logout";
root.powerConfirmDialog.powerConfirmTitle = "Logout";
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to logout?";
root.powerConfirmDialog.powerConfirmVisible = true;
}
}
}
@@ -443,7 +451,9 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Reboot
@@ -451,9 +461,7 @@ PanelWindow {
width: 100
height: 34
radius: Theme.cornerRadius
color: rebootButton.containsMouse ?
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
color: rebootButton.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
@@ -474,21 +482,22 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false
root.powerOptionsExpanded = false;
if (typeof root !== "undefined" && root.powerConfirmDialog) {
root.powerConfirmDialog.powerConfirmAction = "reboot"
root.powerConfirmDialog.powerConfirmTitle = "Restart"
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to restart?"
root.powerConfirmDialog.powerConfirmVisible = true
root.powerConfirmDialog.powerConfirmAction = "reboot";
root.powerConfirmDialog.powerConfirmTitle = "Restart";
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to restart?";
root.powerConfirmDialog.powerConfirmVisible = true;
}
}
}
@@ -498,7 +507,9 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Shutdown
@@ -506,9 +517,7 @@ PanelWindow {
width: 100
height: 34
radius: Theme.cornerRadius
color: shutdownButton.containsMouse ?
Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
color: shutdownButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
@@ -529,21 +538,22 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: shutdownButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false
root.powerOptionsExpanded = false;
if (typeof root !== "undefined" && root.powerConfirmDialog) {
root.powerConfirmDialog.powerConfirmAction = "poweroff"
root.powerConfirmDialog.powerConfirmTitle = "Shutdown"
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to shutdown?"
root.powerConfirmDialog.powerConfirmVisible = true
root.powerConfirmDialog.powerConfirmAction = "poweroff";
root.powerConfirmDialog.powerConfirmTitle = "Shutdown";
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to shutdown?";
root.powerConfirmDialog.powerConfirmVisible = true;
}
}
}
@@ -553,9 +563,30 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
// Single coordinated animation for power options
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Tab buttons
@@ -565,36 +596,51 @@ PanelWindow {
Repeater {
model: {
let tabs = [
{name: "Network", icon: "wifi", id: "network", available: true}
]
let tabs = [{
"name": "Network",
"icon": "wifi",
"id": "network",
"available": true
}];
// Always show audio
tabs.push({name: "Audio", icon: "volume_up", id: "audio", available: true})
tabs.push({
"name": "Audio",
"icon": "volume_up",
"id": "audio",
"available": true
});
// Show Bluetooth only if available
if (BluetoothService.available) {
tabs.push({name: "Bluetooth", icon: "bluetooth", id: "bluetooth", available: true})
}
if (BluetoothService.available)
tabs.push({
"name": "Bluetooth",
"icon": "bluetooth",
"id": "bluetooth",
"available": true
});
// Always show display
tabs.push({name: "Display", icon: "brightness_6", id: "display", available: true})
return tabs
tabs.push({
"name": "Display",
"icon": "brightness_6",
"id": "display",
"available": true
});
return tabs;
}
Rectangle {
property int tabCount: {
let count = 3 // Network + Audio + Display (always visible)
if (BluetoothService.available) count++
return count
let count = 3; // Network + Audio + Display (always visible)
if (BluetoothService.available)
count++;
return count;
}
width: (parent.width - Theme.spacingXS * (tabCount - 1)) / tabCount
height: 40
radius: Theme.cornerRadius
color: root.currentTab === modelData.id ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
tabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
color: root.currentTab === modelData.id ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : tabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
Row {
anchors.centerIn: parent
@@ -615,16 +661,17 @@ PanelWindow {
font.weight: root.currentTab === modelData.id ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: tabArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentTab = modelData.id
root.currentTab = modelData.id;
}
}
@@ -633,10 +680,15 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
// Tab content area
@@ -646,19 +698,11 @@ PanelWindow {
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.1)
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
// Network Tab
NetworkTab {
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: root.currentTab === "network"
}
// Audio Tab
@@ -680,10 +724,37 @@ PanelWindow {
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: root.currentTab === "display"
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
// Power menu height animation
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration // Faster for height changes
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Click outside to close
@@ -691,7 +762,8 @@ PanelWindow {
anchors.fill: parent
z: -1
onClicked: {
controlCenterVisible = false
controlCenterVisible = false;
}
}
}

View File

@@ -1,16 +1,16 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
ScrollView {
id: displayTab
clip: true
clip: true
Column {
width: parent.width
@@ -35,11 +35,11 @@ ScrollView {
leftIcon: "brightness_low"
rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable
onSliderValueChanged: function(newValue) {
BrightnessService.setBrightness(newValue)
BrightnessService.setBrightness(newValue);
}
}
}
// Display settings
@@ -64,9 +64,7 @@ ScrollView {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Prefs.nightModeEnabled ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
(nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: Prefs.nightModeEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: Prefs.nightModeEnabled ? Theme.primary : "transparent"
border.width: Prefs.nightModeEnabled ? 1 : 0
@@ -89,26 +87,28 @@ ScrollView {
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: nightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (Prefs.nightModeEnabled) {
// Disable night mode - kill any running color temperature processes
nightModeDisableProcess.running = true
Prefs.setNightModeEnabled(false)
nightModeDisableProcess.running = true;
Prefs.setNightModeEnabled(false);
} else {
// Enable night mode using wlsunset or redshift
nightModeEnableProcess.running = true
Prefs.setNightModeEnabled(true)
nightModeEnableProcess.running = true;
Prefs.setNightModeEnabled(true);
}
}
}
}
// Light/Dark mode toggle
@@ -116,9 +116,7 @@ ScrollView {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Theme.isLightMode ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
(lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: Theme.isLightMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: Theme.isLightMode ? Theme.primary : "transparent"
border.width: Theme.isLightMode ? 1 : 0
@@ -141,16 +139,17 @@ ScrollView {
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: lightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.toggleLightMode()
Theme.toggleLightMode();
}
}
@@ -159,35 +158,41 @@ ScrollView {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
// Night mode processes
Process {
id: nightModeEnableProcess
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Failed to enable night mode")
Prefs.setNightModeEnabled(false)
console.warn("Failed to enable night mode");
Prefs.setNightModeEnabled(false);
}
}
}
Process {
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Failed to disable night mode")
}
if (exitCode !== 0)
console.warn("Failed to disable night mode");
}
}
}

View File

@@ -1,22 +1,26 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
Item {
// Default to WiFi when nothing is connected
id: networkTab
property int networkSubTab: {
// Default to WiFi tab if WiFi is connected, otherwise Ethernet
if (NetworkService.networkStatus === "wifi") return 1
else if (NetworkService.networkStatus === "ethernet") return 0
else return 1 // Default to WiFi when nothing is connected
if (NetworkService.networkStatus === "wifi")
return 1;
else if (NetworkService.networkStatus === "ethernet")
return 0;
else
return 1;
}
Column {
anchors.fill: parent
spacing: Theme.spacingM
@@ -30,9 +34,7 @@ Item {
width: (parent.width - Theme.spacingXS) / 2
height: 36
radius: Theme.cornerRadiusSmall
color: networkTab.networkSubTab === 0 ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
ethernetTabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
color: networkTab.networkSubTab === 0 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : ethernetTabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
Row {
anchors.centerIn: parent
@@ -53,27 +55,28 @@ Item {
font.weight: networkTab.networkSubTab === 0 ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: ethernetTabArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
networkTab.networkSubTab = 0
WifiService.autoRefreshEnabled = false
networkTab.networkSubTab = 0;
WifiService.autoRefreshEnabled = false;
}
}
}
Rectangle {
width: (parent.width - Theme.spacingXS) / 2
height: 36
radius: Theme.cornerRadiusSmall
color: networkTab.networkSubTab === 1 ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
wifiTabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
color: networkTab.networkSubTab === 1 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : wifiTabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
Row {
anchors.centerIn: parent
@@ -94,22 +97,26 @@ Item {
font.weight: networkTab.networkSubTab === 1 ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: wifiTabArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
networkTab.networkSubTab = 1
WifiService.autoRefreshEnabled = true
if (NetworkService.wifiEnabled) {
WifiService.scanWifi()
}
networkTab.networkSubTab = 1;
WifiService.autoRefreshEnabled = true;
if (NetworkService.wifiEnabled)
WifiService.scanWifi();
}
}
}
}
// Ethernet Tab Content
@@ -124,12 +131,9 @@ Item {
flickDeceleration: 8000
maximumFlickVelocity: 15000
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
Column {
id: ethernetContent
width: parent.width
spacing: Theme.spacingL
@@ -173,6 +177,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
// Force Ethernet preference button
@@ -185,22 +190,16 @@ Item {
radius: 6
anchors.verticalCenter: parent.verticalCenter
z: 10
opacity: networkTab.changingNetworkPreference ? 0.6 : 1.0
opacity: networkTab.changingNetworkPreference ? 0.6 : 1
visible: networkTab.networkStatus !== "ethernet"
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
id: ethernetPreferenceIcon
text: networkTab.changingNetworkPreference ? "sync" : ""
font.family: Theme.iconFont
font.pixelSize: Theme.fontSizeSmall
@@ -218,16 +217,17 @@ Item {
duration: 1000
loops: Animation.Infinite
}
}
Text {
text: networkTab.changingNetworkPreference ? "Switching..." :
(networkTab.networkStatus === "ethernet" ? "" : "Prefer over WiFi")
text: networkTab.changingNetworkPreference ? "Switching..." : (networkTab.networkStatus === "ethernet" ? "" : "Prefer over WiFi")
font.pixelSize: Theme.fontSizeSmall
color: networkTab.networkStatus === "ethernet" ? Theme.background : Theme.primary
anchors.verticalCenter: parent.verticalCenter
font.weight: Font.Medium
}
}
MouseArea {
@@ -237,18 +237,29 @@ Item {
propagateComposedEvents: false
enabled: !networkTab.changingNetworkPreference
onClicked: {
console.log("*** ETHERNET PREFERENCE BUTTON CLICKED ***")
console.log("*** ETHERNET PREFERENCE BUTTON CLICKED ***");
if (networkTab.networkStatus !== "ethernet") {
console.log("Setting preference to ethernet")
NetworkService.setNetworkPreference("ethernet")
console.log("Setting preference to ethernet");
NetworkService.setNetworkPreference("ethernet");
} else {
console.log("Setting preference to auto")
NetworkService.setNetworkPreference("auto")
console.log("Setting preference to auto");
NetworkService.setNetworkPreference("auto");
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
// Ethernet control button
@@ -279,19 +290,28 @@ Item {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: ethernetControlArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NetworkService.toggleNetworkConnection("ethernet")
NetworkService.toggleNetworkConnection("ethernet");
}
}
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
// WiFi Tab Content
@@ -306,12 +326,9 @@ Item {
flickDeceleration: 8000
maximumFlickVelocity: 15000
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
Column {
id: wifiContent
width: parent.width
spacing: Theme.spacingL
@@ -322,14 +339,7 @@ Item {
radius: Theme.cornerRadius
color: wifiToggleArea.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)
visible: NetworkService.wifiAvailable
opacity: NetworkService.wifiToggling ? 0.6 : 1.0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
opacity: NetworkService.wifiToggling ? 0.6 : 1
Row {
anchors.left: parent.left
@@ -339,6 +349,7 @@ Item {
Text {
id: wifiToggleIcon
text: NetworkService.wifiToggling ? "sync" : "power_settings_new"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
@@ -361,7 +372,9 @@ Item {
duration: 200
easing.type: Easing.OutQuad
}
}
}
Text {
@@ -371,17 +384,28 @@ Item {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: wifiToggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
NetworkService.toggleWifiRadio()
NetworkService.toggleWifiRadio();
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Current WiFi connection (if connected)
@@ -401,11 +425,7 @@ Item {
spacing: Theme.spacingM
Text {
text: networkTab.networkStatus === "wifi" ?
(WifiService.wifiSignalStrength === "excellent" ? "wifi" :
WifiService.wifiSignalStrength === "good" ? "wifi_2_bar" :
WifiService.wifiSignalStrength === "fair" ? "wifi_1_bar" :
WifiService.wifiSignalStrength === "poor" ? "wifi_calling_3" : "wifi") : "wifi"
text: networkTab.networkStatus === "wifi" ? (WifiService.wifiSignalStrength === "excellent" ? "wifi" : WifiService.wifiSignalStrength === "good" ? "wifi_2_bar" : WifiService.wifiSignalStrength === "fair" ? "wifi_1_bar" : WifiService.wifiSignalStrength === "poor" ? "wifi_calling_3" : "wifi") : "wifi"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge
color: networkTab.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
@@ -428,6 +448,7 @@ Item {
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
// Force WiFi preference button
@@ -439,22 +460,16 @@ Item {
border.width: 1
radius: 6
anchors.verticalCenter: parent.verticalCenter
opacity: networkTab.changingNetworkPreference ? 0.6 : 1.0
opacity: networkTab.changingNetworkPreference ? 0.6 : 1
visible: networkTab.networkStatus !== "wifi"
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
id: wifiPreferenceIcon
text: networkTab.changingNetworkPreference ? "sync" : ""
font.family: Theme.iconFont
font.pixelSize: Theme.fontSizeSmall
@@ -472,16 +487,17 @@ Item {
duration: 1000
loops: Animation.Infinite
}
}
Text {
text: NetworkService.changingNetworkPreference ? "Switching..." :
(NetworkService.networkStatus === "wifi" ? "" : "Prefer over Ethernet")
text: NetworkService.changingNetworkPreference ? "Switching..." : (NetworkService.networkStatus === "wifi" ? "" : "Prefer over Ethernet")
font.pixelSize: Theme.fontSizeSmall
color: NetworkService.networkStatus === "wifi" ? Theme.background : Theme.primary
anchors.verticalCenter: parent.verticalCenter
font.weight: Font.Medium
}
}
MouseArea {
@@ -491,16 +507,26 @@ Item {
propagateComposedEvents: false
enabled: !networkTab.changingNetworkPreference
onClicked: {
console.log("Force WiFi preference clicked")
if (NetworkService.networkStatus !== "wifi") {
NetworkService.setNetworkPreference("wifi")
} else {
NetworkService.setNetworkPreference("auto")
console.log("Force WiFi preference clicked");
if (NetworkService.networkStatus !== "wifi")
NetworkService.setNetworkPreference("wifi");
else
NetworkService.setNetworkPreference("auto");
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
// Available WiFi Networks
@@ -519,17 +545,20 @@ Item {
font.weight: Font.Medium
}
Item { width: parent.width - 200; height: 1 }
Item {
width: parent.width - 200
height: 1
}
Rectangle {
width: 32
height: 32
radius: 16
color: refreshArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
WifiService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
color: refreshArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : WifiService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
Text {
id: refreshIcon
anchors.centerIn: parent
text: WifiService.isScanning ? "sync" : "refresh"
font.family: Theme.iconFont
@@ -552,24 +581,28 @@ Item {
duration: 200
easing.type: Easing.OutQuad
}
}
}
MouseArea {
id: refreshArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: !WifiService.isScanning
onClicked: {
if (NetworkService.wifiEnabled) {
WifiService.scanWifi()
}
}
}
if (NetworkService.wifiEnabled)
WifiService.scanWifi();
}
}
}
}
// Connection status indicator
Rectangle {
@@ -577,24 +610,22 @@ Item {
height: 40
radius: Theme.cornerRadius
color: {
if (WifiService.connectionStatus === "connecting") {
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
} else if (WifiService.connectionStatus === "failed") {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
} else if (WifiService.connectionStatus === "connected") {
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12)
}
return "transparent"
if (WifiService.connectionStatus === "connecting")
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
else if (WifiService.connectionStatus === "failed")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
else if (WifiService.connectionStatus === "connected")
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12);
return "transparent";
}
border.color: {
if (WifiService.connectionStatus === "connecting") {
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3)
} else if (WifiService.connectionStatus === "failed") {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3)
} else if (WifiService.connectionStatus === "connected") {
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3)
}
return "transparent"
if (WifiService.connectionStatus === "connecting")
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3);
else if (WifiService.connectionStatus === "failed")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
else if (WifiService.connectionStatus === "connected")
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3);
return "transparent";
}
border.width: WifiService.connectionStatus !== "" ? 1 : 0
visible: WifiService.connectionStatus !== ""
@@ -605,19 +636,32 @@ Item {
Text {
id: connectionIcon
text: {
if (WifiService.connectionStatus === "connecting") return "sync"
if (WifiService.connectionStatus === "failed") return "error"
if (WifiService.connectionStatus === "connected") return "check_circle"
return ""
if (WifiService.connectionStatus === "connecting")
return "sync";
if (WifiService.connectionStatus === "failed")
return "error";
if (WifiService.connectionStatus === "connected")
return "check_circle";
return "";
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
color: {
if (WifiService.connectionStatus === "connecting") return Theme.warning
if (WifiService.connectionStatus === "failed") return Theme.error
if (WifiService.connectionStatus === "connected") return Theme.success
return Theme.surfaceText
if (WifiService.connectionStatus === "connecting")
return Theme.warning;
if (WifiService.connectionStatus === "failed")
return Theme.error;
if (WifiService.connectionStatus === "connected")
return Theme.success;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
rotation: WifiService.connectionStatus === "connecting" ? connectionIcon.rotation : 0
@@ -637,25 +681,40 @@ Item {
duration: 200
easing.type: Easing.OutQuad
}
}
}
Text {
text: {
if (WifiService.connectionStatus === "connecting") return "Connecting to " + WifiService.connectingSSID
if (WifiService.connectionStatus === "failed") return "Failed to connect to " + WifiService.connectingSSID
if (WifiService.connectionStatus === "connected") return "Connected to " + WifiService.connectingSSID
return ""
if (WifiService.connectionStatus === "connecting")
return "Connecting to " + WifiService.connectingSSID;
if (WifiService.connectionStatus === "failed")
return "Failed to connect to " + WifiService.connectingSSID;
if (WifiService.connectionStatus === "connected")
return "Connected to " + WifiService.connectingSSID;
return "";
}
font.pixelSize: Theme.fontSizeMedium
color: {
if (WifiService.connectionStatus === "connecting") return Theme.warning
if (WifiService.connectionStatus === "failed") return Theme.error
if (WifiService.connectionStatus === "connected") return Theme.success
return Theme.surfaceText
if (WifiService.connectionStatus === "connecting")
return Theme.warning;
if (WifiService.connectionStatus === "failed")
return Theme.error;
if (WifiService.connectionStatus === "connected")
return Theme.success;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
}
Behavior on color {
@@ -663,7 +722,9 @@ Item {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// WiFi networks list (only show if WiFi is available and enabled)
@@ -674,8 +735,7 @@ Item {
width: parent.width
height: 50
radius: Theme.cornerRadiusSmall
color: networkArea.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"
color: networkArea.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
@@ -686,12 +746,10 @@ Item {
// Signal strength icon
Text {
id: signalIcon
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: modelData.signalStrength === "excellent" ? "wifi" :
modelData.signalStrength === "good" ? "wifi_2_bar" :
modelData.signalStrength === "fair" ? "wifi_1_bar" :
modelData.signalStrength === "poor" ? "wifi_calling_3" : "wifi"
text: modelData.signalStrength === "excellent" ? "wifi" : modelData.signalStrength === "good" ? "wifi_2_bar" : modelData.signalStrength === "fair" ? "wifi_1_bar" : modelData.signalStrength === "poor" ? "wifi_calling_3" : "wifi"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: modelData.connected ? Theme.primary : Theme.surfaceText
@@ -718,19 +776,25 @@ Item {
Text {
width: parent.width
text: {
if (modelData.connected) return "Connected"
if (modelData.saved) return "Saved" + (modelData.secured ? " • Secured" : " • Open")
return modelData.secured ? "Secured" : "Open"
if (modelData.connected)
return "Connected";
if (modelData.saved)
return "Saved" + (modelData.secured ? " • Secured" : " • Open");
return modelData.secured ? "Secured" : "Open";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
}
}
// Right side icons
Row {
id: rightIcons
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
@@ -763,44 +827,52 @@ Item {
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
WifiService.forgetWifiNetwork(modelData.ssid)
WifiService.forgetWifiNetwork(modelData.ssid);
}
}
}
}
}
MouseArea {
// Already connected, do nothing or show info
id: networkArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected) {
// Already connected, do nothing or show info
return
}
if (modelData.connected)
return ;
if (modelData.saved) {
// Saved network, connect directly
WifiService.connectToWifi(modelData.ssid)
WifiService.connectToWifi(modelData.ssid);
} else if (modelData.secured) {
// Secured network, need password - use root dialog
wifiPasswordDialog.wifiPasswordSSID = modelData.ssid
wifiPasswordDialog.wifiPasswordInput = ""
wifiPasswordDialog.wifiPasswordDialogVisible = true
wifiPasswordDialog.wifiPasswordSSID = modelData.ssid;
wifiPasswordDialog.wifiPasswordInput = "";
wifiPasswordDialog.wifiPasswordDialogVisible = true;
} else {
// Open network, connect directly
WifiService.connectToWifi(modelData.ssid)
WifiService.connectToWifi(modelData.ssid);
}
}
}
}
}
}
// WiFi disabled message
@@ -831,9 +903,17 @@ Item {
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
}
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
}
}
}

View File

@@ -1,8 +1,8 @@
import "."
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import "."
Rectangle {
id: cpuWidget
@@ -13,19 +13,17 @@ Rectangle {
width: 55
height: 30
radius: Theme.cornerRadius
color: cpuArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: cpuArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
MouseArea {
id: cpuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
ProcessMonitorService.setSortBy("cpu")
processListDropdown.toggle()
ProcessMonitorService.setSortBy("cpu");
processListDropdown.toggle();
}
}
@@ -40,9 +38,13 @@ Rectangle {
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: {
if (SystemMonitorService.cpuUsage > 80) return Theme.error
if (SystemMonitorService.cpuUsage > 60) return Theme.warning
return Theme.surfaceText
if (SystemMonitorService.cpuUsage > 80)
return Theme.error;
if (SystemMonitorService.cpuUsage > 60)
return Theme.warning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
@@ -55,5 +57,7 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}

View File

@@ -50,48 +50,48 @@ Item {
// Slider track
Rectangle {
id: sliderTrack
width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
height: 6
radius: 3
color: slider.enabled ?
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
anchors.verticalCenter: parent.verticalCenter
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
height: 6
radius: 3
color: slider.enabled ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
anchors.verticalCenter: parent.verticalCenter
// Fill
Rectangle {
id: sliderFill
width: parent.width * ((slider.value - slider.minimum) / (slider.maximum - slider.minimum))
height: parent.height
radius: parent.radius
color: slider.enabled ? Theme.primary : Theme.surfaceVariantText
Behavior on width {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
// Draggable handle
Rectangle {
id: sliderHandle
width: 18
height: 18
radius: 9
color: slider.enabled ? Theme.primary : Theme.surfaceVariantText
border.color: slider.enabled ? Qt.lighter(Theme.primary, 1.3) : Qt.lighter(Theme.surfaceVariantText, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, sliderFill.width - width/2))
x: Math.max(0, Math.min(parent.width - width, sliderFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: sliderMouseArea.containsMouse || sliderMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
scale: sliderMouseArea.containsMouse || sliderMouseArea.pressed ? 1.2 : 1
// Handle glow effect when active
Rectangle {
@@ -105,57 +105,67 @@ Item {
visible: sliderMouseArea.containsMouse && slider.enabled
Behavior on opacity {
NumberAnimation { duration: 150 }
NumberAnimation {
duration: 150
}
}
}
Behavior on scale {
NumberAnimation {
duration: 150
}
}
}
Item {
id: sliderContainer
anchors.fill: parent
MouseArea {
id: sliderMouseArea
property bool isDragging: false
anchors.fill: parent
hoverEnabled: true
cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: slider.enabled
preventStealing: true
property bool isDragging: false
onPressed: (mouse) => {
if (slider.enabled) {
isDragging = true
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / width));
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum));
slider.value = newValue;
slider.sliderValueChanged(newValue);
}
}
onReleased: {
if (slider.enabled) {
isDragging = false
slider.sliderDragFinished(slider.value)
isDragging = false;
slider.sliderDragFinished(slider.value);
}
}
onPositionChanged: (mouse) => {
if (pressed && isDragging && slider.enabled) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
let ratio = Math.max(0, Math.min(1, mouse.x / width));
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum));
slider.value = newValue;
slider.sliderValueChanged(newValue);
}
}
onClicked: (mouse) => {
if (slider.enabled) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
let ratio = Math.max(0, Math.min(1, mouse.x / width));
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum));
slider.value = newValue;
slider.sliderValueChanged(newValue);
}
}
}
@@ -163,29 +173,30 @@ Item {
// Global mouse area for drag tracking
MouseArea {
id: sliderGlobalMouseArea
anchors.fill: sliderContainer
enabled: sliderMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (sliderMouseArea.isDragging && slider.enabled) {
let globalPos = mapToItem(sliderTrack, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / sliderTrack.width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
let globalPos = mapToItem(sliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / sliderTrack.width));
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum));
slider.value = newValue;
slider.sliderValueChanged(newValue);
}
}
onReleased: {
if (sliderMouseArea.isDragging && slider.enabled) {
sliderMouseArea.isDragging = false;
slider.sliderDragFinished(slider.value);
}
}
}
onReleased: {
if (sliderMouseArea.isDragging && slider.enabled) {
sliderMouseArea.isDragging = false
slider.sliderDragFinished(slider.value)
}
}
}
}
}
// Right icon
@@ -197,6 +208,9 @@ Item {
anchors.verticalCenter: parent.verticalCenter
visible: slider.rightIcon.length > 0
}
}
}
}

View File

@@ -1,8 +1,8 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
PanelWindow {
@@ -20,7 +20,34 @@ PanelWindow {
signal confirmed(string value)
signal cancelled()
function showDialog(title, subtitle, placeholder, isPass, confirmText, cancelText) {
dialogTitle = title || "Input Required";
dialogSubtitle = subtitle || "Please enter the required information";
inputPlaceholder = placeholder || "Enter text";
isPassword = isPass || false;
confirmButtonText = confirmText || "Confirm";
cancelButtonText = cancelText || "Cancel";
inputValue = "";
dialogVisible = true;
}
function hideDialog() {
dialogVisible = false;
inputValue = "";
}
visible: dialogVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: dialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
onVisibleChanged: {
if (visible) {
textInput.forceActiveFocus();
textInput.text = inputValue;
}
}
anchors {
top: true
left: true
@@ -28,54 +55,27 @@ PanelWindow {
bottom: true
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: dialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
onVisibleChanged: {
if (visible) {
textInput.forceActiveFocus()
textInput.text = inputValue
}
}
function showDialog(title, subtitle, placeholder, isPass, confirmText, cancelText) {
dialogTitle = title || "Input Required"
dialogSubtitle = subtitle || "Please enter the required information"
inputPlaceholder = placeholder || "Enter text"
isPassword = isPass || false
confirmButtonText = confirmText || "Confirm"
cancelButtonText = cancelText || "Cancel"
inputValue = ""
dialogVisible = true
}
function hideDialog() {
dialogVisible = false
inputValue = ""
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: dialogVisible ? 1.0 : 0.0
opacity: dialogVisible ? 1 : 0
MouseArea {
anchors.fill: parent
onClicked: {
inputDialog.cancelled();
hideDialog();
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
MouseArea {
anchors.fill: parent
onClicked: {
inputDialog.cancelled()
hideDialog()
}
}
}
Rectangle {
@@ -86,23 +86,8 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: dialogVisible ? 1.0 : 0.0
scale: dialogVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: dialogVisible ? 1 : 0
scale: dialogVisible ? 1 : 0.9
Column {
anchors.fill: parent
@@ -133,6 +118,7 @@ PanelWindow {
wrapMode: Text.WordWrap
maximumLineCount: 2
}
}
Rectangle {
@@ -151,15 +137,18 @@ PanelWindow {
MouseArea {
id: closeDialogArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
inputDialog.cancelled()
hideDialog()
inputDialog.cancelled();
hideDialog();
}
}
}
}
// Text input
@@ -173,6 +162,7 @@ PanelWindow {
TextInput {
id: textInput
anchors.fill: parent
anchors.margins: Theme.spacingM
font.pixelSize: Theme.fontSizeMedium
@@ -181,6 +171,18 @@ PanelWindow {
verticalAlignment: TextInput.AlignVCenter
cursorVisible: activeFocus
selectByMouse: true
onTextChanged: {
inputValue = text;
}
onAccepted: {
inputDialog.confirmed(inputValue);
hideDialog();
}
Component.onCompleted: {
if (dialogVisible)
forceActiveFocus();
}
Text {
anchors.fill: parent
@@ -191,29 +193,16 @@ PanelWindow {
visible: parent.text.length === 0
}
onTextChanged: {
inputValue = text
}
onAccepted: {
inputDialog.confirmed(inputValue)
hideDialog()
}
Component.onCompleted: {
if (dialogVisible) {
forceActiveFocus()
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: {
textInput.forceActiveFocus()
textInput.forceActiveFocus();
}
}
}
// Show password checkbox (only visible for password inputs)
@@ -223,6 +212,7 @@ PanelWindow {
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
@@ -246,9 +236,10 @@ PanelWindow {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
}
}
}
Text {
@@ -257,6 +248,7 @@ PanelWindow {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
// Buttons
@@ -279,6 +271,7 @@ PanelWindow {
Text {
id: cancelText
anchors.centerIn: parent
text: cancelButtonText
font.pixelSize: Theme.fontSizeMedium
@@ -288,14 +281,16 @@ PanelWindow {
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
inputDialog.cancelled()
hideDialog()
inputDialog.cancelled();
hideDialog();
}
}
}
Rectangle {
@@ -304,10 +299,11 @@ PanelWindow {
radius: Theme.cornerRadius
color: confirmArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: inputValue.length > 0
opacity: enabled ? 1.0 : 0.5
opacity: enabled ? 1 : 0.5
Text {
id: confirmText
anchors.centerIn: parent
text: confirmButtonText
font.pixelSize: Theme.fontSizeMedium
@@ -317,13 +313,14 @@ PanelWindow {
MouseArea {
id: confirmArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
inputDialog.confirmed(inputValue)
hideDialog()
inputDialog.confirmed(inputValue);
hideDialog();
}
}
@@ -332,10 +329,33 @@ PanelWindow {
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

@@ -2,8 +2,8 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
@@ -13,14 +13,11 @@ PanelWindow {
property bool notificationHistoryVisible: false
visible: notificationHistoryVisible
implicitWidth: 400
implicitHeight: 500
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
@@ -34,7 +31,7 @@ PanelWindow {
MouseArea {
anchors.fill: parent
onClicked: {
notificationHistoryVisible = false
notificationHistoryVisible = false;
}
}
@@ -47,43 +44,65 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0.5
opacity: notificationHistoryVisible ? 1 : 0
// Animation
transform: [
Scale {
id: scaleTransform
origin.x: 400 // Use fixed width since popup is 400px wide
origin.y: 0
xScale: notificationHistoryVisible ? 1.0 : 0.95
yScale: notificationHistoryVisible ? 1.0 : 0.8
xScale: notificationHistoryVisible ? 1 : 0.95
yScale: notificationHistoryVisible ? 1 : 0.8
},
Translate {
id: translateTransform
x: notificationHistoryVisible ? 0 : 15
y: notificationHistoryVisible ? 0 : -30
}
]
opacity: notificationHistoryVisible ? 1.0 : 0.0
states: [
State {
name: "visible"
when: notificationHistoryVisible
PropertyChanges { target: scaleTransform; xScale: 1.0; yScale: 1.0 }
PropertyChanges { target: translateTransform; x: 0; y: 0 }
PropertyChanges {
target: scaleTransform
xScale: 1
yScale: 1
}
PropertyChanges {
target: translateTransform
x: 0
y: 0
}
},
State {
name: "hidden"
when: !notificationHistoryVisible
PropertyChanges { target: scaleTransform; xScale: 0.95; yScale: 0.8 }
PropertyChanges { target: translateTransform; x: 15; y: -30 }
PropertyChanges {
target: scaleTransform
xScale: 0.95
yScale: 0.8
}
PropertyChanges {
target: translateTransform
x: 15
y: -30
}
}
]
transitions: [
Transition {
from: "*"; to: "*"
from: "*"
to: "*"
ParallelAnimation {
NumberAnimation {
targets: [scaleTransform, translateTransform]
@@ -91,22 +110,18 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
// Prevent clicks from propagating to background
MouseArea {
// Stop propagation - do nothing
anchors.fill: parent
onClicked: {
// Stop propagation - do nothing
}
}
@@ -137,14 +152,8 @@ PanelWindow {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: NotificationService.notifications.length > 0
color: clearArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Theme.surfaceContainer
border.color: clearArea.containsMouse ?
Theme.primary :
Theme.outline
color: clearArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.surfaceContainer
border.color: clearArea.containsMouse ? Theme.primary : Theme.outline
border.width: 1
Row {
@@ -166,14 +175,15 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.clearAllNotifications()
}
@@ -182,6 +192,7 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
@@ -189,8 +200,11 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
// Notification List
@@ -203,6 +217,8 @@ PanelWindow {
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ListView {
// Quick reply height
model: NotificationService.groupedNotifications
spacing: Theme.spacingL
interactive: true
@@ -220,13 +236,16 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
properties: "height"
from: 0
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
remove: Transition {
@@ -235,6 +254,7 @@ PanelWindow {
PauseAnimation {
duration: 50
}
ParallelAnimation {
NumberAnimation {
properties: "opacity"
@@ -242,14 +262,18 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
properties: "height,anchors.topMargin,anchors.bottomMargin"
to: 0
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
displaced: Transition {
@@ -258,6 +282,7 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
// Add move transition for internal content changes
@@ -267,37 +292,35 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
delegate: Rectangle {
required property var modelData
readonly property bool expanded: NotificationService.expandedGroups[modelData.key] || false
width: ListView.view.width
height: {
if (expanded) {
// Calculate expanded height: header (48) + spacing (16) + individual notifications
let headerHeight = 48 + Theme.spacingM
let notificationHeight = modelData.notifications.length * (60 + Theme.spacingS) // Each notification ~60px + spacing
let totalExpandedHeight = headerHeight + notificationHeight + Theme.spacingL * 2
return Math.max(totalExpandedHeight, 200) // Minimum expanded height
let headerHeight = 48 + Theme.spacingM;
let notificationHeight = modelData.notifications.length * (60 + Theme.spacingS); // Each notification ~60px + spacing
let totalExpandedHeight = headerHeight + notificationHeight + Theme.spacingL * 2;
return Math.max(totalExpandedHeight, 200); // Minimum expanded height
} else {
// Collapsed height: icon + content + quick reply (if any)
let collapsedHeight = 72 + Theme.spacingS * 2 // Header height + spacing
if (modelData.latestNotification.notification.hasInlineReply) {
collapsedHeight += 36 + Theme.spacingS // Quick reply height
}
return collapsedHeight + Theme.spacingL * 2
let collapsedHeight = 72 + Theme.spacingS * 2;
// Header height + spacing
if (modelData.latestNotification.notification.hasInlineReply)
collapsedHeight += 36 + Theme.spacingS;
return collapsedHeight + Theme.spacingL * 2;
}
}
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: modelData.latestNotification.urgency === 2 ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) :
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.color: modelData.latestNotification.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: modelData.latestNotification.urgency === 2 ? 2 : 1
clip: true
// Priority indicator for urgent notifications
@@ -312,19 +335,10 @@ PanelWindow {
visible: modelData.latestNotification.urgency === 2
}
Behavior on height {
SequentialAnimation {
PauseAnimation { duration: 25 }
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Collapsed view - shows app header and latest notification
Column {
id: collapsedContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
@@ -340,6 +354,7 @@ PanelWindow {
// App icon with proper fallback handling
Item {
id: iconContainer
width: 48
height: 48
anchors.left: parent.left
@@ -358,34 +373,34 @@ PanelWindow {
anchors.fill: parent
anchors.margins: 6
source: {
if (modelData.latestNotification.appIcon && modelData.latestNotification.appIcon !== "") {
return Quickshell.iconPath(modelData.latestNotification.appIcon, "")
}
return ""
if (modelData.latestNotification.appIcon && modelData.latestNotification.appIcon !== "")
return Quickshell.iconPath(modelData.latestNotification.appIcon, "");
return "";
}
visible: status === Image.Ready
onStatusChanged: {
if (status === Image.Error || status === Image.Null || source === "") {
fallbackIcon.visible = true
} else if (status === Image.Ready) {
fallbackIcon.visible = false
}
if (status === Image.Error || status === Image.Null || source === "")
fallbackIcon.visible = true;
else if (status === Image.Ready)
fallbackIcon.visible = false;
}
}
Text {
id: fallbackIcon
anchors.centerIn: parent
visible: true
text: {
const appName = modelData.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 20
font.weight: Font.Bold
color: Theme.primaryText
}
}
// Count badge for multiple notifications
@@ -407,7 +422,9 @@ PanelWindow {
font.pixelSize: 9
font.weight: Font.Bold
}
}
}
// Content area with proper spacing
@@ -424,11 +441,10 @@ PanelWindow {
Text {
width: parent.width
text: {
if (modelData.latestNotification.timeStr.length > 0) {
return modelData.appName + " • " + modelData.latestNotification.timeStr
} else {
return modelData.appName
}
if (modelData.latestNotification.timeStr.length > 0)
return modelData.appName + " • " + modelData.latestNotification.timeStr;
else
return modelData.appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -460,11 +476,13 @@ PanelWindow {
wrapMode: Text.WordWrap
visible: text.length > 0
}
}
// Controls with fixed positioning
Item {
id: controlsContainer
width: 72
height: 32
anchors.right: parent.right
@@ -475,9 +493,7 @@ PanelWindow {
height: 32
radius: 16
anchors.left: parent.left
color: expandArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: expandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
visible: modelData.count > 1
Text {
@@ -493,16 +509,20 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
id: expandArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.toggleGroupExpansion(modelData.key)
}
}
Rectangle {
@@ -510,9 +530,7 @@ PanelWindow {
height: 32
radius: 16
anchors.right: parent.right
color: dismissArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: dismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
@@ -524,13 +542,17 @@ PanelWindow {
MouseArea {
id: dismissArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissGroup(modelData.key)
}
}
}
}
// Enhanced quick reply for conversations
@@ -549,20 +571,24 @@ PanelWindow {
TextField {
id: quickReplyField
anchors.fill: parent
anchors.margins: Theme.spacingS
placeholderText: modelData.latestNotification.notification.inlineReplyPlaceholder || "Quick reply..."
background: Item {}
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
onAccepted: {
if (text.length > 0) {
modelData.latestNotification.notification.sendInlineReply(text)
text = ""
modelData.latestNotification.notification.sendInlineReply(text);
text = "";
}
}
background: Item {
}
}
}
Rectangle {
@@ -586,8 +612,8 @@ PanelWindow {
enabled: quickReplyField.text.length > 0
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
modelData.latestNotification.notification.sendInlineReply(quickReplyField.text)
quickReplyField.text = ""
modelData.latestNotification.notification.sendInlineReply(quickReplyField.text);
quickReplyField.text = "";
}
}
@@ -596,14 +622,19 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
// Expanded view - shows all notifications stacked
Column {
id: expandedContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
@@ -638,13 +669,14 @@ PanelWindow {
anchors.centerIn: parent
visible: !modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === ""
text: {
const appName = modelData.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 16
font.weight: Font.Bold
color: Theme.primaryText
}
}
Text {
@@ -668,9 +700,7 @@ PanelWindow {
height: 32
radius: 16
anchors.left: parent.left
color: collapseArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: collapseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
@@ -682,11 +712,13 @@ PanelWindow {
MouseArea {
id: collapseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.toggleGroupExpansion(modelData.key)
}
}
Rectangle {
@@ -694,9 +726,7 @@ PanelWindow {
height: 32
radius: 16
anchors.right: parent.right
color: dismissAllArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: dismissAllArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
@@ -708,13 +738,17 @@ PanelWindow {
MouseArea {
id: dismissAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissGroup(modelData.key)
}
}
}
}
// Individual notifications
@@ -732,14 +766,13 @@ PanelWindow {
height: Math.max(48, 32 + contentColumn.height + Theme.spacingM * 2) // 32 for icon height, plus content
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
border.color: modelData.urgency === 2 ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) :
"transparent"
border.color: modelData.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.width: modelData.urgency === 2 ? 1 : 0
clip: true
Item {
id: notifContent
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
@@ -759,13 +792,14 @@ PanelWindow {
Text {
anchors.centerIn: parent
text: {
const appName = modelData.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 12
font.weight: Font.Bold
color: Theme.primaryText
}
}
Rectangle {
@@ -774,9 +808,7 @@ PanelWindow {
radius: 12
anchors.right: parent.right
anchors.top: parent.top
color: individualDismissArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
@@ -788,15 +820,18 @@ PanelWindow {
MouseArea {
id: individualDismissArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissNotification(modelData)
}
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.leftMargin: 44
anchors.right: parent.right
@@ -840,20 +875,24 @@ PanelWindow {
TextField {
id: replyField
anchors.fill: parent
anchors.margins: Theme.spacingXS
placeholderText: modelData.notification.inlineReplyPlaceholder || "Reply..."
background: Item {}
color: Theme.surfaceText
font.pixelSize: 11
onAccepted: {
if (text.length > 0) {
modelData.notification.sendInlineReply(text)
text = ""
modelData.notification.sendInlineReply(text);
text = "";
}
}
background: Item {
}
}
}
Rectangle {
@@ -875,17 +914,25 @@ PanelWindow {
enabled: replyField.text.length > 0
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
modelData.notification.sendInlineReply(replyField.text)
replyField.text = ""
modelData.notification.sendInlineReply(replyField.text);
replyField.text = "";
}
}
}
}
}
}
}
}
}
}
// Tap to expand for collapsed groups
@@ -895,7 +942,24 @@ PanelWindow {
onClicked: NotificationService.toggleGroupExpansion(modelData.key)
z: -1
}
Behavior on height {
SequentialAnimation {
PauseAnimation {
duration: 25
}
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
// Empty state
@@ -936,10 +1000,23 @@ PanelWindow {
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Click outside to close
@@ -948,4 +1025,5 @@ PanelWindow {
z: -1
onClicked: notificationHistoryVisible = false
}
}

View File

@@ -1,35 +1,13 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
PanelWindow {
id: notificationPopup
objectName: "notificationPopup"
visible: NotificationService.groupedPopups.length > 0
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
right: true
}
margins {
top: Theme.barHeight
right: 12
}
implicitWidth: 400
implicitHeight: notificationsList.height + 32
// Expose key child objects for testing
// Expose the currently visible quickReplyField for testing
@@ -41,8 +19,28 @@ PanelWindow {
// Expose the currently visible hoverArea for testing
property MouseArea hoverArea: null
objectName: "notificationPopup"
visible: NotificationService.groupedPopups.length > 0
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
implicitWidth: 400
implicitHeight: notificationsList.height + 32
anchors {
top: true
right: true
}
margins {
top: Theme.barHeight
right: 12
}
Column {
id: notificationsList
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 16
@@ -55,7 +53,6 @@ PanelWindow {
delegate: Rectangle {
required property var modelData
// Context detection for popup
readonly property bool isPopupContext: true
readonly property bool expanded: NotificationService.expandedGroups[modelData.key] || false
@@ -65,61 +62,32 @@ PanelWindow {
let calculatedHeight;
if (expanded) {
// Calculate expanded height properly: header (48) + spacing + notifications
let headerHeight = 48 + Theme.spacingM
let maxNotificationsInPopup = Math.min(modelData.notifications.length, 5)
let notificationHeight = maxNotificationsInPopup * (60 + Theme.spacingS)
calculatedHeight = headerHeight + notificationHeight + Theme.spacingL * 2
let headerHeight = 48 + Theme.spacingM;
let maxNotificationsInPopup = Math.min(modelData.notifications.length, 5);
let notificationHeight = maxNotificationsInPopup * (60 + Theme.spacingS);
calculatedHeight = headerHeight + notificationHeight + Theme.spacingL * 2;
} else {
// Collapsed height: header (72) + quick reply if present
calculatedHeight = 72 + Theme.spacingS * 2
if (modelData.latestNotification.notification.hasInlineReply) {
calculatedHeight += 36 + Theme.spacingS
}
calculatedHeight += Theme.spacingL * 2
}
calculatedHeight = 72 + Theme.spacingS * 2;
if (modelData.latestNotification.notification.hasInlineReply)
calculatedHeight += 36 + Theme.spacingS;
// Add extra height for single notifications in popup context
if (isPopupContext && modelData.count === 1) {
calculatedHeight += 12;
calculatedHeight += Theme.spacingL * 2;
}
// Add extra height for single notifications in popup context
if (isPopupContext && modelData.count === 1)
calculatedHeight += 12;
return calculatedHeight;
}
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: modelData.latestNotification.urgency === 2 ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) :
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.color: modelData.latestNotification.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: modelData.latestNotification.urgency === 2 ? 2 : 1
// Stabilize layout during content changes
clip: true
// Smooth popup animations
transform: Translate {
x: notificationPopup.visible ? 0 : 400
Behavior on x {
NumberAnimation {
duration: 350
easing.type: Easing.OutCubic
}
}
}
opacity: notificationPopup.visible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
scale: notificationPopup.visible ? 1.0 : 0.98
Behavior on scale {
NumberAnimation {
duration: 350
easing.type: Easing.OutCubic
}
}
opacity: notificationPopup.visible ? 1 : 0
scale: notificationPopup.visible ? 1 : 0.98
// Priority indicator for urgent notifications
Rectangle {
@@ -133,20 +101,10 @@ PanelWindow {
visible: modelData.latestNotification.urgency === 2
}
Behavior on height {
enabled: !isPopupContext // Disable automatic height animation in popup to prevent glitches
SequentialAnimation {
PauseAnimation { duration: 25 }
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Collapsed view - shows app header and latest notification
Column {
id: collapsedContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
@@ -162,9 +120,10 @@ PanelWindow {
// Round app icon with proper API usage
Item {
id: iconContainer
Component.onCompleted: {
// Expose this iconContainer to the root for testing if visible
notificationPopup.iconContainer = iconContainer
notificationPopup.iconContainer = iconContainer;
}
width: 48
height: 48
@@ -184,36 +143,36 @@ PanelWindow {
anchors.fill: parent
anchors.margins: 6
source: {
if (modelData.latestNotification.appIcon && modelData.latestNotification.appIcon !== "") {
return Quickshell.iconPath(modelData.latestNotification.appIcon, "")
}
return ""
if (modelData.latestNotification.appIcon && modelData.latestNotification.appIcon !== "")
return Quickshell.iconPath(modelData.latestNotification.appIcon, "");
return "";
}
visible: status === Image.Ready
onStatusChanged: {
if (status === Image.Error || status === Image.Null || source === "") {
fallbackIcon.visible = true
} else if (status === Image.Ready) {
fallbackIcon.visible = false
}
if (status === Image.Error || status === Image.Null || source === "")
fallbackIcon.visible = true;
else if (status === Image.Ready)
fallbackIcon.visible = false;
}
}
// Fallback icon - show by default, hide when real icon loads
Text {
id: fallbackIcon
anchors.centerIn: parent
visible: true // Start visible, hide when real icon loads
text: {
// Use first letter of app name as fallback
const appName = modelData.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 20
font.weight: Font.Bold
color: Theme.primaryText
}
}
// Count badge for multiple notifications - smaller circle
@@ -235,7 +194,9 @@ PanelWindow {
font.pixelSize: 9
font.weight: Font.Bold
}
}
}
// App info and latest notification content
@@ -252,11 +213,10 @@ PanelWindow {
Text {
width: parent.width
text: {
if (modelData.latestNotification.timeStr.length > 0) {
return modelData.appName + " • " + modelData.latestNotification.timeStr
} else {
return modelData.appName
}
if (modelData.latestNotification.timeStr.length > 0)
return modelData.appName + " • " + modelData.latestNotification.timeStr;
else
return modelData.appName;
}
color: Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeSmall
@@ -288,11 +248,13 @@ PanelWindow {
wrapMode: Text.WordWrap
visible: text.length > 0
}
}
// Expand/dismiss controls - use anchored layout for stability
Item {
id: controlsContainer
width: 72
height: 32
anchors.right: parent.right
@@ -303,9 +265,7 @@ PanelWindow {
height: 32
radius: 16
anchors.left: parent.left
color: expandArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: expandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
visible: modelData.count > 1
Text {
@@ -321,21 +281,25 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
// ...existing code...
id: expandArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
console.log("Expand clicked - pausing timer")
dismissTimer.stop()
NotificationService.toggleGroupExpansion(modelData.key)
console.log("Expand clicked - pausing timer");
dismissTimer.stop();
NotificationService.toggleGroupExpansion(modelData.key);
}
}
}
Rectangle {
@@ -343,9 +307,7 @@ PanelWindow {
height: 32
radius: 16
anchors.right: parent.right
color: dismissArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: dismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
@@ -357,13 +319,17 @@ PanelWindow {
MouseArea {
id: dismissArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissGroup(modelData.key)
}
}
}
}
// Quick reply for conversations (only if latest notification supports it)
@@ -382,20 +348,24 @@ PanelWindow {
TextField {
id: quickReplyField
anchors.fill: parent
anchors.margins: Theme.spacingS
placeholderText: modelData.latestNotification.notification.inlineReplyPlaceholder || "Quick reply..."
background: Item {}
color: Theme.surfaceText
font.pixelSize: Theme.fontSizeSmall
onAccepted: {
if (text.length > 0) {
modelData.latestNotification.notification.sendInlineReply(text)
text = ""
modelData.latestNotification.notification.sendInlineReply(text);
text = "";
}
}
background: Item {
}
}
}
Rectangle {
@@ -419,8 +389,8 @@ PanelWindow {
enabled: quickReplyField.text.length > 0
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
modelData.latestNotification.notification.sendInlineReply(quickReplyField.text)
quickReplyField.text = ""
modelData.latestNotification.notification.sendInlineReply(quickReplyField.text);
quickReplyField.text = "";
}
}
@@ -429,17 +399,22 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
// Expanded view - shows all notifications stacked
Column {
id: expandedContent
Component.onCompleted: {
// Expose this expandedContent to the root for testing if visible
notificationPopup.expandedContent = expandedContent
notificationPopup.expandedContent = expandedContent;
}
anchors.top: parent.top
anchors.left: parent.left
@@ -477,13 +452,14 @@ PanelWindow {
anchors.centerIn: parent
visible: !modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === ""
text: {
const appName = modelData.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 16
font.weight: Font.Bold
color: Theme.primaryText
}
}
// App name and count badge - centered area
@@ -509,9 +485,7 @@ PanelWindow {
height: 32
radius: 16
anchors.left: parent.left
color: collapseArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: collapseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
@@ -523,15 +497,17 @@ PanelWindow {
MouseArea {
id: collapseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
console.log("Expand clicked - pausing timer")
dismissTimer.stop()
NotificationService.toggleGroupExpansion(modelData.key)
console.log("Expand clicked - pausing timer");
dismissTimer.stop();
NotificationService.toggleGroupExpansion(modelData.key);
}
}
}
Rectangle {
@@ -539,9 +515,7 @@ PanelWindow {
height: 32
radius: 16
anchors.right: parent.right
color: dismissAllArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: dismissAllArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
@@ -553,13 +527,17 @@ PanelWindow {
MouseArea {
id: dismissAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissGroup(modelData.key)
}
}
}
}
// Stacked individual notifications with smooth transitions
@@ -577,16 +555,14 @@ PanelWindow {
height: notifContent.height + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
border.color: modelData.urgency === 2 ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) :
"transparent"
border.color: modelData.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.width: modelData.urgency === 2 ? 1 : 0
// Stabilize layout during dismiss operations
clip: true
Item {
id: notifContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
@@ -617,13 +593,14 @@ PanelWindow {
anchors.centerIn: parent
visible: !modelData.appIcon || modelData.appIcon === ""
text: {
const appName = modelData.appName || "?"
return appName.charAt(0).toUpperCase()
const appName = modelData.appName || "?";
return appName.charAt(0).toUpperCase();
}
font.pixelSize: 12
font.weight: Font.Bold
color: Theme.primaryText
}
}
// Individual dismiss button - fixed position on right
@@ -633,9 +610,7 @@ PanelWindow {
radius: 12
anchors.right: parent.right
anchors.top: parent.top
color: individualDismissArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
Text {
anchors.centerIn: parent
@@ -647,16 +622,19 @@ PanelWindow {
MouseArea {
id: individualDismissArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NotificationService.dismissNotification(modelData)
}
}
// Notification content - fills space between icon and dismiss button
Column {
id: contentColumn
anchors.left: parent.left
anchors.leftMargin: 44
anchors.right: parent.right
@@ -702,20 +680,24 @@ PanelWindow {
TextField {
id: replyField
anchors.fill: parent
anchors.margins: Theme.spacingXS
placeholderText: modelData.notification.inlineReplyPlaceholder || "Reply..."
background: Item {}
color: Theme.surfaceText
font.pixelSize: 11
onAccepted: {
if (text.length > 0) {
modelData.notification.sendInlineReply(text)
text = ""
modelData.notification.sendInlineReply(text);
text = "";
}
}
background: Item {
}
}
}
Rectangle {
@@ -737,69 +719,129 @@ PanelWindow {
enabled: replyField.text.length > 0
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
modelData.notification.sendInlineReply(replyField.text)
replyField.text = ""
modelData.notification.sendInlineReply(replyField.text);
replyField.text = "";
}
}
}
}
}
}
}
}
}
}
// Hover to pause auto-dismiss - MUST be properly configured
MouseArea {
id: hoverArea
Component.onCompleted: {
// Expose this hoverArea to the root for testing if visible
notificationPopup.hoverArea = hoverArea
notificationPopup.hoverArea = hoverArea;
}
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
z: 10 // Higher z-order to ensure hover detection
propagateComposedEvents: true
onEntered: {
console.log("Notification hover entered - pausing timer")
dismissTimer.stop()
console.log("Notification hover entered - pausing timer");
dismissTimer.stop();
}
onExited: {
console.log("Notification hover exited - resuming timer")
if (modelData.latestNotification.popup && !expanded) {
dismissTimer.restart()
}
console.log("Notification hover exited - resuming timer");
if (modelData.latestNotification.popup && !expanded)
dismissTimer.restart();
}
}
// Auto-dismiss timer - properly pauses on hover
Timer {
id: dismissTimer
running: modelData.latestNotification.popup && !expanded
interval: modelData.latestNotification.notification.expireTimeout > 0 ?
modelData.latestNotification.notification.expireTimeout * 1000 : 5000
interval: modelData.latestNotification.notification.expireTimeout > 0 ? modelData.latestNotification.notification.expireTimeout * 1000 : 5000
onTriggered: {
console.log("Timer triggered - hover state:", hoverArea.containsMouse, "expanded:", expanded)
console.log("Timer triggered - hover state:", hoverArea.containsMouse, "expanded:", expanded);
if (!hoverArea.containsMouse && !expanded) {
console.log("Dismissing notification")
modelData.latestNotification.popup = false
console.log("Dismissing notification");
modelData.latestNotification.popup = false;
} else {
console.log("Conditions not met - not dismissing")
console.log("Conditions not met - not dismissing");
}
}
}
// Smooth popup animations
transform: Translate {
x: notificationPopup.visible ? 0 : 400
Behavior on x {
NumberAnimation {
duration: 350
easing.type: Easing.OutCubic
}
}
}
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Behavior on scale {
NumberAnimation {
duration: 350
easing.type: Easing.OutCubic
}
}
Behavior on height {
enabled: !isPopupContext // Disable automatic height animation in popup to prevent glitches
SequentialAnimation {
PauseAnimation {
duration: 25
}
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
// Smooth height animation
Behavior on implicitHeight {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}

View File

@@ -1,9 +1,9 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
PanelWindow {
@@ -14,15 +14,35 @@ PanelWindow {
property string powerConfirmTitle: ""
property string powerConfirmMessage: ""
visible: powerConfirmVisible
function executePowerAction(action) {
console.log("Executing power action:", action);
let command = [];
switch (action) {
case "logout":
command = ["niri", "msg", "action", "quit", "-s"];
break;
case "suspend":
command = ["systemctl", "suspend"];
break;
case "reboot":
command = ["systemctl", "reboot"];
break;
case "poweroff":
command = ["systemctl", "poweroff"];
break;
}
if (command.length > 0) {
powerActionProcess.command = command;
powerActionProcess.running = true;
}
}
visible: powerConfirmVisible
implicitWidth: 400
implicitHeight: 300
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
color: "transparent"
anchors {
@@ -47,23 +67,8 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: powerConfirmVisible ? 1.0 : 0.0
scale: powerConfirmVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: powerConfirmVisible ? 1 : 0
scale: powerConfirmVisible ? 1 : 0.9
Column {
anchors.centerIn: parent
@@ -75,10 +80,13 @@ PanelWindow {
text: powerConfirmTitle
font.pixelSize: Theme.fontSizeLarge
color: {
switch(powerConfirmAction) {
case "poweroff": return Theme.error
case "reboot": return Theme.warning
default: return Theme.surfaceText
switch (powerConfirmAction) {
case "poweroff":
return Theme.error;
case "reboot":
return Theme.warning;
default:
return Theme.surfaceText;
}
}
font.weight: Font.Medium
@@ -96,7 +104,9 @@ PanelWindow {
wrapMode: Text.WordWrap
}
Item { height: Theme.spacingL }
Item {
height: Theme.spacingL
}
// Buttons
Row {
@@ -120,13 +130,15 @@ PanelWindow {
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false
powerConfirmVisible = false;
}
}
}
// Confirm button
@@ -135,15 +147,19 @@ PanelWindow {
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
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
return confirmButton.containsMouse ? Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) : baseColor;
}
Text {
@@ -156,52 +172,49 @@ PanelWindow {
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false
executePowerAction(powerConfirmAction)
}
}
}
}
powerConfirmVisible = false;
executePowerAction(powerConfirmAction);
}
}
function executePowerAction(action) {
console.log("Executing power action:", action)
let command = []
switch(action) {
case "logout":
command = ["niri", "msg", "action", "quit", "-s"]
break
case "suspend":
command = ["systemctl", "suspend"]
break
case "reboot":
command = ["systemctl", "reboot"]
break
case "poweroff":
command = ["systemctl", "poweroff"]
break
}
if (command.length > 0) {
powerActionProcess.command = command
powerActionProcess.running = true
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Process {
id: powerActionProcess
running: false
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.error("Power action failed with exit code:", exitCode)
}
if (exitCode !== 0)
console.error("Power action failed with exit code:", exitCode);
}
}
}

View File

@@ -1,9 +1,9 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
PanelWindow {
@@ -12,14 +12,11 @@ PanelWindow {
property bool powerMenuVisible: false
visible: powerMenuVisible
implicitWidth: 400
implicitHeight: 320
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
@@ -33,7 +30,7 @@ PanelWindow {
MouseArea {
anchors.fill: parent
onClicked: {
powerMenuVisible = false
powerMenuVisible = false;
}
}
@@ -46,29 +43,15 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: powerMenuVisible ? 1.0 : 0.0
scale: powerMenuVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
// Prevent click-through to background
MouseArea {
// Consume the click to prevent it from reaching the background
anchors.fill: parent
onClicked: {
// Consume the click to prevent it from reaching the background
}
}
@@ -89,7 +72,10 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 150; height: 1 }
Item {
width: parent.width - 150
height: 1
}
Rectangle {
width: 32
@@ -107,14 +93,17 @@ PanelWindow {
MouseArea {
id: closePowerArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
powerMenuVisible = false;
}
}
}
}
// Power options
@@ -150,21 +139,24 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "logout"
root.powerConfirmTitle = "Log Out"
root.powerConfirmMessage = "Are you sure you want to log out?"
root.powerConfirmVisible = true
powerMenuVisible = false;
root.powerConfirmAction = "logout";
root.powerConfirmTitle = "Log Out";
root.powerConfirmMessage = "Are you sure you want to log out?";
root.powerConfirmVisible = true;
}
}
}
// Suspend
@@ -195,21 +187,24 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "suspend"
root.powerConfirmTitle = "Suspend"
root.powerConfirmMessage = "Are you sure you want to suspend the system?"
root.powerConfirmVisible = true
powerMenuVisible = false;
root.powerConfirmAction = "suspend";
root.powerConfirmTitle = "Suspend";
root.powerConfirmMessage = "Are you sure you want to suspend the system?";
root.powerConfirmVisible = true;
}
}
}
// Reboot
@@ -240,21 +235,24 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "reboot"
root.powerConfirmTitle = "Reboot"
root.powerConfirmMessage = "Are you sure you want to reboot the system?"
root.powerConfirmVisible = true
powerMenuVisible = false;
root.powerConfirmAction = "reboot";
root.powerConfirmTitle = "Reboot";
root.powerConfirmMessage = "Are you sure you want to reboot the system?";
root.powerConfirmVisible = true;
}
}
}
// Power Off
@@ -285,23 +283,46 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "poweroff"
root.powerConfirmTitle = "Power Off"
root.powerConfirmMessage = "Are you sure you want to power off the system?"
root.powerConfirmVisible = true
powerMenuVisible = false;
root.powerConfirmAction = "poweroff";
root.powerConfirmTitle = "Power Off";
root.powerConfirmMessage = "Are you sure you want to power off the system?";
root.powerConfirmVisible = true;
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}

View File

@@ -3,9 +3,9 @@ import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
@@ -15,20 +15,34 @@ PanelWindow {
property bool isVisible: false
property var parentWidget: null
visible: isVisible
// Monitor process dropdown visibility to enable/disable process monitoring
onIsVisibleChanged: {
ProcessMonitorService.enableMonitoring(isVisible)
// Close dropdown when clicking outside
function hide() {
isVisible = false;
}
function show() {
isVisible = true;
ProcessMonitorService.updateSystemInfo();
ProcessMonitorService.updateProcessList();
}
function toggle() {
if (isVisible)
hide();
else
show();
}
visible: isVisible
// Monitor process dropdown visibility to enable/disable process monitoring
onIsVisibleChanged: {
ProcessMonitorService.enableMonitoring(isVisible);
}
implicitWidth: 600
implicitHeight: 600
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
@@ -46,31 +60,35 @@ PanelWindow {
Rectangle {
id: dropdownContent
width: Math.min(600, Screen.width - Theme.spacingL * 2)
height: Math.min(600, Screen.height - Theme.barHeight - Theme.spacingS * 2)
x: Math.max(Theme.spacingL, Screen.width - width - Theme.spacingL)
y: Theme.barHeight + Theme.spacingXS
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
clip: true
opacity: processListDropdown.isVisible ? 1 : 0
// Add shadow effect
layer.enabled: true
// TopBar dropdown animation - slide down from bar
transform: [
Scale {
id: scaleTransform
origin.x: parent.width * 0.85 // Scale from top-right
origin.y: 0
xScale: processListDropdown.isVisible ? 1.0 : 0.95
yScale: processListDropdown.isVisible ? 1.0 : 0.8
xScale: processListDropdown.isVisible ? 1 : 0.95
yScale: processListDropdown.isVisible ? 1 : 0.8
Behavior on xScale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on yScale {
@@ -78,10 +96,13 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
},
Translate {
id: translateTransform
x: processListDropdown.isVisible ? 0 : 20
y: processListDropdown.isVisible ? 0 : -30
@@ -90,6 +111,7 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on y {
@@ -97,36 +119,18 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
opacity: processListDropdown.isVisible ? 1.0 : 0.0
// Add shadow effect
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: processListDropdown.isVisible ? 0.15 : 0
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
// Click inside dropdown - consume the event
MouseArea {
// Consume clicks inside dropdown to prevent closing
anchors.fill: parent
onClicked: {
// Consume clicks inside dropdown to prevent closing
}
}
@@ -151,35 +155,25 @@ PanelWindow {
height: 80
radius: Theme.cornerRadiusLarge
color: {
if (ProcessMonitorService.sortBy === "cpu") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16)
} else if (cpuCardMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
if (ProcessMonitorService.sortBy === "cpu")
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
else if (cpuCardMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
else
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
}
}
border.color: ProcessMonitorService.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: ProcessMonitorService.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: ProcessMonitorService.sortBy === "cpu" ? 2 : 1
MouseArea {
id: cpuCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: ProcessMonitorService.setSortBy("cpu")
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
@@ -191,7 +185,7 @@ PanelWindow {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: ProcessMonitorService.sortBy === "cpu" ? Theme.primary : Theme.secondary
opacity: ProcessMonitorService.sortBy === "cpu" ? 1.0 : 0.8
opacity: ProcessMonitorService.sortBy === "cpu" ? 1 : 0.8
}
Text {
@@ -207,7 +201,23 @@ PanelWindow {
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
// Memory Overview Card (clickable for sorting)
@@ -216,35 +226,25 @@ PanelWindow {
height: 80
radius: Theme.cornerRadiusLarge
color: {
if (ProcessMonitorService.sortBy === "memory") {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16)
} else if (memoryCardMouseArea.containsMouse) {
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12)
} else {
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
if (ProcessMonitorService.sortBy === "memory")
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16);
else if (memoryCardMouseArea.containsMouse)
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12);
else
return Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08);
}
}
border.color: ProcessMonitorService.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: ProcessMonitorService.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: ProcessMonitorService.sortBy === "memory" ? 2 : 1
MouseArea {
id: memoryCardMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: ProcessMonitorService.setSortBy("memory")
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
Behavior on border.color {
ColorAnimation { duration: Theme.shortDuration }
}
Column {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
@@ -256,7 +256,7 @@ PanelWindow {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: ProcessMonitorService.sortBy === "memory" ? Theme.primary : Theme.secondary
opacity: ProcessMonitorService.sortBy === "memory" ? 1.0 : 0.8
opacity: ProcessMonitorService.sortBy === "memory" ? 1 : 0.8
}
Text {
@@ -272,7 +272,23 @@ PanelWindow {
color: Theme.surfaceText
opacity: 0.7
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
// Swap Overview Card
@@ -280,12 +296,8 @@ PanelWindow {
width: (parent.width - Theme.spacingM * 2) / 3
height: 80
radius: Theme.cornerRadiusLarge
color: ProcessMonitorService.totalSwapKB > 0 ?
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04)
border.color: ProcessMonitorService.totalSwapKB > 0 ?
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12)
color: ProcessMonitorService.totalSwapKB > 0 ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04)
border.color: ProcessMonitorService.totalSwapKB > 0 ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12)
border.width: 1
Column {
@@ -315,10 +327,12 @@ PanelWindow {
color: Theme.surfaceText
opacity: 0.7
}
}
}
}
}
}
// Separator
Rectangle {
@@ -326,11 +340,13 @@ PanelWindow {
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
}
// Headers
Item {
id: columnHeaders
Layout.fillWidth: true
Layout.leftMargin: 8
height: 24
@@ -364,6 +380,7 @@ PanelWindow {
opacity: 0.7
anchors.centerIn: parent
}
}
// RAM header - positioned exactly like memory badge
@@ -383,6 +400,7 @@ PanelWindow {
opacity: 0.7
anchors.centerIn: parent
}
}
// PID header - positioned exactly like PID text
@@ -404,9 +422,7 @@ PanelWindow {
width: 28
height: 28
radius: Theme.cornerRadius
color: sortOrderArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
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
@@ -420,6 +436,7 @@ PanelWindow {
MouseArea {
id: sortOrderArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -427,9 +444,14 @@ PanelWindow {
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
// Process list
@@ -438,12 +460,12 @@ PanelWindow {
Layout.fillHeight: true
Layout.minimumHeight: 200
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: processListView
anchors.fill: parent
model: ProcessMonitorService.processes
spacing: 4
@@ -452,37 +474,32 @@ PanelWindow {
width: processListView.width
height: 40
radius: Theme.cornerRadiusLarge
color: processMouseArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
"transparent"
border.color: processMouseArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
"transparent"
color: processMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, 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 (modelData && modelData.pid > 0) {
processContextMenuWindow.processData = modelData
let globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y)
processContextMenuWindow.show(globalPos.x, globalPos.y)
processContextMenuWindow.processData = modelData;
let globalPos = processMouseArea.mapToGlobal(mouse.x, mouse.y);
processContextMenuWindow.show(globalPos.x, globalPos.y);
}
}
}
onPressAndHold: {
// Context menu for kill process etc
if (modelData && modelData.pid > 0) {
processContextMenuWindow.processData = modelData
let globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2)
processContextMenuWindow.show(globalPos.x, globalPos.y)
processContextMenuWindow.processData = modelData;
let globalPos = processMouseArea.mapToGlobal(processMouseArea.width / 2, processMouseArea.height / 2);
processContextMenuWindow.show(globalPos.x, globalPos.y);
}
}
}
@@ -494,13 +511,18 @@ PanelWindow {
// Process icon
Text {
id: processIcon
text: ProcessMonitorService.getProcessIcon(modelData ? modelData.command : "")
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4
color: {
if (modelData && modelData.cpu > 80) return Theme.error
if (modelData && modelData.cpu > 50) return Theme.warning
return Theme.surfaceText
if (modelData && modelData.cpu > 80)
return Theme.error;
if (modelData && modelData.cpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
opacity: 0.8
anchors.left: parent.left
@@ -520,17 +542,21 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
}
// CPU usage
Rectangle {
id: cpuBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (modelData && modelData.cpu > 80) return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
if (modelData && modelData.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)
if (modelData && modelData.cpu > 80)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
if (modelData && modelData.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 // 28 (menu) + 12 + 50 (pid) + 12 + 80 (mem) + 12 spacing
@@ -541,24 +567,36 @@ PanelWindow {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: {
if (modelData && modelData.cpu > 80) return Theme.error
if (modelData && modelData.cpu > 50) return Theme.warning
return Theme.surfaceText
if (modelData && modelData.cpu > 80)
return Theme.error;
if (modelData && modelData.cpu > 50)
return Theme.warning;
return Theme.surfaceText;
}
anchors.centerIn: parent
}
}
// Memory usage
Rectangle {
id: memoryBadge
width: 80
height: 20
radius: Theme.cornerRadius
color: {
if (modelData && modelData.memoryKB > 1024 * 1024) return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) // > 1GB
if (modelData && modelData.memoryKB > 512 * 1024) return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) // > 512MB
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
if (modelData && modelData.memoryKB > 1024 * 1024)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
// > 1GB
if (modelData && modelData.memoryKB > 512 * 1024)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
// > 512MB
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08);
}
anchors.right: parent.right
anchors.rightMargin: 102 // 28 (menu) + 12 + 50 (pid) + 12 spacing
@@ -569,12 +607,19 @@ PanelWindow {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: {
if (modelData && modelData.memoryKB > 1024 * 1024) return Theme.error // > 1GB
if (modelData && modelData.memoryKB > 512 * 1024) return Theme.warning // > 512MB
return Theme.surfaceText
if (modelData && modelData.memoryKB > 1024 * 1024)
return Theme.error;
// > 1GB
if (modelData && modelData.memoryKB > 512 * 1024)
return Theme.warning;
// > 512MB
return Theme.surfaceText;
}
anchors.centerIn: parent
}
}
// PID
@@ -593,12 +638,11 @@ PanelWindow {
// 3-dot menu button (far right)
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"
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
@@ -614,40 +658,96 @@ PanelWindow {
MouseArea {
id: menuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.pid > 0) {
processContextMenuWindow.processData = modelData
let globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height)
processContextMenuWindow.show(globalPos.x, globalPos.y)
processContextMenuWindow.processData = modelData;
let globalPos = menuButtonArea.mapToGlobal(menuButtonArea.width / 2, menuButtonArea.height);
processContextMenuWindow.show(globalPos.x, globalPos.y);
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}
}
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: processListDropdown.isVisible ? 0.15 : 0
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Styled context menu for process actions - positioned in global coordinates
PanelWindow {
id: processContextMenuWindow
property var processData: null
property bool menuVisible: false
function show(x, y) {
// Smart positioning to prevent off-screen cutoff
const menuWidth = 180;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
// Get screen dimensions from the monitor
const screenWidth = processContextMenuWindow.screen ? processContextMenuWindow.screen.width : 1920;
const screenHeight = processContextMenuWindow.screen ? processContextMenuWindow.screen.height : 1080;
// Calculate optimal position
let finalX = x;
let finalY = y;
// Check horizontal bounds - if too close to right edge, position to the left
if (x + menuWidth > screenWidth - 20)
finalX = x - menuWidth;
// Check vertical bounds - if too close to bottom edge, position above
if (y + menuHeight > screenHeight - 20)
finalY = y - menuHeight;
// Ensure we don't go off the left or top edges
finalX = Math.max(20, finalX);
finalY = Math.max(20, finalY);
processContextMenu.x = finalX;
processContextMenu.y = finalY;
processContextMenuWindow.menuVisible = true;
}
function hide() {
processContextMenuWindow.menuVisible = false;
}
visible: menuVisible
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
@@ -661,12 +761,16 @@ PanelWindow {
Rectangle {
id: processContextMenu
width: 180
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
// Material 3 animations
opacity: processContextMenuWindow.menuVisible ? 1 : 0
scale: processContextMenuWindow.menuVisible ? 1 : 0.85
// Material 3 drop shadow
Rectangle {
@@ -680,26 +784,9 @@ PanelWindow {
z: parent.z - 1
}
// Material 3 animations
opacity: processContextMenuWindow.menuVisible ? 1.0 : 0.0
scale: processContextMenuWindow.menuVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
@@ -723,16 +810,16 @@ PanelWindow {
MouseArea {
id: copyPidArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenuWindow.processData) {
copyPidProcess.command = ["wl-copy", processContextMenuWindow.processData.pid.toString()]
copyPidProcess.running = true
copyPidProcess.command = ["wl-copy", processContextMenuWindow.processData.pid.toString()];
copyPidProcess.running = true;
}
processContextMenuWindow.hide()
processContextMenuWindow.hide();
}
}
@@ -741,7 +828,9 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Copy Process Name
@@ -763,17 +852,17 @@ PanelWindow {
MouseArea {
id: copyNameArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (processContextMenuWindow.processData) {
let processName = processContextMenuWindow.processData.displayName || processContextMenuWindow.processData.command
copyNameProcess.command = ["wl-copy", processName]
copyNameProcess.running = true
let processName = processContextMenuWindow.processData.displayName || processContextMenuWindow.processData.command;
copyNameProcess.command = ["wl-copy", processName];
copyNameProcess.running = true;
}
processContextMenuWindow.hide()
processContextMenuWindow.hide();
}
}
@@ -782,7 +871,9 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Separator
@@ -798,6 +889,7 @@ PanelWindow {
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
// Kill Process
@@ -807,7 +899,7 @@ PanelWindow {
radius: Theme.cornerRadiusSmall
color: killArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
enabled: processContextMenuWindow.processData && processContextMenuWindow.processData.pid > 1000
opacity: enabled ? 1.0 : 0.5
opacity: enabled ? 1 : 0.5
Text {
anchors.left: parent.left
@@ -821,17 +913,17 @@ PanelWindow {
MouseArea {
id: killArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenuWindow.processData) {
killProcess.command = ["kill", processContextMenuWindow.processData.pid.toString()]
killProcess.running = true
killProcess.command = ["kill", processContextMenuWindow.processData.pid.toString()];
killProcess.running = true;
}
processContextMenuWindow.hide()
processContextMenuWindow.hide();
}
}
@@ -840,7 +932,9 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Force Kill Process
@@ -850,7 +944,7 @@ PanelWindow {
radius: Theme.cornerRadiusSmall
color: forceKillArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
enabled: processContextMenuWindow.processData && processContextMenuWindow.processData.pid > 1000
opacity: enabled ? 1.0 : 0.5
opacity: enabled ? 1 : 0.5
Text {
anchors.left: parent.left
@@ -864,17 +958,17 @@ PanelWindow {
MouseArea {
id: forceKillArea
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: parent.enabled
onClicked: {
if (processContextMenuWindow.processData) {
forceKillProcess.command = ["kill", "-9", processContextMenuWindow.processData.pid.toString()]
forceKillProcess.running = true
forceKillProcess.command = ["kill", "-9", processContextMenuWindow.processData.pid.toString()];
forceKillProcess.running = true;
}
processContextMenuWindow.hide()
processContextMenuWindow.hide();
}
}
@@ -883,46 +977,29 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
function show(x, y) {
// Smart positioning to prevent off-screen cutoff
const menuWidth = 180
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
// Get screen dimensions from the monitor
const screenWidth = processContextMenuWindow.screen ? processContextMenuWindow.screen.width : 1920
const screenHeight = processContextMenuWindow.screen ? processContextMenuWindow.screen.height : 1080
// Calculate optimal position
let finalX = x
let finalY = y
// Check horizontal bounds - if too close to right edge, position to the left
if (x + menuWidth > screenWidth - 20) {
finalX = x - menuWidth
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
// Check vertical bounds - if too close to bottom edge, position above
if (y + menuHeight > screenHeight - 20) {
finalY = y - menuHeight
}
// Ensure we don't go off the left or top edges
finalX = Math.max(20, finalX)
finalY = Math.max(20, finalY)
processContextMenu.x = finalX
processContextMenu.y = finalY
processContextMenuWindow.menuVisible = true
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
function hide() {
processContextMenuWindow.menuVisible = false
}
// Click outside to close
@@ -930,48 +1007,35 @@ PanelWindow {
anchors.fill: parent
z: -1
onClicked: {
processContextMenuWindow.menuVisible = false
processContextMenuWindow.menuVisible = false;
}
}
// Process objects for commands
Process {
id: copyPidProcess
running: false
}
Process {
id: copyNameProcess
running: false
}
Process {
id: killProcess
running: false
}
Process {
id: forceKillProcess
running: false
}
}
// Close dropdown when clicking outside
function hide() {
isVisible = false
}
function show() {
isVisible = true
ProcessMonitorService.updateSystemInfo()
ProcessMonitorService.updateProcessList()
}
function toggle() {
if (isVisible) {
hide()
} else {
show()
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import "."
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import "."
Rectangle {
id: ramWidget
@@ -13,19 +13,17 @@ Rectangle {
width: 55
height: 30
radius: Theme.cornerRadius
color: ramArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: ramArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
MouseArea {
id: ramArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
ProcessMonitorService.setSortBy("memory")
processListDropdown.toggle()
ProcessMonitorService.setSortBy("memory");
processListDropdown.toggle();
}
}
@@ -40,9 +38,13 @@ Rectangle {
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: {
if (SystemMonitorService.memoryUsage > 90) return Theme.error
if (SystemMonitorService.memoryUsage > 75) return Theme.warning
return Theme.surfaceText
if (SystemMonitorService.memoryUsage > 90)
return Theme.error;
if (SystemMonitorService.memoryUsage > 75)
return Theme.warning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
@@ -55,5 +57,7 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}

View File

@@ -2,32 +2,29 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
PanelWindow {
id: settingsPopup
property bool settingsVisible: false
signal closingPopup()
onSettingsVisibleChanged: {
if (!settingsVisible) {
closingPopup()
}
}
if (!settingsVisible)
closingPopup();
}
visible: settingsVisible
implicitWidth: 600
implicitHeight: 700
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
color: "transparent"
anchors {
@@ -47,11 +44,13 @@ PanelWindow {
anchors.fill: parent
onClicked: settingsPopup.settingsVisible = false
}
}
// Main settings panel - spotlight-like centered appearance
Rectangle {
id: mainPanel
width: Math.min(600, parent.width - Theme.spacingXL * 2)
height: Math.min(700, parent.height - Theme.spacingXL * 2)
anchors.centerIn: parent
@@ -59,24 +58,11 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
// Simple opacity and scale control tied directly to settingsVisible
opacity: settingsPopup.settingsVisible ? 1.0 : 0.0
scale: settingsPopup.settingsVisible ? 1.0 : 0.95
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: settingsPopup.settingsVisible ? 1 : 0
scale: settingsPopup.settingsVisible ? 1 : 0.95
// Add shadow effect
layer.enabled: true
Column {
anchors.fill: parent
@@ -114,9 +100,7 @@ PanelWindow {
width: 32
height: 32
radius: Theme.cornerRadius
color: closeButton.containsMouse ?
Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) :
"transparent"
color: closeButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Text {
text: "close"
@@ -128,12 +112,15 @@ PanelWindow {
MouseArea {
id: closeButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: settingsPopup.settingsVisible = false
}
}
}
// Settings sections
@@ -175,11 +162,12 @@ PanelWindow {
// Circular profile image preview
Item {
id: avatarContainer
width: 54
height: 54
property bool hasImage: avatarImageSource.status === Image.Ready
width: 54
height: 54
// This rectangle provides the themed ring via its border.
Rectangle {
anchors.fill: parent
@@ -193,12 +181,15 @@ PanelWindow {
// Hidden Image loader. Its only purpose is to load the texture.
Image {
id: avatarImageSource
source: {
if (profileImageInput.text === "") return ""
if (profileImageInput.text.startsWith("/")) {
return "file://" + profileImageInput.text
}
return profileImageInput.text
if (profileImageInput.text === "")
return "";
if (profileImageInput.text.startsWith("/"))
return "file://" + profileImageInput.text;
return profileImageInput.text;
}
smooth: true
asynchronous: true
@@ -215,11 +206,12 @@ PanelWindow {
maskSource: settingsCircularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSpreadAtMin: 1
}
Item {
id: settingsCircularMask
width: 54 - 10
height: 54 - 10
layer.enabled: true
@@ -232,6 +224,7 @@ PanelWindow {
color: "black"
antialiasing: true
}
}
// Fallback for when there is no image.
@@ -248,6 +241,7 @@ PanelWindow {
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
}
}
// Error icon for when the image fails to load.
@@ -259,6 +253,7 @@ PanelWindow {
color: Theme.primaryText
visible: profileImageInput.text !== "" && avatarImageSource.status === Image.Error
}
}
// Input field
@@ -276,6 +271,7 @@ PanelWindow {
TextInput {
id: profileImageInput
anchors.fill: parent
anchors.margins: Theme.spacingM
verticalAlignment: TextInput.AlignVCenter
@@ -283,9 +279,8 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium
text: Prefs.profileImage
selectByMouse: true
onEditingFinished: {
Prefs.setProfileImage(text)
Prefs.setProfileImage(text);
}
// Placeholder text
@@ -303,7 +298,9 @@ PanelWindow {
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
}
}
Text {
@@ -313,10 +310,15 @@ PanelWindow {
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
}
}
// Clock Settings
@@ -332,9 +334,13 @@ PanelWindow {
text: "24-Hour Format"
description: "Use 24-hour time format instead of 12-hour AM/PM"
checked: Prefs.use24HourClock
onToggled: (checked) => Prefs.setClockFormat(checked)
onToggled: (checked) => {
return Prefs.setClockFormat(checked);
}
}
}
}
// Weather Settings
@@ -350,7 +356,9 @@ PanelWindow {
text: "Fahrenheit"
description: "Use Fahrenheit instead of Celsius for temperature"
checked: Prefs.useFahrenheit
onToggled: (checked) => Prefs.setTemperatureUnit(checked)
onToggled: (checked) => {
return Prefs.setTemperatureUnit(checked);
}
}
// Weather Location Setting
@@ -375,6 +383,7 @@ PanelWindow {
TextInput {
id: weatherLocationInput
anchors.fill: parent
anchors.margins: Theme.spacingM
verticalAlignment: TextInput.AlignVCenter
@@ -382,9 +391,8 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium
text: Prefs.weatherLocationOverride
selectByMouse: true
onEditingFinished: {
Prefs.setWeatherLocationOverride(text)
Prefs.setWeatherLocationOverride(text);
}
// Placeholder text
@@ -402,7 +410,9 @@ PanelWindow {
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
}
}
Text {
@@ -412,8 +422,11 @@ PanelWindow {
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
// Widget Visibility Settings
@@ -429,44 +442,58 @@ PanelWindow {
text: "Focused Window"
description: "Show the currently focused application in the top bar"
checked: Prefs.showFocusedWindow
onToggled: (checked) => Prefs.setShowFocusedWindow(checked)
onToggled: (checked) => {
return Prefs.setShowFocusedWindow(checked);
}
}
SettingsToggle {
text: "Weather Widget"
description: "Display weather information in the top bar"
checked: Prefs.showWeather
onToggled: (checked) => Prefs.setShowWeather(checked)
onToggled: (checked) => {
return Prefs.setShowWeather(checked);
}
}
SettingsToggle {
text: "Media Controls"
description: "Show currently playing media in the top bar"
checked: Prefs.showMusic
onToggled: (checked) => Prefs.setShowMusic(checked)
onToggled: (checked) => {
return Prefs.setShowMusic(checked);
}
}
SettingsToggle {
text: "Clipboard Button"
description: "Show clipboard access button in the top bar"
checked: Prefs.showClipboard
onToggled: (checked) => Prefs.setShowClipboard(checked)
onToggled: (checked) => {
return Prefs.setShowClipboard(checked);
}
}
SettingsToggle {
text: "System Resources"
description: "Display CPU and RAM usage indicators"
checked: Prefs.showSystemResources
onToggled: (checked) => Prefs.setShowSystemResources(checked)
onToggled: (checked) => {
return Prefs.setShowSystemResources(checked);
}
}
SettingsToggle {
text: "System Tray"
description: "Show system tray icons in the top bar"
checked: Prefs.showSystemTray
onToggled: (checked) => Prefs.setShowSystemTray(checked)
onToggled: (checked) => {
return Prefs.setShowSystemTray(checked);
}
}
}
}
// Display Settings
@@ -483,12 +510,11 @@ PanelWindow {
description: "Apply warm color temperature to reduce eye strain"
checked: Prefs.nightModeEnabled
onToggled: (checked) => {
Prefs.setNightModeEnabled(checked)
if (checked) {
nightModeEnableProcess.running = true
} else {
nightModeDisableProcess.running = true
}
Prefs.setNightModeEnabled(checked);
if (checked)
nightModeEnableProcess.running = true;
else
nightModeDisableProcess.running = true;
}
}
@@ -497,8 +523,8 @@ PanelWindow {
description: "Use light theme instead of dark theme"
checked: Prefs.isLightMode
onToggled: (checked) => {
Prefs.setLightMode(checked)
Theme.isLightMode = checked
Prefs.setLightMode(checked);
Theme.isLightMode = checked;
}
}
@@ -523,10 +549,9 @@ PanelWindow {
rightIcon: "circle"
unit: "%"
showValue: true
onSliderDragFinished: (finalValue) => {
let transparencyValue = finalValue / 100.0
Prefs.setTopBarTransparency(transparencyValue)
let transparencyValue = finalValue / 100;
Prefs.setTopBarTransparency(transparencyValue);
}
}
@@ -537,6 +562,7 @@ PanelWindow {
wrapMode: Text.WordWrap
width: parent.width
}
}
// Popup Transparency
@@ -560,10 +586,9 @@ PanelWindow {
rightIcon: "circle"
unit: "%"
showValue: true
onSliderDragFinished: (finalValue) => {
let transparencyValue = finalValue / 100.0
Prefs.setPopupTransparency(transparencyValue)
let transparencyValue = finalValue / 100;
Prefs.setPopupTransparency(transparencyValue);
}
}
@@ -574,6 +599,7 @@ PanelWindow {
wrapMode: Text.WordWrap
width: parent.width
}
}
// Theme Picker
@@ -591,48 +617,69 @@ PanelWindow {
ThemePicker {
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
// Add shadow effect
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowBlur: 1
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
}
// Night mode processes
Process {
id: nightModeEnableProcess
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Failed to enable night mode")
Prefs.setNightModeEnabled(false)
console.warn("Failed to enable night mode");
Prefs.setNightModeEnabled(false);
}
}
}
Process {
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Failed to disable night mode")
}
if (exitCode !== 0)
console.warn("Failed to disable night mode");
}
}
@@ -640,7 +687,7 @@ PanelWindow {
FocusScope {
anchors.fill: parent
focus: settingsPopup.settingsVisible
Keys.onEscapePressed: settingsPopup.settingsVisible = false
}
}

View File

@@ -31,6 +31,7 @@ Column {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
// Divider
@@ -43,6 +44,8 @@ Column {
// Content
Loader {
id: contentLoader
width: parent.width
}
}

View File

@@ -13,16 +13,7 @@ Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: toggleArea.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)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
color: toggleArea.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
@@ -51,30 +42,26 @@ Rectangle {
width: Math.min(implicitWidth, root.width - 120)
visible: root.description.length > 0
}
}
}
// Toggle switch
Rectangle {
id: toggle
width: 48
height: 24
radius: 12
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: root.checked ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
id: toggleHandle
width: 20
height: 20
radius: 10
@@ -87,6 +74,7 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
@@ -94,19 +82,39 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
id: toggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.checked = !root.checked
root.toggled(root.checked)
root.checked = !root.checked;
root.toggled(root.checked);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import qs.Services
Column {
id: themePicker
spacing: Theme.spacingS
Text {
@@ -17,22 +18,11 @@ Column {
// Theme description
Text {
text: {
if (Theme.isDynamicTheme) {
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"
]
return descriptions[Theme.currentThemeIndex] || "Select a theme"
if (Theme.isDynamicTheme)
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"];
return descriptions[Theme.currentThemeIndex] || "Select a theme";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
@@ -62,22 +52,7 @@ Column {
color: Theme.themes[index].primary
border.color: Theme.outline
border.width: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 2 : 1
scale: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 1.1 : 1.0
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
scale: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 1.1 : 1
// Theme name tooltip
Rectangle {
@@ -94,24 +69,46 @@ Column {
Text {
id: nameText
text: Theme.themes[index].name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.switchTheme(index, false)
Theme.switchTheme(index, false);
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
// Second row - Red, Cyan, Pink, Amber, Coral
@@ -124,6 +121,7 @@ Column {
Rectangle {
property int themeIndex: index + 5
width: 32
height: 32
radius: 16
@@ -131,22 +129,7 @@ Column {
border.color: Theme.outline
border.width: Theme.currentThemeIndex === themeIndex ? 2 : 1
visible: themeIndex < Theme.themes.length
scale: Theme.currentThemeIndex === themeIndex ? 1.1 : 1.0
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
scale: Theme.currentThemeIndex === themeIndex ? 1.1 : 1
// Theme name tooltip
Rectangle {
@@ -163,26 +146,48 @@ Column {
Text {
id: nameText2
text: themeIndex < Theme.themes.length ? Theme.themes[themeIndex].name : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea2
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (themeIndex < Theme.themes.length) {
Theme.switchTheme(themeIndex)
if (themeIndex < Theme.themes.length)
Theme.switchTheme(themeIndex);
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
// Spacer for better visual separation
@@ -197,26 +202,22 @@ Column {
height: 40
radius: 20
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
}
}
border.color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5)
} else if (Theme.isDynamicTheme) {
return Theme.primary
} else {
return Theme.outline
}
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5);
else if (Theme.isDynamicTheme)
return Theme.primary;
else
return Theme.outline;
}
border.width: Theme.isDynamicTheme ? 2 : 1
scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1)
Row {
anchors.centerIn: parent
@@ -224,14 +225,18 @@ Column {
Text {
text: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") return "error"
else return "palette"
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return "error";
else
return "palette";
}
font.family: Theme.iconFont
font.pixelSize: 16
color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") return Theme.error
else return Theme.surfaceText
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Theme.error;
else
return Theme.surfaceText;
}
font.weight: Theme.iconFontWeight
anchors.verticalCenter: parent.verticalCenter
@@ -239,51 +244,34 @@ Column {
Text {
text: {
if (ToastService.wallpaperErrorStatus === "error") return "Error"
else if (ToastService.wallpaperErrorStatus === "matugen_missing") return "No matugen"
else return "Auto"
if (ToastService.wallpaperErrorStatus === "error")
return "Error";
else if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "No matugen";
else
return "Auto";
}
font.pixelSize: Theme.fontSizeMedium
color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") return Theme.error
else return Theme.surfaceText
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Theme.error;
else
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1.0)
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
MouseArea {
id: autoMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.switchTheme(10, true)
Theme.switchTheme(10, true);
}
}
@@ -302,14 +290,14 @@ Column {
Text {
id: autoTooltipText
text: {
if (ToastService.wallpaperErrorStatus === "error") {
return "Wallpaper symlink missing at ~/quickshell/current_wallpaper"
} else if (ToastService.wallpaperErrorStatus === "matugen_missing") {
return "Install matugen package for dynamic themes"
} else {
return "Dynamic wallpaper-based colors"
}
if (ToastService.wallpaperErrorStatus === "error")
return "Wallpaper symlink missing at ~/quickshell/current_wallpaper";
else if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "Install matugen package for dynamic themes";
else
return "Dynamic wallpaper-based colors";
}
font.pixelSize: Theme.fontSizeSmall
color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceText
@@ -318,7 +306,35 @@ Column {
width: Math.min(implicitWidth, 250)
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
}
}

View File

@@ -1,8 +1,8 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
@@ -10,11 +10,9 @@ PanelWindow {
id: root
visible: ToastService.toastVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
@@ -24,80 +22,47 @@ PanelWindow {
bottom: true
}
// Makes the background transparent to mouse events
mask: Region {
item: toast
}
Rectangle {
id: toast
width: Math.min(400, Screen.width - Theme.spacingL * 2)
height: toastContent.height + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter
y: Theme.barHeight + Theme.spacingL
color: {
switch (ToastService.currentLevel) {
case ToastService.levelError: return Theme.error
case ToastService.levelWarn: return Theme.warning
case ToastService.levelInfo: return Theme.primary
default: return Theme.primary
case ToastService.levelError:
return Theme.error;
case ToastService.levelWarn:
return Theme.warning;
case ToastService.levelInfo:
return Theme.primary;
default:
return Theme.primary;
}
}
radius: Theme.cornerRadiusLarge
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
opacity: ToastService.toastVisible ? 0.9 : 0.0
scale: ToastService.toastVisible ? 1.0 : 0.9
transform: Translate {
y: ToastService.toastVisible ? 0 : -20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
opacity: ToastService.toastVisible ? 0.9 : 0
scale: ToastService.toastVisible ? 1 : 0.9
Row {
id: toastContent
anchors.centerIn: parent
spacing: Theme.spacingM
Text {
text: {
switch (ToastService.currentLevel) {
case ToastService.levelError: return "error"
case ToastService.levelWarn: return "warning"
case ToastService.levelInfo: return "info"
default: return "info"
case ToastService.levelError:
return "error";
case ToastService.levelWarn:
return "warning";
case ToastService.levelInfo:
return "info";
default:
return "info";
}
}
font.family: Theme.iconFont
@@ -115,11 +80,56 @@ PanelWindow {
width: Math.min(implicitWidth, 300)
elide: Text.ElideRight
}
}
MouseArea {
anchors.fill: parent
onClicked: ToastService.hideToast()
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
transform: Translate {
y: ToastService.toastVisible ? 0 : -20
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Makes the background transparent to mouse events
mask: Region {
item: toast
}
}

View File

@@ -8,7 +8,7 @@ import qs.Services
Item {
id: root
property list<real> audioLevels: [0, 0, 0, 0]
property var audioLevels: [0, 0, 0, 0]
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool hasActiveMedia: activePlayer !== null
property bool cavaAvailable: false
@@ -18,56 +18,62 @@ Item {
Process {
id: cavaCheck
command: ["which", "cava"]
running: true
onExited: (exitCode) => {
root.cavaAvailable = exitCode === 0
root.cavaAvailable = exitCode === 0;
if (root.cavaAvailable) {
console.log("cava found - enabling real audio visualization")
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
console.log("cava found - enabling real audio visualization");
cavaProcess.running = Qt.binding(() => {
return root.hasActiveMedia && root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing;
});
} else {
console.log("cava not found - using fallback animation")
fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
console.log("cava not found - using fallback animation");
fallbackTimer.running = Qt.binding(() => {
return root.hasActiveMedia && root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing;
});
}
}
}
Process {
id: cavaProcess
running: false
command: ["sh", "-c", `printf '[general]\nmode=normal\nframerate=30\nautosens=0\nsensitivity=50\nbars=4\n[output]\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nchannels=mono\nmono_option=average\n[smoothing]\nnoise_reduction=20' | cava -p /dev/stdin`]
onRunningChanged: {
if (!running)
root.audioLevels = [0, 0, 0, 0];
}
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
onRead: (data) => {
if (data.trim()) {
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p))
if (points.length >= 4) {
root.audioLevels = [points[0], points[1], points[2], points[3]]
}
let points = data.split(";").map((p) => {
return parseFloat(p.trim());
}).filter((p) => {
return !isNaN(p);
});
if (points.length >= 4)
root.audioLevels = [points[0], points[1], points[2], points[3]];
}
}
}
onRunningChanged: {
if (!running) {
root.audioLevels = [0, 0, 0, 0]
}
}
}
Timer {
id: fallbackTimer
running: false
interval: 100
repeat: true
onTriggered: {
root.audioLevels = [
Math.random() * 40 + 10,
Math.random() * 60 + 20,
Math.random() * 50 + 15,
Math.random() * 35 + 20
]
root.audioLevels = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20];
}
}
@@ -81,14 +87,14 @@ Item {
Rectangle {
width: 3
height: {
if (root.activePlayer?.playbackState === MprisPlaybackState.Playing && root.audioLevels.length > index) {
const rawLevel = root.audioLevels[index] || 0
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100
const maxHeight = Theme.iconSize - 2
const minHeight = 3
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight)
if (root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing && root.audioLevels.length > index) {
const rawLevel = root.audioLevels[index] || 0;
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
const maxHeight = Theme.iconSize - 2;
const minHeight = 3;
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
}
return 3
return 3;
}
radius: 1.5
color: Theme.primary
@@ -99,8 +105,13 @@ Item {
duration: 80
easing.type: Easing.OutQuad
}
}
}
}
}
}

View File

@@ -12,19 +12,14 @@ Rectangle {
width: clockRow.implicitWidth + Theme.spacingS * 2
height: 30
radius: Theme.cornerRadius
color: clockMouseArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
color: clockMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Component.onCompleted: {
root.currentDate = systemClock.date;
}
Row {
id: clockRow
anchors.centerIn: parent
spacing: Theme.spacingS
@@ -50,26 +45,33 @@ Rectangle {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
SystemClock {
id: systemClock
precision: SystemClock.Seconds
onDateChanged: root.currentDate = systemClock.date
}
Component.onCompleted: {
root.currentDate = systemClock.date
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clockClicked()
root.clockClicked();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -12,29 +12,35 @@ Rectangle {
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
height: 30
radius: Theme.cornerRadius
color: controlCenterArea.containsMouse || root.isActive ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: controlCenterArea.containsMouse || root.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
Row {
id: controlIndicators
anchors.centerIn: parent
spacing: Theme.spacingXS
// Network Status Icon
Text {
text: {
if (NetworkService.networkStatus === "ethernet") return "lan"
else if (NetworkService.networkStatus === "wifi") {
if (NetworkService.networkStatus === "ethernet") {
return "lan";
} else if (NetworkService.networkStatus === "wifi") {
switch (WifiService.wifiSignalStrength) {
case "excellent": return "wifi"
case "good": return "wifi_2_bar"
case "fair": return "wifi_1_bar"
case "poor": return "wifi_calling_3"
default: return "wifi"
case "excellent":
return "wifi";
case "good":
return "wifi_2_bar";
case "fair":
return "wifi_1_bar";
case "poor":
return "wifi_calling_3";
default:
return "wifi";
}
} else {
return "wifi_off";
}
else return "wifi_off"
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
@@ -64,39 +70,37 @@ Rectangle {
Text {
id: audioIcon
text: AudioService.sinkMuted ? "volume_off" :
AudioService.volumeLevel < 33 ? "volume_down" : "volume_up"
text: AudioService.sinkMuted ? "volume_off" : AudioService.volumeLevel < 33 ? "volume_down" : "volume_up"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ?
Theme.primary : Theme.surfaceText
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
// Scroll up - increase volume
// Scroll down - decrease volume
id: audioWheelArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y
let currentVolume = AudioService.volumeLevel
let newVolume
if (delta > 0) {
// Scroll up - increase volume
newVolume = Math.min(100, currentVolume + 5)
} else {
// Scroll down - decrease volume
newVolume = Math.max(0, currentVolume - 5)
let delta = wheelEvent.angleDelta.y;
let currentVolume = AudioService.volumeLevel;
let newVolume;
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5);
else
newVolume = Math.max(0, currentVolume - 5);
AudioService.setVolume(newVolume);
wheelEvent.accepted = true;
}
}
AudioService.setVolume(newVolume)
wheelEvent.accepted = true
}
}
}
// Microphone Icon (when active)
@@ -109,16 +113,17 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
visible: false // TODO: Add mic detection
}
}
MouseArea {
id: controlCenterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clicked()
root.clicked();
}
}
@@ -127,5 +132,7 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -8,33 +8,24 @@ Rectangle {
width: Math.max(contentRow.implicitWidth + Theme.spacingS * 2, 60)
height: 30
radius: Theme.cornerRadius
color: mouseArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
color: mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
clip: true
visible: FocusedWindowService.niriAvailable && (FocusedWindowService.focusedAppName || FocusedWindowService.focusedWindowTitle)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
Text {
id: appText
text: FocusedWindowService.focusedAppName || ""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
// Limit app name width
elide: Text.ElideRight
maximumLineCount: 1
@@ -51,25 +42,35 @@ Rectangle {
Text {
id: titleText
text: FocusedWindowService.focusedWindowTitle || ""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
// Limit title width - increased for longer titles
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, 350)
}
}
MouseArea {
// Non-interactive widget - just provides hover state for visual feedback
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
// Non-interactive widget - just provides hover state for visual feedback
}
// Smooth width animation when the text changes
@@ -78,5 +79,7 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -21,12 +21,12 @@ Rectangle {
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
LauncherService.toggleAppLauncher()
LauncherService.toggleAppLauncher();
}
}
@@ -35,5 +35,7 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -39,6 +39,37 @@ Rectangle {
}
]
transitions: [
Transition {
from: "shown"
to: "hidden"
SequentialAnimation {
PauseAnimation {
duration: 500
}
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
},
Transition {
from: "hidden"
to: "shown"
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
]
Row {
id: mediaRow
@@ -64,9 +95,8 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 140
text: {
if (!activePlayer || !activePlayer.trackTitle) {
if (!activePlayer || !activePlayer.trackTitle)
return "";
}
let identity = activePlayer.identity || "";
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari");
@@ -209,38 +239,6 @@ Rectangle {
}
transitions: [
Transition {
from: "shown"
to: "hidden"
SequentialAnimation {
PauseAnimation {
duration: 500
}
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
},
Transition {
from: "hidden"
to: "shown"
NumberAnimation {
properties: "opacity,width"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
]
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration

View File

@@ -6,14 +6,13 @@ Rectangle {
property bool hasUnread: false
property bool isActive: false
signal clicked()
width: 40
height: 30
radius: Theme.cornerRadius
color: notificationArea.containsMouse || root.isActive ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: notificationArea.containsMouse || root.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
Text {
anchors.centerIn: parent
@@ -21,8 +20,7 @@ Rectangle {
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: notificationArea.containsMouse || root.isActive ?
Theme.primary : Theme.surfaceText
color: notificationArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
}
// Notification dot indicator
@@ -40,12 +38,12 @@ Rectangle {
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clicked()
root.clicked();
}
}
@@ -54,5 +52,7 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -15,25 +15,27 @@ Rectangle {
Row {
id: systemTrayRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: SystemTray.items
delegate: Rectangle {
property var trayItem: modelData
width: 24
height: 24
radius: Theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
property var trayItem: modelData
Image {
anchors.centerIn: parent
width: 18
height: 18
source: {
let icon = trayItem?.icon;
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
@@ -51,22 +53,23 @@ Rectangle {
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem) return;
if (!trayItem)
return ;
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu) {
trayItem.activate()
}
if (!trayItem.onlyMenu)
trayItem.activate();
} else if (mouse.button === Qt.RightButton) {
if (trayItem.hasMenu) {
customTrayMenu.showMenu(mouse.x, mouse.y)
}
if (trayItem.hasMenu)
customTrayMenu.showMenu(mouse.x, mouse.y);
}
}
}
@@ -77,13 +80,14 @@ Rectangle {
property bool menuVisible: false
function showMenu(x, y) {
root.menuRequested(customTrayMenu, trayItem, x, y)
menuVisible = true
root.menuRequested(customTrayMenu, trayItem, x, y);
menuVisible = true;
}
function hideMenu() {
menuVisible = false
menuVisible = false;
}
}
Behavior on color {
@@ -91,8 +95,13 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}

View File

@@ -1,61 +1,54 @@
import "../../Common/Utilities.js" as Utils
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Services.Notifications
import Quickshell.Services.Mpris
import Quickshell.Services.Notifications
import Quickshell.Services.SystemTray
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
import "../../Common/Utilities.js" as Utils
PanelWindow {
// Proxy objects for external connections
id: root
property var modelData
screen: modelData
property string screenName: modelData.name
// Transparency property for the top bar background
property real backgroundTransparency: Prefs.topBarTransparency
Connections {
target: Prefs
function onTopBarTransparencyChanged() {
root.backgroundTransparency = Prefs.topBarTransparency
}
}
// Notification properties
readonly property int notificationCount: NotificationService.notifications.length
screen: modelData
implicitHeight: Theme.barHeight - 4
color: "transparent"
Connections {
function onTopBarTransparencyChanged() {
root.backgroundTransparency = Prefs.topBarTransparency;
}
// Proxy objects for external connections
target: Prefs
}
QtObject {
id: notificationHistory
property int count: 0
}
anchors {
top: true
left: true
right: true
}
implicitHeight: Theme.barHeight - 4
color: "transparent"
// Floating panel container with margins
Item {
anchors.fill: parent
@@ -69,16 +62,7 @@ PanelWindow {
anchors.fill: parent
radius: Theme.cornerRadiusXLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, root.backgroundTransparency)
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5 // radius/32, adjusted for visual match
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
Rectangle {
anchors.fill: parent
@@ -96,18 +80,32 @@ PanelWindow {
SequentialAnimation on opacity {
running: false
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
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5 // radius/32, adjusted for visual match
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
}
Item {
@@ -120,6 +118,7 @@ PanelWindow {
Row {
id: leftSection
height: parent.height
spacing: Theme.spacingXS
anchors.left: parent.left
@@ -138,14 +137,15 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
visible: Prefs.showFocusedWindow
}
}
ClockWidget {
id: clockWidget
anchors.centerIn: parent
anchors.centerIn: parent
onClockClicked: {
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible;
}
}
@@ -154,27 +154,26 @@ PanelWindow {
anchors.right: clockWidget.left
anchors.rightMargin: Theme.spacingS
visible: Prefs.showMusic && MprisController.activePlayer
onClicked: {
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible;
}
}
WeatherWidget {
id: weatherWidget
anchors.verticalCenter: parent.verticalCenter
anchors.left: clockWidget.right
anchors.leftMargin: Theme.spacingS
visible: Prefs.showWeather && WeatherService.weather.available && WeatherService.weather.temp > 0 && WeatherService.weather.tempF > 0
onClicked: {
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible;
}
}
Row {
id: rightSection
height: parent.height
spacing: Theme.spacingXS
anchors.right: parent.right
@@ -184,12 +183,12 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
visible: Prefs.showSystemTray
onMenuRequested: (menu, item, x, y) => {
trayMenuPopup.currentTrayMenu = menu
trayMenuPopup.currentTrayItem = item
trayMenuPopup.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL
trayMenuPopup.trayMenuY = Theme.barHeight - Theme.spacingXS
trayMenuPopup.showTrayMenu = true
menu.menuVisible = true
trayMenuPopup.currentTrayMenu = menu;
trayMenuPopup.currentTrayItem = item;
trayMenuPopup.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL;
trayMenuPopup.trayMenuY = Theme.barHeight - Theme.spacingXS;
trayMenuPopup.showTrayMenu = true;
menu.menuVisible = true;
}
}
@@ -212,12 +211,12 @@ PanelWindow {
MouseArea {
id: clipboardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
clipboardHistoryPopup.toggle()
clipboardHistoryPopup.toggle();
}
}
@@ -226,7 +225,9 @@ PanelWindow {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// System Monitor Widgets
@@ -245,7 +246,7 @@ PanelWindow {
hasUnread: root.notificationCount > 0
isActive: notificationCenter.notificationHistoryVisible
onClicked: {
notificationCenter.notificationHistoryVisible = !notificationCenter.notificationHistoryVisible
notificationCenter.notificationHistoryVisible = !notificationCenter.notificationHistoryVisible;
}
}
@@ -254,25 +255,29 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
batteryPopupVisible: batteryControlPopup.batteryPopupVisible
onToggleBatteryPopup: {
batteryControlPopup.batteryPopupVisible = !batteryControlPopup.batteryPopupVisible
batteryControlPopup.batteryPopupVisible = !batteryControlPopup.batteryPopupVisible;
}
}
ControlCenterButton {
// Bluetooth devices are automatically updated via signals
anchors.verticalCenter: parent.verticalCenter
isActive: controlCenterPopup.controlCenterVisible
onClicked: {
controlCenterPopup.controlCenterVisible = !controlCenterPopup.controlCenterVisible
controlCenterPopup.controlCenterVisible = !controlCenterPopup.controlCenterVisible;
if (controlCenterPopup.controlCenterVisible) {
if (NetworkService.wifiEnabled) {
WifiService.scanWifi()
}
// Bluetooth devices are automatically updated via signals
if (NetworkService.wifiEnabled)
WifiService.scanWifi();
}
}
}
}
}
}
}

View File

@@ -5,33 +5,17 @@ import qs.Services
Rectangle {
id: root
signal clicked()
// Visibility is now controlled by TopBar.qml
width: visible ? Math.min(100, weatherRow.implicitWidth + Theme.spacingS * 2) : 0
height: 30
radius: Theme.cornerRadius
color: weatherArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
color: weatherArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Row {
id: weatherRow
anchors.centerIn: parent
spacing: Theme.spacingXS
@@ -50,14 +34,32 @@ Rectangle {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: weatherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -7,6 +7,41 @@ Rectangle {
id: root
property string screenName: ""
property int currentWorkspace: getDisplayActiveWorkspace()
property var workspaceList: getDisplayWorkspaces()
function getDisplayWorkspaces() {
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0)
return [1, 2];
if (!root.screenName)
return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers();
var displayWorkspaces = [];
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i];
if (ws.output === root.screenName)
displayWorkspaces.push(ws.idx + 1);
}
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2];
}
function getDisplayActiveWorkspace() {
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0)
return 1;
if (!root.screenName)
return NiriWorkspaceService.getCurrentWorkspaceNumber();
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i];
if (ws.output === root.screenName && ws.is_active)
return ws.idx + 1;
}
return 1;
}
width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
height: 30
@@ -14,67 +49,29 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
visible: NiriWorkspaceService.niriAvailable
property int currentWorkspace: getDisplayActiveWorkspace()
property var workspaceList: getDisplayWorkspaces()
function getDisplayWorkspaces() {
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
return [1, 2]
}
if (!root.screenName) {
return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers()
}
var displayWorkspaces = []
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i]
if (ws.output === root.screenName) {
displayWorkspaces.push(ws.idx + 1)
}
}
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
}
function getDisplayActiveWorkspace() {
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
return 1
}
if (!root.screenName) {
return NiriWorkspaceService.getCurrentWorkspaceNumber()
}
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
var ws = NiriWorkspaceService.allWorkspaces[i]
if (ws.output === root.screenName && ws.is_active) {
return ws.idx + 1
}
}
return 1
}
Connections {
target: NiriWorkspaceService
function onAllWorkspacesChanged() {
root.workspaceList = root.getDisplayWorkspaces()
root.currentWorkspace = root.getDisplayActiveWorkspace()
root.workspaceList = root.getDisplayWorkspaces();
root.currentWorkspace = root.getDisplayActiveWorkspace();
}
function onFocusedWorkspaceIndexChanged() {
root.currentWorkspace = root.getDisplayActiveWorkspace()
root.currentWorkspace = root.getDisplayActiveWorkspace();
}
function onNiriAvailableChanged() {
if (NiriWorkspaceService.niriAvailable) {
root.workspaceList = root.getDisplayWorkspaces()
root.currentWorkspace = root.getDisplayActiveWorkspace()
root.workspaceList = root.getDisplayWorkspaces();
root.currentWorkspace = root.getDisplayActiveWorkspace();
}
}
target: NiriWorkspaceService
}
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
@@ -89,15 +86,25 @@ Rectangle {
width: isActive ? Theme.spacingXL + Theme.spacingM : Theme.spacingL + Theme.spacingXS
height: Theme.spacingM
radius: height / 2
color: isActive ? Theme.primary :
isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
color: isActive ? Theme.primary : isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", sequentialNumber.toString()]);
}
}
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
@@ -105,19 +112,13 @@ Rectangle {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
}
onClicked: {
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", sequentialNumber.toString()])
}
}
}
}
}
}

View File

@@ -1,8 +1,8 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
@@ -14,6 +14,16 @@ PanelWindow {
property string wifiPasswordInput: ""
visible: wifiPasswordDialogVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: wifiPasswordDialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
onVisibleChanged: {
if (visible)
passwordInput.forceActiveFocus();
}
anchors {
top: true
left: true
@@ -21,37 +31,27 @@ PanelWindow {
bottom: true
}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: wifiPasswordDialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
onVisibleChanged: {
if (visible) {
passwordInput.forceActiveFocus()
}
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: wifiPasswordDialogVisible ? 1.0 : 0.0
opacity: wifiPasswordDialogVisible ? 1 : 0
MouseArea {
anchors.fill: parent
onClicked: {
wifiPasswordDialogVisible = false;
wifiPasswordInput = "";
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
MouseArea {
anchors.fill: parent
onClicked: {
wifiPasswordDialogVisible = false
wifiPasswordInput = ""
}
}
}
Rectangle {
@@ -62,23 +62,8 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: wifiPasswordDialogVisible ? 1.0 : 0.0
scale: wifiPasswordDialogVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: wifiPasswordDialogVisible ? 1 : 0
scale: wifiPasswordDialogVisible ? 1 : 0.9
Column {
anchors.fill: parent
@@ -107,6 +92,7 @@ PanelWindow {
width: parent.width
elide: Text.ElideRight
}
}
Rectangle {
@@ -125,15 +111,18 @@ PanelWindow {
MouseArea {
id: closeDialogArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiPasswordDialogVisible = false
wifiPasswordInput = ""
wifiPasswordDialogVisible = false;
wifiPasswordInput = "";
}
}
}
}
// Password input
@@ -147,6 +136,7 @@ PanelWindow {
TextInput {
id: passwordInput
anchors.fill: parent
anchors.margins: Theme.spacingM
font.pixelSize: Theme.fontSizeMedium
@@ -155,6 +145,17 @@ PanelWindow {
verticalAlignment: TextInput.AlignVCenter
cursorVisible: activeFocus
selectByMouse: true
onTextChanged: {
wifiPasswordInput = text;
}
onAccepted: {
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput);
}
Component.onCompleted: {
if (wifiPasswordDialogVisible)
forceActiveFocus();
}
Text {
anchors.fill: parent
@@ -165,28 +166,16 @@ PanelWindow {
visible: parent.text.length === 0
}
onTextChanged: {
wifiPasswordInput = text
}
onAccepted: {
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput)
}
Component.onCompleted: {
if (wifiPasswordDialogVisible) {
forceActiveFocus()
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: {
passwordInput.forceActiveFocus()
passwordInput.forceActiveFocus();
}
}
}
// Show password checkbox
@@ -195,6 +184,7 @@ PanelWindow {
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
@@ -218,9 +208,10 @@ PanelWindow {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
}
}
}
Text {
@@ -229,6 +220,7 @@ PanelWindow {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
// Buttons
@@ -251,6 +243,7 @@ PanelWindow {
Text {
id: cancelText
anchors.centerIn: parent
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
@@ -260,14 +253,16 @@ PanelWindow {
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiPasswordDialogVisible = false
wifiPasswordInput = ""
wifiPasswordDialogVisible = false;
wifiPasswordInput = "";
}
}
}
Rectangle {
@@ -276,10 +271,11 @@ PanelWindow {
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: wifiPasswordInput.length > 0
opacity: enabled ? 1.0 : 0.5
opacity: enabled ? 1 : 0.5
Text {
id: connectText
anchors.centerIn: parent
text: "Connect"
font.pixelSize: Theme.fontSizeMedium
@@ -289,12 +285,13 @@ PanelWindow {
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput)
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput);
}
}
@@ -303,10 +300,33 @@ PanelWindow {
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

@@ -12,36 +12,46 @@ ShellRoot {
// Multi-monitor support using Variants
Variants {
model: Quickshell.screens
delegate: TopBar {
modelData: item
}
}
// Global popup windows
CenterCommandCenter {
id: centerCommandCenter
}
TrayMenuPopup {
id: trayMenuPopup
}
NotificationCenter {
id: notificationCenter
}
ControlCenterPopup {
id: controlCenterPopup
}
WifiPasswordDialog {
id: wifiPasswordDialog
}
InputDialog {
id: globalInputDialog
}
BatteryControlPopup {
id: batteryControlPopup
}
PowerMenuPopup {
id: powerMenuPopup
}
PowerConfirmDialog {
id: powerConfirmDialog
}