1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

Initial qmlformat

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

View File

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

View File

@@ -1,180 +1,181 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Qt.labs.platform
import QtQuick
import Quickshell
import Quickshell.Io
import Qt.labs.platform
import qs.Services
Singleton {
// This dependency forces re-evaluation when colorUpdateTrigger changes
// Just check if matugen is available
id: root
/* ──────────────── basic state ──────────────── */
signal colorsUpdated()
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
readonly property string homeDir: _homeUrl.startsWith("file://")
? _homeUrl.substring(7)
: _homeUrl
readonly property string homeDir: _homeUrl.startsWith("file://") ? _homeUrl.substring(7) : _homeUrl
readonly property string wallpaperPath: homeDir + "/quickshell/current_wallpaper"
readonly property string notifyPath: homeDir + "/quickshell/wallpaper_changed"
property bool matugenAvailable: false
property string matugenJson: ""
property var matugenColors: ({
})
property 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
property string matugenJson: ""
property var matugenColors: ({})
property bool extractionRequested: false
property int colorUpdateTrigger: 0 // Force property re-evaluation
// ──────────────── basic state ────────────────
signal colorsUpdated()
Component.onCompleted: {
console.log("Colors.qml → home =", homeDir)
// Don't automatically run color extraction - only when requested
matugenCheck.running = true // Just check if matugen is available
// Connect to Theme light mode changes to update colors
if (typeof Theme !== "undefined") {
Theme.isLightModeChanged.connect(root.onLightModeChanged)
}
}
function onLightModeChanged() {
// Force color properties to update when light mode changes
if (matugenColors && Object.keys(matugenColors).length > 0) {
console.log("Light mode changed - updating dynamic colors")
colorUpdateTrigger++ // This will trigger re-evaluation of all color properties
colorsUpdated()
console.log("Light mode changed - updating dynamic colors");
colorUpdateTrigger++; // This will trigger re-evaluation of all color properties
colorsUpdated();
}
}
/* ──────────────── availability checks ──────────────── */
Process {
id: matugenCheck
command: ["which", "matugen"]
onExited: (code) => {
matugenAvailable = (code === 0)
console.log("Matugen in PATH:", matugenAvailable)
if (!matugenAvailable) {
console.warn("Matugen missing → dynamic theme disabled")
ToastService.wallpaperErrorStatus = "matugen_missing"
ToastService.showWarning("matugen not found - dynamic theming disabled")
return
}
// If extraction was requested, continue the process
if (extractionRequested) {
console.log("Continuing with color extraction")
fileChecker.running = true
}
}
}
Process {
id: fileChecker // exists & readable?
command: ["test", "-r", wallpaperPath]
onExited: (code) => {
if (code === 0) {
matugenProcess.running = true
} else {
console.error("code", code)
console.error("Wallpaper not found:", wallpaperPath)
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed")
}
}
}
/* ──────────────── matugen invocation ──────────────── */
Process {
id: matugenProcess
command: ["matugen", "-v", "image", wallpaperPath, "--json", "hex"]
/* ── grab stdout as a stream ── */
stdout: StdioCollector {
id: matugenCollector
onStreamFinished: {
const out = matugenCollector.text
if (!out.length) {
console.error("matugen produced zero bytes\nstderr:", matugenProcess.stderr)
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed")
return
}
try {
root.matugenJson = out
root.matugenColors = JSON.parse(out)
root.colorsUpdated()
ToastService.clearWallpaperError()
ToastService.showInfo("Dynamic theme colors updated")
} catch (e) {
console.error("JSON parse failed:", e)
ToastService.wallpaperErrorStatus = "error"
ToastService.showError("Wallpaper processing failed")
}
}
}
/* grab stderr too, so we can print it above */
stderr: StdioCollector { id: matugenErr }
}
/* ──────────────── wallpaper change monitor ──────────────── */
property string lastWallpaperTimestamp: ""
/* ──────────────── public helper ──────────────── */
// ──────────────── public helper ────────────────
function extractColors() {
console.log("Colors.extractColors() called, matugenAvailable:", matugenAvailable)
extractionRequested = true
console.log("Colors.extractColors() called, matugenAvailable:", matugenAvailable);
extractionRequested = true;
if (matugenAvailable)
fileChecker.running = true
fileChecker.running = true;
else
matugenCheck.running = true
matugenCheck.running = true;
}
function getMatugenColor(path, fallback) {
// Include colorUpdateTrigger in the function to make properties reactive to changes
colorUpdateTrigger // This dependency forces re-evaluation when colorUpdateTrigger changes
colorUpdateTrigger;
// Use light or dark colors based on Theme.isLightMode
const colorMode = (typeof Theme !== "undefined" && Theme.isLightMode) ? "light" : "dark"
let cur = matugenColors?.colors?.[colorMode]
const colorMode = (typeof Theme !== "undefined" && Theme.isLightMode) ? "light" : "dark";
let cur = matugenColors && matugenColors.colors && matugenColors.colors[colorMode];
for (const part of path.split(".")) {
if (!cur || typeof cur !== "object" || !(part in cur))
return fallback
cur = cur[part]
return fallback;
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) {
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,321 +1,314 @@
pragma Singleton
import QtQuick
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
// "auto", "wifi", "ethernet"
// Alphabetical tiebreaker
id: root
property int themeIndex: 0
property bool themeIsDynamic: false
property bool isLightMode: false
property real topBarTransparency: 0.75
property real popupTransparency: 0.92
property var recentlyUsedApps: []
// New global preferences
property bool use24HourClock: true
property bool useFahrenheit: false
property bool nightModeEnabled: false
property string profileImage: ""
property string weatherLocationOverride: "New York, NY"
// Widget visibility preferences for TopBar
property bool showFocusedWindow: true
property bool showWeather: true
property bool showWeather: true
property bool showMusic: true
property bool showClipboard: true
property bool showSystemResources: true
property bool showSystemTray: true
// View mode preferences for launchers
property string appLauncherViewMode: "list"
property string spotlightLauncherViewMode: "list"
// Network preference
property string networkPreference: "auto" // "auto", "wifi", "ethernet"
Component.onCompleted: loadSettings()
// Monitor system resources preference changes to control service monitoring
onShowSystemResourcesChanged: {
console.log("Prefs: System resources monitoring", showSystemResources ? "enabled" : "disabled")
// Control SystemMonitorService based on whether system monitor widgets are visible
if (typeof SystemMonitorService !== 'undefined') {
SystemMonitorService.enableTopBarMonitoring(showSystemResources)
property string networkPreference: "auto"
function loadSettings() {
parseSettings(settingsFile.text());
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content);
themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0;
themeIsDynamic = settings.themeIsDynamic !== undefined ? settings.themeIsDynamic : false;
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false;
topBarTransparency = settings.topBarTransparency !== undefined ? (settings.topBarTransparency > 1 ? settings.topBarTransparency / 100 : settings.topBarTransparency) : 0.75;
popupTransparency = settings.popupTransparency !== undefined ? (settings.popupTransparency > 1 ? settings.popupTransparency / 100 : settings.popupTransparency) : 0.92;
recentlyUsedApps = settings.recentlyUsedApps || [];
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
profileImage = settings.profileImage !== undefined ? settings.profileImage : "";
weatherLocationOverride = settings.weatherLocationOverride !== undefined ? settings.weatherLocationOverride : "New York, NY";
showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true;
showWeather = settings.showWeather !== undefined ? settings.showWeather : true;
showMusic = settings.showMusic !== undefined ? settings.showMusic : true;
showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true;
showSystemResources = settings.showSystemResources !== undefined ? settings.showSystemResources : true;
showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true;
appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list";
spotlightLauncherViewMode = settings.spotlightLauncherViewMode !== undefined ? settings.spotlightLauncherViewMode : "list";
networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto";
console.log("Loaded settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length);
applyStoredTheme();
} else {
console.log("Settings file is empty - applying default theme");
applyStoredTheme();
}
} catch (e) {
console.log("Could not parse settings, using defaults:", e);
applyStoredTheme();
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
"themeIndex": themeIndex,
"themeIsDynamic": themeIsDynamic,
"isLightMode": isLightMode,
"topBarTransparency": topBarTransparency,
"popupTransparency": popupTransparency,
"recentlyUsedApps": recentlyUsedApps,
"use24HourClock": use24HourClock,
"useFahrenheit": useFahrenheit,
"nightModeEnabled": nightModeEnabled,
"profileImage": profileImage,
"weatherLocationOverride": weatherLocationOverride,
"showFocusedWindow": showFocusedWindow,
"showWeather": showWeather,
"showMusic": showMusic,
"showClipboard": showClipboard,
"showSystemResources": showSystemResources,
"showSystemTray": showSystemTray,
"appLauncherViewMode": appLauncherViewMode,
"spotlightLauncherViewMode": spotlightLauncherViewMode,
"networkPreference": networkPreference
}, null, 2));
console.log("Saving settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length);
}
function applyStoredTheme() {
console.log("Applying stored theme:", themeIndex, themeIsDynamic, "lightMode:", isLightMode);
if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode;
Theme.switchTheme(themeIndex, themeIsDynamic, false);
} else {
Qt.callLater(() => {
if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode;
Theme.switchTheme(themeIndex, themeIsDynamic, false);
}
});
}
}
function setTheme(index, isDynamic) {
console.log("Prefs setTheme called - themeIndex:", index, "isDynamic:", isDynamic);
themeIndex = index;
themeIsDynamic = isDynamic;
saveSettings();
}
function setLightMode(lightMode) {
console.log("Prefs setLightMode called - isLightMode:", lightMode);
isLightMode = lightMode;
saveSettings();
}
function setTopBarTransparency(transparency) {
console.log("Prefs setTopBarTransparency called - topBarTransparency:", transparency);
topBarTransparency = transparency;
saveSettings();
}
function setPopupTransparency(transparency) {
console.log("Prefs setPopupTransparency called - popupTransparency:", transparency);
popupTransparency = transparency;
saveSettings();
}
function addRecentApp(app) {
if (!app)
return ;
var execProp = app.execString || app.exec || "";
if (!execProp)
return ;
var existingIndex = -1;
for (var i = 0; i < recentlyUsedApps.length; i++) {
if (recentlyUsedApps[i].exec === execProp) {
existingIndex = i;
break;
}
}
if (existingIndex >= 0) {
// App exists, increment usage count
recentlyUsedApps[existingIndex].usageCount = (recentlyUsedApps[existingIndex].usageCount || 1) + 1;
recentlyUsedApps[existingIndex].lastUsed = Date.now();
} else {
// New app, create entry
var appData = {
"name": app.name || "",
"exec": execProp,
"icon": app.icon || "application-x-executable",
"comment": app.comment || "",
"usageCount": 1,
"lastUsed": Date.now()
};
recentlyUsedApps.push(appData);
}
// Sort by usage count (descending), then alphabetically by name
var sortedApps = recentlyUsedApps.sort(function(a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount;
// Higher usage count first
return a.name.localeCompare(b.name);
});
// Limit to 10 apps
if (sortedApps.length > 10)
sortedApps = sortedApps.slice(0, 10);
// Reassign to trigger property change signal
recentlyUsedApps = sortedApps;
saveSettings();
}
function getRecentApps() {
return recentlyUsedApps;
}
// New preference setters
function setClockFormat(use24Hour) {
console.log("Prefs setClockFormat called - use24HourClock:", use24Hour);
use24HourClock = use24Hour;
saveSettings();
}
function setTemperatureUnit(fahrenheit) {
console.log("Prefs setTemperatureUnit called - useFahrenheit:", fahrenheit);
useFahrenheit = fahrenheit;
saveSettings();
}
function setNightModeEnabled(enabled) {
console.log("Prefs setNightModeEnabled called - nightModeEnabled:", enabled);
nightModeEnabled = enabled;
saveSettings();
}
function setProfileImage(imageUrl) {
console.log("Prefs setProfileImage called - profileImage:", imageUrl);
profileImage = imageUrl;
saveSettings();
}
// Widget visibility setters
function setShowFocusedWindow(enabled) {
console.log("Prefs setShowFocusedWindow called - showFocusedWindow:", enabled);
showFocusedWindow = enabled;
saveSettings();
}
function setShowWeather(enabled) {
console.log("Prefs setShowWeather called - showWeather:", enabled);
showWeather = enabled;
saveSettings();
}
function setShowMusic(enabled) {
console.log("Prefs setShowMusic called - showMusic:", enabled);
showMusic = enabled;
saveSettings();
}
function setShowClipboard(enabled) {
console.log("Prefs setShowClipboard called - showClipboard:", enabled);
showClipboard = enabled;
saveSettings();
}
function setShowSystemResources(enabled) {
console.log("Prefs setShowSystemResources called - showSystemResources:", enabled);
showSystemResources = enabled;
saveSettings();
}
function setShowSystemTray(enabled) {
console.log("Prefs setShowSystemTray called - showSystemTray:", enabled);
showSystemTray = enabled;
saveSettings();
}
// View mode setters
function setAppLauncherViewMode(mode) {
console.log("Prefs setAppLauncherViewMode called - appLauncherViewMode:", mode);
appLauncherViewMode = mode;
saveSettings();
}
function setSpotlightLauncherViewMode(mode) {
console.log("Prefs setSpotlightLauncherViewMode called - spotlightLauncherViewMode:", mode);
spotlightLauncherViewMode = mode;
saveSettings();
}
// Weather location override setter
function setWeatherLocationOverride(location) {
console.log("Prefs setWeatherLocationOverride called - weatherLocationOverride:", location);
weatherLocationOverride = location;
saveSettings();
}
// Network preference setter
function setNetworkPreference(preference) {
console.log("Prefs setNetworkPreference called - networkPreference:", preference);
networkPreference = preference;
saveSettings();
}
Component.onCompleted: loadSettings()
// Monitor system resources preference changes to control service monitoring
onShowSystemResourcesChanged: {
console.log("Prefs: System resources monitoring", showSystemResources ? "enabled" : "disabled");
// Control SystemMonitorService based on whether system monitor widgets are visible
if (typeof SystemMonitorService !== 'undefined')
SystemMonitorService.enableTopBarMonitoring(showSystemResources);
}
FileView {
id: settingsFile
path: StandardPaths.writableLocation(StandardPaths.ConfigLocation) + "/DankMaterialShell/settings.json"
blockLoading: true
blockWrites: true
watchChanges: true
onLoaded: {
console.log("Settings file loaded successfully")
parseSettings(settingsFile.text())
console.log("Settings file loaded successfully");
parseSettings(settingsFile.text());
}
onLoadFailed: (error) => {
console.log("Settings file not found, using defaults. Error:", error)
applyStoredTheme()
console.log("Settings file not found, using defaults. Error:", error);
applyStoredTheme();
}
}
function loadSettings() {
parseSettings(settingsFile.text())
}
function parseSettings(content) {
try {
if (content && content.trim()) {
var settings = JSON.parse(content)
themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0
themeIsDynamic = settings.themeIsDynamic !== undefined ? settings.themeIsDynamic : false
isLightMode = settings.isLightMode !== undefined ? settings.isLightMode : false
topBarTransparency = settings.topBarTransparency !== undefined ?
(settings.topBarTransparency > 1 ? settings.topBarTransparency / 100.0 : settings.topBarTransparency) : 0.75
popupTransparency = settings.popupTransparency !== undefined ?
(settings.popupTransparency > 1 ? settings.popupTransparency / 100.0 : settings.popupTransparency) : 0.92
recentlyUsedApps = settings.recentlyUsedApps || []
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
profileImage = settings.profileImage !== undefined ? settings.profileImage : ""
weatherLocationOverride = settings.weatherLocationOverride !== undefined ? settings.weatherLocationOverride : "New York, NY"
showFocusedWindow = settings.showFocusedWindow !== undefined ? settings.showFocusedWindow : true
showWeather = settings.showWeather !== undefined ? settings.showWeather : true
showMusic = settings.showMusic !== undefined ? settings.showMusic : true
showClipboard = settings.showClipboard !== undefined ? settings.showClipboard : true
showSystemResources = settings.showSystemResources !== undefined ? settings.showSystemResources : true
showSystemTray = settings.showSystemTray !== undefined ? settings.showSystemTray : true
appLauncherViewMode = settings.appLauncherViewMode !== undefined ? settings.appLauncherViewMode : "list"
spotlightLauncherViewMode = settings.spotlightLauncherViewMode !== undefined ? settings.spotlightLauncherViewMode : "list"
networkPreference = settings.networkPreference !== undefined ? settings.networkPreference : "auto"
console.log("Loaded settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length)
applyStoredTheme()
} else {
console.log("Settings file is empty - applying default theme")
applyStoredTheme()
}
} catch (e) {
console.log("Could not parse settings, using defaults:", e)
applyStoredTheme()
}
}
function saveSettings() {
settingsFile.setText(JSON.stringify({
themeIndex,
themeIsDynamic,
isLightMode,
topBarTransparency,
popupTransparency,
recentlyUsedApps,
use24HourClock,
useFahrenheit,
nightModeEnabled,
profileImage,
weatherLocationOverride,
showFocusedWindow,
showWeather,
showMusic,
showClipboard,
showSystemResources,
showSystemTray,
appLauncherViewMode,
spotlightLauncherViewMode,
networkPreference
}, null, 2))
console.log("Saving settings - themeIndex:", themeIndex, "isDynamic:", themeIsDynamic, "lightMode:", isLightMode, "transparency:", topBarTransparency, "recentApps:", recentlyUsedApps.length)
}
function applyStoredTheme() {
console.log("Applying stored theme:", themeIndex, themeIsDynamic, "lightMode:", isLightMode)
if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode
Theme.switchTheme(themeIndex, themeIsDynamic, false)
} else {
Qt.callLater(() => {
if (typeof Theme !== "undefined") {
Theme.isLightMode = isLightMode
Theme.switchTheme(themeIndex, themeIsDynamic, false)
}
})
}
}
function setTheme(index, isDynamic) {
console.log("Prefs setTheme called - themeIndex:", index, "isDynamic:", isDynamic)
themeIndex = index
themeIsDynamic = isDynamic
saveSettings()
}
function setLightMode(lightMode) {
console.log("Prefs setLightMode called - isLightMode:", lightMode)
isLightMode = lightMode
saveSettings()
}
function setTopBarTransparency(transparency) {
console.log("Prefs setTopBarTransparency called - topBarTransparency:", transparency)
topBarTransparency = transparency
saveSettings()
}
function setPopupTransparency(transparency) {
console.log("Prefs setPopupTransparency called - popupTransparency:", transparency)
popupTransparency = transparency
saveSettings()
}
function addRecentApp(app) {
if (!app) return
var execProp = app.execString || app.exec || ""
if (!execProp) return
var existingIndex = -1
for (var i = 0; i < recentlyUsedApps.length; i++) {
if (recentlyUsedApps[i].exec === execProp) {
existingIndex = i
break
}
}
if (existingIndex >= 0) {
// App exists, increment usage count
recentlyUsedApps[existingIndex].usageCount = (recentlyUsedApps[existingIndex].usageCount || 1) + 1
recentlyUsedApps[existingIndex].lastUsed = Date.now()
} else {
// New app, create entry
var appData = {
name: app.name || "",
exec: execProp,
icon: app.icon || "application-x-executable",
comment: app.comment || "",
usageCount: 1,
lastUsed: Date.now()
}
recentlyUsedApps.push(appData)
}
// Sort by usage count (descending), then alphabetically by name
var sortedApps = recentlyUsedApps.sort(function(a, b) {
if (a.usageCount !== b.usageCount) {
return b.usageCount - a.usageCount // Higher usage count first
}
return a.name.localeCompare(b.name) // Alphabetical tiebreaker
})
// Limit to 10 apps
if (sortedApps.length > 10) {
sortedApps = sortedApps.slice(0, 10)
}
// Reassign to trigger property change signal
recentlyUsedApps = sortedApps
saveSettings()
}
function getRecentApps() {
return recentlyUsedApps
}
// New preference setters
function setClockFormat(use24Hour) {
console.log("Prefs setClockFormat called - use24HourClock:", use24Hour)
use24HourClock = use24Hour
saveSettings()
}
function setTemperatureUnit(fahrenheit) {
console.log("Prefs setTemperatureUnit called - useFahrenheit:", fahrenheit)
useFahrenheit = fahrenheit
saveSettings()
}
function setNightModeEnabled(enabled) {
console.log("Prefs setNightModeEnabled called - nightModeEnabled:", enabled)
nightModeEnabled = enabled
saveSettings()
}
function setProfileImage(imageUrl) {
console.log("Prefs setProfileImage called - profileImage:", imageUrl)
profileImage = imageUrl
saveSettings()
}
// Widget visibility setters
function setShowFocusedWindow(enabled) {
console.log("Prefs setShowFocusedWindow called - showFocusedWindow:", enabled)
showFocusedWindow = enabled
saveSettings()
}
function setShowWeather(enabled) {
console.log("Prefs setShowWeather called - showWeather:", enabled)
showWeather = enabled
saveSettings()
}
function setShowMusic(enabled) {
console.log("Prefs setShowMusic called - showMusic:", enabled)
showMusic = enabled
saveSettings()
}
function setShowClipboard(enabled) {
console.log("Prefs setShowClipboard called - showClipboard:", enabled)
showClipboard = enabled
saveSettings()
}
function setShowSystemResources(enabled) {
console.log("Prefs setShowSystemResources called - showSystemResources:", enabled)
showSystemResources = enabled
saveSettings()
}
function setShowSystemTray(enabled) {
console.log("Prefs setShowSystemTray called - showSystemTray:", enabled)
showSystemTray = enabled
saveSettings()
}
// View mode setters
function setAppLauncherViewMode(mode) {
console.log("Prefs setAppLauncherViewMode called - appLauncherViewMode:", mode)
appLauncherViewMode = mode
saveSettings()
}
function setSpotlightLauncherViewMode(mode) {
console.log("Prefs setSpotlightLauncherViewMode called - spotlightLauncherViewMode:", mode)
spotlightLauncherViewMode = mode
saveSettings()
}
// Weather location override setter
function setWeatherLocationOverride(location) {
console.log("Prefs setWeatherLocationOverride called - weatherLocationOverride:", location)
weatherLocationOverride = location
saveSettings()
}
// Network preference setter
function setNetworkPreference(preference) {
console.log("Prefs setNetworkPreference called - networkPreference:", preference)
networkPreference = preference
saveSettings()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +1,59 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import Quickshell.Services.UPower
PanelWindow {
id: root
property bool batteryPopupVisible: false
function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined")
return false;
return PowerProfiles.profile === profile;
}
function setProfile(profile) {
if (typeof PowerProfiles === "undefined") {
errorToast.show();
return ;
}
PowerProfiles.profile = profile;
if (PowerProfiles.profile !== profile)
errorToast.show();
else
console.log("Set power profile to: " + PowerProfile.toString(profile));
}
visible: batteryPopupVisible
implicitWidth: 400
implicitHeight: 300
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
// Click outside to dismiss overlay
MouseArea {
anchors.fill: parent
onClicked: {
batteryPopupVisible = false
batteryPopupVisible = false;
}
}
Rectangle {
width: Math.min(380, parent.width - Theme.spacingL * 2)
height: Math.min(450, parent.height - Theme.barHeight - Theme.spacingS * 2)
@@ -47,45 +63,31 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: batteryPopupVisible ? 1.0 : 0.0
scale: batteryPopupVisible ? 1.0 : 0.85
opacity: batteryPopupVisible ? 1 : 0
scale: batteryPopupVisible ? 1 : 0.85
// Prevent click-through to background
MouseArea {
// Consume the click to prevent it from reaching the background
anchors.fill: parent
onClicked: {
// Consume the click to prevent it from reaching the background
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
ScrollView {
anchors.fill: parent
anchors.margins: Theme.spacingL
clip: true
Column {
width: parent.width
spacing: Theme.spacingL
// Header
Row {
width: parent.width
Text {
text: BatteryService.batteryAvailable ? "Battery Information" : "Power Management"
font.pixelSize: Theme.fontSizeLarge
@@ -93,15 +95,18 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 200; height: 1 }
Item {
width: parent.width - 200
height: 1
}
Rectangle {
width: 32
height: 32
radius: 16
color: closeBatteryArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "close"
@@ -109,19 +114,22 @@ PanelWindow {
font.pixelSize: Theme.iconSize - 4
color: closeBatteryArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeBatteryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
batteryPopupVisible = false
batteryPopupVisible = false;
}
}
}
}
Rectangle {
width: parent.width
height: 80
@@ -130,72 +138,88 @@ PanelWindow {
border.color: BatteryService.isCharging ? Theme.primary : (BatteryService.isLowBattery ? Theme.error : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12))
border.width: BatteryService.isCharging || BatteryService.isLowBattery ? 2 : 1
visible: BatteryService.batteryAvailable
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingL
Text {
text: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingM
Text {
text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeLarge
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
font.weight: Font.Bold
}
Text {
text: BatteryService.batteryStatus
font.pixelSize: Theme.fontSizeMedium
color: {
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: {
let time = BatteryService.formatTimeRemaining()
if (time !== "Unknown") {
return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time
}
return ""
let time = BatteryService.formatTimeRemaining();
if (time !== "Unknown")
return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time;
return "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
// No battery info card
Rectangle {
width: parent.width
@@ -205,11 +229,11 @@ PanelWindow {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
visible: !BatteryService.batteryAvailable
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
Text {
text: Theme.getBatteryIcon(0, false, false)
font.family: Theme.iconFont
@@ -217,125 +241,131 @@ PanelWindow {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Text {
text: "No Battery Detected"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: "Power profile management is available"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
}
// Battery details
Column {
width: parent.width
spacing: Theme.spacingM
visible: BatteryService.batteryAvailable
Text {
text: "Battery Details"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingXL
// Health
Column {
spacing: 2
width: (parent.width - Theme.spacingXL) / 2
Text {
text: "Health"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Medium
}
Text {
text: BatteryService.batteryHealth
font.pixelSize: Theme.fontSizeMedium
color: {
if (BatteryService.batteryHealth === "N/A") return Theme.surfaceText
var healthNum = parseInt(BatteryService.batteryHealth)
return healthNum < 80 ? Theme.error : Theme.surfaceText
if (BatteryService.batteryHealth === "N/A")
return Theme.surfaceText;
var healthNum = parseInt(BatteryService.batteryHealth);
return healthNum < 80 ? Theme.error : Theme.surfaceText;
}
}
}
// Capacity
Column {
spacing: 2
width: (parent.width - Theme.spacingXL) / 2
Text {
text: "Capacity"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Medium
}
Text {
text: BatteryService.batteryCapacity > 0 ? BatteryService.batteryCapacity.toFixed(1) + " Wh" : "Unknown"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
}
}
}
}
// Power profiles
Column {
width: parent.width
spacing: Theme.spacingM
visible: true
Text {
text: "Power Profile"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Column {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: (typeof PowerProfiles !== "undefined") ?
[PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) :
[PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
model: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: profileArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(batteryControlPopup.isActiveProfile(modelData) ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: profileArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (batteryControlPopup.isActiveProfile(modelData) ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: batteryControlPopup.isActiveProfile(modelData) ? Theme.primary : "transparent"
border.width: 2
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: Theme.getPowerProfileIcon(PowerProfile.toString(modelData))
font.family: Theme.iconFont
@@ -343,41 +373,47 @@ PanelWindow {
color: batteryControlPopup.isActiveProfile(modelData) ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: Theme.getPowerProfileLabel(PowerProfile.toString(modelData))
font.pixelSize: Theme.fontSizeMedium
color: batteryControlPopup.isActiveProfile(modelData) ? Theme.primary : Theme.surfaceText
font.weight: batteryControlPopup.isActiveProfile(modelData) ? Font.Medium : Font.Normal
}
Text {
text: Theme.getPowerProfileDescription(PowerProfile.toString(modelData))
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
MouseArea {
id: profileArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
batteryControlPopup.setProfile(modelData)
batteryControlPopup.setProfile(modelData);
}
}
}
}
}
}
// Degradation reason warning
Rectangle {
width: parent.width
@@ -387,13 +423,13 @@ PanelWindow {
border.color: Theme.error
border.width: 2
visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "warning"
font.family: Theme.iconFont
@@ -401,33 +437,61 @@ PanelWindow {
color: Theme.error
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: "Power Profile Degradation"
font.pixelSize: Theme.fontSizeMedium
color: Theme.error
font.weight: Font.Medium
}
Text {
text: (typeof PowerProfiles !== "undefined") ? PerformanceDegradationReason.toString(PowerProfiles.degradationReason) : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8)
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Error toast
Rectangle {
id: errorToast
function show() {
visible = true;
hideTimer.restart();
}
width: Math.min(300, parent.width - Theme.spacingL * 2)
height: 50
radius: Theme.cornerRadius
@@ -437,7 +501,7 @@ PanelWindow {
anchors.topMargin: Theme.spacingL
visible: false
z: 1000
Text {
anchors.centerIn: parent
text: "power-profiles-daemon not available"
@@ -445,37 +509,14 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
Timer {
id: hideTimer
interval: 3000
onTriggered: errorToast.visible = false
}
function show() {
visible = true
hideTimer.restart()
}
}
function isActiveProfile(profile) {
if (typeof PowerProfiles === "undefined") return false
return PowerProfiles.profile === profile
}
function setProfile(profile) {
if (typeof PowerProfiles === "undefined") {
errorToast.show()
return
}
PowerProfiles.profile = profile
if (PowerProfiles.profile !== profile) {
errorToast.show()
} else {
console.log("Set power profile to: " + PowerProfile.toString(profile))
}
}
}
}

View File

@@ -1,76 +1,100 @@
import QtQuick
import Quickshell.Services.UPower
import qs.Common
import qs.Services
import Quickshell.Services.UPower
Rectangle {
id: batteryWidget
property bool batteryPopupVisible: false
signal toggleBatteryPopup()
width: BatteryService.batteryAvailable ? 70 : 40
height: 30
radius: Theme.cornerRadius
color: batteryArea.containsMouse || batteryPopupVisible ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: batteryArea.containsMouse || batteryPopupVisible ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
visible: true
Row {
anchors.centerIn: parent
spacing: 4
Text {
text: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
color: {
if (!BatteryService.batteryAvailable) return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
if (!BatteryService.batteryAvailable)
return Theme.surfaceText;
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
SequentialAnimation on opacity {
running: BatteryService.isCharging
loops: Animation.Infinite
NumberAnimation { to: 0.6; duration: 1000; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1.0; duration: 1000; easing.type: Easing.InOutQuad }
NumberAnimation {
to: 0.6
duration: 1000
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1
duration: 1000
easing.type: Easing.InOutQuad
}
}
}
Text {
text: BatteryService.batteryLevel + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: {
if (!BatteryService.batteryAvailable) return Theme.surfaceText
if (BatteryService.isLowBattery && !BatteryService.isCharging) return Theme.error
if (BatteryService.isCharging) return Theme.primary
return Theme.surfaceText
if (!BatteryService.batteryAvailable)
return Theme.surfaceText;
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging)
return Theme.primary;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
}
}
MouseArea {
id: batteryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
toggleBatteryPopup()
toggleBatteryPopup();
}
}
// Tooltip on hover
Rectangle {
id: batteryTooltip
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
@@ -78,57 +102,63 @@ Rectangle {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
visible: batteryArea.containsMouse && !batteryPopupVisible
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
opacity: batteryArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
opacity: batteryArea.containsMouse ? 1 : 0
Column {
anchors.centerIn: parent
spacing: 2
Text {
id: tooltipText
text: {
if (!BatteryService.batteryAvailable) {
if (typeof PowerProfiles === "undefined") return "Power Management"
switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "Power Profile: Power Saver"
case PowerProfile.Performance: return "Power Profile: Performance"
default: return "Power Profile: Balanced"
if (typeof PowerProfiles === "undefined")
return "Power Management";
switch (PowerProfiles.profile) {
case PowerProfile.PowerSaver:
return "Power Profile: Power Saver";
case PowerProfile.Performance:
return "Power Profile: Performance";
default:
return "Power Profile: Balanced";
}
}
let status = BatteryService.batteryStatus
let level = BatteryService.batteryLevel + "%"
let time = BatteryService.formatTimeRemaining()
if (time !== "Unknown") {
return status + " • " + level + " • " + time
} else {
return status + " • " + level
}
let status = BatteryService.batteryStatus;
let level = BatteryService.batteryLevel + "%";
let time = BatteryService.formatTimeRemaining();
if (time !== "Unknown")
return status + " • " + level + " • " + time;
else
return status + " • " + level;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}

View File

@@ -6,63 +6,61 @@ import qs.Services
Column {
id: calendarWidget
property date displayDate: new Date()
property date selectedDate: new Date()
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable)
return ;
// Calculate date range with padding
let firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
let dayOfWeek = firstDay.getDay();
let startDate = new Date(firstDay);
startDate.setDate(startDate.getDate() - dayOfWeek - 7); // Extra week padding
let lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0);
let endDate = new Date(lastDay);
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7); // Extra week padding
CalendarService.loadEvents(startDate, endDate);
}
spacing: Theme.spacingM
// Load events when display date changes
onDisplayDateChanged: {
loadEventsForMonth()
loadEventsForMonth();
}
Component.onCompleted: {
console.log("CalendarWidget: Component completed, CalendarService available:", !!CalendarService);
if (CalendarService)
console.log("CalendarWidget: khal available:", CalendarService.khalAvailable);
loadEventsForMonth();
}
// Load events when calendar service becomes available
Connections {
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable)
loadEventsForMonth();
}
target: CalendarService
enabled: CalendarService !== null
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable) {
loadEventsForMonth()
}
}
}
Component.onCompleted: {
console.log("CalendarWidget: Component completed, CalendarService available:", !!CalendarService)
if (CalendarService) {
console.log("CalendarWidget: khal available:", CalendarService.khalAvailable)
}
loadEventsForMonth()
}
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) return
// Calculate date range with padding
let firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
let dayOfWeek = firstDay.getDay()
let startDate = new Date(firstDay)
startDate.setDate(startDate.getDate() - dayOfWeek - 7) // Extra week padding
let lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0)
let endDate = new Date(lastDay)
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7) // Extra week padding
CalendarService.loadEvents(startDate, endDate)
}
// Month navigation header
Row {
width: parent.width
height: 40
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "chevron_left"
@@ -71,21 +69,22 @@ Column {
color: Theme.primary
font.weight: Theme.iconFontWeight
}
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() - 1)
displayDate = newDate
let newDate = new Date(displayDate);
newDate.setMonth(newDate.getMonth() - 1);
displayDate = newDate;
}
}
}
Text {
width: parent.width - 80
height: 40
@@ -96,13 +95,13 @@ Column {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "chevron_right"
@@ -111,35 +110,37 @@ Column {
color: Theme.primary
font.weight: Theme.iconFontWeight
}
MouseArea {
id: nextMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(displayDate)
newDate.setMonth(newDate.getMonth() + 1)
displayDate = newDate
let newDate = new Date(displayDate);
newDate.setMonth(newDate.getMonth() + 1);
displayDate = newDate;
}
}
}
}
// Days of week header
Row {
width: parent.width
height: 32
Repeater {
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Rectangle {
width: parent.width / 7
height: 32
color: "transparent"
Text {
anchors.centerIn: parent
text: modelData
@@ -147,61 +148,59 @@ Column {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
// Calendar grid
Grid {
property date firstDay: {
let date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
let dayOfWeek = date.getDay();
date.setDate(date.getDate() - dayOfWeek);
return date;
}
width: parent.width
height: 200 // Fixed height for calendar
columns: 7
rows: 6
property date firstDay: {
let date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
let dayOfWeek = date.getDay()
date.setDate(date.getDate() - dayOfWeek)
return date
}
Repeater {
model: 42
Rectangle {
width: parent.width / 7
height: parent.height / 6
property date dayDate: {
let date = new Date(parent.firstDay)
date.setDate(date.getDate() + index)
return date
let date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
property bool isCurrentMonth: dayDate.getMonth() === displayDate.getMonth()
property bool isToday: dayDate.toDateString() === new Date().toDateString()
property bool isSelected: dayDate.toDateString() === selectedDate.toDateString()
color: isSelected ? Theme.primary :
isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
width: parent.width / 7
height: parent.height / 6
color: isSelected ? Theme.primary : isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadiusSmall
Text {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeMedium
color: isSelected ? Theme.surface :
isToday ? Theme.primary :
isCurrentMonth ? Theme.surfaceText :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
}
// Event indicator - full-width elegant bar
Rectangle {
// Use a lighter tint of primary for selected state
id: eventIndicator
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
@@ -209,65 +208,67 @@ Column {
height: 3
radius: 1.5
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
// Dynamic color based on state with opacity
color: {
if (isSelected) {
// Use a lighter tint of primary for selected state
return Qt.lighter(Theme.primary, 1.3)
} else if (isToday) {
return Theme.primary
} else {
return Theme.primary
}
if (isSelected)
return Qt.lighter(Theme.primary, 1.3);
else if (isToday)
return Theme.primary;
else
return Theme.primary;
}
opacity: {
if (isSelected) {
return 0.9
} else if (isToday) {
return 0.8
} else {
return 0.6
}
if (isSelected)
return 0.9;
else if (isToday)
return 0.8;
else
return 0.6;
}
// Subtle animation on hover
scale: dayArea.containsMouse ? 1.05 : 1.0
scale: dayArea.containsMouse ? 1.05 : 1
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedDate = dayDate
selectedDate = dayDate;
}
}
}
}
}
}
}

View File

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

View File

@@ -7,17 +7,26 @@ import qs.Services
// Events widget for selected date - Material Design 3 style
Rectangle {
id: eventsWidget
property date selectedDate: new Date()
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
onSelectedDateEventsChanged: {
console.log("EventsWidget: selectedDateEvents changed, count:", selectedDateEvents.length)
eventsList.model = selectedDateEvents
}
property bool shouldShow: CalendarService && CalendarService.khalAvailable
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
let events = CalendarService.getEventsForDate(selectedDate);
console.log("EventsWidget: Updating events for", Qt.formatDate(selectedDate, "yyyy-MM-dd"), "found", events.length, "events");
selectedDateEvents = events;
} else {
selectedDateEvents = [];
}
}
onSelectedDateEventsChanged: {
console.log("EventsWidget: selectedDateEvents changed, count:", selectedDateEvents.length);
eventsList.model = selectedDateEvents;
}
width: parent.width
height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0
radius: Theme.cornerRadiusLarge
@@ -25,64 +34,39 @@ Rectangle {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
visible: shouldShow
// Material elevation shadow
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.25
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
// Update events when selected date or events change
Connections {
target: CalendarService
enabled: CalendarService !== null
function onEventsByDateChanged() {
updateSelectedDateEvents()
}
function onKhalAvailableChanged() {
updateSelectedDateEvents()
}
}
Component.onCompleted: {
updateSelectedDateEvents()
updateSelectedDateEvents();
}
onSelectedDateChanged: {
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 = []
// Update events when selected date or events change
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents();
}
function onKhalAvailableChanged() {
updateSelectedDateEvents();
}
target: CalendarService
enabled: CalendarService !== null
}
// Header - always visible when widget is shown
Row {
id: headerRow
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
Text {
text: "event"
font.family: Theme.iconFont
@@ -90,19 +74,17 @@ Rectangle {
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: hasEvents ?
(Qt.formatDate(selectedDate, "MMM d") + " • " +
(selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) :
Qt.formatDate(selectedDate, "MMM d")
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
// No events placeholder - centered in entire widget (not just content area)
Column {
anchors.centerIn: parent
@@ -116,7 +98,7 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: "No events"
font.pixelSize: Theme.fontSizeMedium
@@ -124,11 +106,13 @@ Rectangle {
font.weight: Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Events list - positioned below header when there are events
ListView {
id: eventsList
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
@@ -136,45 +120,44 @@ Rectangle {
anchors.margins: Theme.spacingL
anchors.topMargin: Theme.spacingM
visible: opacity > 0
opacity: hasEvents ? 1.0 : 0.0
opacity: hasEvents ? 1 : 0
clip: true
spacing: Theme.spacingS
boundsMovement: Flickable.StopAtBounds
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {
policy: eventsList.contentHeight > eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
delegate: Rectangle {
width: eventsList.width
height: eventContent.implicitHeight + Theme.spacingM
radius: Theme.cornerRadius
color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06)
}
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06)
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06);
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
}
return "transparent"
if (modelData.url && eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
else if (eventMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
return "transparent";
}
border.width: 1
// Event indicator strip
Rectangle {
width: 4
@@ -186,123 +169,151 @@ Rectangle {
color: Theme.primary
opacity: 0.8
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL + 4
anchors.rightMargin: Theme.spacingM
spacing: 6
Text {
width: parent.width
text: modelData.title
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Item {
width: parent.width
height: Math.max(timeRow.height, locationRow.height)
Row {
id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Text {
text: "schedule"
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: {
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)
}
}
}
Text {
width: parent.width
text: modelData.title
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Item {
width: parent.width
height: Math.max(timeRow.height, locationRow.height)
Row {
id: timeRow
spacing: 4
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Text {
text: "schedule"
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: {
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 {
id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
console.warn("Couldn't open", modelData.url)
}
if (Qt.openUrlExternally(modelData.url) === false)
console.warn("Couldn't open", modelData.url);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
layer.effect: MultiEffect {
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

@@ -8,43 +8,444 @@ import qs.Services
Rectangle {
id: mediaPlayerWidget
property MprisPlayer activePlayer: MprisController.activePlayer
property string lastValidTitle: ""
property string lastValidArtist: ""
property string lastValidAlbum: ""
property string lastValidArtUrl: ""
Timer {
id: clearCacheTimer
interval: 2000
onTriggered: {
if (!activePlayer) {
lastValidTitle = ""
lastValidArtist = ""
lastValidAlbum = ""
lastValidArtUrl = ""
}
}
property real currentPosition: 0
// Simple progress ratio calculation
function ratio() {
return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0;
}
onActivePlayerChanged: {
if (!activePlayer) {
clearCacheTimer.restart()
} else {
clearCacheTimer.stop()
}
if (!activePlayer)
clearCacheTimer.restart();
else
clearCacheTimer.stop();
}
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
Timer {
id: clearCacheTimer
interval: 2000
onTriggered: {
if (!activePlayer) {
lastValidTitle = "";
lastValidArtist = "";
lastValidAlbum = "";
lastValidArtUrl = "";
}
}
}
// Updates progress bar every 2 seconds when playing
Timer {
id: positionTimer
interval: 2000
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.length > 0 && !progressMouseArea.isSeeking
repeat: true
onTriggered: {
if (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking)
currentPosition = activePlayer.position;
}
}
// Backend events
Connections {
function onPositionChanged() {
if (!progressMouseArea.isSeeking)
currentPosition = activePlayer.position;
}
function onPostTrackChanged() {
currentPosition = activePlayer && activePlayer.position || 0;
}
function onTrackTitleChanged() {
currentPosition = activePlayer && activePlayer.position || 0;
}
target: activePlayer
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
// Placeholder when no media - centered in entire widget
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "")
Text {
text: "music_note"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: "No Media Playing"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Active content in a column
Column {
anchors.fill: parent
spacing: Theme.spacingS
visible: activePlayer && activePlayer.trackTitle !== "" || lastValidTitle !== ""
// Normal media info when playing
Row {
width: parent.width
height: 60
spacing: Theme.spacingM
// Album Art
Rectangle {
width: 60
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
id: albumArt
anchors.fill: parent
source: activePlayer && activePlayer.trackArtUrl || lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer && activePlayer.trackArtUrl)
lastValidArtUrl = activePlayer.trackArtUrl;
}
fillMode: Image.PreserveAspectCrop
smooth: true
}
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready
color: "transparent"
Text {
anchors.centerIn: parent
text: "album"
font.family: Theme.iconFont
font.pixelSize: 28
color: Theme.surfaceVariantText
}
}
}
}
// Track Info
Column {
width: parent.width - 60 - Theme.spacingM
height: parent.height
spacing: Theme.spacingXS
Text {
text: activePlayer && activePlayer.trackTitle || lastValidTitle || "Unknown Track"
onTextChanged: {
if (activePlayer && activePlayer.trackTitle)
lastValidTitle = activePlayer.trackTitle;
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: activePlayer && activePlayer.trackArtist || lastValidArtist || "Unknown Artist"
onTextChanged: {
if (activePlayer && activePlayer.trackArtist)
lastValidArtist = activePlayer.trackArtist;
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
}
Text {
text: activePlayer && activePlayer.trackAlbum || lastValidAlbum || ""
onTextChanged: {
if (activePlayer && activePlayer.trackAlbum)
lastValidAlbum = activePlayer.trackAlbum;
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Progress bar
Item {
id: progressBarContainer
width: parent.width
height: 24
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
visible: activePlayer !== null
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: parent.width * ratio()
Behavior on width {
NumberAnimation {
duration: 100
}
}
}
// 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 {
id: progressMouseArea
property bool isSeeking: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
preventStealing: true
onPressed: function(mouse) {
isSeeking = true;
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onReleased: {
isSeeking = false;
}
onPositionChanged: function(mouse) {
if (pressed && isSeeking && activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
onClicked: function(mouse) {
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
let seekPosition = ratio * activePlayer.length;
activePlayer.position = seekPosition;
currentPosition = seekPosition;
}
}
}
// 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
Item {
width: parent.width
height: 32
visible: activePlayer !== null
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
height: parent.height
// Previous button
Rectangle {
width: 28
height: 28
radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: Theme.iconFont
font.pixelSize: 16
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();
}
}
}
}
// 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()
}
}
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
@@ -53,383 +454,5 @@ Rectangle {
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
property real currentPosition: 0
// Simple progress ratio calculation
function ratio() {
return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0
}
// Updates progress bar every 2 seconds when playing
Timer {
id: positionTimer
interval: 2000
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && activePlayer.length > 0 && !progressMouseArea.isSeeking
repeat: true
onTriggered: {
if (activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking) {
currentPosition = activePlayer.position
}
}
}
// Backend events
Connections {
target: activePlayer
function onPositionChanged() {
if (!progressMouseArea.isSeeking) {
currentPosition = activePlayer.position
}
}
function onPostTrackChanged() {
currentPosition = activePlayer?.position || 0
}
function onTrackTitleChanged() {
currentPosition = activePlayer?.position || 0
}
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
// Placeholder when no media - centered in entire widget
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "")
Text {
text: "music_note"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize + 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: "No Media Playing"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Active content in a column
Column {
anchors.fill: parent
spacing: Theme.spacingS
visible: activePlayer && activePlayer.trackTitle !== "" || lastValidTitle !== ""
// Normal media info when playing
Row {
width: parent.width
height: 60
spacing: Theme.spacingM
// Album Art
Rectangle {
width: 60
height: 60
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
id: albumArt
anchors.fill: parent
source: activePlayer?.trackArtUrl || lastValidArtUrl || ""
onSourceChanged: {
if (activePlayer?.trackArtUrl) {
lastValidArtUrl = activePlayer.trackArtUrl;
}
}
fillMode: Image.PreserveAspectCrop
smooth: true
}
Rectangle {
anchors.fill: parent
visible: albumArt.status !== Image.Ready
color: "transparent"
Text {
anchors.centerIn: parent
text: "album"
font.family: Theme.iconFont
font.pixelSize: 28
color: Theme.surfaceVariantText
}
}
}
}
// Track Info
Column {
width: parent.width - 60 - Theme.spacingM
height: parent.height
spacing: Theme.spacingXS
Text {
text: activePlayer?.trackTitle || lastValidTitle || "Unknown Track"
onTextChanged: {
if (activePlayer?.trackTitle) {
lastValidTitle = activePlayer.trackTitle;
}
}
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: activePlayer?.trackArtist || lastValidArtist || "Unknown Artist"
onTextChanged: {
if (activePlayer?.trackArtist) {
lastValidArtist = activePlayer.trackArtist;
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
width: parent.width
elide: Text.ElideRight
}
Text {
text: activePlayer?.trackAlbum || lastValidAlbum || ""
onTextChanged: {
if (activePlayer?.trackAlbum) {
lastValidAlbum = activePlayer.trackAlbum;
}
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Progress bar
Item {
id: progressBarContainer
width: parent.width
height: 24
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
visible: activePlayer !== null
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: progressFill
height: parent.height
radius: parent.radius
color: Theme.primary
width: parent.width * ratio()
Behavior on width {
NumberAnimation { duration: 100 }
}
}
// 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.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
}
}
MouseArea {
id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
preventStealing: true
property bool isSeeking: false
onPressed: function(mouse) {
isSeeking = true
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
}
}
}
// 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
Item {
width: parent.width
height: 32
visible: activePlayer !== null
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
height: parent.height
// Previous button
Rectangle {
width: 28
height: 28
radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: Theme.iconFont
font.pixelSize: 16
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()
}
}
}
}
// 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()
}
}
}
}
}
}
}
}

View File

@@ -6,31 +6,21 @@ import qs.Services
Rectangle {
id: weatherWidget
width: parent.width
height: parent.height
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
// Placeholder when no weather - centered in entire widget
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
Text {
text: "cloud_off"
font.family: Theme.iconFont
@@ -38,31 +28,32 @@ Rectangle {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: "No Weather Data"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Weather content when available - original Column structure
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingS
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
// Weather header info
Item {
width: parent.width
height: 60
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
// Weather icon
Text {
text: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
@@ -71,44 +62,53 @@ Rectangle {
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
Text {
text: (Prefs.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + (Prefs.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Light
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: if (WeatherService.weather.available) Prefs.setTemperatureUnit(!Prefs.useFahrenheit)
onClicked: {
if (WeatherService.weather.available)
Prefs.setTemperatureUnit(!Prefs.useFahrenheit);
}
enabled: WeatherService.weather.available
}
}
Text {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
// Weather details grid
Grid {
columns: 2
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: Theme.spacingXS
Text {
text: "humidity_low"
font.family: Theme.iconFont
@@ -116,16 +116,19 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
Text {
text: "air"
font.family: Theme.iconFont
@@ -133,16 +136,19 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
Text {
text: "wb_twilight"
font.family: Theme.iconFont
@@ -150,16 +156,19 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: WeatherService.weather.sunrise || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: Theme.spacingXS
Text {
text: "bedtime"
font.family: Theme.iconFont
@@ -167,13 +176,27 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: WeatherService.weather.sunset || "--"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 2
shadowBlur: 0.5
shadowColor: Qt.rgba(0, 0, 0, 0.1)
shadowOpacity: 0.1
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,15 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
Item {
id: audioTab
property int audioSubTab: 0 // 0: Output, 1: Input
readonly property real volumeLevel: AudioService.volumeLevel
readonly property real micLevel: AudioService.micLevel
readonly property bool volumeMuted: AudioService.sinkMuted
@@ -19,23 +18,23 @@ Item {
readonly property string currentAudioSource: AudioService.currentAudioSource
readonly property var audioSinks: AudioService.audioSinks
readonly property var audioSources: AudioService.audioSources
Column {
anchors.fill: parent
spacing: Theme.spacingM
// Audio Sub-tabs
Row {
width: parent.width
height: 40
spacing: 2
Rectangle {
width: parent.width / 2 - 1
height: parent.height
radius: Theme.cornerRadius
color: audioTab.audioSubTab === 0 ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Text {
anchors.centerIn: parent
text: "Output"
@@ -43,21 +42,22 @@ Item {
color: audioTab.audioSubTab === 0 ? Theme.primaryText : Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: audioTab.audioSubTab = 0
}
}
Rectangle {
width: parent.width / 2 - 1
height: parent.height
radius: Theme.cornerRadius
color: audioTab.audioSubTab === 1 ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Text {
anchors.centerIn: parent
text: "Input"
@@ -65,163 +65,174 @@ Item {
color: audioTab.audioSubTab === 1 ? Theme.primaryText : Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: audioTab.audioSubTab = 1
}
}
}
// Output Tab Content
ScrollView {
width: parent.width
height: parent.height - 48
visible: audioTab.audioSubTab === 0
clip: true
Column {
width: parent.width
spacing: Theme.spacingL
// Volume Control
Column {
width: parent.width
spacing: Theme.spacingM
Text {
text: "Volume"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingM
Text {
text: audioTab.volumeMuted ? "volume_off" : "volume_down"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: audioTab.volumeMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMute()
}
}
Item {
id: volumeSliderContainer
width: parent.width - 80
height: 32
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: volumeSliderTrack
width: parent.width
height: 8
radius: 4
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: volumeSliderFill
width: parent.width * (audioTab.volumeLevel / 100)
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation { duration: 100 }
NumberAnimation {
duration: 100
}
}
}
// Draggable handle
Rectangle {
id: volumeHandle
width: 18
height: 18
radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width/2))
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1.0
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation { duration: 150 }
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
id: volumeMouseArea
property bool isDragging: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
property bool isDragging: false
onPressed: (mouse) => {
isDragging = true
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume);
}
onReleased: {
isDragging = false
isDragging = false;
}
onPositionChanged: (mouse) => {
if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume);
}
}
onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume);
}
}
// Global mouse area for drag tracking
MouseArea {
id: volumeGlobalMouseArea
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
enabled: volumeMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (volumeMouseArea.isDragging) {
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width));
let newVolume = Math.round(ratio * 100);
AudioService.setVolume(newVolume);
}
}
onReleased: {
volumeMouseArea.isDragging = false
volumeMouseArea.isDragging = false;
}
}
}
Text {
text: "volume_up"
font.family: Theme.iconFont
@@ -229,21 +240,23 @@ Item {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Output Devices
Column {
width: parent.width
spacing: Theme.spacingM
Text {
text: "Output Device"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
// Current device indicator
Rectangle {
width: parent.width
@@ -253,250 +266,270 @@ Item {
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 1
visible: audioTab.currentAudioSink !== ""
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Text {
text: "check_circle"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4
color: Theme.primary
}
Text {
text: "Current: " + (AudioService.currentSinkDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
}
}
}
// Real audio devices
Repeater {
model: audioTab.audioSinks
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.active ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.active ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.active ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: {
if (modelData.name.includes("bluez")) return "headset"
else if (modelData.name.includes("hdmi")) return "tv"
else if (modelData.name.includes("usb")) return "headset"
else return "speaker"
if (modelData.name.includes("bluez"))
return "headset";
else if (modelData.name.includes("hdmi"))
return "tv";
else if (modelData.name.includes("usb"))
return "headset";
else
return "speaker";
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: modelData.active ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.displayName
font.pixelSize: Theme.fontSizeMedium
color: modelData.active ? Theme.primary : Theme.surfaceText
font.weight: modelData.active ? Font.Medium : Font.Normal
}
Text {
text: {
if (modelData.subtitle && modelData.subtitle !== "") {
return modelData.subtitle + (modelData.active ? " • Selected" : "")
} else {
return modelData.active ? "Selected" : ""
}
if (modelData.subtitle && modelData.subtitle !== "")
return modelData.subtitle + (modelData.active ? " • Selected" : "");
else
return modelData.active ? "Selected" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: deviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
AudioService.setAudioSink(modelData.name)
AudioService.setAudioSink(modelData.name);
}
}
}
}
}
}
}
// Input Tab Content
ScrollView {
width: parent.width
height: parent.height - 48
visible: audioTab.audioSubTab === 1
clip: true
Column {
width: parent.width
spacing: Theme.spacingL
// Microphone Level Control
Column {
width: parent.width
spacing: Theme.spacingM
Text {
text: "Microphone Level"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: Theme.spacingM
Text {
text: audioTab.micMuted ? "mic_off" : "mic"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: audioTab.micMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMicMute()
}
}
Item {
id: micSliderContainer
width: parent.width - 80
height: 32
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: micSliderTrack
width: parent.width
height: 8
radius: 4
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: micSliderFill
width: parent.width * (audioTab.micLevel / 100)
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation { duration: 100 }
NumberAnimation {
duration: 100
}
}
}
// Draggable handle
Rectangle {
id: micHandle
width: 18
height: 18
radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width/2))
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1.0
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
Behavior on scale {
NumberAnimation { duration: 150 }
NumberAnimation {
duration: 150
}
}
}
}
MouseArea {
id: micMouseArea
property bool isDragging: false
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
property bool isDragging: false
onPressed: (mouse) => {
isDragging = true
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel);
}
onReleased: {
isDragging = false
isDragging = false;
}
onPositionChanged: (mouse) => {
if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel);
}
}
onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel);
}
}
// Global mouse area for drag tracking
MouseArea {
id: micGlobalMouseArea
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
enabled: micMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (micMouseArea.isDragging) {
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width));
let newMicLevel = Math.round(ratio * 100);
AudioService.setMicLevel(newMicLevel);
}
}
onReleased: {
micMouseArea.isDragging = false
micMouseArea.isDragging = false;
}
}
}
Text {
text: "mic"
font.family: Theme.iconFont
@@ -504,22 +537,23 @@ Item {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Input Devices
Column {
width: parent.width
spacing: Theme.spacingM
Text {
text: "Input Device"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
// Current device indicator
Rectangle {
width: parent.width
@@ -529,100 +563,112 @@ Item {
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 1
visible: audioTab.currentAudioSource !== ""
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Text {
text: "check_circle"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4
color: Theme.primary
}
Text {
text: "Current: " + (AudioService.currentSourceDisplayName || "None")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
}
}
}
// Real audio input devices
Repeater {
model: audioTab.audioSources
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: sourceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.active ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: sourceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.active ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.active ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: {
if (modelData.name.includes("bluez")) return "headset_mic"
else if (modelData.name.includes("usb")) return "headset_mic"
else return "mic"
if (modelData.name.includes("bluez"))
return "headset_mic";
else if (modelData.name.includes("usb"))
return "headset_mic";
else
return "mic";
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: modelData.active ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.displayName
font.pixelSize: Theme.fontSizeMedium
color: modelData.active ? Theme.primary : Theme.surfaceText
font.weight: modelData.active ? Font.Medium : Font.Normal
}
Text {
text: {
if (modelData.subtitle && modelData.subtitle !== "") {
return modelData.subtitle + (modelData.active ? " • Selected" : "")
} else {
return modelData.active ? "Selected" : ""
}
if (modelData.subtitle && modelData.subtitle !== "")
return modelData.subtitle + (modelData.active ? " • Selected" : "");
else
return modelData.active ? "Selected" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text !== ""
}
}
}
MouseArea {
id: sourceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
AudioService.setAudioSource(modelData.name)
AudioService.setAudioSource(modelData.name);
}
}
}
}
}
}
}
}
}
}

View File

@@ -1,41 +1,39 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import Quickshell.Bluetooth
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
Item {
id: bluetoothTab
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width
spacing: Theme.spacingL
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
(BluetoothService.adapter && BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (BluetoothService.adapter && BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : "transparent"
border.width: 2
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "bluetooth"
font.family: Theme.iconFont
@@ -43,68 +41,72 @@ Item {
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: "Bluetooth"
font.pixelSize: Theme.fontSizeLarge
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: BluetoothService.adapter && BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
MouseArea {
id: bluetoothToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BluetoothService.toggleAdapter()
BluetoothService.toggleAdapter();
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Text {
text: "Paired Devices"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Repeater {
model: BluetoothService.adapter && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter(dev => dev && dev.paired && BluetoothService.isValidDevice(dev)) : []
model: BluetoothService.adapter && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter((dev) => {
return dev && dev.paired && BluetoothService.isValidDevice(dev);
}) : []
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: BluetoothService.getDeviceIcon(modelData)
font.family: Theme.iconFont
@@ -112,59 +114,59 @@ Item {
color: modelData.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Text {
text: modelData.connected ? "Connected" : "Disconnected"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
Text {
text: {
if (modelData.batteryAvailable && modelData.battery > 0) {
return "• " + Math.round(modelData.battery * 100) + "%"
}
var btBattery = BatteryService.bluetoothDevices.find(dev =>
dev.name === (modelData.name || modelData.deviceName) ||
dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) ||
(modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase())
)
return btBattery ? "• " + btBattery.percentage + "%" : ""
if (modelData.batteryAvailable && modelData.battery > 0)
return "• " + Math.round(modelData.battery * 100) + "%";
var btBattery = BatteryService.bluetoothDevices.find((dev) => {
return dev.name === (modelData.name || modelData.deviceName) || dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || (modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase());
});
return btBattery ? "• " + btBattery.percentage + "%" : "";
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
}
}
Rectangle {
id: btMenuButton
width: 32
height: 32
radius: Theme.cornerRadius
color: btMenuButtonArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
color: btMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
Text {
text: "more_vert"
font.family: Theme.iconFont
@@ -174,50 +176,57 @@ Item {
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: btMenuButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
bluetoothContextMenuWindow.deviceData = modelData
let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height)
bluetoothContextMenuWindow.show(localPos.x, localPos.y)
bluetoothContextMenuWindow.deviceData = modelData;
let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height);
bluetoothContextMenuWindow.show(localPos.x, localPos.y);
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
ColorAnimation {
duration: Theme.shortDuration
}
}
}
MouseArea {
id: btDeviceArea
anchors.fill: parent
anchors.rightMargin: 40
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BluetoothService.debugDevice(modelData)
BluetoothService.toggle(modelData.address)
BluetoothService.debugDevice(modelData);
BluetoothService.toggle(modelData.address);
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Row {
width: parent.width
spacing: Theme.spacingM
Text {
text: "Available Devices"
font.pixelSize: Theme.fontSizeLarge
@@ -225,9 +234,12 @@ Item {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { width: 1; height: 1 }
Item {
width: 1
height: 1
}
Rectangle {
width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2)
height: 36
@@ -235,11 +247,11 @@ Item {
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Theme.primary
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
font.family: Theme.iconFont
@@ -247,112 +259,140 @@ Item {
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: scanText
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Stop Scanning" : "Start Scanning"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.adapter && BluetoothService.adapter.discovering) {
BluetoothService.stopScan()
} else {
BluetoothService.startScan()
}
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
BluetoothService.stopScan();
else
BluetoothService.startScan();
}
}
}
}
Repeater {
model: BluetoothService.availableDevices
Rectangle {
property bool canPair: BluetoothService.canPair(modelData)
property string pairingStatus: BluetoothService.getPairingStatus(modelData)
width: parent.width
height: 70
radius: Theme.cornerRadius
color: {
if (availableDeviceArea.containsMouse) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
if (modelData.pairing) return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
if (modelData.blocked) return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
if (availableDeviceArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
if (modelData.pairing)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
if (modelData.blocked)
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: {
if (modelData.pairing) return Theme.warning
if (modelData.blocked) return Theme.error
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
}
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: BluetoothService.getDeviceIcon(modelData)
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: {
if (modelData.pairing) return Theme.warning
if (modelData.blocked) return Theme.error
return Theme.surfaceText
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name || modelData.deviceName
font.pixelSize: Theme.fontSizeMedium
color: {
if (modelData.pairing) return Theme.warning
if (modelData.blocked) return Theme.error
return Theme.surfaceText
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Theme.surfaceText;
}
font.weight: modelData.pairing ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Row {
spacing: Theme.spacingS
Text {
text: {
switch (pairingStatus) {
case "pairing": return "Pairing..."
case "blocked": return "Blocked"
default: return BluetoothService.getSignalStrength(modelData)
case "pairing":
return "Pairing...";
case "blocked":
return "Blocked";
default:
return BluetoothService.getSignalStrength(modelData);
}
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (modelData.pairing) return Theme.warning
if (modelData.blocked) return Theme.error
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
if (modelData.pairing)
return Theme.warning;
if (modelData.blocked)
return Theme.error;
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
}
}
Text {
text: BluetoothService.getSignalIcon(modelData)
font.family: Theme.iconFont
@@ -360,18 +400,22 @@ Item {
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: modelData.rssi !== undefined && modelData.rssi !== 0 && pairingStatus === "available"
}
Text {
text: (modelData.rssi !== undefined && modelData.rssi !== 0) ? modelData.rssi + "dBm" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: modelData.rssi !== undefined && modelData.rssi !== 0 && pairingStatus === "available"
}
}
}
}
}
Rectangle {
width: 80
height: 28
@@ -380,74 +424,85 @@ Item {
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: {
if (!canPair && !modelData.pairing) return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
if (actionButtonArea.containsMouse) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
return "transparent"
if (!canPair && !modelData.pairing)
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
if (actionButtonArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
return "transparent";
}
border.color: canPair || modelData.pairing ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
opacity: canPair || modelData.pairing ? 1.0 : 0.5
opacity: canPair || modelData.pairing ? 1 : 0.5
Text {
anchors.centerIn: parent
text: {
if (modelData.pairing) return "Pairing..."
if (modelData.blocked) return "Blocked"
return "Pair"
if (modelData.pairing)
return "Pairing...";
if (modelData.blocked)
return "Blocked";
return "Pair";
}
font.pixelSize: Theme.fontSizeSmall
color: canPair || modelData.pairing ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
font.weight: Font.Medium
}
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canPair ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: canPair
onClicked: {
if (canPair) {
BluetoothService.pair(modelData.address)
}
if (canPair)
BluetoothService.pair(modelData.address);
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90 // Don't overlap with action button
anchors.rightMargin: 90 // Don't overlap with action button
hoverEnabled: true
cursorShape: canPair ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: canPair
onClicked: {
if (canPair) {
BluetoothService.pair(modelData.address)
}
if (canPair)
BluetoothService.pair(modelData.address);
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.discovering && BluetoothService.availableDevices.length === 0
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Text {
text: "sync"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
@@ -455,8 +510,9 @@ Item {
to: 360
duration: 2000
}
}
Text {
text: "Scanning for devices..."
font.pixelSize: Theme.fontSizeLarge
@@ -464,16 +520,18 @@ Item {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: "Make sure your device is in pairing mode"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Text {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pixelSize: Theme.fontSizeMedium
@@ -483,15 +541,39 @@ Item {
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
}
}
Rectangle {
id: bluetoothContextMenuWindow
property var deviceData: null
property bool menuVisible: false
function show(x, y) {
const menuWidth = 160;
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
let finalX = x - menuWidth / 2;
let finalY = y;
finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth));
finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight));
bluetoothContextMenuWindow.x = finalX;
bluetoothContextMenuWindow.y = finalY;
bluetoothContextMenuWindow.visible = true;
bluetoothContextMenuWindow.menuVisible = true;
}
function hide() {
bluetoothContextMenuWindow.menuVisible = false;
Qt.callLater(() => {
bluetoothContextMenuWindow.visible = false;
});
}
visible: false
width: 160
height: menuColumn.implicitHeight + Theme.spacingS * 2
@@ -500,7 +582,9 @@ Item {
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 1000
opacity: menuVisible ? 1 : 0
scale: menuVisible ? 1 : 0.85
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
@@ -511,42 +595,26 @@ Item {
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
opacity: menuVisible ? 1.0 : 0.0
scale: menuVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Text {
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "link_off" : "link"
font.family: Theme.iconFont
@@ -555,7 +623,7 @@ Item {
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
@@ -563,56 +631,60 @@ Item {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow.deviceData) {
BluetoothService.toggle(bluetoothContextMenuWindow.deviceData.address)
}
bluetoothContextMenuWindow.hide()
if (bluetoothContextMenuWindow.deviceData)
BluetoothService.toggle(bluetoothContextMenuWindow.deviceData.address);
bluetoothContextMenuWindow.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Text {
text: "delete"
font.family: Theme.iconFont
@@ -621,7 +693,7 @@ Item {
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Forget Device"
font.pixelSize: Theme.fontSizeSmall
@@ -629,60 +701,60 @@ Item {
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow.deviceData) {
BluetoothService.forget(bluetoothContextMenuWindow.deviceData.address)
}
bluetoothContextMenuWindow.hide()
if (bluetoothContextMenuWindow.deviceData)
BluetoothService.forget(bluetoothContextMenuWindow.deviceData.address);
bluetoothContextMenuWindow.hide();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
function show(x, y) {
const menuWidth = 160
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x - menuWidth / 2
let finalY = y
finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth))
finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight))
bluetoothContextMenuWindow.x = finalX
bluetoothContextMenuWindow.y = finalY
bluetoothContextMenuWindow.visible = true
bluetoothContextMenuWindow.menuVisible = true
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
function hide() {
bluetoothContextMenuWindow.menuVisible = false
Qt.callLater(() => { bluetoothContextMenuWindow.visible = false })
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
visible: bluetoothContextMenuWindow.visible
onClicked: {
bluetoothContextMenuWindow.hide()
bluetoothContextMenuWindow.hide();
}
MouseArea {
x: bluetoothContextMenuWindow.x
y: bluetoothContextMenuWindow.y
@@ -691,5 +763,7 @@ Item {
onClicked: {
}
}
}
}
}

View File

@@ -2,43 +2,38 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
PanelWindow {
id: root
property bool controlCenterVisible: false
property string currentTab: "network" // "network", "audio", "bluetooth", "display"
property bool powerOptionsExpanded: false
visible: controlCenterVisible
onVisibleChanged: {
// Enable/disable WiFi auto-refresh based on control center visibility
WifiService.autoRefreshEnabled = visible && NetworkService.wifiEnabled
WifiService.autoRefreshEnabled = visible && NetworkService.wifiEnabled;
}
implicitWidth: 600
implicitHeight: 500
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
property string currentTab: "network" // "network", "audio", "bluetooth", "display"
property bool powerOptionsExpanded: false
Rectangle {
width: Math.min(600, Screen.width - Theme.spacingL * 2)
height: root.powerOptionsExpanded ? 570 : 500
@@ -48,50 +43,66 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: controlCenterVisible ? 1 : 0
// TopBar dropdown animation - optimized for performance
transform: [
Scale {
id: scaleTransform
origin.x: 600 // Use fixed width since popup is max 600px wide
origin.x: 600 // Use fixed width since popup is max 600px wide
origin.y: 0
xScale: controlCenterVisible ? 1.0 : 0.95
yScale: controlCenterVisible ? 1.0 : 0.8
xScale: controlCenterVisible ? 1 : 0.95
yScale: controlCenterVisible ? 1 : 0.8
},
Translate {
id: translateTransform
x: controlCenterVisible ? 0 : 15 // Slide slightly left when hidden
x: controlCenterVisible ? 0 : 15 // Slide slightly left when hidden
y: controlCenterVisible ? 0 : -30
}
]
// Single coordinated animation for better performance
states: [
State {
name: "visible"
when: controlCenterVisible
PropertyChanges { target: scaleTransform; xScale: 1.0; yScale: 1.0 }
PropertyChanges { target: translateTransform; x: 0; y: 0 }
PropertyChanges {
target: scaleTransform
xScale: 1
yScale: 1
}
PropertyChanges {
target: translateTransform
x: 0
y: 0
}
},
State {
name: "hidden"
when: !controlCenterVisible
PropertyChanges { target: scaleTransform; xScale: 0.95; yScale: 0.8 }
PropertyChanges { target: translateTransform; x: 15; y: -30 }
PropertyChanges {
target: scaleTransform
xScale: 0.95
yScale: 0.8
}
PropertyChanges {
target: translateTransform
x: 15
y: -30
}
}
]
// Power menu height animation
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration // Faster for height changes
easing.type: Theme.standardEasing
}
}
transitions: [
Transition {
from: "*"; to: "*"
from: "*"
to: "*"
ParallelAnimation {
NumberAnimation {
targets: [scaleTransform, translateTransform]
@@ -99,29 +110,22 @@ PanelWindow {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
]
opacity: controlCenterVisible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
// Elegant User Header
Column {
width: parent.width
spacing: Theme.spacingL
Rectangle {
width: parent.width
height: 90
@@ -129,22 +133,23 @@ PanelWindow {
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingL
// Profile Picture Container
Item {
id: avatarContainer
width: 64
height: 64
property bool hasImage: profileImageLoader.status === Image.Ready
width: 64
height: 64
// This rectangle provides the themed ring via its border.
Rectangle {
anchors.fill: parent
@@ -158,12 +163,15 @@ PanelWindow {
// Hidden Image loader. Its only purpose is to load the texture.
Image {
id: profileImageLoader
source: {
if (Prefs.profileImage === "") return ""
if (Prefs.profileImage.startsWith("/")) {
return "file://" + Prefs.profileImage
}
return Prefs.profileImage
if (Prefs.profileImage === "")
return "";
if (Prefs.profileImage.startsWith("/"))
return "file://" + Prefs.profileImage;
return Prefs.profileImage;
}
smooth: true
asynchronous: true
@@ -180,11 +188,12 @@ PanelWindow {
maskSource: circularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSpreadAtMin: 1
}
Item {
id: circularMask
width: 64 - 10
height: 64 - 10
layer.enabled: true
@@ -197,6 +206,7 @@ PanelWindow {
color: "black"
antialiasing: true
}
}
// Fallback for when there is no image.
@@ -213,6 +223,7 @@ PanelWindow {
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
}
}
// Error icon for when the image fails to load.
@@ -224,46 +235,46 @@ PanelWindow {
color: Theme.primaryText
visible: Prefs.profileImage !== "" && profileImageLoader.status === Image.Error
}
}
// User Info Text
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Text {
text: UserInfoService.fullName || UserInfoService.username || "User"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: "Uptime: " + (UserInfoService.uptime || "Unknown")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
font.weight: Font.Normal
}
}
}
// Action Buttons - Power and Settings
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingS
// Power Button
Rectangle {
width: 40
height: 40
radius: 20
color: powerButton.containsMouse || root.powerOptionsExpanded ?
Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
color: powerButton.containsMouse || root.powerOptionsExpanded ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Rectangle {
anchors.centerIn: parent
width: parent.width
@@ -271,65 +282,74 @@ PanelWindow {
radius: parent.radius
color: "transparent"
clip: true
Text {
anchors.centerIn: parent
text: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 2
color: powerButton.containsMouse || root.powerOptionsExpanded ? Theme.error : Theme.surfaceText
Behavior on text {
// Smooth icon transition
SequentialAnimation {
NumberAnimation {
target: parent
property: "opacity"
to: 0.0
to: 0
duration: Theme.shortDuration / 2
easing.type: Theme.standardEasing
}
PropertyAction { target: parent; property: "text" }
PropertyAction {
target: parent
property: "text"
}
NumberAnimation {
target: parent
property: "opacity"
to: 1.0
to: 1
duration: Theme.shortDuration / 2
easing.type: Theme.standardEasing
}
}
}
}
}
MouseArea {
id: powerButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = !root.powerOptionsExpanded
root.powerOptionsExpanded = !root.powerOptionsExpanded;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Settings Button
Rectangle {
width: 40
height: 40
radius: 20
color: settingsButton.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
color: settingsButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Text {
anchors.centerIn: parent
text: "settings"
@@ -337,29 +357,33 @@ PanelWindow {
font.pixelSize: Theme.iconSize - 2
color: Theme.surfaceText
}
MouseArea {
id: settingsButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
controlCenterVisible = false
settingsPopup.settingsVisible = true
controlCenterVisible = false;
settingsPopup.settingsVisible = true;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
// Animated Collapsible Power Options (optimized)
Rectangle {
width: parent.width
@@ -368,42 +392,25 @@ PanelWindow {
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: root.powerOptionsExpanded ? 1 : 0
opacity: root.powerOptionsExpanded ? 1.0 : 0.0
opacity: root.powerOptionsExpanded ? 1 : 0
clip: true
// Single coordinated animation for power options
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingL
visible: root.powerOptionsExpanded
// Logout
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: logoutButton.containsMouse ?
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
color: logoutButton.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: "logout"
font.family: Theme.iconFont
@@ -411,7 +418,7 @@ PanelWindow {
color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Logout"
font.pixelSize: Theme.fontSizeSmall
@@ -419,46 +426,47 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false
root.powerOptionsExpanded = false;
if (typeof root !== "undefined" && root.powerConfirmDialog) {
root.powerConfirmDialog.powerConfirmAction = "logout"
root.powerConfirmDialog.powerConfirmTitle = "Logout"
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to logout?"
root.powerConfirmDialog.powerConfirmVisible = true
root.powerConfirmDialog.powerConfirmAction = "logout";
root.powerConfirmDialog.powerConfirmTitle = "Logout";
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to logout?";
root.powerConfirmDialog.powerConfirmVisible = true;
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Reboot
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: rebootButton.containsMouse ?
Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
color: rebootButton.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: "restart_alt"
font.family: Theme.iconFont
@@ -466,7 +474,7 @@ PanelWindow {
color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Restart"
font.pixelSize: Theme.fontSizeSmall
@@ -474,46 +482,47 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false
root.powerOptionsExpanded = false;
if (typeof root !== "undefined" && root.powerConfirmDialog) {
root.powerConfirmDialog.powerConfirmAction = "reboot"
root.powerConfirmDialog.powerConfirmTitle = "Restart"
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to restart?"
root.powerConfirmDialog.powerConfirmVisible = true
root.powerConfirmDialog.powerConfirmAction = "reboot";
root.powerConfirmDialog.powerConfirmTitle = "Restart";
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to restart?";
root.powerConfirmDialog.powerConfirmVisible = true;
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Shutdown
Rectangle {
width: 100
height: 34
radius: Theme.cornerRadius
color: shutdownButton.containsMouse ?
Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
color: shutdownButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: "power_settings_new"
font.family: Theme.iconFont
@@ -521,7 +530,7 @@ PanelWindow {
color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Shutdown"
font.pixelSize: Theme.fontSizeSmall
@@ -529,77 +538,114 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: shutdownButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.powerOptionsExpanded = false
root.powerOptionsExpanded = false;
if (typeof root !== "undefined" && root.powerConfirmDialog) {
root.powerConfirmDialog.powerConfirmAction = "poweroff"
root.powerConfirmDialog.powerConfirmTitle = "Shutdown"
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to shutdown?"
root.powerConfirmDialog.powerConfirmVisible = true
root.powerConfirmDialog.powerConfirmAction = "poweroff";
root.powerConfirmDialog.powerConfirmTitle = "Shutdown";
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to shutdown?";
root.powerConfirmDialog.powerConfirmVisible = true;
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
// Single coordinated animation for power options
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// Tab buttons
Row {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: {
let tabs = [
{name: "Network", icon: "wifi", id: "network", available: true}
]
let tabs = [{
"name": "Network",
"icon": "wifi",
"id": "network",
"available": true
}];
// Always show audio
tabs.push({name: "Audio", icon: "volume_up", id: "audio", available: true})
tabs.push({
"name": "Audio",
"icon": "volume_up",
"id": "audio",
"available": true
});
// Show Bluetooth only if available
if (BluetoothService.available) {
tabs.push({name: "Bluetooth", icon: "bluetooth", id: "bluetooth", available: true})
}
if (BluetoothService.available)
tabs.push({
"name": "Bluetooth",
"icon": "bluetooth",
"id": "bluetooth",
"available": true
});
// Always show display
tabs.push({name: "Display", icon: "brightness_6", id: "display", available: true})
return tabs
tabs.push({
"name": "Display",
"icon": "brightness_6",
"id": "display",
"available": true
});
return tabs;
}
Rectangle {
property int tabCount: {
let count = 3 // Network + Audio + Display (always visible)
if (BluetoothService.available) count++
return count
let count = 3; // Network + Audio + Display (always visible)
if (BluetoothService.available)
count++;
return count;
}
width: (parent.width - Theme.spacingXS * (tabCount - 1)) / tabCount
height: 40
radius: Theme.cornerRadius
color: root.currentTab === modelData.id ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
tabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
color: root.currentTab === modelData.id ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : tabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: modelData.icon
font.family: Theme.iconFont
@@ -607,7 +653,7 @@ PanelWindow {
color: root.currentTab === modelData.id ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
@@ -615,75 +661,100 @@ PanelWindow {
font.weight: root.currentTab === modelData.id ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: tabArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentTab = modelData.id
root.currentTab = modelData.id;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
// Tab content area
Rectangle {
width: parent.width
height: root.powerOptionsExpanded ? 240 : 300
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.1)
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
// Network Tab
NetworkTab {
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: root.currentTab === "network"
}
// Audio Tab
AudioTab {
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: root.currentTab === "audio"
}
// Bluetooth Tab
BluetoothTab {
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: BluetoothService.available && root.currentTab === "bluetooth"
}
// Display Tab
DisplayTab {
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: root.currentTab === "display"
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
// Power menu height animation
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration // Faster for height changes
easing.type: Theme.standardEasing
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
// Click outside to close
@@ -691,7 +762,8 @@ PanelWindow {
anchors.fill: parent
z: -1
onClicked: {
controlCenterVisible = false
controlCenterVisible = false;
}
}
}
}

View File

@@ -1,79 +1,77 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
ScrollView {
id: displayTab
clip: true
Column {
width: parent.width
spacing: Theme.spacingL
// Brightness Control
Column {
width: parent.width
spacing: Theme.spacingM
visible: BrightnessService.brightnessAvailable
Text {
text: "Brightness"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
CustomSlider {
width: parent.width
value: BrightnessService.brightnessLevel
leftIcon: "brightness_low"
rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable
onSliderValueChanged: function(newValue) {
BrightnessService.setBrightness(newValue)
BrightnessService.setBrightness(newValue);
}
}
}
// Display settings
Column {
width: parent.width
spacing: Theme.spacingM
Text {
text: "Display Settings"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
// Mode toggles row (Night Mode + Light/Dark Mode)
Row {
width: parent.width
spacing: Theme.spacingM
// Night mode toggle
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Prefs.nightModeEnabled ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
(nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: Prefs.nightModeEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: Prefs.nightModeEnabled ? Theme.primary : "transparent"
border.width: Prefs.nightModeEnabled ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Text {
text: Prefs.nightModeEnabled ? "nightlight" : "dark_mode"
font.family: Theme.iconFont
@@ -81,7 +79,7 @@ ScrollView {
color: Prefs.nightModeEnabled ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: "Night Mode"
font.pixelSize: Theme.fontSizeMedium
@@ -89,43 +87,43 @@ ScrollView {
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: nightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (Prefs.nightModeEnabled) {
// Disable night mode - kill any running color temperature processes
nightModeDisableProcess.running = true
Prefs.setNightModeEnabled(false)
nightModeDisableProcess.running = true;
Prefs.setNightModeEnabled(false);
} else {
// Enable night mode using wlsunset or redshift
nightModeEnableProcess.running = true
Prefs.setNightModeEnabled(true)
nightModeEnableProcess.running = true;
Prefs.setNightModeEnabled(true);
}
}
}
}
// Light/Dark mode toggle
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadius
color: Theme.isLightMode ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
(lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
color: Theme.isLightMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: Theme.isLightMode ? Theme.primary : "transparent"
border.width: Theme.isLightMode ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Text {
text: Theme.isLightMode ? "light_mode" : "palette"
font.family: Theme.iconFont
@@ -133,7 +131,7 @@ ScrollView {
color: Theme.isLightMode ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: Theme.isLightMode ? "Light Mode" : "Dark Mode"
font.pixelSize: Theme.fontSizeMedium
@@ -141,53 +139,60 @@ ScrollView {
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: lightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.toggleLightMode()
Theme.toggleLightMode();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
// Night mode processes
Process {
id: nightModeEnableProcess
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Failed to enable night mode")
Prefs.setNightModeEnabled(false)
console.warn("Failed to enable night mode");
Prefs.setNightModeEnabled(false);
}
}
}
Process {
id: nightModeDisableProcess
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Failed to disable night mode")
}
if (exitCode !== 0)
console.warn("Failed to disable night mode");
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,54 @@
import "."
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import "."
Rectangle {
id: cpuWidget
property bool showPercentage: true
property bool showIcon: true
width: 55
height: 30
radius: Theme.cornerRadius
color: cpuArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: cpuArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
MouseArea {
id: cpuArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
ProcessMonitorService.setSortBy("cpu")
processListDropdown.toggle()
ProcessMonitorService.setSortBy("cpu");
processListDropdown.toggle();
}
}
Row {
anchors.centerIn: parent
spacing: 3
// CPU icon
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.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: {
if (SystemMonitorService.cpuUsage > 80) return Theme.error
if (SystemMonitorService.cpuUsage > 60) return Theme.warning
return Theme.surfaceText
if (SystemMonitorService.cpuUsage > 80)
return Theme.error;
if (SystemMonitorService.cpuUsage > 60)
return Theme.warning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
// Percentage text
Text {
text: (SystemMonitorService.cpuUsage || 0).toFixed(0) + "%"
@@ -55,5 +57,7 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}

View File

@@ -3,7 +3,7 @@ import qs.Common
Item {
id: slider
property int value: 50
property int minimum: 0
property int maximum: 100
@@ -12,16 +12,16 @@ Item {
property bool enabled: true
property string unit: "%"
property bool showValue: true
signal sliderValueChanged(int newValue)
signal sliderDragFinished(int finalValue)
height: 80
Column {
anchors.fill: parent
spacing: Theme.spacingM
// Value display
Text {
text: slider.value + slider.unit
@@ -31,12 +31,12 @@ Item {
visible: slider.showValue
anchors.horizontalCenter: parent.horizontalCenter
}
// Slider row
Row {
width: parent.width
spacing: Theme.spacingM
// Left icon
Text {
text: slider.leftIcon
@@ -46,53 +46,53 @@ Item {
anchors.verticalCenter: parent.verticalCenter
visible: slider.leftIcon.length > 0
}
// Slider track
Rectangle {
id: sliderTrack
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
height: 6
radius: 3
color: slider.enabled ?
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
color: slider.enabled ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.1)
anchors.verticalCenter: parent.verticalCenter
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
// Fill
Rectangle {
id: sliderFill
width: parent.width * ((slider.value - slider.minimum) / (slider.maximum - slider.minimum))
height: parent.height
radius: parent.radius
color: slider.enabled ? Theme.primary : Theme.surfaceVariantText
Behavior on width {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
}
// Draggable handle
Rectangle {
id: sliderHandle
width: 18
height: 18
radius: 9
color: slider.enabled ? Theme.primary : Theme.surfaceVariantText
border.color: slider.enabled ? Qt.lighter(Theme.primary, 1.3) : Qt.lighter(Theme.surfaceVariantText, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, sliderFill.width - width/2))
x: Math.max(0, Math.min(parent.width - width, sliderFill.width - width / 2))
anchors.verticalCenter: parent.verticalCenter
scale: sliderMouseArea.containsMouse || sliderMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
scale: sliderMouseArea.containsMouse || sliderMouseArea.pressed ? 1.2 : 1
// Handle glow effect when active
Rectangle {
anchors.centerIn: parent
@@ -103,91 +103,102 @@ Item {
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
visible: sliderMouseArea.containsMouse && slider.enabled
Behavior on opacity {
NumberAnimation { duration: 150 }
NumberAnimation {
duration: 150
}
}
}
Behavior on scale {
NumberAnimation {
duration: 150
}
}
}
Item {
id: sliderContainer
anchors.fill: parent
MouseArea {
id: sliderMouseArea
property bool isDragging: false
anchors.fill: parent
hoverEnabled: true
cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: slider.enabled
preventStealing: true
property bool isDragging: false
onPressed: (mouse) => {
if (slider.enabled) {
isDragging = true
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
isDragging = true;
let ratio = Math.max(0, Math.min(1, mouse.x / width));
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum));
slider.value = newValue;
slider.sliderValueChanged(newValue);
}
}
onReleased: {
if (slider.enabled) {
isDragging = false
slider.sliderDragFinished(slider.value)
isDragging = false;
slider.sliderDragFinished(slider.value);
}
}
onPositionChanged: (mouse) => {
if (pressed && isDragging && slider.enabled) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
let ratio = Math.max(0, Math.min(1, mouse.x / width));
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum));
slider.value = newValue;
slider.sliderValueChanged(newValue);
}
}
onClicked: (mouse) => {
if (slider.enabled) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
let ratio = Math.max(0, Math.min(1, mouse.x / width));
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum));
slider.value = newValue;
slider.sliderValueChanged(newValue);
}
}
}
// Global mouse area for drag tracking
MouseArea {
id: sliderGlobalMouseArea
anchors.fill: sliderContainer
enabled: sliderMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (sliderMouseArea.isDragging && slider.enabled) {
let globalPos = mapToItem(sliderTrack, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / sliderTrack.width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
let globalPos = mapToItem(sliderTrack, mouse.x, mouse.y);
let ratio = Math.max(0, Math.min(1, globalPos.x / sliderTrack.width));
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum));
slider.value = newValue;
slider.sliderValueChanged(newValue);
}
}
onReleased: {
if (sliderMouseArea.isDragging && slider.enabled) {
sliderMouseArea.isDragging = false
slider.sliderDragFinished(slider.value)
sliderMouseArea.isDragging = false;
slider.sliderDragFinished(slider.value);
}
}
}
}
}
// Right icon
Text {
text: slider.rightIcon
@@ -197,6 +208,9 @@ Item {
anchors.verticalCenter: parent.verticalCenter
visible: slider.rightIcon.length > 0
}
}
}
}
}

View File

@@ -1,13 +1,13 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
PanelWindow {
id: inputDialog
property bool dialogVisible: false
property string dialogTitle: "Input Required"
property string dialogSubtitle: "Please enter the required information"
@@ -16,68 +16,68 @@ PanelWindow {
property bool isPassword: false
property string confirmButtonText: "Confirm"
property string cancelButtonText: "Cancel"
signal confirmed(string value)
signal cancelled()
function showDialog(title, subtitle, placeholder, isPass, confirmText, cancelText) {
dialogTitle = title || "Input Required";
dialogSubtitle = subtitle || "Please enter the required information";
inputPlaceholder = placeholder || "Enter text";
isPassword = isPass || false;
confirmButtonText = confirmText || "Confirm";
cancelButtonText = cancelText || "Cancel";
inputValue = "";
dialogVisible = true;
}
function hideDialog() {
dialogVisible = false;
inputValue = "";
}
visible: dialogVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: dialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
onVisibleChanged: {
if (visible) {
textInput.forceActiveFocus();
textInput.text = inputValue;
}
}
anchors {
top: true
left: true
right: 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 {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: dialogVisible ? 1.0 : 0.0
opacity: dialogVisible ? 1 : 0
MouseArea {
anchors.fill: parent
onClicked: {
inputDialog.cancelled();
hideDialog();
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
MouseArea {
anchors.fill: parent
onClicked: {
inputDialog.cancelled()
hideDialog()
}
}
}
Rectangle {
width: Math.min(400, parent.width - Theme.spacingL * 2)
height: Math.min(250, parent.height - Theme.spacingL * 2)
@@ -86,44 +86,29 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: dialogVisible ? 1.0 : 0.0
scale: dialogVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: dialogVisible ? 1 : 0
scale: dialogVisible ? 1 : 0.9
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
// Header
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
Text {
text: dialogTitle
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: dialogSubtitle
font.pixelSize: Theme.fontSizeMedium
@@ -133,14 +118,15 @@ PanelWindow {
wrapMode: Text.WordWrap
maximumLineCount: 2
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: closeDialogArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "close"
@@ -148,20 +134,23 @@ PanelWindow {
font.pixelSize: Theme.iconSize - 4
color: closeDialogArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeDialogArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
inputDialog.cancelled()
hideDialog()
inputDialog.cancelled();
hideDialog();
}
}
}
}
// Text input
Rectangle {
width: parent.width
@@ -170,9 +159,10 @@ PanelWindow {
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
border.color: textInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: textInput.activeFocus ? 2 : 1
TextInput {
id: textInput
anchors.fill: parent
anchors.margins: Theme.spacingM
font.pixelSize: Theme.fontSizeMedium
@@ -181,7 +171,19 @@ PanelWindow {
verticalAlignment: TextInput.AlignVCenter
cursorVisible: activeFocus
selectByMouse: true
onTextChanged: {
inputValue = text;
}
onAccepted: {
inputDialog.confirmed(inputValue);
hideDialog();
}
Component.onCompleted: {
if (dialogVisible)
forceActiveFocus();
}
Text {
anchors.fill: parent
text: inputPlaceholder
@@ -190,48 +192,36 @@ PanelWindow {
verticalAlignment: Text.AlignVCenter
visible: parent.text.length === 0
}
onTextChanged: {
inputValue = text
}
onAccepted: {
inputDialog.confirmed(inputValue)
hideDialog()
}
Component.onCompleted: {
if (dialogVisible) {
forceActiveFocus()
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: {
textInput.forceActiveFocus()
textInput.forceActiveFocus();
}
}
}
// Show password checkbox (only visible for password inputs)
Row {
spacing: Theme.spacingS
visible: isPassword
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
border.width: 2
Text {
anchors.centerIn: parent
text: "check"
@@ -240,35 +230,37 @@ PanelWindow {
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
}
}
}
Text {
text: "Show password"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
// Buttons
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
@@ -276,66 +268,94 @@ PanelWindow {
color: cancelArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
Text {
id: cancelText
anchors.centerIn: parent
text: cancelButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
inputDialog.cancelled()
hideDialog()
inputDialog.cancelled();
hideDialog();
}
}
}
Rectangle {
width: Math.max(80, confirmText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: confirmArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: inputValue.length > 0
opacity: enabled ? 1.0 : 0.5
opacity: enabled ? 1 : 0.5
Text {
id: confirmText
anchors.centerIn: parent
text: confirmButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: confirmArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
inputDialog.confirmed(inputValue)
hideDialog()
inputDialog.confirmed(inputValue);
hideDialog();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,64 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
PanelWindow {
id: root
property bool powerConfirmVisible: false
property string powerConfirmAction: ""
property string powerConfirmTitle: ""
property string powerConfirmMessage: ""
function executePowerAction(action) {
console.log("Executing power action:", action);
let command = [];
switch (action) {
case "logout":
command = ["niri", "msg", "action", "quit", "-s"];
break;
case "suspend":
command = ["systemctl", "suspend"];
break;
case "reboot":
command = ["systemctl", "reboot"];
break;
case "poweroff":
command = ["systemctl", "poweroff"];
break;
}
if (command.length > 0) {
powerActionProcess.command = command;
powerActionProcess.running = true;
}
}
visible: powerConfirmVisible
implicitWidth: 400
implicitHeight: 300
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
// Darkened background
Rectangle {
anchors.fill: parent
color: "black"
opacity: 0.5
}
Rectangle {
width: Math.min(400, parent.width - Theme.spacingL * 2)
height: Math.min(200, parent.height - Theme.spacingL * 2)
@@ -47,45 +67,33 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: powerConfirmVisible ? 1.0 : 0.0
scale: powerConfirmVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: powerConfirmVisible ? 1 : 0
scale: powerConfirmVisible ? 1 : 0.9
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingL * 2
spacing: Theme.spacingL
// Title
Text {
text: powerConfirmTitle
font.pixelSize: Theme.fontSizeLarge
color: {
switch(powerConfirmAction) {
case "poweroff": return Theme.error
case "reboot": return Theme.warning
default: return Theme.surfaceText
switch (powerConfirmAction) {
case "poweroff":
return Theme.error;
case "reboot":
return Theme.warning;
default:
return Theme.surfaceText;
}
}
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
// Message
Text {
text: powerConfirmMessage
@@ -95,21 +103,23 @@ PanelWindow {
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
Item { height: Theme.spacingL }
Item {
height: Theme.spacingL
}
// Buttons
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
// Cancel button
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: cancelButton.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Text {
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
@@ -117,35 +127,41 @@ PanelWindow {
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false
powerConfirmVisible = false;
}
}
}
// Confirm button
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
let baseColor
switch(powerConfirmAction) {
case "poweroff": baseColor = Theme.error; break
case "reboot": baseColor = Theme.warning; break
default: baseColor = Theme.primary; break
let baseColor;
switch (powerConfirmAction) {
case "poweroff":
baseColor = Theme.error;
break;
case "reboot":
baseColor = Theme.warning;
break;
default:
baseColor = Theme.primary;
break;
}
return confirmButton.containsMouse ?
Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) :
baseColor
return confirmButton.containsMouse ? Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) : baseColor;
}
Text {
text: "Confirm"
font.pixelSize: Theme.fontSizeMedium
@@ -153,55 +169,52 @@ PanelWindow {
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerConfirmVisible = false
executePowerAction(powerConfirmAction)
powerConfirmVisible = false;
executePowerAction(powerConfirmAction);
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
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
}
}
Process {
id: powerActionProcess
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.error("Power action failed with exit code:", exitCode)
}
if (exitCode !== 0)
console.error("Power action failed with exit code:", exitCode);
}
}
}
}

View File

@@ -1,86 +1,69 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
PanelWindow {
id: root
property bool powerMenuVisible: false
visible: powerMenuVisible
implicitWidth: 400
implicitHeight: 320
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
// Click outside to dismiss overlay
MouseArea {
anchors.fill: parent
onClicked: {
powerMenuVisible = false
powerMenuVisible = false;
}
}
Rectangle {
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)
y: Theme.barHeight + Theme.spacingXS
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: powerMenuVisible ? 1.0 : 0.0
scale: powerMenuVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
// Prevent click-through to background
MouseArea {
// Consume the click to prevent it from reaching the background
anchors.fill: parent
onClicked: {
// Consume the click to prevent it from reaching the background
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
// Header
Row {
width: parent.width
Text {
text: "Power Options"
font.pixelSize: Theme.fontSizeLarge
@@ -88,15 +71,18 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 150; height: 1 }
Item {
width: parent.width - 150
height: 1
}
Rectangle {
width: 32
height: 32
radius: 16
color: closePowerArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "close"
@@ -104,37 +90,40 @@ PanelWindow {
font.pixelSize: Theme.iconSize - 4
color: closePowerArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closePowerArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
powerMenuVisible = false;
}
}
}
}
// Power options
Column {
width: parent.width
spacing: Theme.spacingS
// Log Out
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "logout"
font.family: Theme.iconFont
@@ -142,7 +131,7 @@ PanelWindow {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Log Out"
font.pixelSize: Theme.fontSizeMedium
@@ -150,36 +139,39 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "logout"
root.powerConfirmTitle = "Log Out"
root.powerConfirmMessage = "Are you sure you want to log out?"
root.powerConfirmVisible = true
powerMenuVisible = false;
root.powerConfirmAction = "logout";
root.powerConfirmTitle = "Log Out";
root.powerConfirmMessage = "Are you sure you want to log out?";
root.powerConfirmVisible = true;
}
}
}
// Suspend
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "bedtime"
font.family: Theme.iconFont
@@ -187,7 +179,7 @@ PanelWindow {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Suspend"
font.pixelSize: Theme.fontSizeMedium
@@ -195,36 +187,39 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "suspend"
root.powerConfirmTitle = "Suspend"
root.powerConfirmMessage = "Are you sure you want to suspend the system?"
root.powerConfirmVisible = true
powerMenuVisible = false;
root.powerConfirmAction = "suspend";
root.powerConfirmTitle = "Suspend";
root.powerConfirmMessage = "Are you sure you want to suspend the system?";
root.powerConfirmVisible = true;
}
}
}
// Reboot
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "restart_alt"
font.family: Theme.iconFont
@@ -232,7 +227,7 @@ PanelWindow {
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Reboot"
font.pixelSize: Theme.fontSizeMedium
@@ -240,36 +235,39 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "reboot"
root.powerConfirmTitle = "Reboot"
root.powerConfirmMessage = "Are you sure you want to reboot the system?"
root.powerConfirmVisible = true
powerMenuVisible = false;
root.powerConfirmAction = "reboot";
root.powerConfirmTitle = "Reboot";
root.powerConfirmMessage = "Are you sure you want to reboot the system?";
root.powerConfirmVisible = true;
}
}
}
// Power Off
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "power_settings_new"
font.family: Theme.iconFont
@@ -277,7 +275,7 @@ PanelWindow {
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Power Off"
font.pixelSize: Theme.fontSizeMedium
@@ -285,23 +283,46 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerConfirmAction = "poweroff"
root.powerConfirmTitle = "Power Off"
root.powerConfirmMessage = "Are you sure you want to power off the system?"
root.powerConfirmVisible = true
powerMenuVisible = false;
root.powerConfirmAction = "poweroff";
root.powerConfirmTitle = "Power Off";
root.powerConfirmMessage = "Are you sure you want to power off the system?";
root.powerConfirmVisible = true;
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +1,54 @@
import "."
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import "."
Rectangle {
id: ramWidget
property bool showPercentage: true
property bool showIcon: true
width: 55
height: 30
radius: Theme.cornerRadius
color: ramArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: ramArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
MouseArea {
id: ramArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
ProcessMonitorService.setSortBy("memory")
processListDropdown.toggle()
ProcessMonitorService.setSortBy("memory");
processListDropdown.toggle();
}
}
Row {
anchors.centerIn: parent
spacing: 3
// RAM icon
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.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: {
if (SystemMonitorService.memoryUsage > 90) return Theme.error
if (SystemMonitorService.memoryUsage > 75) return Theme.warning
return Theme.surfaceText
if (SystemMonitorService.memoryUsage > 90)
return Theme.error;
if (SystemMonitorService.memoryUsage > 75)
return Theme.warning;
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
// Percentage text
Text {
text: (SystemMonitorService.memoryUsage || 0).toFixed(0) + "%"
@@ -55,5 +57,7 @@ Rectangle {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}

View File

@@ -2,56 +2,55 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
PanelWindow {
id: settingsPopup
property bool settingsVisible: false
signal closingPopup()
onSettingsVisibleChanged: {
if (!settingsVisible) {
closingPopup()
}
if (!settingsVisible)
closingPopup();
}
visible: settingsVisible
implicitWidth: 600
implicitHeight: 700
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
// Darkened background
Rectangle {
anchors.fill: parent
color: "black"
opacity: 0.5
MouseArea {
anchors.fill: parent
onClicked: settingsPopup.settingsVisible = false
}
}
// Main settings panel - spotlight-like centered appearance
Rectangle {
id: mainPanel
width: Math.min(600, parent.width - Theme.spacingXL * 2)
height: Math.min(700, parent.height - Theme.spacingXL * 2)
anchors.centerIn: parent
@@ -59,35 +58,22 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
// Simple opacity and scale control tied directly to settingsVisible
opacity: settingsPopup.settingsVisible ? 1.0 : 0.0
scale: settingsPopup.settingsVisible ? 1.0 : 0.95
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: settingsPopup.settingsVisible ? 1 : 0
scale: settingsPopup.settingsVisible ? 1 : 0.95
// Add shadow effect
layer.enabled: true
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
// Header
Row {
width: parent.width
spacing: Theme.spacingM
Text {
text: "settings"
font.family: Theme.iconFont
@@ -95,7 +81,7 @@ PanelWindow {
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Settings"
font.pixelSize: Theme.fontSizeXLarge
@@ -103,21 +89,19 @@ PanelWindow {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
Item {
width: parent.width - 175 // Spacer to push close button to the right
height: 1
}
// Close button
Rectangle {
width: 32
height: 32
radius: Theme.cornerRadius
color: closeButton.containsMouse ?
Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) :
"transparent"
color: closeButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Text {
text: "close"
font.family: Theme.iconFont
@@ -125,61 +109,65 @@ PanelWindow {
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: closeButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: settingsPopup.settingsVisible = false
}
}
}
// Settings sections
ScrollView {
width: parent.width
height: parent.height - 80
clip: true
Column {
width: parent.width
spacing: Theme.spacingL
// Profile Settings
SettingsSection {
title: "Profile"
iconName: "person"
content: Column {
width: parent.width
spacing: Theme.spacingM
// Profile Image Preview and Input
Column {
width: parent.width
spacing: Theme.spacingM
Text {
text: "Profile Image"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
// Profile Image Preview with circular crop
Row {
width: parent.width
spacing: Theme.spacingM
// Circular profile image preview
Item {
id: avatarContainer
width: 54
height: 54
property bool hasImage: avatarImageSource.status === Image.Ready
width: 54
height: 54
// This rectangle provides the themed ring via its border.
Rectangle {
anchors.fill: parent
@@ -193,12 +181,15 @@ PanelWindow {
// Hidden Image loader. Its only purpose is to load the texture.
Image {
id: avatarImageSource
source: {
if (profileImageInput.text === "") return ""
if (profileImageInput.text.startsWith("/")) {
return "file://" + profileImageInput.text
}
return profileImageInput.text
if (profileImageInput.text === "")
return "";
if (profileImageInput.text.startsWith("/"))
return "file://" + profileImageInput.text;
return profileImageInput.text;
}
smooth: true
asynchronous: true
@@ -215,11 +206,12 @@ PanelWindow {
maskSource: settingsCircularMask
visible: avatarContainer.hasImage
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSpreadAtMin: 1
}
Item {
id: settingsCircularMask
width: 54 - 10
height: 54 - 10
layer.enabled: true
@@ -232,6 +224,7 @@ PanelWindow {
color: "black"
antialiasing: true
}
}
// Fallback for when there is no image.
@@ -248,6 +241,7 @@ PanelWindow {
font.pixelSize: Theme.iconSize + 8
color: Theme.primaryText
}
}
// Error icon for when the image fails to load.
@@ -259,13 +253,14 @@ PanelWindow {
color: Theme.primaryText
visible: profileImageInput.text !== "" && avatarImageSource.status === Image.Error
}
}
// Input field
Column {
width: parent.width - 80 - Theme.spacingM
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 48
@@ -273,9 +268,10 @@ PanelWindow {
color: Theme.surfaceVariant
border.color: profileImageInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: profileImageInput.activeFocus ? 2 : 1
TextInput {
id: profileImageInput
anchors.fill: parent
anchors.margins: Theme.spacingM
verticalAlignment: TextInput.AlignVCenter
@@ -283,11 +279,10 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium
text: Prefs.profileImage
selectByMouse: true
onEditingFinished: {
Prefs.setProfileImage(text)
Prefs.setProfileImage(text);
}
// Placeholder text
Text {
anchors.verticalCenter: parent.verticalCenter
@@ -296,16 +291,18 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium
visible: profileImageInput.text.length === 0 && !profileImageInput.activeFocus
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
}
}
Text {
text: "Local filesystem path or URL to an image file."
font.pixelSize: Theme.fontSizeSmall
@@ -313,58 +310,69 @@ PanelWindow {
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
}
}
// Clock Settings
SettingsSection {
title: "Clock & Time"
iconName: "schedule"
content: Column {
width: parent.width
spacing: Theme.spacingM
SettingsToggle {
text: "24-Hour Format"
description: "Use 24-hour time format instead of 12-hour AM/PM"
checked: Prefs.use24HourClock
onToggled: (checked) => Prefs.setClockFormat(checked)
onToggled: (checked) => {
return Prefs.setClockFormat(checked);
}
}
}
}
// Weather Settings
SettingsSection {
title: "Weather"
iconName: "wb_sunny"
content: Column {
width: parent.width
spacing: Theme.spacingM
SettingsToggle {
text: "Fahrenheit"
description: "Use Fahrenheit instead of Celsius for temperature"
checked: Prefs.useFahrenheit
onToggled: (checked) => Prefs.setTemperatureUnit(checked)
onToggled: (checked) => {
return Prefs.setTemperatureUnit(checked);
}
}
// Weather Location Setting
Column {
width: parent.width
spacing: Theme.spacingS
Text {
text: "Location"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Rectangle {
width: parent.width
height: 48
@@ -372,9 +380,10 @@ PanelWindow {
color: Theme.surfaceVariant
border.color: weatherLocationInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: weatherLocationInput.activeFocus ? 2 : 1
TextInput {
id: weatherLocationInput
anchors.fill: parent
anchors.margins: Theme.spacingM
verticalAlignment: TextInput.AlignVCenter
@@ -382,11 +391,10 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium
text: Prefs.weatherLocationOverride
selectByMouse: true
onEditingFinished: {
Prefs.setWeatherLocationOverride(text)
Prefs.setWeatherLocationOverride(text);
}
// Placeholder text
Text {
anchors.verticalCenter: parent.verticalCenter
@@ -395,16 +403,18 @@ PanelWindow {
font.pixelSize: Theme.fontSizeMedium
visible: weatherLocationInput.text.length === 0 && !weatherLocationInput.activeFocus
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
}
}
Text {
text: "Examples: \"New York, NY\", \"London\", \"Tokyo\""
font.pixelSize: Theme.fontSizeSmall
@@ -412,108 +422,124 @@ PanelWindow {
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
// Widget Visibility Settings
SettingsSection {
title: "Top Bar Widgets"
iconName: "widgets"
content: Column {
width: parent.width
spacing: Theme.spacingM
SettingsToggle {
text: "Focused Window"
description: "Show the currently focused application in the top bar"
checked: Prefs.showFocusedWindow
onToggled: (checked) => Prefs.setShowFocusedWindow(checked)
onToggled: (checked) => {
return Prefs.setShowFocusedWindow(checked);
}
}
SettingsToggle {
text: "Weather Widget"
description: "Display weather information in the top bar"
checked: Prefs.showWeather
onToggled: (checked) => Prefs.setShowWeather(checked)
onToggled: (checked) => {
return Prefs.setShowWeather(checked);
}
}
SettingsToggle {
text: "Media Controls"
description: "Show currently playing media in the top bar"
checked: Prefs.showMusic
onToggled: (checked) => Prefs.setShowMusic(checked)
onToggled: (checked) => {
return Prefs.setShowMusic(checked);
}
}
SettingsToggle {
text: "Clipboard Button"
description: "Show clipboard access button in the top bar"
checked: Prefs.showClipboard
onToggled: (checked) => Prefs.setShowClipboard(checked)
onToggled: (checked) => {
return Prefs.setShowClipboard(checked);
}
}
SettingsToggle {
text: "System Resources"
description: "Display CPU and RAM usage indicators"
checked: Prefs.showSystemResources
onToggled: (checked) => Prefs.setShowSystemResources(checked)
onToggled: (checked) => {
return Prefs.setShowSystemResources(checked);
}
}
SettingsToggle {
text: "System Tray"
description: "Show system tray icons in the top bar"
checked: Prefs.showSystemTray
onToggled: (checked) => Prefs.setShowSystemTray(checked)
onToggled: (checked) => {
return Prefs.setShowSystemTray(checked);
}
}
}
}
// Display Settings
SettingsSection {
title: "Display & Appearance"
iconName: "palette"
content: Column {
width: parent.width
spacing: Theme.spacingL
SettingsToggle {
text: "Night Mode"
description: "Apply warm color temperature to reduce eye strain"
checked: Prefs.nightModeEnabled
onToggled: (checked) => {
Prefs.setNightModeEnabled(checked)
if (checked) {
nightModeEnableProcess.running = true
} else {
nightModeDisableProcess.running = true
}
Prefs.setNightModeEnabled(checked);
if (checked)
nightModeEnableProcess.running = true;
else
nightModeDisableProcess.running = true;
}
}
SettingsToggle {
text: "Light Mode"
description: "Use light theme instead of dark theme"
checked: Prefs.isLightMode
onToggled: (checked) => {
Prefs.setLightMode(checked)
Theme.isLightMode = checked
Prefs.setLightMode(checked);
Theme.isLightMode = checked;
}
}
// Top Bar Transparency
Column {
width: parent.width
spacing: Theme.spacingS
Text {
text: "Top Bar Transparency"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
CustomSlider {
width: parent.width
value: Math.round(Prefs.topBarTransparency * 100)
@@ -523,13 +549,12 @@ PanelWindow {
rightIcon: "circle"
unit: "%"
showValue: true
onSliderDragFinished: (finalValue) => {
let transparencyValue = finalValue / 100.0
Prefs.setTopBarTransparency(transparencyValue)
let transparencyValue = finalValue / 100;
Prefs.setTopBarTransparency(transparencyValue);
}
}
Text {
text: "Adjust the transparency of the top bar background"
font.pixelSize: Theme.fontSizeSmall
@@ -537,20 +562,21 @@ PanelWindow {
wrapMode: Text.WordWrap
width: parent.width
}
}
// Popup Transparency
Column {
width: parent.width
spacing: Theme.spacingS
Text {
text: "Popup Transparency"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
CustomSlider {
width: parent.width
value: Math.round(Prefs.popupTransparency * 100)
@@ -560,13 +586,12 @@ PanelWindow {
rightIcon: "circle"
unit: "%"
showValue: true
onSliderDragFinished: (finalValue) => {
let transparencyValue = finalValue / 100.0
Prefs.setPopupTransparency(transparencyValue)
let transparencyValue = finalValue / 100;
Prefs.setPopupTransparency(transparencyValue);
}
}
Text {
text: "Adjust transparency for dialogs, menus, and popups"
font.pixelSize: Theme.fontSizeSmall
@@ -574,65 +599,87 @@ PanelWindow {
wrapMode: Text.WordWrap
width: parent.width
}
}
// Theme Picker
Column {
width: parent.width
spacing: Theme.spacingS
Text {
text: "Theme Color"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
ThemePicker {
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}
// Add shadow effect
layer.enabled: true
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowBlur: 1
shadowColor: Qt.rgba(0, 0, 0, 0.3)
shadowOpacity: 0.3
}
}
// Night mode processes
Process {
id: nightModeEnableProcess
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Failed to enable night mode")
Prefs.setNightModeEnabled(false)
console.warn("Failed to enable night mode");
Prefs.setNightModeEnabled(false);
}
}
}
Process {
id: nightModeDisableProcess
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Failed to disable night mode")
}
if (exitCode !== 0)
console.warn("Failed to disable night mode");
}
}
@@ -640,7 +687,7 @@ PanelWindow {
FocusScope {
anchors.fill: parent
focus: settingsPopup.settingsVisible
Keys.onEscapePressed: settingsPopup.settingsVisible = false
}
}
}

View File

@@ -3,19 +3,19 @@ import qs.Common
Column {
id: root
property string title: ""
property string iconName: ""
property alias content: contentLoader.sourceComponent
width: parent.width
spacing: Theme.spacingM
// Section header
Row {
width: parent.width
spacing: Theme.spacingS
Text {
text: iconName
font.family: Theme.iconFont
@@ -23,7 +23,7 @@ Column {
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: title
font.pixelSize: Theme.fontSizeLarge
@@ -31,18 +31,21 @@ Column {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
// Divider
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
// Content
Loader {
id: contentLoader
width: parent.width
}
}
}

View File

@@ -3,27 +3,18 @@ import qs.Common
Rectangle {
id: root
property string text: ""
property string description: ""
property bool checked: false
signal toggled(bool checked)
width: parent.width
height: 60
radius: Theme.cornerRadius
color: toggleArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
color: toggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
Row {
anchors.left: parent.left
anchors.right: toggle.left
@@ -31,18 +22,18 @@ Rectangle {
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingXS
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Text {
text: root.text
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: root.description
font.pixelSize: Theme.fontSizeSmall
@@ -51,62 +42,79 @@ Rectangle {
width: Math.min(implicitWidth, root.width - 120)
visible: root.description.length > 0
}
}
}
// Toggle switch
Rectangle {
id: toggle
width: 48
height: 24
radius: 12
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: root.checked ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
id: toggleHandle
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
x: root.checked ? parent.width - width - 2 : 2
color: root.checked ? Theme.primaryText : Theme.surfaceText
Behavior on x {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
MouseArea {
id: toggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.checked = !root.checked
root.toggled(root.checked)
root.checked = !root.checked;
root.toggled(root.checked);
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,9 @@ import qs.Services
Column {
id: themePicker
spacing: Theme.spacingS
Text {
text: "Current Theme: " + (Theme.isDynamicTheme ? "Auto" : (Theme.currentThemeIndex < Theme.themes.length ? Theme.themes[Theme.currentThemeIndex].name : "Blue"))
font.pixelSize: Theme.fontSizeMedium
@@ -13,26 +14,15 @@ Column {
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
// Theme description
Text {
text: {
if (Theme.isDynamicTheme) {
return "Wallpaper-based dynamic colors"
}
var descriptions = [
"Material blue inspired by modern interfaces",
"Deep blue inspired by material 3",
"Rich purple tones for BB elegance",
"Natural green for productivity",
"Energetic orange for creativity",
"Bold red for impact",
"Cool cyan for tranquility",
"Vibrant pink for expression",
"Warm amber for comfort",
"Soft coral for gentle warmth"
]
return descriptions[Theme.currentThemeIndex] || "Select a theme"
if (Theme.isDynamicTheme)
return "Wallpaper-based dynamic colors";
var descriptions = ["Material blue inspired by modern interfaces", "Deep blue inspired by material 3", "Rich purple tones for BB elegance", "Natural green for productivity", "Energetic orange for creativity", "Bold red for impact", "Cool cyan for tranquility", "Vibrant pink for expression", "Warm amber for comfort", "Soft coral for gentle warmth"];
return descriptions[Theme.currentThemeIndex] || "Select a theme";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
@@ -41,20 +31,20 @@ Column {
width: Math.min(parent.width, 200)
horizontalAlignment: Text.AlignHCenter
}
// Grid layout for 10 themes (2 rows of 5)
Column {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
// First row - Blue, Deep Blue, Purple, Green, Orange
Row {
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: 5
Rectangle {
width: 32
height: 32
@@ -62,23 +52,8 @@ Column {
color: Theme.themes[index].primary
border.color: Theme.outline
border.width: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 2 : 1
scale: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 1.1 : 1.0
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
scale: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 1.1 : 1
// Theme name tooltip
Rectangle {
width: nameText.contentWidth + Theme.spacingS * 2
@@ -91,39 +66,62 @@ Column {
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea.containsMouse
Text {
id: nameText
text: Theme.themes[index].name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.switchTheme(index, false)
Theme.switchTheme(index, false);
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
// Second row - Red, Cyan, Pink, Amber, Coral
Row {
spacing: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: 5
Rectangle {
property int themeIndex: index + 5
width: 32
height: 32
radius: 16
@@ -131,23 +129,8 @@ Column {
border.color: Theme.outline
border.width: Theme.currentThemeIndex === themeIndex ? 2 : 1
visible: themeIndex < Theme.themes.length
scale: Theme.currentThemeIndex === themeIndex ? 1.1 : 1.0
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
scale: Theme.currentThemeIndex === themeIndex ? 1.1 : 1
// Theme name tooltip
Rectangle {
width: nameText2.contentWidth + Theme.spacingS * 2
@@ -160,133 +143,138 @@ Column {
anchors.bottomMargin: Theme.spacingXS
anchors.horizontalCenter: parent.horizontalCenter
visible: mouseArea2.containsMouse && themeIndex < Theme.themes.length
Text {
id: nameText2
text: themeIndex < Theme.themes.length ? Theme.themes[themeIndex].name : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.centerIn: parent
}
}
MouseArea {
id: mouseArea2
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (themeIndex < Theme.themes.length) {
Theme.switchTheme(themeIndex)
}
if (themeIndex < Theme.themes.length)
Theme.switchTheme(themeIndex);
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
// Spacer for better visual separation
Item {
width: 1
height: Theme.spacingM
}
// Auto theme button - prominent oval below the grid
Rectangle {
width: 120
height: 40
radius: 20
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
} else {
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
}
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
else
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
}
border.color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") {
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5)
} else if (Theme.isDynamicTheme) {
return Theme.primary
} else {
return Theme.outline
}
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5);
else if (Theme.isDynamicTheme)
return Theme.primary;
else
return Theme.outline;
}
border.width: Theme.isDynamicTheme ? 2 : 1
scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1)
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
Text {
text: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") return "error"
else return "palette"
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return "error";
else
return "palette";
}
font.family: Theme.iconFont
font.pixelSize: 16
color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") return Theme.error
else return Theme.surfaceText
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Theme.error;
else
return Theme.surfaceText;
}
font.weight: Theme.iconFontWeight
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: {
if (ToastService.wallpaperErrorStatus === "error") return "Error"
else if (ToastService.wallpaperErrorStatus === "matugen_missing") return "No matugen"
else return "Auto"
if (ToastService.wallpaperErrorStatus === "error")
return "Error";
else if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "No matugen";
else
return "Auto";
}
font.pixelSize: Theme.fontSizeMedium
color: {
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") return Theme.error
else return Theme.surfaceText
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
return Theme.error;
else
return Theme.surfaceText;
}
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1.0)
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
MouseArea {
id: autoMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
Theme.switchTheme(10, true)
Theme.switchTheme(10, true);
}
}
// Tooltip for Auto button
Rectangle {
width: autoTooltipText.contentWidth + Theme.spacingM * 2
@@ -299,17 +287,17 @@ Column {
anchors.bottomMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
visible: autoMouseArea.containsMouse && (!Theme.isDynamicTheme || ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
Text {
id: autoTooltipText
text: {
if (ToastService.wallpaperErrorStatus === "error") {
return "Wallpaper symlink missing at ~/quickshell/current_wallpaper"
} else if (ToastService.wallpaperErrorStatus === "matugen_missing") {
return "Install matugen package for dynamic themes"
} else {
return "Dynamic wallpaper-based colors"
}
if (ToastService.wallpaperErrorStatus === "error")
return "Wallpaper symlink missing at ~/quickshell/current_wallpaper";
else if (ToastService.wallpaperErrorStatus === "matugen_missing")
return "Install matugen package for dynamic themes";
else
return "Dynamic wallpaper-based colors";
}
font.pixelSize: Theme.fontSizeSmall
color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceText
@@ -318,7 +306,35 @@ Column {
width: Math.min(implicitWidth, 250)
horizontalAlignment: Text.AlignHCenter
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
}
}

View File

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

View File

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

View File

@@ -4,30 +4,25 @@ import qs.Common
Rectangle {
id: root
property date currentDate: new Date()
signal clockClicked()
width: clockRow.implicitWidth + Theme.spacingS * 2
height: 30
radius: Theme.cornerRadius
color: clockMouseArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
color: clockMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Component.onCompleted: {
root.currentDate = systemClock.date;
}
Row {
id: clockRow
anchors.centerIn: parent
spacing: Theme.spacingS
Text {
text: Prefs.use24HourClock ? Qt.formatTime(root.currentDate, "H:mm") : Qt.formatTime(root.currentDate, "h:mm AP")
font.pixelSize: Theme.fontSizeMedium
@@ -35,14 +30,14 @@ Rectangle {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "•"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: Qt.formatDate(root.currentDate, "ddd d")
font.pixelSize: Theme.fontSizeMedium
@@ -50,26 +45,33 @@ Rectangle {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
SystemClock {
id: systemClock
precision: SystemClock.Seconds
onDateChanged: root.currentDate = systemClock.date
}
Component.onCompleted: {
root.currentDate = systemClock.date
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clockClicked()
root.clockClicked();
}
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -4,37 +4,43 @@ import qs.Services
Rectangle {
id: root
property bool isActive: false
signal clicked()
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
height: 30
radius: Theme.cornerRadius
color: controlCenterArea.containsMouse || root.isActive ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: controlCenterArea.containsMouse || root.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
Row {
id: controlIndicators
anchors.centerIn: parent
spacing: Theme.spacingXS
// Network Status Icon
Text {
text: {
if (NetworkService.networkStatus === "ethernet") return "lan"
else if (NetworkService.networkStatus === "wifi") {
if (NetworkService.networkStatus === "ethernet") {
return "lan";
} else if (NetworkService.networkStatus === "wifi") {
switch (WifiService.wifiSignalStrength) {
case "excellent": return "wifi"
case "good": return "wifi_2_bar"
case "fair": return "wifi_1_bar"
case "poor": return "wifi_calling_3"
default: return "wifi"
case "excellent":
return "wifi";
case "good":
return "wifi_2_bar";
case "fair":
return "wifi_1_bar";
case "poor":
return "wifi_calling_3";
default:
return "wifi";
}
} else {
return "wifi_off";
}
else return "wifi_off"
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
@@ -43,7 +49,7 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
visible: true
}
// Bluetooth Icon (when available and enabled) - moved next to network
Text {
text: "bluetooth"
@@ -54,51 +60,49 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
visible: BluetoothService.available && BluetoothService.enabled
}
// Audio Icon with scroll wheel support
Rectangle {
width: audioIcon.implicitWidth + 4
height: audioIcon.implicitHeight + 4
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
Text {
id: audioIcon
text: AudioService.sinkMuted ? "volume_off" :
AudioService.volumeLevel < 33 ? "volume_down" : "volume_up"
text: AudioService.sinkMuted ? "volume_off" : AudioService.volumeLevel < 33 ? "volume_down" : "volume_up"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8
font.weight: Theme.iconFontWeight
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ?
Theme.primary : Theme.surfaceText
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
// Scroll up - increase volume
// Scroll down - decrease volume
id: audioWheelArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y
let currentVolume = AudioService.volumeLevel
let newVolume
if (delta > 0) {
// Scroll up - increase volume
newVolume = Math.min(100, currentVolume + 5)
} else {
// Scroll down - decrease volume
newVolume = Math.max(0, currentVolume - 5)
}
AudioService.setVolume(newVolume)
wheelEvent.accepted = true
let delta = wheelEvent.angleDelta.y;
let currentVolume = AudioService.volumeLevel;
let newVolume;
if (delta > 0)
newVolume = Math.min(100, currentVolume + 5);
else
newVolume = Math.max(0, currentVolume - 5);
AudioService.setVolume(newVolume);
wheelEvent.accepted = true;
}
}
}
// Microphone Icon (when active)
Text {
text: "mic"
@@ -109,23 +113,26 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
visible: false // TODO: Add mic detection
}
}
MouseArea {
id: controlCenterArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clicked()
root.clicked();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}

View File

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

View File

@@ -4,12 +4,12 @@ import qs.Services
Rectangle {
id: root
width: 40
height: 30
radius: Theme.cornerRadius
color: launcherArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Text {
anchors.centerIn: parent
text: OSDetectorService.osLogo || "apps"
@@ -18,22 +18,24 @@ Rectangle {
font.weight: Theme.iconFontWeight
color: Theme.surfaceText
}
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
LauncherService.toggleAppLauncher()
LauncherService.toggleAppLauncher();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}

View File

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

View File

@@ -3,28 +3,26 @@ import qs.Common
Rectangle {
id: root
property bool hasUnread: false
property bool isActive: false
signal clicked()
width: 40
height: 30
radius: Theme.cornerRadius
color: notificationArea.containsMouse || root.isActive ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
color: notificationArea.containsMouse || root.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
Text {
anchors.centerIn: parent
text: "notifications"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 6
font.weight: Theme.iconFontWeight
color: notificationArea.containsMouse || root.isActive ?
Theme.primary : Theme.surfaceText
color: notificationArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
}
// Notification dot indicator
Rectangle {
width: 8
@@ -37,22 +35,24 @@ Rectangle {
anchors.topMargin: 6
visible: root.hasUnread
}
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.clicked()
root.clicked();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}

View File

@@ -4,36 +4,38 @@ import qs.Common
Rectangle {
id: root
signal menuRequested(var menu, var item, real x, real y)
width: Math.max(40, systemTrayRow.implicitWidth + Theme.spacingS * 2)
height: 30
radius: Theme.cornerRadius
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
visible: systemTrayRow.children.length > 0
Row {
id: systemTrayRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Repeater {
model: SystemTray.items
delegate: Rectangle {
property var trayItem: modelData
width: 24
height: 24
radius: Theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
property var trayItem: modelData
Image {
anchors.centerIn: parent
width: 18
height: 18
source: {
let icon = trayItem?.icon;
let icon = trayItem && trayItem.icon;
if (typeof icon === 'string' || icon instanceof String) {
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
@@ -48,51 +50,58 @@ Rectangle {
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem) return;
if (!trayItem)
return ;
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu) {
trayItem.activate()
}
if (!trayItem.onlyMenu)
trayItem.activate();
} else if (mouse.button === Qt.RightButton) {
if (trayItem.hasMenu) {
customTrayMenu.showMenu(mouse.x, mouse.y)
}
if (trayItem.hasMenu)
customTrayMenu.showMenu(mouse.x, mouse.y);
}
}
}
QtObject {
id: customTrayMenu
property bool menuVisible: false
function showMenu(x, y) {
root.menuRequested(customTrayMenu, trayItem, x, y)
menuVisible = true
root.menuRequested(customTrayMenu, trayItem, x, y);
menuVisible = true;
}
function hideMenu() {
menuVisible = false
menuVisible = false;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}

View File

@@ -1,61 +1,54 @@
import "../../Common/Utilities.js" as Utils
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Services.Notifications
import Quickshell.Services.Mpris
import Quickshell.Services.Notifications
import Quickshell.Services.SystemTray
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
import "../../Common/Utilities.js" as Utils
PanelWindow {
// Proxy objects for external connections
id: root
property var modelData
screen: modelData
property string screenName: modelData.name
// Transparency property for the top bar background
property real backgroundTransparency: Prefs.topBarTransparency
Connections {
target: Prefs
function onTopBarTransparencyChanged() {
root.backgroundTransparency = Prefs.topBarTransparency
}
}
// Notification properties
readonly property int notificationCount: NotificationService.notifications.length
// Proxy objects for external connections
screen: modelData
implicitHeight: Theme.barHeight - 4
color: "transparent"
Connections {
function onTopBarTransparencyChanged() {
root.backgroundTransparency = Prefs.topBarTransparency;
}
target: Prefs
}
QtObject {
id: notificationHistory
property int count: 0
}
anchors {
top: true
left: true
right: true
}
implicitHeight: Theme.barHeight - 4
color: "transparent"
// Floating panel container with margins
Item {
anchors.fill: parent
@@ -64,22 +57,13 @@ PanelWindow {
anchors.bottomMargin: 0
anchors.leftMargin: 8
anchors.rightMargin: 8
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadiusXLarge
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, root.backgroundTransparency)
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5 // radius/32, adjusted for visual match
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
Rectangle {
anchors.fill: parent
color: "transparent"
@@ -87,29 +71,43 @@ PanelWindow {
border.width: 1
radius: parent.radius
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: false
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.5 // radius/32, adjusted for visual match
shadowColor: Qt.rgba(0, 0, 0, 0.15)
shadowOpacity: 0.15
}
}
Item {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
@@ -117,82 +115,83 @@ PanelWindow {
anchors.topMargin: Theme.spacingXS
anchors.bottomMargin: Theme.spacingXS
clip: true
Row {
id: leftSection
height: parent.height
spacing: Theme.spacingXS
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
LauncherButton {
anchors.verticalCenter: parent.verticalCenter
}
WorkspaceSwitcher {
anchors.verticalCenter: parent.verticalCenter
screenName: root.screenName
}
FocusedAppWidget {
anchors.verticalCenter: parent.verticalCenter
visible: Prefs.showFocusedWindow
}
}
ClockWidget {
id: clockWidget
anchors.centerIn: parent
onClockClicked: {
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible;
}
}
MediaWidget {
anchors.verticalCenter: parent.verticalCenter
anchors.right: clockWidget.left
anchors.rightMargin: Theme.spacingS
visible: Prefs.showMusic && MprisController.activePlayer
onClicked: {
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible;
}
}
WeatherWidget {
id: weatherWidget
anchors.verticalCenter: parent.verticalCenter
anchors.left: clockWidget.right
anchors.leftMargin: Theme.spacingS
visible: Prefs.showWeather && WeatherService.weather.available && WeatherService.weather.temp > 0 && WeatherService.weather.tempF > 0
onClicked: {
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible;
}
}
Row {
id: rightSection
height: parent.height
spacing: Theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
SystemTrayWidget {
anchors.verticalCenter: parent.verticalCenter
visible: Prefs.showSystemTray
onMenuRequested: (menu, item, x, y) => {
trayMenuPopup.currentTrayMenu = menu
trayMenuPopup.currentTrayItem = item
trayMenuPopup.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL
trayMenuPopup.trayMenuY = Theme.barHeight - Theme.spacingXS
trayMenuPopup.showTrayMenu = true
menu.menuVisible = true
trayMenuPopup.currentTrayMenu = menu;
trayMenuPopup.currentTrayItem = item;
trayMenuPopup.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL;
trayMenuPopup.trayMenuY = Theme.barHeight - Theme.spacingXS;
trayMenuPopup.showTrayMenu = true;
menu.menuVisible = true;
}
}
Rectangle {
width: 40
height: 30
@@ -200,7 +199,7 @@ PanelWindow {
color: clipboardArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
visible: Prefs.showClipboard
Text {
anchors.centerIn: parent
text: "content_paste"
@@ -209,26 +208,28 @@ PanelWindow {
font.weight: Theme.iconFontWeight
color: Theme.surfaceText
}
MouseArea {
id: clipboardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
clipboardHistoryPopup.toggle()
clipboardHistoryPopup.toggle();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
// System Monitor Widgets
CpuMonitorWidget {
anchors.verticalCenter: parent.verticalCenter
@@ -239,40 +240,44 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
visible: Prefs.showSystemResources
}
NotificationCenterButton {
anchors.verticalCenter: parent.verticalCenter
hasUnread: root.notificationCount > 0
isActive: notificationCenter.notificationHistoryVisible
onClicked: {
notificationCenter.notificationHistoryVisible = !notificationCenter.notificationHistoryVisible
notificationCenter.notificationHistoryVisible = !notificationCenter.notificationHistoryVisible;
}
}
// Battery Widget
BatteryWidget {
anchors.verticalCenter: parent.verticalCenter
batteryPopupVisible: batteryControlPopup.batteryPopupVisible
onToggleBatteryPopup: {
batteryControlPopup.batteryPopupVisible = !batteryControlPopup.batteryPopupVisible
batteryControlPopup.batteryPopupVisible = !batteryControlPopup.batteryPopupVisible;
}
}
ControlCenterButton {
// Bluetooth devices are automatically updated via signals
anchors.verticalCenter: parent.verticalCenter
isActive: controlCenterPopup.controlCenterVisible
onClicked: {
controlCenterPopup.controlCenterVisible = !controlCenterPopup.controlCenterVisible
controlCenterPopup.controlCenterVisible = !controlCenterPopup.controlCenterVisible;
if (controlCenterPopup.controlCenterVisible) {
if (NetworkService.wifiEnabled) {
WifiService.scanWifi()
}
// Bluetooth devices are automatically updated via signals
if (NetworkService.wifiEnabled)
WifiService.scanWifi();
}
}
}
}
}
}
}
}

View File

@@ -4,37 +4,21 @@ import qs.Services
Rectangle {
id: root
signal clicked()
// Visibility is now controlled by TopBar.qml
width: visible ? Math.min(100, weatherRow.implicitWidth + Theme.spacingS * 2) : 0
height: 30
radius: Theme.cornerRadius
color: weatherArea.containsMouse ?
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
color: weatherArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
Row {
id: weatherRow
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
font.family: Theme.iconFont
@@ -42,7 +26,7 @@ Rectangle {
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: (Prefs.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + (Prefs.useFahrenheit ? "F" : "C")
font.pixelSize: Theme.fontSizeSmall
@@ -50,14 +34,32 @@ Rectangle {
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: weatherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

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

View File

@@ -1,59 +1,59 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
PanelWindow {
id: root
property bool wifiPasswordDialogVisible: false
property string wifiPasswordSSID: ""
property string wifiPasswordInput: ""
visible: wifiPasswordDialogVisible
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: wifiPasswordDialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
color: "transparent"
onVisibleChanged: {
if (visible)
passwordInput.forceActiveFocus();
}
anchors {
top: true
left: true
right: 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 {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: wifiPasswordDialogVisible ? 1.0 : 0.0
opacity: wifiPasswordDialogVisible ? 1 : 0
MouseArea {
anchors.fill: parent
onClicked: {
wifiPasswordDialogVisible = false;
wifiPasswordInput = "";
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
MouseArea {
anchors.fill: parent
onClicked: {
wifiPasswordDialogVisible = false
wifiPasswordInput = ""
}
}
}
Rectangle {
width: Math.min(400, parent.width - Theme.spacingL * 2)
height: Math.min(250, parent.height - Theme.spacingL * 2)
@@ -62,44 +62,29 @@ PanelWindow {
radius: Theme.cornerRadiusLarge
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 1
opacity: wifiPasswordDialogVisible ? 1.0 : 0.0
scale: wifiPasswordDialogVisible ? 1.0 : 0.9
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
opacity: wifiPasswordDialogVisible ? 1 : 0
scale: wifiPasswordDialogVisible ? 1 : 0.9
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
// Header
Row {
width: parent.width
Column {
width: parent.width - 40
spacing: Theme.spacingXS
Text {
text: "Connect to Wi-Fi"
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: "Enter password for \"" + wifiPasswordSSID + "\""
font.pixelSize: Theme.fontSizeMedium
@@ -107,14 +92,15 @@ PanelWindow {
width: parent.width
elide: Text.ElideRight
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: closeDialogArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "close"
@@ -122,20 +108,23 @@ PanelWindow {
font.pixelSize: Theme.iconSize - 4
color: closeDialogArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeDialogArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiPasswordDialogVisible = false
wifiPasswordInput = ""
wifiPasswordDialogVisible = false;
wifiPasswordInput = "";
}
}
}
}
// Password input
Rectangle {
width: parent.width
@@ -144,9 +133,10 @@ PanelWindow {
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
border.color: passwordInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: passwordInput.activeFocus ? 2 : 1
TextInput {
id: passwordInput
anchors.fill: parent
anchors.margins: Theme.spacingM
font.pixelSize: Theme.fontSizeMedium
@@ -155,7 +145,18 @@ PanelWindow {
verticalAlignment: TextInput.AlignVCenter
cursorVisible: activeFocus
selectByMouse: true
onTextChanged: {
wifiPasswordInput = text;
}
onAccepted: {
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput);
}
Component.onCompleted: {
if (wifiPasswordDialogVisible)
forceActiveFocus();
}
Text {
anchors.fill: parent
text: "Enter password"
@@ -164,46 +165,35 @@ PanelWindow {
verticalAlignment: Text.AlignVCenter
visible: parent.text.length === 0
}
onTextChanged: {
wifiPasswordInput = text
}
onAccepted: {
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput)
}
Component.onCompleted: {
if (wifiPasswordDialogVisible) {
forceActiveFocus()
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.IBeamCursor
onClicked: {
passwordInput.forceActiveFocus()
passwordInput.forceActiveFocus();
}
}
}
// Show password checkbox
Row {
spacing: Theme.spacingS
Rectangle {
id: showPasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
border.width: 2
Text {
anchors.centerIn: parent
text: "check"
@@ -212,35 +202,37 @@ PanelWindow {
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
}
}
}
Text {
text: "Show password"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
// Buttons
Item {
width: parent.width
height: 40
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Rectangle {
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
height: 36
@@ -248,65 +240,93 @@ PanelWindow {
color: cancelArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1
Text {
id: cancelText
anchors.centerIn: parent
text: "Cancel"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
MouseArea {
id: cancelArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
wifiPasswordDialogVisible = false
wifiPasswordInput = ""
wifiPasswordDialogVisible = false;
wifiPasswordInput = "";
}
}
}
Rectangle {
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
height: 36
radius: Theme.cornerRadius
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: wifiPasswordInput.length > 0
opacity: enabled ? 1.0 : 0.5
opacity: enabled ? 1 : 0.5
Text {
id: connectText
anchors.centerIn: parent
text: "Connect"
font.pixelSize: Theme.fontSizeMedium
color: Theme.background
font.weight: Font.Medium
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: {
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput)
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput);
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}

View File

@@ -8,71 +8,81 @@ import qs.Widgets.TopBar
ShellRoot {
id: root
// Multi-monitor support using Variants
Variants {
model: Quickshell.screens
delegate: TopBar {
modelData: item
}
}
// Global popup windows
CenterCommandCenter {
id: centerCommandCenter
}
TrayMenuPopup {
id: trayMenuPopup
}
NotificationCenter {
id: notificationCenter
}
ControlCenterPopup {
id: controlCenterPopup
}
WifiPasswordDialog {
id: wifiPasswordDialog
}
InputDialog {
id: globalInputDialog
}
BatteryControlPopup {
id: batteryControlPopup
}
PowerMenuPopup {
id: powerMenuPopup
}
PowerConfirmDialog {
id: powerConfirmDialog
}
ProcessListDropdown {
id: processListDropdown
}
SettingsPopup {
id: settingsPopup
}
// Application and clipboard components
AppLauncher {
id: appLauncher
}
SpotlightLauncher {
id: spotlightLauncher
}
ProcessListWidget {
id: processListWidget
}
ClipboardHistory {
id: clipboardHistoryPopup
}
ToastWidget {
id: toastWidget
}
}
}