1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 16:02: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 # Or use the shorthand
qs -p . 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 ## Architecture Overview
@@ -119,13 +123,19 @@ shell.qml # Main entry point (minimal orchestration)
- `id` should be the first property - `id` should be the first property
- Properties before signal handlers before child components - Properties before signal handlers before child components
- Prefer property bindings over imperative code - 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**: 2. **Naming Conventions**:
- **Services**: Use `Singleton` type with `id: root` - **Services**: Use `Singleton` type with `id: root`
- **Components**: Use descriptive names (e.g., `CustomSlider`, `TopBar`) - **Components**: Use descriptive names (e.g., `CustomSlider`, `TopBar`)
- **Properties**: camelCase for properties, PascalCase for types - **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 ```qml
// For regular components // For regular components
Item { Item {
@@ -230,11 +240,12 @@ The shell uses Quickshell's `Variants` pattern for multi-monitor support:
When modifying the shell: When modifying the shell:
1. **Test changes**: `qs -p .` (automatic reload on file changes) 1. **Test changes**: `qs -p .` (automatic reload on file changes)
2. **Performance**: Ensure animations remain smooth (60 FPS target) 2. **Code quality**: Run `qmlformat -i **/*.qml` and `qmllint **/*.qml` to ensure proper formatting and syntax
3. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency 3. **Performance**: Ensure animations remain smooth (60 FPS target)
4. **Wayland compatibility**: Test on Wayland session 4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
5. **Multi-monitor**: Verify behavior with multiple displays 5. **Wayland compatibility**: Test on Wayland session
6. **Feature detection**: Test on systems with/without required tools 6. **Multi-monitor**: Verify behavior with multiple displays
7. **Feature detection**: Test on systems with/without required tools
### Adding New Widgets ### Adding New Widgets

View File

@@ -1,180 +1,181 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
import Qt.labs.platform
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Qt.labs.platform
import qs.Services import qs.Services
Singleton { Singleton {
// This dependency forces re-evaluation when colorUpdateTrigger changes
// Just check if matugen is available
id: root id: root
/* ──────────────── basic state ──────────────── */
signal colorsUpdated()
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation) readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
readonly property string homeDir: _homeUrl.startsWith("file://") readonly property string homeDir: _homeUrl.startsWith("file://") ? _homeUrl.substring(7) : _homeUrl
? _homeUrl.substring(7)
: _homeUrl
readonly property string wallpaperPath: homeDir + "/quickshell/current_wallpaper" readonly property string wallpaperPath: homeDir + "/quickshell/current_wallpaper"
readonly property string notifyPath: homeDir + "/quickshell/wallpaper_changed" readonly property string notifyPath: homeDir + "/quickshell/wallpaper_changed"
property bool matugenAvailable: false
property string matugenJson: ""
property var matugenColors: ({
})
property bool extractionRequested: false
property int colorUpdateTrigger: 0 // Force property re-evaluation
// ──────────────── wallpaper change monitor ────────────────
property string lastWallpaperTimestamp: ""
// ──────────────── 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
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
property color surfaceText: getMatugenColor("on_background", "#e3e8ef")
property color primaryText: getMatugenColor("on_primary", "#ffffff")
property color surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5")
// containers & misc
property color primaryContainer: getMatugenColor("primary_container", "#1976d2")
property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8")
property color outline: getMatugenColor("outline", "#8e918f")
// legacy aliases
property color accentHi: primary
property color accentLo: secondary
property bool matugenAvailable: false // ──────────────── basic state ────────────────
property string matugenJson: "" signal colorsUpdated()
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() { function onLightModeChanged() {
// Force color properties to update when light mode changes // Force color properties to update when light mode changes
if (matugenColors && Object.keys(matugenColors).length > 0) { if (matugenColors && Object.keys(matugenColors).length > 0) {
console.log("Light mode changed - updating dynamic colors") console.log("Light mode changed - updating dynamic colors");
colorUpdateTrigger++ // This will trigger re-evaluation of all color properties colorUpdateTrigger++; // This will trigger re-evaluation of all color properties
colorsUpdated() colorsUpdated();
} }
} }
/* ──────────────── availability checks ──────────────── */ // ──────────────── public helper ────────────────
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 ──────────────── */
property string lastWallpaperTimestamp: ""
/* ──────────────── public helper ──────────────── */
function extractColors() { function extractColors() {
console.log("Colors.extractColors() called, matugenAvailable:", matugenAvailable) console.log("Colors.extractColors() called, matugenAvailable:", matugenAvailable);
extractionRequested = true extractionRequested = true;
if (matugenAvailable) if (matugenAvailable)
fileChecker.running = true fileChecker.running = true;
else else
matugenCheck.running = true matugenCheck.running = true;
} }
function getMatugenColor(path, fallback) { function getMatugenColor(path, fallback) {
// Include colorUpdateTrigger in the function to make properties reactive to changes // Include colorUpdateTrigger in the function to make properties reactive to changes
colorUpdateTrigger // This dependency forces re-evaluation when colorUpdateTrigger changes colorUpdateTrigger;
// Use light or dark colors based on Theme.isLightMode // Use light or dark colors based on Theme.isLightMode
const colorMode = (typeof Theme !== "undefined" && Theme.isLightMode) ? "light" : "dark" const colorMode = (typeof Theme !== "undefined" && Theme.isLightMode) ? "light" : "dark";
let cur = matugenColors?.colors?.[colorMode] let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode];
for (const part of path.split(".")) { for (const part of path.split(".")) {
if (!cur || typeof cur !== "object" || !(part in cur)) if (!cur || typeof cur !== "object" || !(part in cur))
return fallback return fallback;
cur = cur[part]
cur = cur[part];
} }
return cur || fallback return cur || fallback;
} }
/* ──────────────── 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 */
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 */
property color surfaceText: getMatugenColor("on_background", "#e3e8ef")
property color primaryText: getMatugenColor("on_primary", "#ffffff")
property color surfaceVariantText: getMatugenColor("on_surface_variant", "#c4c7c5")
/* containers & misc */
property color primaryContainer: getMatugenColor("primary_container", "#1976d2")
property color surfaceTint: getMatugenColor("surface_tint", "#8ab4f8")
property color outline: getMatugenColor("outline", "#8e918f")
/* legacy aliases */
property color accentHi: primary
property color accentLo: secondary
function isColorDark(c) { function isColorDark(c) {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5 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 pragma Singleton
import QtQuick pragma ComponentBehavior: Bound
import QtCore import QtCore
import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
Singleton { Singleton {
// "auto", "wifi", "ethernet"
// Alphabetical tiebreaker
id: root id: root
property int themeIndex: 0 property int themeIndex: 0
@@ -13,14 +18,12 @@ Singleton {
property real topBarTransparency: 0.75 property real topBarTransparency: 0.75
property real popupTransparency: 0.92 property real popupTransparency: 0.92
property var recentlyUsedApps: [] property var recentlyUsedApps: []
// New global preferences // New global preferences
property bool use24HourClock: true property bool use24HourClock: true
property bool useFahrenheit: false property bool useFahrenheit: false
property bool nightModeEnabled: false property bool nightModeEnabled: false
property string profileImage: "" property string profileImage: ""
property string weatherLocationOverride: "New York, NY" property string weatherLocationOverride: "New York, NY"
// Widget visibility preferences for TopBar // Widget visibility preferences for TopBar
property bool showFocusedWindow: true property bool showFocusedWindow: true
property bool showWeather: true property bool showWeather: true
@@ -28,294 +31,284 @@ Singleton {
property bool showClipboard: true property bool showClipboard: true
property bool showSystemResources: true property bool showSystemResources: true
property bool showSystemTray: true property bool showSystemTray: true
// View mode preferences for launchers // View mode preferences for launchers
property string appLauncherViewMode: "list" property string appLauncherViewMode: "list"
property string spotlightLauncherViewMode: "list" property string spotlightLauncherViewMode: "list"
// Network preference // Network preference
property string networkPreference: "auto" // "auto", "wifi", "ethernet" property string networkPreference: "auto"
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()
}
}
function loadSettings() { function loadSettings() {
parseSettings(settingsFile.text()) parseSettings(settingsFile.text());
} }
function parseSettings(content) { function parseSettings(content) {
try { try {
if (content && content.trim()) { if (content && content.trim()) {
var settings = JSON.parse(content) var settings = JSON.parse(content);
themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0 themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0;
themeIsDynamic = settings.themeIsDynamic !== undefined ? settings.themeIsDynamic : false themeIsDynamic = settings.themeIsDynamic !== undefined ? settings.themeIsDynamic : false;
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false;
topBarTransparency = settings.topBarTransparency !== undefined ? topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 0.75;
(settings.topBarTransparency > 1 ? settings.topBarTransparency / 100.0 : settings.topBarTransparency) : 0.75 popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 0.92;
popupTransparency = settings.popupTransparency !== undefined ? recentlyUsedApps = settings.recentlyUsedApps || [];
(settings.popupTransparency > 1 ? settings.popupTransparency / 100.0 : settings.popupTransparency) : 0.92 use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
recentlyUsedApps = settings.recentlyUsedApps || [] useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false profileImage = settings.profileImage !== undefined ? settings.profileImage : "";
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false weatherLocationOverride = settings.weatherLocationOverride !== undefined ? settings.weatherLocationOverride : "New York, NY";
profileImage = settings.profileImage !== undefined ? settings.profileImage : "" showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true;
weatherLocationOverride = settings.weatherLocationOverride !== undefined ? settings.weatherLocationOverride : "New York, NY" showWeather = settings.showWeather !== undefined ? settings.showWeather : true;
showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true showMusic = settings.showMusic !== undefined ? settings.showMusic : true;
showWeather = settings.showWeather !== undefined ? settings.showWeather : true showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true;
showMusic = settings.showMusic !== undefined ? settings.showMusic : true showSystemResources = settings.showSystemResources !== undefined ? settings.showSystemResources : true;
showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true;
showSystemResources = settings.showSystemResources !== undefined ? settings.showSystemResources : true appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list";
showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true spotlightLauncherViewMode = settings.spotlightLauncherViewMode !== undefined ? settings.spotlightLauncherViewMode : "list";
appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list" networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto";
spotlightLauncherViewMode = settings.spotlightLauncherViewMode !== undefined ? settings.spotlightLauncherViewMode : "list" console.log("Loaded settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length);
networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto" applyStoredTheme();
console.log("Loaded settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length)
applyStoredTheme()
} else { } else {
console.log("Settings file is empty - applying default theme") console.log("Settings file is empty - applying default theme");
applyStoredTheme() applyStoredTheme();
} }
} catch (e) { } catch (e) {
console.log("Could not parse settings, using defaults:", e) console.log("Could not parse settings, using defaults:", e);
applyStoredTheme() applyStoredTheme();
} }
} }
function saveSettings() { function saveSettings() {
settingsFile.setText(JSON.stringify({ settingsFile.setText(JSON.stringify({
themeIndex, "themeIndex": themeIndex,
themeIsDynamic, "themeIsDynamic": themeIsDynamic,
isLightMode, "isLightMode": isLightMode,
topBarTransparency, "topBarTransparency": topBarTransparency,
popupTransparency, "popupTransparency": popupTransparency,
recentlyUsedApps, "recentlyUsedApps": recentlyUsedApps,
use24HourClock, "use24HourClock": use24HourClock,
useFahrenheit, "useFahrenheit": useFahrenheit,
nightModeEnabled, "nightModeEnabled": nightModeEnabled,
profileImage, "profileImage": profileImage,
weatherLocationOverride, "weatherLocationOverride": weatherLocationOverride,
showFocusedWindow, "showFocusedWindow": showFocusedWindow,
showWeather, "showWeather": showWeather,
showMusic, "showMusic": showMusic,
showClipboard, "showClipboard": showClipboard,
showSystemResources, "showSystemResources": showSystemResources,
showSystemTray, "showSystemTray": showSystemTray,
appLauncherViewMode, "appLauncherViewMode": appLauncherViewMode,
spotlightLauncherViewMode, "spotlightLauncherViewMode": spotlightLauncherViewMode,
networkPreference "networkPreference": networkPreference
}, null, 2)) }, null, 2));
console.log("Saving settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length) console.log("Saving settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length);
} }
function applyStoredTheme() { function applyStoredTheme() {
console.log("Applying stored theme:", themeIndex, themeIsDynamic, "lightMode:", isLightMode) console.log("Applying stored theme:", themeIndex, themeIsDynamic, "lightMode:", isLightMode);
if (typeof Theme !== "undefined") { if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode Theme.isLightMode = isLightMode;
Theme.switchTheme(themeIndex, themeIsDynamic, false) Theme.switchTheme(themeIndex, themeIsDynamic, false);
} else { } else {
Qt.callLater(() => { Qt.callLater(() => {
if (typeof Theme !== "undefined") { if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode Theme.isLightMode = isLightMode;
Theme.switchTheme(themeIndex, themeIsDynamic, false) Theme.switchTheme(themeIndex, themeIsDynamic, false);
} }
}) });
} }
} }
function setTheme(index, isDynamic) { function setTheme(index, isDynamic) {
console.log("Prefs setTheme called - themeIndex:", index, "isDynamic:", isDynamic) console.log("Prefs setTheme called - themeIndex:", index, "isDynamic:", isDynamic);
themeIndex = index themeIndex = index;
themeIsDynamic = isDynamic themeIsDynamic = isDynamic;
saveSettings() saveSettings();
} }
function setLightMode(lightMode) { function setLightMode(lightMode) {
console.log("Prefs setLightMode called - isLightMode:", lightMode) console.log("Prefs setLightMode called - isLightMode:", lightMode);
isLightMode = lightMode isLightMode = lightMode;
saveSettings() saveSettings();
} }
function setTopBarTransparency(transparency) { function setTopBarTransparency(transparency) {
console.log("Prefs setTopBarTransparency called - topBarTransparency:", transparency) console.log("Prefs setTopBarTransparency called - topBarTransparency:", transparency);
topBarTransparency = transparency topBarTransparency = transparency;
saveSettings() saveSettings();
} }
function setPopupTransparency(transparency) { function setPopupTransparency(transparency) {
console.log("Prefs setPopupTransparency called - popupTransparency:", transparency) console.log("Prefs setPopupTransparency called - popupTransparency:", transparency);
popupTransparency = transparency popupTransparency = transparency;
saveSettings() saveSettings();
} }
function addRecentApp(app) { function addRecentApp(app) {
if (!app) return if (!app)
return ;
var execProp = app.execString || app.exec || "" var execProp = app.execString || app.exec || "";
if (!execProp) return if (!execProp)
return ;
var existingIndex = -1 var existingIndex = -1;
for (var i = 0; i < recentlyUsedApps.length; i++) { for (var i = 0; i < recentlyUsedApps.length; i++) {
if (recentlyUsedApps[i].exec === execProp) { if (recentlyUsedApps[i].exec === execProp) {
existingIndex = i existingIndex = i;
break break;
} }
} }
if (existingIndex >= 0) { if (existingIndex >= 0) {
// App exists, increment usage count // App exists, increment usage count
recentlyUsedApps[existingIndex].usageCount = (recentlyUsedApps[existingIndex].usageCount || 1) + 1 recentlyUsedApps[existingIndex].usageCount = (recentlyUsedApps[existingIndex].usageCount || 1) + 1;
recentlyUsedApps[existingIndex].lastUsed = Date.now() recentlyUsedApps[existingIndex].lastUsed = Date.now();
} else { } else {
// New app, create entry // New app, create entry
var appData = { var appData = {
name: app.name || "", "name": app.name || "",
exec: execProp, "exec": execProp,
icon: app.icon || "application-x-executable", "icon": app.icon || "application-x-executable",
comment: app.comment || "", "comment": app.comment || "",
usageCount: 1, "usageCount": 1,
lastUsed: Date.now() "lastUsed": Date.now()
} };
recentlyUsedApps.push(appData) recentlyUsedApps.push(appData);
} }
// Sort by usage count (descending), then alphabetically by name // Sort by usage count (descending), then alphabetically by name
var sortedApps = recentlyUsedApps.sort(function(a, b) { var sortedApps = recentlyUsedApps.sort(function(a, b) {
if (a.usageCount !== b.usageCount) { if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount // Higher usage count first return b.usageCount - a.usageCount;
}
return a.name.localeCompare(b.name) // Alphabetical tiebreaker
})
// Higher usage count first
return a.name.localeCompare(b.name);
});
// Limit to 10 apps // Limit to 10 apps
if (sortedApps.length > 10) { if (sortedApps.length > 10)
sortedApps = sortedApps.slice(0, 10) sortedApps = sortedApps.slice(0, 10);
}
// Reassign to trigger property change signal // Reassign to trigger property change signal
recentlyUsedApps = sortedApps recentlyUsedApps = sortedApps;
saveSettings();
saveSettings()
} }
function getRecentApps() { function getRecentApps() {
return recentlyUsedApps return recentlyUsedApps;
} }
// New preference setters // New preference setters
function setClockFormat(use24Hour) { function setClockFormat(use24Hour) {
console.log("Prefs setClockFormat called - use24HourClock:", use24Hour) console.log("Prefs setClockFormat called - use24HourClock:", use24Hour);
use24HourClock = use24Hour use24HourClock = use24Hour;
saveSettings() saveSettings();
} }
function setTemperatureUnit(fahrenheit) { function setTemperatureUnit(fahrenheit) {
console.log("Prefs setTemperatureUnit called - useFahrenheit:", fahrenheit) console.log("Prefs setTemperatureUnit called - useFahrenheit:", fahrenheit);
useFahrenheit = fahrenheit useFahrenheit = fahrenheit;
saveSettings() saveSettings();
} }
function setNightModeEnabled(enabled) { function setNightModeEnabled(enabled) {
console.log("Prefs setNightModeEnabled called - nightModeEnabled:", enabled) console.log("Prefs setNightModeEnabled called - nightModeEnabled:", enabled);
nightModeEnabled = enabled nightModeEnabled = enabled;
saveSettings() saveSettings();
} }
function setProfileImage(imageUrl) { function setProfileImage(imageUrl) {
console.log("Prefs setProfileImage called - profileImage:", imageUrl) console.log("Prefs setProfileImage called - profileImage:", imageUrl);
profileImage = imageUrl profileImage = imageUrl;
saveSettings() saveSettings();
} }
// Widget visibility setters // Widget visibility setters
function setShowFocusedWindow(enabled) { function setShowFocusedWindow(enabled) {
console.log("Prefs setShowFocusedWindow called - showFocusedWindow:", enabled) console.log("Prefs setShowFocusedWindow called - showFocusedWindow:", enabled);
showFocusedWindow = enabled showFocusedWindow = enabled;
saveSettings() saveSettings();
} }
function setShowWeather(enabled) { function setShowWeather(enabled) {
console.log("Prefs setShowWeather called - showWeather:", enabled) console.log("Prefs setShowWeather called - showWeather:", enabled);
showWeather = enabled showWeather = enabled;
saveSettings() saveSettings();
} }
function setShowMusic(enabled) { function setShowMusic(enabled) {
console.log("Prefs setShowMusic called - showMusic:", enabled) console.log("Prefs setShowMusic called - showMusic:", enabled);
showMusic = enabled showMusic = enabled;
saveSettings() saveSettings();
} }
function setShowClipboard(enabled) { function setShowClipboard(enabled) {
console.log("Prefs setShowClipboard called - showClipboard:", enabled) console.log("Prefs setShowClipboard called - showClipboard:", enabled);
showClipboard = enabled showClipboard = enabled;
saveSettings() saveSettings();
} }
function setShowSystemResources(enabled) { function setShowSystemResources(enabled) {
console.log("Prefs setShowSystemResources called - showSystemResources:", enabled) console.log("Prefs setShowSystemResources called - showSystemResources:", enabled);
showSystemResources = enabled showSystemResources = enabled;
saveSettings() saveSettings();
} }
function setShowSystemTray(enabled) { function setShowSystemTray(enabled) {
console.log("Prefs setShowSystemTray called - showSystemTray:", enabled) console.log("Prefs setShowSystemTray called - showSystemTray:", enabled);
showSystemTray = enabled showSystemTray = enabled;
saveSettings() saveSettings();
} }
// View mode setters // View mode setters
function setAppLauncherViewMode(mode) { function setAppLauncherViewMode(mode) {
console.log("Prefs setAppLauncherViewMode called - appLauncherViewMode:", mode) console.log("Prefs setAppLauncherViewMode called - appLauncherViewMode:", mode);
appLauncherViewMode = mode appLauncherViewMode = mode;
saveSettings() saveSettings();
} }
function setSpotlightLauncherViewMode(mode) { function setSpotlightLauncherViewMode(mode) {
console.log("Prefs setSpotlightLauncherViewMode called - spotlightLauncherViewMode:", mode) console.log("Prefs setSpotlightLauncherViewMode called - spotlightLauncherViewMode:", mode);
spotlightLauncherViewMode = mode spotlightLauncherViewMode = mode;
saveSettings() saveSettings();
} }
// Weather location override setter // Weather location override setter
function setWeatherLocationOverride(location) { function setWeatherLocationOverride(location) {
console.log("Prefs setWeatherLocationOverride called - weatherLocationOverride:", location) console.log("Prefs setWeatherLocationOverride called - weatherLocationOverride:", location);
weatherLocationOverride = location weatherLocationOverride = location;
saveSettings() saveSettings();
} }
// Network preference setter // Network preference setter
function setNetworkPreference(preference) { function setNetworkPreference(preference) {
console.log("Prefs setNetworkPreference called - networkPreference:", preference) console.log("Prefs setNetworkPreference called - networkPreference:", preference);
networkPreference = preference networkPreference = preference;
saveSettings() 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 Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,40 +1,21 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Widgets import Quickshell.Widgets
pragma Singleton
pragma ComponentBehavior: Bound
Singleton { Singleton {
id: root id: root
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
property MprisPlayer activePlayer: null property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
property MprisPlayer _candidatePlayer: availablePlayers.find(p => p.isPlaying)
?? availablePlayers.find(p => p.canControl && p.canPlay) ?? availablePlayers.find(p => p.canControl && p.canPlay)
?? null ?? 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 { IpcHandler {
target: "mpris" target: "mpris"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,42 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Services.UPower
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
import Quickshell.Services.UPower
PanelWindow { PanelWindow {
id: root id: root
property bool batteryPopupVisible: false 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 implicitWidth: 400
implicitHeight: 300 implicitHeight: 300
WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent" color: "transparent"
anchors { anchors {
@@ -34,7 +50,7 @@ PanelWindow {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
batteryPopupVisible = false batteryPopupVisible = false;
} }
} }
@@ -47,29 +63,15 @@ PanelWindow {
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: 1
opacity: batteryPopupVisible ? 1 : 0
opacity: batteryPopupVisible ? 1.0 : 0.0 scale: batteryPopupVisible ? 1 : 0.85
scale: batteryPopupVisible ? 1.0 : 0.85
// Prevent click-through to background // Prevent click-through to background
MouseArea { MouseArea {
// Consume the click to prevent it from reaching the background
anchors.fill: parent anchors.fill: parent
onClicked: { 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 anchors.verticalCenter: parent.verticalCenter
} }
Item { width: parent.width - 200; height: 1 } Item {
width: parent.width - 200
height: 1
}
Rectangle { Rectangle {
width: 32 width: 32
@@ -112,14 +117,17 @@ PanelWindow {
MouseArea { MouseArea {
id: closeBatteryArea id: closeBatteryArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
batteryPopupVisible = false batteryPopupVisible = false;
} }
} }
} }
} }
Rectangle { Rectangle {
@@ -142,9 +150,13 @@ PanelWindow {
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge font.pixelSize: Theme.iconSizeLarge
color: { color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error if (BatteryService.isLowBattery && !BatteryService.isCharging)
if (BatteryService.isCharging) return Theme.primary return Theme.error;
return Theme.surfaceText
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -160,9 +172,13 @@ PanelWindow {
text: BatteryService.batteryLevel + "%" text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: { color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error if (BatteryService.isLowBattery && !BatteryService.isCharging)
if (BatteryService.isCharging) return Theme.primary return Theme.error;
return Theme.surfaceText
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
} }
font.weight: Font.Bold font.weight: Font.Bold
} }
@@ -171,29 +187,37 @@ PanelWindow {
text: BatteryService.batteryStatus text: BatteryService.batteryStatus
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: { color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error if (BatteryService.isLowBattery && !BatteryService.isCharging)
if (BatteryService.isCharging) return Theme.primary return Theme.error;
return Theme.surfaceText
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
} }
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Text { Text {
text: { text: {
let time = BatteryService.formatTimeRemaining() let time = BatteryService.formatTimeRemaining();
if (time !== "Unknown") { if (time !== "Unknown")
return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time;
}
return "" return "";
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0 visible: text.length > 0
} }
} }
} }
} }
// No battery info card // No battery info card
@@ -234,8 +258,11 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
} }
} }
} }
} }
// Battery details // Battery details
@@ -271,11 +298,14 @@ PanelWindow {
text: BatteryService.batteryHealth text: BatteryService.batteryHealth
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: { color: {
if (BatteryService.batteryHealth === "N/A") return Theme.surfaceText if (BatteryService.batteryHealth === "N/A")
var healthNum = parseInt(BatteryService.batteryHealth) return Theme.surfaceText;
return healthNum < 80 ? Theme.error : Theme.surfaceText
var healthNum = parseInt(BatteryService.batteryHealth);
return healthNum < 80 ? Theme.error : Theme.surfaceText;
} }
} }
} }
// Capacity // Capacity
@@ -295,8 +325,11 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
} }
} }
} }
} }
// Power profiles // Power profiles
@@ -317,16 +350,13 @@ PanelWindow {
spacing: Theme.spacingS spacing: Theme.spacingS
Repeater { Repeater {
model: (typeof PowerProfiles !== "undefined") ? model: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
[PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) :
[PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: profileArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.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))
(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.color: batteryControlPopup.isActiveProfile(modelData) ? Theme.primary : "transparent"
border.width: 2 border.width: 2
@@ -360,22 +390,28 @@ PanelWindow {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
} }
} }
} }
MouseArea { MouseArea {
id: profileArea id: profileArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
batteryControlPopup.setProfile(modelData) batteryControlPopup.setProfile(modelData);
} }
} }
} }
} }
} }
} }
// Degradation reason warning // Degradation reason warning
@@ -418,16 +454,44 @@ PanelWindow {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8) 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 // Error toast
Rectangle { Rectangle {
id: errorToast id: errorToast
function show() {
visible = true;
hideTimer.restart();
}
width: Math.min(300, parent.width - Theme.spacingL * 2) width: Math.min(300, parent.width - Theme.spacingL * 2)
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -448,34 +512,11 @@ PanelWindow {
Timer { Timer {
id: hideTimer id: hideTimer
interval: 3000 interval: 3000
onTriggered: errorToast.visible = false 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 QtQuick
import Quickshell.Services.UPower
import qs.Common import qs.Common
import qs.Services import qs.Services
import Quickshell.Services.UPower
Rectangle { Rectangle {
id: batteryWidget id: batteryWidget
@@ -13,9 +13,7 @@ Rectangle {
width: BatteryService.batteryAvailable ? 70 : 40 width: BatteryService.batteryAvailable ? 70 : 40
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: batteryArea.containsMouse || batteryPopupVisible ? 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)
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 visible: true
Row { Row {
@@ -27,19 +25,37 @@ Rectangle {
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6 font.pixelSize: Theme.iconSize - 6
color: { color: {
if (!BatteryService.batteryAvailable) return Theme.surfaceText if (!BatteryService.batteryAvailable)
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error return Theme.surfaceText;
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 anchors.verticalCenter: parent.verticalCenter
SequentialAnimation on opacity { SequentialAnimation on opacity {
running: BatteryService.isCharging running: BatteryService.isCharging
loops: Animation.Infinite 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 { Text {
@@ -47,30 +63,38 @@ Rectangle {
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: { color: {
if (!BatteryService.batteryAvailable) return Theme.surfaceText if (!BatteryService.batteryAvailable)
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error return Theme.surfaceText;
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 anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable visible: BatteryService.batteryAvailable
} }
} }
MouseArea { MouseArea {
id: batteryArea id: batteryArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
toggleBatteryPopup() toggleBatteryPopup();
} }
} }
// Tooltip on hover // Tooltip on hover
Rectangle { Rectangle {
id: batteryTooltip id: batteryTooltip
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2) width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
height: tooltipText.contentHeight + Theme.spacingS * 2 height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -78,19 +102,10 @@ Rectangle {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1 border.width: 1
visible: batteryArea.containsMouse && !batteryPopupVisible visible: batteryArea.containsMouse && !batteryPopupVisible
anchors.bottom: parent.top anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
opacity: batteryArea.containsMouse ? 1 : 0
opacity: batteryArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
@@ -98,31 +113,44 @@ Rectangle {
Text { Text {
id: tooltipText id: tooltipText
text: { text: {
if (!BatteryService.batteryAvailable) { if (!BatteryService.batteryAvailable) {
if (typeof PowerProfiles === "undefined") return "Power Management" if (typeof PowerProfiles === "undefined")
switch(PowerProfiles.profile) { return "Power Management";
case PowerProfile.PowerSaver: return "Power Profile: Power Saver"
case PowerProfile.Performance: return "Power Profile: Performance" switch (PowerProfiles.profile) {
default: return "Power Profile: Balanced" 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 status = BatteryService.batteryStatus let level = BatteryService.batteryLevel + "%";
let level = BatteryService.batteryLevel + "%" let time = BatteryService.formatTimeRemaining();
let time = BatteryService.formatTimeRemaining() if (time !== "Unknown")
return status + " • " + level + " • " + time;
if (time !== "Unknown") { else
return status + " • " + level + " • " + time return status + " • " + level;
} else {
return status + " • " + level
}
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
} }
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
Behavior on color { Behavior on color {
@@ -130,5 +158,7 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }

View File

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

View File

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

View File

@@ -11,13 +11,22 @@ Rectangle {
property date selectedDate: new Date() property date selectedDate: new Date()
property var selectedDateEvents: [] property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0 property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
onSelectedDateEventsChanged: {
console.log("EventsWidget: selectedDateEvents changed, count:", selectedDateEvents.length)
eventsList.model = selectedDateEvents
}
property bool shouldShow: CalendarService && CalendarService.khalAvailable 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 width: parent.width
height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0 height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
@@ -25,58 +34,33 @@ Rectangle {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: 1
visible: shouldShow visible: shouldShow
// Material elevation shadow // Material elevation shadow
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { Component.onCompleted: {
shadowEnabled: true updateSelectedDateEvents();
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
} }
onSelectedDateChanged: {
Behavior on height { updateSelectedDateEvents();
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
} }
// Update events when selected date or events change // Update events when selected date or events change
Connections { Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents();
}
function onKhalAvailableChanged() {
updateSelectedDateEvents();
}
target: CalendarService target: CalendarService
enabled: CalendarService !== null 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 // Header - always visible when widget is shown
Row { Row {
id: headerRow id: headerRow
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@@ -92,15 +76,13 @@ Rectangle {
} }
Text { Text {
text: hasEvents ? text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d")
(Qt.formatDate(selectedDate, "MMM d") + " • " +
(selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) :
Qt.formatDate(selectedDate, "MMM d")
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
// No events placeholder - centered in entire widget (not just content area) // No events placeholder - centered in entire widget (not just content area)
@@ -124,11 +106,13 @@ Rectangle {
font.weight: Font.Normal font.weight: Font.Normal
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
// Events list - positioned below header when there are events // Events list - positioned below header when there are events
ListView { ListView {
id: eventsList id: eventsList
anchors.top: headerRow.bottom anchors.top: headerRow.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
@@ -136,7 +120,7 @@ Rectangle {
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingM anchors.topMargin: Theme.spacingM
visible: opacity > 0 visible: opacity > 0
opacity: hasEvents ? 1.0 : 0.0 opacity: hasEvents ? 1 : 0
clip: true clip: true
spacing: Theme.spacingS spacing: Theme.spacingS
boundsMovement: Flickable.StopAtBounds boundsMovement: Flickable.StopAtBounds
@@ -151,6 +135,7 @@ Rectangle {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
delegate: Rectangle { delegate: Rectangle {
@@ -158,20 +143,18 @@ Rectangle {
height: eventContent.implicitHeight + Theme.spacingM height: eventContent.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (modelData.url && eventMouseArea.containsMouse) { if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else if (eventMouseArea.containsMouse) { else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) 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);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06)
} }
border.color: { border.color: {
if (modelData.url && eventMouseArea.containsMouse) { if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
} else if (eventMouseArea.containsMouse) { else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
} return "transparent";
return "transparent"
} }
border.width: 1 border.width: 1
@@ -189,6 +172,7 @@ Rectangle {
Column { Column {
id: eventContent id: eventContent
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -196,96 +180,101 @@ Rectangle {
anchors.rightMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM
spacing: 6 spacing: 6
Text { Text {
width: parent.width width: parent.width
text: modelData.title text: modelData.title
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
elide: Text.ElideRight elide: Text.ElideRight
wrapMode: Text.Wrap wrapMode: Text.Wrap
maximumLineCount: 2 maximumLineCount: 2
} }
Item { Item {
width: parent.width width: parent.width
height: Math.max(timeRow.height, locationRow.height) height: Math.max(timeRow.height, locationRow.height)
Row { Row {
id: timeRow id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Text { spacing: 4
text: "schedule" anchors.left: parent.left
font.family: Theme.iconFont anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
Text { Text {
text: { text: "schedule"
if (modelData.allDay) { font.family: Theme.iconFont
return "All day" font.pixelSize: Theme.fontSizeSmall
} else { color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
let timeFormat = Prefs.use24HourClock ? "H:mm" : "h:mm AP" anchors.verticalCenter: parent.verticalCenter
let startTime = Qt.formatTime(modelData.start, timeFormat)
if (modelData.start.toDateString() !== modelData.end.toDateString() ||
modelData.start.getTime() !== modelData.end.getTime()) {
return startTime + " " + Qt.formatTime(modelData.end, timeFormat)
}
return startTime
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: locationRow
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: modelData.location !== ""
Text {
text: "location_on"
font.family: Theme.iconFont
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: modelData.location
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
width: Math.min(implicitWidth, 200)
}
}
}
} }
Text {
text: {
if (modelData.allDay) {
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;
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: locationRow
spacing: 4
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: modelData.location !== ""
Text {
text: "location_on"
font.family: Theme.iconFont
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: modelData.location
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
width: Math.min(implicitWidth, 200)
}
}
}
}
MouseArea { MouseArea {
id: eventMouseArea id: eventMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== "" enabled: modelData.url !== ""
onClicked: { onClicked: {
if (modelData.url && modelData.url !== "") { if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) { if (Qt.openUrlExternally(modelData.url) === false)
console.warn("Couldn't open", modelData.url) console.warn("Couldn't open", modelData.url);
}
} }
} }
} }
@@ -295,6 +284,7 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
Behavior on border.color { Behavior on border.color {
@@ -302,7 +292,28 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing 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 id: mediaPlayerWidget
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
property string lastValidTitle: "" property string lastValidTitle: ""
property string lastValidArtist: "" property string lastValidArtist: ""
property string lastValidAlbum: "" property string lastValidAlbum: ""
property string lastValidArtUrl: "" property string lastValidArtUrl: ""
property real currentPosition: 0
Timer { // Simple progress ratio calculation
id: clearCacheTimer function ratio() {
interval: 2000 return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0;
onTriggered: {
if (!activePlayer) {
lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
}
}
} }
onActivePlayerChanged: { onActivePlayerChanged: {
if (!activePlayer) { if (!activePlayer)
clearCacheTimer.restart() clearCacheTimer.restart();
} else { else
clearCacheTimer.stop() clearCacheTimer.stop();
}
} }
width: parent.width width: parent.width
height: parent.height height: parent.height
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: 1
layer.enabled: true 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
}
property real currentPosition: 0 Timer {
id: clearCacheTimer
// Simple progress ratio calculation interval: 2000
function ratio() { onTriggered: {
return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0 if (!activePlayer) {
lastValidTitle = "";
lastValidArtist = "";
lastValidAlbum = "";
lastValidArtUrl = "";
}
}
} }
// Updates progress bar every 2 seconds when playing // Updates progress bar every 2 seconds when playing
Timer { Timer {
id: positionTimer id: positionTimer
interval: 2000 interval: 2000
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.length > 0 && !progressMouseArea.isSeeking running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.length > 0 && !progressMouseArea.isSeeking
repeat: true repeat: true
onTriggered: { onTriggered: {
if (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking) { if (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking)
currentPosition = activePlayer.position currentPosition = activePlayer.position;
}
} }
} }
// Backend events // Backend events
Connections { Connections {
target: activePlayer
function onPositionChanged() { function onPositionChanged() {
if (!progressMouseArea.isSeeking) { if (!progressMouseArea.isSeeking)
currentPosition = activePlayer.position currentPosition = activePlayer.position;
}
} }
function onPostTrackChanged() { function onPostTrackChanged() {
currentPosition = activePlayer?.position || 0 currentPosition = activePlayer && activePlayer.position || 0;
} }
function onTrackTitleChanged() { function onTrackTitleChanged() {
currentPosition = activePlayer?.position || 0 currentPosition = activePlayer && activePlayer.position || 0;
} }
target: activePlayer
} }
Item { Item {
@@ -117,6 +106,7 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
// Active content in a column // Active content in a column
@@ -125,11 +115,11 @@ Rectangle {
spacing: Theme.spacingS spacing: Theme.spacingS
visible: activePlayer && activePlayer.trackTitle !== "" || lastValidTitle !== "" visible: activePlayer && activePlayer.trackTitle !== "" || lastValidTitle !== ""
// Normal media info when playing // Normal media info when playing
Row { Row {
width: parent.width width: parent.width
height: 60 height: 60
spacing: Theme.spacingM spacing: Theme.spacingM
// Album Art // Album Art
Rectangle { Rectangle {
@@ -144,12 +134,13 @@ Rectangle {
Image { Image {
id: albumArt id: albumArt
anchors.fill: parent anchors.fill: parent
source: activePlayer?.trackArtUrl || lastValidArtUrl || "" source: activePlayer && activePlayer.trackArtUrl || lastValidArtUrl || ""
onSourceChanged: { onSourceChanged: {
if (activePlayer?.trackArtUrl) { if (activePlayer && activePlayer.trackArtUrl)
lastValidArtUrl = activePlayer.trackArtUrl; lastValidArtUrl = activePlayer.trackArtUrl;
}
} }
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
smooth: true smooth: true
@@ -167,8 +158,11 @@ Rectangle {
font.pixelSize: 28 font.pixelSize: 28
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }
} }
} }
} }
// Track Info // Track Info
@@ -178,11 +172,11 @@ Rectangle {
spacing: Theme.spacingXS spacing: Theme.spacingXS
Text { Text {
text: activePlayer?.trackTitle || lastValidTitle || "Unknown Track" text: activePlayer && activePlayer.trackTitle || lastValidTitle || "Unknown Track"
onTextChanged: { onTextChanged: {
if (activePlayer?.trackTitle) { if (activePlayer && activePlayer.trackTitle)
lastValidTitle = activePlayer.trackTitle; lastValidTitle = activePlayer.trackTitle;
}
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold font.weight: Font.Bold
@@ -192,11 +186,11 @@ Rectangle {
} }
Text { Text {
text: activePlayer?.trackArtist || lastValidArtist || "Unknown Artist" text: activePlayer && activePlayer.trackArtist || lastValidArtist || "Unknown Artist"
onTextChanged: { onTextChanged: {
if (activePlayer?.trackArtist) { if (activePlayer && activePlayer.trackArtist)
lastValidArtist = activePlayer.trackArtist; lastValidArtist = activePlayer.trackArtist;
}
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
@@ -205,11 +199,11 @@ Rectangle {
} }
Text { Text {
text: activePlayer?.trackAlbum || lastValidAlbum || "" text: activePlayer && activePlayer.trackAlbum || lastValidAlbum || ""
onTextChanged: { onTextChanged: {
if (activePlayer?.trackAlbum) { if (activePlayer && activePlayer.trackAlbum)
lastValidAlbum = activePlayer.trackAlbum; lastValidAlbum = activePlayer.trackAlbum;
}
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
@@ -217,124 +211,133 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
visible: text.length > 0 visible: text.length > 0
} }
} }
} }
// Progress bar // Progress bar
Item { Item {
id: progressBarContainer id: progressBarContainer
width: parent.width
height: 24
Rectangle {
id: progressBarBackground
width: parent.width width: parent.width
height: 6 height: 24
radius: 3
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
visible: activePlayer !== null
anchors.verticalCenter: parent.verticalCenter
Rectangle { Rectangle {
id: progressFill id: progressBarBackground
height: parent.height
radius: parent.radius
color: Theme.primary
width: parent.width * ratio() width: parent.width
height: 6
Behavior on width { radius: 3
NumberAnimation { duration: 100 } color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
} visible: activePlayer !== null
}
// 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))
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.length > 0 Rectangle {
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: parent.width * ratio()
Behavior on width {
NumberAnimation {
duration: 100
}
}
Behavior on scale {
NumberAnimation { duration: 150 }
} }
// 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))
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.length > 0
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation {
duration: 150
}
}
}
} }
}
MouseArea { MouseArea {
id: progressMouseArea id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
preventStealing: true
property bool isSeeking: false property bool isSeeking: false
onPressed: function(mouse) { anchors.fill: parent
isSeeking = true hoverEnabled: true
if (activePlayer && activePlayer.length > 0) { cursorShape: Qt.PointingHandCursor
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width)) enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
let seekPosition = ratio * activePlayer.length preventStealing: true
activePlayer.position = seekPosition onPressed: function(mouse) {
currentPosition = seekPosition isSeeking = true;
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
isSeeking = false;
}
onPositionChanged: function(mouse) {
if (pressed && isSeeking && activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onClicked: function(mouse) {
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
} }
} }
onReleased: { // Global mouse area for drag tracking
isSeeking = false MouseArea {
} id: progressGlobalMouseArea
onPositionChanged: function(mouse) { anchors.fill: parent.parent.parent // Fill the entire media player widget
if (pressed && isSeeking && activePlayer && activePlayer.length > 0) { enabled: progressMouseArea.isSeeking
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width)) visible: false
let seekPosition = ratio * activePlayer.length preventStealing: true
activePlayer.position = seekPosition onPositionChanged: function(mouse) {
currentPosition = seekPosition if (progressMouseArea.isSeeking && activePlayer && activePlayer.length > 0) {
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
progressMouseArea.isSeeking = false;
} }
} }
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
}
}
}
// 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
}
}
onReleased: {
progressMouseArea.isSeeking = false
}
}
} }
// Control buttons - always visible // Control buttons - always visible
@@ -348,88 +351,108 @@ Rectangle {
spacing: Theme.spacingM spacing: Theme.spacingM
height: parent.height height: parent.height
// Previous button // Previous button
Rectangle { Rectangle {
width: 28 width: 28
height: 28 height: 28
radius: 14 radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent" color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "skip_previous" text: "skip_previous"
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: 16 font.pixelSize: 16
color: Theme.surfaceText color: Theme.surfaceText
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer) return
// >8 s → jump to start, otherwise previous track
if (currentPosition > 8 && activePlayer.canSeek) {
activePlayer.position = 0
currentPosition = 0
} else {
activePlayer.previous()
} }
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer)
return ;
// >8 s → jump to start, otherwise previous track
if (currentPosition > 8 && activePlayer.canSeek) {
activePlayer.position = 0;
currentPosition = 0;
} else {
activePlayer.previous();
}
}
}
} }
// Play/Pause button
Rectangle {
width: 32
height: 32
radius: 16
color: Theme.primary
Text {
anchors.centerIn: parent
text: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.togglePlaying()
}
}
// Next button
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: Theme.iconFont
font.pixelSize: 16
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer && activePlayer.next()
}
}
} }
} }
// Play/Pause button
Rectangle {
width: 32
height: 32
radius: 16
color: Theme.primary
Text {
anchors.centerIn: parent
text: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
font.family: Theme.iconFont
font.pixelSize: 20
color: Theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.togglePlaying()
}
}
// Next button
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: Theme.iconFont
font.pixelSize: 16
color: Theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: 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 { Rectangle {
id: weatherWidget id: weatherWidget
width: parent.width width: parent.width
height: parent.height height: parent.height
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: 1
layer.enabled: true 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 // Placeholder when no weather - centered in entire widget
Column { Column {
@@ -45,6 +35,7 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
// Weather content when available - original Column structure // Weather content when available - original Column structure
@@ -86,9 +77,14 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor 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 enabled: WeatherService.weather.available
} }
} }
Text { Text {
@@ -97,8 +93,11 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0 visible: text.length > 0
} }
} }
} }
} }
// Weather details grid // Weather details grid
@@ -109,6 +108,7 @@ Rectangle {
Row { Row {
spacing: Theme.spacingXS spacing: Theme.spacingXS
Text { Text {
text: "humidity_low" text: "humidity_low"
font.family: Theme.iconFont font.family: Theme.iconFont
@@ -116,16 +116,19 @@ Rectangle {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Text { Text {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--" text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Row { Row {
spacing: Theme.spacingXS spacing: Theme.spacingXS
Text { Text {
text: "air" text: "air"
font.family: Theme.iconFont font.family: Theme.iconFont
@@ -133,16 +136,19 @@ Rectangle {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Text { Text {
text: WeatherService.weather.wind || "--" text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Row { Row {
spacing: Theme.spacingXS spacing: Theme.spacingXS
Text { Text {
text: "wb_twilight" text: "wb_twilight"
font.family: Theme.iconFont font.family: Theme.iconFont
@@ -150,16 +156,19 @@ Rectangle {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Text { Text {
text: WeatherService.weather.sunrise || "--" text: WeatherService.weather.sunrise || "--"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Row { Row {
spacing: Theme.spacingXS spacing: Theme.spacingXS
Text { Text {
text: "bedtime" text: "bedtime"
font.family: Theme.iconFont font.family: Theme.iconFont
@@ -167,13 +176,27 @@ Rectangle {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Text { Text {
text: WeatherService.weather.sunset || "--" text: WeatherService.weather.sunset || "--"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter 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
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Widgets
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -10,7 +10,6 @@ Item {
id: audioTab id: audioTab
property int audioSubTab: 0 // 0: Output, 1: Input property int audioSubTab: 0 // 0: Output, 1: Input
readonly property real volumeLevel: AudioService.volumeLevel readonly property real volumeLevel: AudioService.volumeLevel
readonly property real micLevel: AudioService.micLevel readonly property real micLevel: AudioService.micLevel
readonly property bool volumeMuted: AudioService.sinkMuted readonly property bool volumeMuted: AudioService.sinkMuted
@@ -50,6 +49,7 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: audioTab.audioSubTab = 0 onClicked: audioTab.audioSubTab = 0
} }
} }
Rectangle { Rectangle {
@@ -72,7 +72,9 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: audioTab.audioSubTab = 1 onClicked: audioTab.audioSubTab = 1
} }
} }
} }
// Output Tab Content // Output Tab Content
@@ -115,16 +117,19 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMute() onClicked: AudioService.toggleMute()
} }
} }
Item { Item {
id: volumeSliderContainer id: volumeSliderContainer
width: parent.width - 80 width: parent.width - 80
height: 32 height: 32
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Rectangle { Rectangle {
id: volumeSliderTrack id: volumeSliderTrack
width: parent.width width: parent.width
height: 8 height: 8
radius: 4 radius: 4
@@ -133,93 +138,99 @@ Item {
Rectangle { Rectangle {
id: volumeSliderFill id: volumeSliderFill
width: parent.width * (audioTab.volumeLevel / 100) width: parent.width * (audioTab.volumeLevel / 100)
height: parent.height height: parent.height
radius: parent.radius radius: parent.radius
color: Theme.primary color: Theme.primary
Behavior on width { Behavior on width {
NumberAnimation { duration: 100 } NumberAnimation {
duration: 100
}
} }
} }
// Draggable handle // Draggable handle
Rectangle { Rectangle {
id: volumeHandle id: volumeHandle
width: 18 width: 18
height: 18 height: 18
radius: 9 radius: 9
color: Theme.primary color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3) border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2 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 anchors.verticalCenter: parent.verticalCenter
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1.0
Behavior on scale { Behavior on scale {
NumberAnimation { duration: 150 } NumberAnimation {
duration: 150
}
} }
} }
} }
MouseArea { MouseArea {
id: volumeMouseArea id: volumeMouseArea
property bool isDragging: false
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
preventStealing: true preventStealing: true
property bool isDragging: false
onPressed: (mouse) => { onPressed: (mouse) => {
isDragging = true isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width)) let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100) let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume) AudioService.setVolume(newVolume);
} }
onReleased: { onReleased: {
isDragging = false isDragging = false;
} }
onPositionChanged: (mouse) => { onPositionChanged: (mouse) => {
if (pressed && isDragging) { if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width)) let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100) let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume) AudioService.setVolume(newVolume);
} }
} }
onClicked: (mouse) => { onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width)) let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100) let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume) AudioService.setVolume(newVolume);
} }
} }
// Global mouse area for drag tracking // Global mouse area for drag tracking
MouseArea { MouseArea {
id: volumeGlobalMouseArea id: volumeGlobalMouseArea
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
enabled: volumeMouseArea.isDragging enabled: volumeMouseArea.isDragging
visible: false visible: false
preventStealing: true preventStealing: true
onPositionChanged: (mouse) => { onPositionChanged: (mouse) => {
if (volumeMouseArea.isDragging) { if (volumeMouseArea.isDragging) {
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y) let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width)) let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100) let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume) AudioService.setVolume(newVolume);
} }
} }
onReleased: { onReleased: {
volumeMouseArea.isDragging = false volumeMouseArea.isDragging = false;
} }
} }
} }
Text { Text {
@@ -229,7 +240,9 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
} }
// Output Devices // Output Devices
@@ -273,7 +286,9 @@ Item {
color: Theme.primary color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
} }
} }
} }
// Real audio devices // Real audio devices
@@ -284,8 +299,7 @@ Item {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.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))
(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.color: modelData.active ? Theme.primary : "transparent"
border.width: 1 border.width: 1
@@ -297,10 +311,14 @@ Item {
Text { Text {
text: { text: {
if (modelData.name.includes("bluez")) return "headset" if (modelData.name.includes("bluez"))
else if (modelData.name.includes("hdmi")) return "tv" return "headset";
else if (modelData.name.includes("usb")) return "headset" else if (modelData.name.includes("hdmi"))
else return "speaker" return "tv";
else if (modelData.name.includes("usb"))
return "headset";
else
return "speaker";
} }
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize font.pixelSize: Theme.iconSize
@@ -321,33 +339,39 @@ Item {
Text { Text {
text: { text: {
if (modelData.subtitle && modelData.subtitle !== "") { if (modelData.subtitle && modelData.subtitle !== "")
return modelData.subtitle + (modelData.active ? " • Selected" : "") return modelData.subtitle + (modelData.active ? " • Selected" : "");
} else { else
return modelData.active ? "Selected" : "" return modelData.active ? "Selected" : "";
}
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== "" visible: text !== ""
} }
} }
} }
MouseArea { MouseArea {
id: deviceArea id: deviceArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
AudioService.setAudioSink(modelData.name) AudioService.setAudioSink(modelData.name);
} }
} }
} }
} }
} }
} }
} }
// Input Tab Content // Input Tab Content
@@ -390,16 +414,19 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMicMute() onClicked: AudioService.toggleMicMute()
} }
} }
Item { Item {
id: micSliderContainer id: micSliderContainer
width: parent.width - 80 width: parent.width - 80
height: 32 height: 32
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Rectangle { Rectangle {
id: micSliderTrack id: micSliderTrack
width: parent.width width: parent.width
height: 8 height: 8
radius: 4 radius: 4
@@ -408,93 +435,99 @@ Item {
Rectangle { Rectangle {
id: micSliderFill id: micSliderFill
width: parent.width * (audioTab.micLevel / 100) width: parent.width * (audioTab.micLevel / 100)
height: parent.height height: parent.height
radius: parent.radius radius: parent.radius
color: Theme.primary color: Theme.primary
Behavior on width { Behavior on width {
NumberAnimation { duration: 100 } NumberAnimation {
duration: 100
}
} }
} }
// Draggable handle // Draggable handle
Rectangle { Rectangle {
id: micHandle id: micHandle
width: 18 width: 18
height: 18 height: 18
radius: 9 radius: 9
color: Theme.primary color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3) border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2 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 anchors.verticalCenter: parent.verticalCenter
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1.0
Behavior on scale { Behavior on scale {
NumberAnimation { duration: 150 } NumberAnimation {
duration: 150
}
} }
} }
} }
MouseArea { MouseArea {
id: micMouseArea id: micMouseArea
property bool isDragging: false
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
preventStealing: true preventStealing: true
property bool isDragging: false
onPressed: (mouse) => { onPressed: (mouse) => {
isDragging = true isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width)) let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100) let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel) AudioService.setMicLevel(newMicLevel);
} }
onReleased: { onReleased: {
isDragging = false isDragging = false;
} }
onPositionChanged: (mouse) => { onPositionChanged: (mouse) => {
if (pressed && isDragging) { if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width)) let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100) let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel) AudioService.setMicLevel(newMicLevel);
} }
} }
onClicked: (mouse) => { onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width)) let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100) let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel) AudioService.setMicLevel(newMicLevel);
} }
} }
// Global mouse area for drag tracking // Global mouse area for drag tracking
MouseArea { MouseArea {
id: micGlobalMouseArea id: micGlobalMouseArea
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
enabled: micMouseArea.isDragging enabled: micMouseArea.isDragging
visible: false visible: false
preventStealing: true preventStealing: true
onPositionChanged: (mouse) => { onPositionChanged: (mouse) => {
if (micMouseArea.isDragging) { if (micMouseArea.isDragging) {
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y) let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width)) let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100) let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel) AudioService.setMicLevel(newMicLevel);
} }
} }
onReleased: { onReleased: {
micMouseArea.isDragging = false micMouseArea.isDragging = false;
} }
} }
} }
Text { Text {
@@ -504,6 +537,7 @@ Item {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
} }
@@ -549,7 +583,9 @@ Item {
color: Theme.primary color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
} }
} }
} }
// Real audio input devices // Real audio input devices
@@ -560,8 +596,7 @@ Item {
width: parent.width width: parent.width
height: 50 height: 50
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: sourceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.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))
(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.color: modelData.active ? Theme.primary : "transparent"
border.width: 1 border.width: 1
@@ -573,9 +608,12 @@ Item {
Text { Text {
text: { text: {
if (modelData.name.includes("bluez")) return "headset_mic" if (modelData.name.includes("bluez"))
else if (modelData.name.includes("usb")) return "headset_mic" return "headset_mic";
else return "mic" else if (modelData.name.includes("usb"))
return "headset_mic";
else
return "mic";
} }
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize font.pixelSize: Theme.iconSize
@@ -596,33 +634,41 @@ Item {
Text { Text {
text: { text: {
if (modelData.subtitle && modelData.subtitle !== "") { if (modelData.subtitle && modelData.subtitle !== "")
return modelData.subtitle + (modelData.active ? " • Selected" : "") return modelData.subtitle + (modelData.active ? " • Selected" : "");
} else { else
return modelData.active ? "Selected" : "" return modelData.active ? "Selected" : "";
}
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== "" visible: text !== ""
} }
} }
} }
MouseArea { MouseArea {
id: sourceArea id: sourceArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
AudioService.setAudioSource(modelData.name) AudioService.setAudioSource(modelData.name);
} }
} }
} }
} }
} }
} }
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
import "."
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import qs.Common import qs.Common
import qs.Services import qs.Services
import "."
Rectangle { Rectangle {
id: ramWidget id: ramWidget
@@ -13,19 +13,17 @@ Rectangle {
width: 55 width: 55
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: ramArea.containsMouse ? 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)
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 { MouseArea {
id: ramArea id: ramArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
ProcessMonitorService.setSortBy("memory") ProcessMonitorService.setSortBy("memory");
processListDropdown.toggle() processListDropdown.toggle();
} }
} }
@@ -35,14 +33,18 @@ Rectangle {
// RAM icon // RAM icon
Text { Text {
text: "developer_board" // Material Design CPU/processor icon (swapped from CPU widget) text: "developer_board" // Material Design CPU/processor icon (swapped from CPU widget)
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8 font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight font.weight: Theme.iconFontWeight
color: { color: {
if (SystemMonitorService.memoryUsage > 90) return Theme.error if (SystemMonitorService.memoryUsage > 90)
if (SystemMonitorService.memoryUsage > 75) return Theme.warning return Theme.error;
return Theme.surfaceText
if (SystemMonitorService.memoryUsage > 75)
return Theme.warning;
return Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -55,5 +57,7 @@ Rectangle {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
} }

View File

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

View File

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

View File

@@ -13,16 +13,7 @@ Rectangle {
width: parent.width width: parent.width
height: 60 height: 60
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: toggleArea.containsMouse ? 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)
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
}
}
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -51,30 +42,26 @@ Rectangle {
width: Math.min(implicitWidth, root.width - 120) width: Math.min(implicitWidth, root.width - 120)
visible: root.description.length > 0 visible: root.description.length > 0
} }
} }
} }
// Toggle switch // Toggle switch
Rectangle { Rectangle {
id: toggle id: toggle
width: 48 width: 48
height: 24 height: 24
radius: 12 radius: 12
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: Theme.spacingM anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: root.checked ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5) 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 { Rectangle {
id: toggleHandle id: toggleHandle
width: 20 width: 20
height: 20 height: 20
radius: 10 radius: 10
@@ -87,6 +74,7 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
Behavior on color { Behavior on color {
@@ -94,19 +82,39 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
} }
MouseArea { MouseArea {
id: toggleArea id: toggleArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
root.checked = !root.checked root.checked = !root.checked;
root.toggled(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 { Column {
id: themePicker id: themePicker
spacing: Theme.spacingS spacing: Theme.spacingS
Text { Text {
@@ -17,22 +18,11 @@ Column {
// Theme description // Theme description
Text { Text {
text: { text: {
if (Theme.isDynamicTheme) { if (Theme.isDynamicTheme)
return "Wallpaper-based dynamic colors" return "Wallpaper-based dynamic colors";
}
var descriptions = [ 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"];
"Material blue inspired by modern interfaces", return descriptions[Theme.currentThemeIndex] || "Select a theme";
"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 font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -62,22 +52,7 @@ Column {
color: Theme.themes[index].primary color: Theme.themes[index].primary
border.color: Theme.outline border.color: Theme.outline
border.width: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 2 : 1 border.width: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 2 : 1
scale: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 1.1 : 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
}
}
// Theme name tooltip // Theme name tooltip
Rectangle { Rectangle {
@@ -94,24 +69,46 @@ Column {
Text { Text {
id: nameText id: nameText
text: Theme.themes[index].name text: Theme.themes[index].name
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.centerIn: parent anchors.centerIn: parent
} }
} }
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { 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 // Second row - Red, Cyan, Pink, Amber, Coral
@@ -124,6 +121,7 @@ Column {
Rectangle { Rectangle {
property int themeIndex: index + 5 property int themeIndex: index + 5
width: 32 width: 32
height: 32 height: 32
radius: 16 radius: 16
@@ -131,22 +129,7 @@ Column {
border.color: Theme.outline border.color: Theme.outline
border.width: Theme.currentThemeIndex === themeIndex ? 2 : 1 border.width: Theme.currentThemeIndex === themeIndex ? 2 : 1
visible: themeIndex < Theme.themes.length visible: themeIndex < Theme.themes.length
scale: Theme.currentThemeIndex === themeIndex ? 1.1 : 1
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
}
}
// Theme name tooltip // Theme name tooltip
Rectangle { Rectangle {
@@ -163,26 +146,48 @@ Column {
Text { Text {
id: nameText2 id: nameText2
text: themeIndex < Theme.themes.length ? Theme.themes[themeIndex].name : "" text: themeIndex < Theme.themes.length ? Theme.themes[themeIndex].name : ""
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.centerIn: parent anchors.centerIn: parent
} }
} }
MouseArea { MouseArea {
id: mouseArea2 id: mouseArea2
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (themeIndex < Theme.themes.length) { if (themeIndex < Theme.themes.length)
Theme.switchTheme(themeIndex) 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 // Spacer for better visual separation
@@ -197,26 +202,22 @@ Column {
height: 40 height: 40
radius: 20 radius: 20
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: { color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") { if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
} else { else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
}
} }
border.color: { border.color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") { if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5) return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5);
} else if (Theme.isDynamicTheme) { else if (Theme.isDynamicTheme)
return Theme.primary return Theme.primary;
} else { else
return Theme.outline return Theme.outline;
}
} }
border.width: Theme.isDynamicTheme ? 2 : 1 border.width: Theme.isDynamicTheme ? 2 : 1
scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1)
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
@@ -224,14 +225,18 @@ Column {
Text { Text {
text: { text: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") return "error" if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
else return "palette" return "error";
else
return "palette";
} }
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: 16 font.pixelSize: 16
color: { color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") return Theme.error if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
else return Theme.surfaceText return Theme.error;
else
return Theme.surfaceText;
} }
font.weight: Theme.iconFontWeight font.weight: Theme.iconFontWeight
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -239,51 +244,34 @@ Column {
Text { Text {
text: { text: {
if (ToastService.wallpaperErrorStatus === "error") return "Error" if (ToastService.wallpaperErrorStatus === "error")
else if (ToastService.wallpaperErrorStatus === "matugen_missing") return "No matugen" return "Error";
else return "Auto" else if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "No matugen";
else
return "Auto";
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: { color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") return Theme.error if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
else return Theme.surfaceText return Theme.error;
else
return Theme.surfaceText;
} }
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter 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 { MouseArea {
id: autoMouseArea id: autoMouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
Theme.switchTheme(10, true) Theme.switchTheme(10, true);
} }
} }
@@ -302,14 +290,14 @@ Column {
Text { Text {
id: autoTooltipText id: autoTooltipText
text: { text: {
if (ToastService.wallpaperErrorStatus === "error") { if (ToastService.wallpaperErrorStatus === "error")
return "Wallpaper symlink missing at ~/quickshell/current_wallpaper" return "Wallpaper symlink missing at ~/quickshell/current_wallpaper";
} else if (ToastService.wallpaperErrorStatus === "matugen_missing") { else if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "Install matugen package for dynamic themes" return "Install matugen package for dynamic themes";
} else { else
return "Dynamic wallpaper-based colors" return "Dynamic wallpaper-based colors";
}
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceText color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceText
@@ -318,7 +306,35 @@ Column {
width: Math.min(implicitWidth, 250) width: Math.min(implicitWidth, 250)
horizontalAlignment: Text.AlignHCenter 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
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -10,11 +10,9 @@ PanelWindow {
id: root id: root
visible: ToastService.toastVisible visible: ToastService.toastVisible
WlrLayershell.layer: WlrLayershell.Overlay WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent" color: "transparent"
anchors { anchors {
@@ -24,80 +22,47 @@ PanelWindow {
bottom: true bottom: true
} }
// Makes the background transparent to mouse events
mask: Region {
item: toast
}
Rectangle { Rectangle {
id: toast id: toast
width: Math.min(400, Screen.width - Theme.spacingL * 2) width: Math.min(400, Screen.width - Theme.spacingL * 2)
height: toastContent.height + Theme.spacingL * 2 height: toastContent.height + Theme.spacingL * 2
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
y: Theme.barHeight + Theme.spacingL y: Theme.barHeight + Theme.spacingL
color: { color: {
switch (ToastService.currentLevel) { switch (ToastService.currentLevel) {
case ToastService.levelError: return Theme.error case ToastService.levelError:
case ToastService.levelWarn: return Theme.warning return Theme.error;
case ToastService.levelInfo: return Theme.primary case ToastService.levelWarn:
default: return Theme.primary return Theme.warning;
case ToastService.levelInfo:
return Theme.primary;
default:
return Theme.primary;
} }
} }
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
layer.enabled: true layer.enabled: true
layer.effect: MultiEffect { opacity: ToastService.toastVisible ? 0.9 : 0
shadowEnabled: true scale: ToastService.toastVisible ? 1 : 0.9
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
}
}
Row { Row {
id: toastContent id: toastContent
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingM spacing: Theme.spacingM
Text { Text {
text: { text: {
switch (ToastService.currentLevel) { switch (ToastService.currentLevel) {
case ToastService.levelError: return "error" case ToastService.levelError:
case ToastService.levelWarn: return "warning" return "error";
case ToastService.levelInfo: return "info" case ToastService.levelWarn:
default: return "info" return "warning";
case ToastService.levelInfo:
return "info";
default:
return "info";
} }
} }
font.family: Theme.iconFont font.family: Theme.iconFont
@@ -115,11 +80,56 @@ PanelWindow {
width: Math.min(implicitWidth, 300) width: Math.min(implicitWidth, 300)
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: ToastService.hideToast() 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,8 +8,8 @@ import qs.Services
Item { Item {
id: root 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 MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool hasActiveMedia: activePlayer !== null readonly property bool hasActiveMedia: activePlayer !== null
property bool cavaAvailable: false property bool cavaAvailable: false
@@ -18,56 +18,62 @@ Item {
Process { Process {
id: cavaCheck id: cavaCheck
command: ["which", "cava"] command: ["which", "cava"]
running: true running: true
onExited: (exitCode) => { onExited: (exitCode) => {
root.cavaAvailable = exitCode === 0 root.cavaAvailable = exitCode === 0;
if (root.cavaAvailable) { if (root.cavaAvailable) {
console.log("cava found - enabling real audio visualization") console.log("cava found - enabling real audio visualization");
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing) cavaProcess.running = Qt.binding(() => {
return root.hasActiveMedia && root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing;
});
} else { } else {
console.log("cava not found - using fallback animation") console.log("cava not found - using fallback animation");
fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing) fallbackTimer.running = Qt.binding(() => {
return root.hasActiveMedia && root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing;
});
} }
} }
} }
Process { Process {
id: cavaProcess id: cavaProcess
running: false 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`] 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 { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"
onRead: data => { onRead: (data) => {
if (data.trim()) { if (data.trim()) {
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p)) let points = data.split(";").map((p) => {
if (points.length >= 4) { return parseFloat(p.trim());
root.audioLevels = [points[0], points[1], points[2], points[3]] }).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 { Timer {
id: fallbackTimer id: fallbackTimer
running: false running: false
interval: 100 interval: 100
repeat: true repeat: true
onTriggered: { onTriggered: {
root.audioLevels = [ root.audioLevels = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20];
Math.random() * 40 + 10,
Math.random() * 60 + 20,
Math.random() * 50 + 15,
Math.random() * 35 + 20
]
} }
} }
@@ -81,14 +87,14 @@ Item {
Rectangle { Rectangle {
width: 3 width: 3
height: { height: {
if (root.activePlayer?.playbackState === MprisPlaybackState.Playing && root.audioLevels.length > index) { if (root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing && root.audioLevels.length > index) {
const rawLevel = root.audioLevels[index] || 0 const rawLevel = root.audioLevels[index] || 0;
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100 const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
const maxHeight = Theme.iconSize - 2 const maxHeight = Theme.iconSize - 2;
const minHeight = 3 const minHeight = 3;
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight) return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
} }
return 3 return 3;
} }
radius: 1.5 radius: 1.5
color: Theme.primary color: Theme.primary
@@ -99,8 +105,13 @@ Item {
duration: 80 duration: 80
easing.type: Easing.OutQuad easing.type: Easing.OutQuad
} }
} }
} }
} }
} }
} }

View File

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

View File

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

View File

@@ -21,12 +21,12 @@ Rectangle {
MouseArea { MouseArea {
id: launcherArea id: launcherArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
LauncherService.toggleAppLauncher() LauncherService.toggleAppLauncher();
} }
} }
@@ -35,5 +35,7 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing 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 { Row {
id: mediaRow id: mediaRow
@@ -64,9 +95,8 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 140 width: 140
text: { text: {
if (!activePlayer || !activePlayer.trackTitle) { if (!activePlayer || !activePlayer.trackTitle)
return ""; return "";
}
let identity = activePlayer.identity || ""; let identity = activePlayer.identity || "";
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari"); let 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 { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Theme.shortDuration duration: Theme.shortDuration

View File

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

View File

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

View File

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

View File

@@ -5,33 +5,17 @@ import qs.Services
Rectangle { Rectangle {
id: root id: root
signal clicked() signal clicked()
// Visibility is now controlled by TopBar.qml // Visibility is now controlled by TopBar.qml
width: visible ? Math.min(100, weatherRow.implicitWidth + Theme.spacingS * 2) : 0 width: visible ? Math.min(100, weatherRow.implicitWidth + Theme.spacingS * 2) : 0
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: weatherArea.containsMouse ? 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)
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
}
}
Row { Row {
id: weatherRow id: weatherRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
@@ -50,14 +34,32 @@ Rectangle {
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { MouseArea {
id: weatherArea id: weatherArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: root.clicked() 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 id: root
property string screenName: "" 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) width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
height: 30 height: 30
@@ -14,67 +49,29 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
visible: NiriWorkspaceService.niriAvailable 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 { Connections {
target: NiriWorkspaceService
function onAllWorkspacesChanged() { function onAllWorkspacesChanged() {
root.workspaceList = root.getDisplayWorkspaces() root.workspaceList = root.getDisplayWorkspaces();
root.currentWorkspace = root.getDisplayActiveWorkspace() root.currentWorkspace = root.getDisplayActiveWorkspace();
} }
function onFocusedWorkspaceIndexChanged() { function onFocusedWorkspaceIndexChanged() {
root.currentWorkspace = root.getDisplayActiveWorkspace() root.currentWorkspace = root.getDisplayActiveWorkspace();
} }
function onNiriAvailableChanged() { function onNiriAvailableChanged() {
if (NiriWorkspaceService.niriAvailable) { if (NiriWorkspaceService.niriAvailable) {
root.workspaceList = root.getDisplayWorkspaces() root.workspaceList = root.getDisplayWorkspaces();
root.currentWorkspace = root.getDisplayActiveWorkspace() root.currentWorkspace = root.getDisplayActiveWorkspace();
} }
} }
target: NiriWorkspaceService
} }
Row { Row {
id: workspaceRow id: workspaceRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
@@ -89,15 +86,25 @@ Rectangle {
width: isActive ? Theme.spacingXL + Theme.spacingM : Theme.spacingL + Theme.spacingXS width: isActive ? Theme.spacingXL + Theme.spacingM : Theme.spacingL + Theme.spacingXS
height: Theme.spacingM height: Theme.spacingM
radius: height / 2 radius: height / 2
color: isActive ? Theme.primary : 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)
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 { Behavior on width {
NumberAnimation { NumberAnimation {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing easing.type: Theme.emphasizedEasing
} }
} }
Behavior on color { Behavior on color {
@@ -105,19 +112,13 @@ Rectangle {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing 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
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Services import qs.Services
@@ -14,6 +14,16 @@ PanelWindow {
property string wifiPasswordInput: "" property string wifiPasswordInput: ""
visible: wifiPasswordDialogVisible visible: wifiPasswordDialogVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: wifiPasswordDialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
onVisibleChanged: {
if (visible)
passwordInput.forceActiveFocus();
}
anchors { anchors {
top: true top: true
left: true left: true
@@ -21,37 +31,27 @@ PanelWindow {
bottom: true bottom: true
} }
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: wifiPasswordDialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
onVisibleChanged: {
if (visible) {
passwordInput.forceActiveFocus()
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5) 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 { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Theme.mediumDuration duration: Theme.mediumDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
MouseArea {
anchors.fill: parent
onClicked: {
wifiPasswordDialogVisible = false
wifiPasswordInput = ""
}
}
} }
Rectangle { Rectangle {
@@ -62,23 +62,8 @@ PanelWindow {
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1 border.width: 1
opacity: wifiPasswordDialogVisible ? 1 : 0
opacity: wifiPasswordDialogVisible ? 1.0 : 0.0 scale: wifiPasswordDialogVisible ? 1 : 0.9
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
}
}
Column { Column {
anchors.fill: parent anchors.fill: parent
@@ -107,6 +92,7 @@ PanelWindow {
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
Rectangle { Rectangle {
@@ -125,15 +111,18 @@ PanelWindow {
MouseArea { MouseArea {
id: closeDialogArea id: closeDialogArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
wifiPasswordDialogVisible = false wifiPasswordDialogVisible = false;
wifiPasswordInput = "" wifiPasswordInput = "";
} }
} }
} }
} }
// Password input // Password input
@@ -147,6 +136,7 @@ PanelWindow {
TextInput { TextInput {
id: passwordInput id: passwordInput
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -155,6 +145,17 @@ PanelWindow {
verticalAlignment: TextInput.AlignVCenter verticalAlignment: TextInput.AlignVCenter
cursorVisible: activeFocus cursorVisible: activeFocus
selectByMouse: true selectByMouse: true
onTextChanged: {
wifiPasswordInput = text;
}
onAccepted: {
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput);
}
Component.onCompleted: {
if (wifiPasswordDialogVisible)
forceActiveFocus();
}
Text { Text {
anchors.fill: parent anchors.fill: parent
@@ -165,28 +166,16 @@ PanelWindow {
visible: parent.text.length === 0 visible: parent.text.length === 0
} }
onTextChanged: {
wifiPasswordInput = text
}
onAccepted: {
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput)
}
Component.onCompleted: {
if (wifiPasswordDialogVisible) {
forceActiveFocus()
}
}
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.IBeamCursor cursorShape: Qt.IBeamCursor
onClicked: { onClicked: {
passwordInput.forceActiveFocus() passwordInput.forceActiveFocus();
} }
} }
} }
// Show password checkbox // Show password checkbox
@@ -195,6 +184,7 @@ PanelWindow {
Rectangle { Rectangle {
id: showPasswordCheckbox id: showPasswordCheckbox
property bool checked: false property bool checked: false
width: 20 width: 20
@@ -218,9 +208,10 @@ PanelWindow {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
} }
} }
} }
Text { Text {
@@ -229,6 +220,7 @@ PanelWindow {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
// Buttons // Buttons
@@ -251,6 +243,7 @@ PanelWindow {
Text { Text {
id: cancelText id: cancelText
anchors.centerIn: parent anchors.centerIn: parent
text: "Cancel" text: "Cancel"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -260,14 +253,16 @@ PanelWindow {
MouseArea { MouseArea {
id: cancelArea id: cancelArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
wifiPasswordDialogVisible = false wifiPasswordDialogVisible = false;
wifiPasswordInput = "" wifiPasswordInput = "";
} }
} }
} }
Rectangle { Rectangle {
@@ -276,10 +271,11 @@ PanelWindow {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: wifiPasswordInput.length > 0 enabled: wifiPasswordInput.length > 0
opacity: enabled ? 1.0 : 0.5 opacity: enabled ? 1 : 0.5
Text { Text {
id: connectText id: connectText
anchors.centerIn: parent anchors.centerIn: parent
text: "Connect" text: "Connect"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -289,12 +285,13 @@ PanelWindow {
MouseArea { MouseArea {
id: connectArea id: connectArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: parent.enabled enabled: parent.enabled
onClicked: { onClicked: {
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput) WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput);
} }
} }
@@ -303,10 +300,33 @@ PanelWindow {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing 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 // Multi-monitor support using Variants
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens
delegate: TopBar { delegate: TopBar {
modelData: item modelData: item
} }
} }
// Global popup windows // Global popup windows
CenterCommandCenter { CenterCommandCenter {
id: centerCommandCenter id: centerCommandCenter
} }
TrayMenuPopup { TrayMenuPopup {
id: trayMenuPopup id: trayMenuPopup
} }
NotificationCenter { NotificationCenter {
id: notificationCenter id: notificationCenter
} }
ControlCenterPopup { ControlCenterPopup {
id: controlCenterPopup id: controlCenterPopup
} }
WifiPasswordDialog { WifiPasswordDialog {
id: wifiPasswordDialog id: wifiPasswordDialog
} }
InputDialog { InputDialog {
id: globalInputDialog id: globalInputDialog
} }
BatteryControlPopup { BatteryControlPopup {
id: batteryControlPopup id: batteryControlPopup
} }
PowerMenuPopup { PowerMenuPopup {
id: powerMenuPopup id: powerMenuPopup
} }
PowerConfirmDialog { PowerConfirmDialog {
id: powerConfirmDialog id: powerConfirmDialog
} }