Initial commit

This commit is contained in:
ly-sec 2025-07-11 14:14:28 +02:00
commit a8c2f88654
53 changed files with 9269 additions and 0 deletions

31
Widgets/Background.qml Normal file
View file

@ -0,0 +1,31 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Helpers
import qs.Settings
ShellRoot {
property string wallpaperSource: Settings.currentWallpaper !== "" ? Settings.currentWallpaper : "/home/lysec/nixos/assets/wallpapers/lantern.png"
PanelWindow {
anchors {
bottom: true
top: true
right: true
left: true
}
margins {
top: 0
}
color: "transparent"
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell-wallpaper"
Image {
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: wallpaperSource
cache: true
smooth: true
}
}
}

780
Widgets/LockScreen.qml Normal file
View file

@ -0,0 +1,780 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import Quickshell.Wayland
import Quickshell
import Quickshell.Services.Pam
import Quickshell.Io
import qs.Settings
import qs.Helpers
import "../Helpers/Weather.js" as WeatherHelper
// Password-only lockscreen for all screens
WlSessionLock {
id: lock
property bool demoMode: true // Set to true for demo/recording mode
property string errorMessage: ""
property bool authenticating: false
property string password: ""
property bool pamAvailable: typeof PamContext !== "undefined"
property string weatherCity: Settings.weatherCity
property var weatherData: null
property string weatherError: ""
property string weatherInfo: ""
property string weatherIcon: ""
property double currentTemp: 0
locked: false // Start unlocked, only lock when button is clicked
// On component completed, fetch weather data
Component.onCompleted: {
fetchWeatherData()
}
// Weather fetching function
function fetchWeatherData() {
WeatherHelper.fetchCityWeather(weatherCity,
function(result) {
weatherData = result.weather;
weatherError = "";
},
function(err) {
weatherError = err;
}
);
}
function materialSymbolForCode(code) {
if (code === 0) return "sunny";
if (code === 1 || code === 2) return "partly_cloudy_day";
if (code === 3) return "cloud";
if (code >= 45 && code <= 48) return "foggy";
if (code >= 51 && code <= 67) return "rainy";
if (code >= 71 && code <= 77) return "weather_snowy";
if (code >= 80 && code <= 82) return "rainy";
if (code >= 95 && code <= 99) return "thunderstorm";
return "cloud";
}
// Authentication function
function unlockAttempt() {
console.log("Unlock attempt started");
if (!pamAvailable) {
lock.errorMessage = "PAM authentication not available.";
console.log("PAM not available");
return;
}
if (!lock.password) {
lock.errorMessage = "Password required.";
console.log("No password entered");
return;
}
console.log("Starting PAM authentication...");
lock.authenticating = true;
lock.errorMessage = "";
console.log("[LockScreen] About to create PAM context with userName:", Quickshell.env("USER"))
var pam = Qt.createQmlObject('import Quickshell.Services.Pam; PamContext { config: "login"; user: "' + Quickshell.env("USER") + '" }', lock);
console.log("PamContext created", pam);
pam.onCompleted.connect(function(result) {
console.log("PAM completed with result:", result);
lock.authenticating = false;
if (result === PamResult.Success) {
console.log("Authentication successful, unlocking...");
lock.locked = false;
lock.password = "";
lock.errorMessage = "";
} else {
console.log("Authentication failed");
lock.errorMessage = "Authentication failed.";
lock.password = "";
}
pam.destroy();
});
pam.onError.connect(function(error) {
console.log("PAM error:", error);
lock.authenticating = false;
lock.errorMessage = pam.message || "Authentication error.";
lock.password = "";
pam.destroy();
});
pam.onPamMessage.connect(function() {
console.log("PAM message:", pam.message, "isError:", pam.messageIsError);
if (pam.messageIsError) {
lock.errorMessage = pam.message;
}
});
pam.onResponseRequiredChanged.connect(function() {
console.log("PAM response required:", pam.responseRequired);
if (pam.responseRequired && lock.authenticating) {
console.log("Responding to PAM with password");
pam.respond(lock.password);
}
});
var started = pam.start();
console.log("PAM start result:", started);
}
// Remove the surface property and use a Loader instead
Loader {
anchors.fill: parent
active: true
sourceComponent: demoMode ? demoComponent : lockComponent
}
Component {
id: demoComponent
Window {
id: demoWindow
visible: true
width: 900
height: 600
color: "transparent"
flags: Qt.Window | Qt.FramelessWindowHint
// Blurred wallpaper background
Image {
id: demoBgImage
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: WallpaperManager.currentWallpaper !== "" ? WallpaperManager.currentWallpaper : "/home/lysec/nixos/assets/wallpapers/lantern.png"
cache: true
smooth: true
sourceSize.width: 2560
sourceSize.height: 1440
visible: true // Show the original for FastBlur input
}
FastBlur {
anchors.fill: parent
source: demoBgImage
radius: 48 // Adjust blur strength as needed
transparentBorder: true
}
// Main content container (moved up, Rectangle removed)
ColumnLayout {
anchors.centerIn: parent
spacing: 30
width: Math.min(parent.width * 0.8, 400)
// User avatar/icon
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 80
height: 80
radius: 40
color: Theme.accentPrimary
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 4
source: Settings.profileImage
fillMode: Image.PreserveAspectCrop
visible: false // Only show the masked version
asynchronous: true
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: avatarImage.width / 2
visible: false
}
visible: Settings.profileImage !== ""
}
// Fallback icon
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.onAccent
visible: Settings.profileImage === ""
}
// Glow effect
layer.enabled: true
layer.effect: Glow {
color: Theme.accentPrimary
radius: 8
samples: 16
}
}
// Username
Text {
Layout.alignment: Qt.AlignHCenter
text: Settings.userName
font.pixelSize: 24
font.weight: Font.Medium
color: Theme.textPrimary
}
// Password input container
Rectangle {
Layout.fillWidth: true
height: 50
radius: 25
color: Theme.surface
opacity: 0.3
border.color: passwordInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 2
TextInput {
id: passwordInput
anchors.fill: parent
anchors.margins: 15
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
font.pixelSize: 16
color: Theme.textPrimary
echoMode: TextInput.Password
passwordCharacter: "●"
passwordMaskDelay: 0
text: lock.password
onTextChanged: lock.password = text
// Placeholder text
Text {
anchors.centerIn: parent
text: "Enter password..."
color: Theme.textSecondary
opacity: 0.6
font.pixelSize: 16
visible: !passwordInput.text && !passwordInput.activeFocus
}
// Handle Enter key
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lock.unlockAttempt()
}
}
}
}
// Error message
Text {
Layout.alignment: Qt.AlignHCenter
text: lock.errorMessage
color: Theme.error
font.pixelSize: 14
visible: lock.errorMessage !== ""
opacity: lock.errorMessage !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
// Unlock button
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 22
color: unlockButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 2
opacity: lock.authenticating ? 0.5 : 0.8
enabled: !lock.authenticating
Text {
anchors.centerIn: parent
text: lock.authenticating ? "Authenticating..." : "Unlock"
font.pixelSize: 16
font.bold: true
color: unlockButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: unlockButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!lock.authenticating) {
lock.unlockAttempt()
}
}
}
}
// Bypass Login button
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 22
color: bypassButtonArea.containsMouse ? Theme.accentSecondary : "transparent"
border.color: Theme.accentSecondary
border.width: 2
opacity: lock.authenticating ? 0.5 : 0.8
enabled: !lock.authenticating
Text {
anchors.centerIn: parent
text: "Bypass Login"
font.pixelSize: 16
font.bold: true
color: bypassButtonArea.containsMouse ? Theme.onAccent : Theme.accentSecondary
}
MouseArea {
id: bypassButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!lock.authenticating) {
lock.locked = false;
lock.errorMessage = "";
lock.password = "";
}
}
}
}
}
// Top-center info panel (clock + weather)
ColumnLayout {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 40
spacing: 8
// Clock
Text {
id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm")
font.pixelSize: 48
font.bold: true
color: Theme.textPrimary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
Text {
id: dateText
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
font.pixelSize: 16
color: Theme.textSecondary
opacity: 0.8
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
// Weather info (centered, no city)
RowLayout {
spacing: 6
Layout.alignment: Qt.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
visible: weatherData && weatherData.current_weather
Text {
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
font.family: "Material Symbols Outlined"
font.pixelSize: 28
color: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
}
Text {
text: weatherData && weatherData.current_weather ? (Settings.useFahrenheit ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : (Settings.useFahrenheit ? "--°F" : "--°C")
font.pixelSize: 18
color: Theme.textSecondary
verticalAlignment: Text.AlignVCenter
}
}
// Weather error
Text {
text: weatherError
color: Theme.error
visible: weatherError !== ""
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
// Update clock every second
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
timeText.text = Qt.formatDateTime(new Date(), "HH:mm")
dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d")
}
}
// Update weather every 10 minutes
Timer {
interval: 600000 // 10 minutes
running: true
repeat: true
onTriggered: {
fetchWeatherData()
}
}
}
}
Component {
id: lockComponent
WlSessionLockSurface {
// Blurred wallpaper background
Image {
id: lockBgImage
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: WallpaperManager.currentWallpaper !== "" ? WallpaperManager.currentWallpaper : "/home/lysec/nixos/assets/wallpapers/lantern.png"
cache: true
smooth: true
sourceSize.width: 2560
sourceSize.height: 1440
visible: true // Show the original for FastBlur input
}
FastBlur {
anchors.fill: parent
source: lockBgImage
radius: 48 // Adjust blur strength as needed
transparentBorder: true
}
// Main content container (moved up, Rectangle removed)
ColumnLayout {
anchors.centerIn: parent
spacing: 30
width: Math.min(parent.width * 0.8, 400)
// User avatar/icon
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 80
height: 80
radius: 40
color: Theme.accentPrimary
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 4
source: Settings.profileImage
fillMode: Image.PreserveAspectCrop
visible: false // Only show the masked version
asynchronous: true
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: avatarImage.width / 2
visible: false
}
visible: Settings.profileImage !== ""
}
// Fallback icon
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.onAccent
visible: Settings.profileImage === ""
}
// Glow effect
layer.enabled: true
layer.effect: Glow {
color: Theme.accentPrimary
radius: 8
samples: 16
}
}
// Username
Text {
Layout.alignment: Qt.AlignHCenter
text: Quickshell.env("USER")
font.pixelSize: 24
font.weight: Font.Medium
color: Theme.textPrimary
}
// Password input container
Rectangle {
Layout.fillWidth: true
height: 50
radius: 25
color: Theme.surface
opacity: 0.3
border.color: passwordInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 2
TextInput {
id: passwordInput
anchors.fill: parent
anchors.margins: 15
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
font.pixelSize: 16
color: Theme.textPrimary
echoMode: TextInput.Password
passwordCharacter: "●"
passwordMaskDelay: 0
text: lock.password
onTextChanged: lock.password = text
// Placeholder text
Text {
anchors.centerIn: parent
text: "Enter password..."
color: Theme.textSecondary
opacity: 0.6
font.pixelSize: 16
visible: !passwordInput.text && !passwordInput.activeFocus
}
// Handle Enter key
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
lock.unlockAttempt()
}
}
}
}
// Error message
Text {
Layout.alignment: Qt.AlignHCenter
text: lock.errorMessage
color: Theme.error
font.pixelSize: 14
visible: lock.errorMessage !== ""
opacity: lock.errorMessage !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
}
// Unlock button
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 22
color: unlockButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 2
opacity: lock.authenticating ? 0.5 : 0.8
enabled: !lock.authenticating
Text {
anchors.centerIn: parent
text: lock.authenticating ? "Authenticating..." : "Unlock"
font.pixelSize: 16
font.bold: true
color: unlockButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: unlockButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!lock.authenticating) {
lock.unlockAttempt()
}
}
}
}
// Bypass Login button
Rectangle {
Layout.alignment: Qt.AlignHCenter
width: 120
height: 44
radius: 22
color: bypassButtonArea.containsMouse ? Theme.accentSecondary : "transparent"
border.color: Theme.accentSecondary
border.width: 2
opacity: lock.authenticating ? 0.5 : 0.8
enabled: !lock.authenticating
Text {
anchors.centerIn: parent
text: "Bypass Login"
font.pixelSize: 16
font.bold: true
color: bypassButtonArea.containsMouse ? Theme.onAccent : Theme.accentSecondary
}
MouseArea {
id: bypassButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (!lock.authenticating) {
lock.locked = false;
lock.errorMessage = "";
lock.password = "";
}
}
}
}
}
// Top-center info panel (clock + weather)
ColumnLayout {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 40
spacing: 8
// Clock
Text {
id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm")
font.pixelSize: 48
font.bold: true
color: Theme.textPrimary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
Text {
id: dateText
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
font.pixelSize: 16
color: Theme.textSecondary
opacity: 0.8
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
// Weather info (centered, no city)
RowLayout {
spacing: 6
Layout.alignment: Qt.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
visible: weatherData && weatherData.current_weather
Text {
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
font.family: "Material Symbols Outlined"
font.pixelSize: 28
color: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
}
Text {
text: weatherData && weatherData.current_weather ? (Settings.useFahrenheit ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : (Settings.useFahrenheit ? "--°F" : "--°C")
font.pixelSize: 18
color: Theme.textSecondary
verticalAlignment: Text.AlignVCenter
}
}
// Weather error
Text {
text: weatherError
color: Theme.error
visible: weatherError !== ""
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
// Update clock every second
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
timeText.text = Qt.formatDateTime(new Date(), "HH:mm")
dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d")
}
}
// Update weather every 10 minutes
Timer {
interval: 600000 // 10 minutes
running: true
repeat: true
onTriggered: {
fetchWeatherData()
}
}
// System control buttons (bottom right)
ColumnLayout {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 32
spacing: 12
// Shutdown
Rectangle {
width: 48; height: 48; radius: 24
color: shutdownArea.containsMouse ? Theme.error : "transparent"
border.color: Theme.error
border.width: 1
MouseArea {
id: shutdownArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Qt.createQmlObject('import Quickshell.Io; Process { command: ["shutdown", "-h", "now"]; running: true }', lock)
}
}
Text {
anchors.centerIn: parent
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: shutdownArea.containsMouse ? Theme.onAccent : Theme.error
}
}
// Reboot
Rectangle {
width: 48; height: 48; radius: 24
color: rebootArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Qt.createQmlObject('import Quickshell.Io; Process { command: ["reboot"]; running: true }', lock)
}
}
Text {
anchors.centerIn: parent
text: "refresh"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: rebootArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
}
// Logout
Rectangle {
width: 48; height: 48; radius: 24
color: logoutArea.containsMouse ? Theme.accentSecondary : "transparent"
border.color: Theme.accentSecondary
border.width: 1
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Qt.createQmlObject('import Quickshell.Io; Process { command: ["loginctl", "terminate-user", "' + Quickshell.env("USER") + '"]; running: true }', lock)
}
}
Text {
anchors.centerIn: parent
text: "exit_to_app"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: logoutArea.containsMouse ? Theme.onAccent : Theme.accentSecondary
}
}
}
}
}
}

View file

@ -0,0 +1,178 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Settings
PanelWindow {
id: window
width: 350
implicitHeight: notificationColumn.implicitHeight + 20
color: "transparent"
visible: false
screen: Quickshell.primaryScreen
focusable: false
anchors.top: true
anchors.right: true
margins.top: -20 // keep as you want
margins.right: 6
property var notifications: []
property int maxVisible: 5
property int spacing: 10
function addNotification(notification) {
var notifObj = {
id: notification.id,
appName: notification.appName || "Notification",
summary: notification.summary || "",
body: notification.body || "",
rawNotification: notification
};
notifications.unshift(notifObj);
if (notifications.length > maxVisible) {
notifications = notifications.slice(0, maxVisible);
}
visible = true;
notificationsChanged();
}
function dismissNotification(id) {
notifications = notifications.filter(n => n.id !== id);
if (notifications.length === 0) {
visible = false;
}
notificationsChanged();
}
Column {
id: notificationColumn
anchors.right: parent.right
spacing: window.spacing
width: parent.width
clip: false // prevent clipping during animation
Repeater {
model: notifications
delegate: Rectangle {
id: notificationDelegate
width: parent.width
height: contentColumn.height + 20
color: Theme.backgroundPrimary
radius: 20
opacity: 1
Column {
id: contentColumn
width: parent.width - 20
anchors.centerIn: parent
spacing: 5
Text {
text: modelData.appName
width: parent.width
color: "white"
font.bold: true
font.pixelSize: 14
elide: Text.ElideRight
}
Text {
text: modelData.summary
width: parent.width
color: "#eeeeee"
font.pixelSize: 13
wrapMode: Text.Wrap
visible: text !== ""
}
Text {
text: modelData.body
width: parent.width
color: "#cccccc"
font.pixelSize: 12
wrapMode: Text.Wrap
visible: text !== ""
}
}
Timer {
interval: 4000
running: true
onTriggered: {
dismissAnimation.start();
if (modelData.rawNotification) {
modelData.rawNotification.expire();
}
}
}
MouseArea {
anchors.fill: parent
onClicked: {
dismissAnimation.start();
if (modelData.rawNotification) {
modelData.rawNotification.dismiss();
}
}
}
ParallelAnimation {
id: dismissAnimation
NumberAnimation {
target: notificationDelegate
property: "opacity"
to: 0
duration: 300
}
NumberAnimation {
target: notificationDelegate
property: "height"
to: 0
duration: 300
}
onFinished: window.dismissNotification(modelData.id)
}
Component.onCompleted: {
opacity = 0;
height = 0;
appearAnimation.start();
}
ParallelAnimation {
id: appearAnimation
NumberAnimation {
target: notificationDelegate
property: "opacity"
to: 1
duration: 300
}
NumberAnimation {
target: notificationDelegate
property: "height"
to: contentColumn.height + 20
duration: 300
}
}
}
}
}
onNotificationsChanged: {
height = notificationColumn.implicitHeight + 20
}
Connections {
target: Quickshell
function onScreensChanged() {
if (window.screen) {
x = window.screen.width - width - 20
// y stays as it is (margins.top = -20)
}
}
}
}

View file

@ -0,0 +1,266 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Settings
PanelWindow {
id: window
implicitWidth: 350
implicitHeight: notificationColumn.implicitHeight + 60
color: "transparent"
visible: notificationModel.count > 0
screen: Quickshell.primaryScreen !== undefined ? Quickshell.primaryScreen : null
focusable: false
property bool barVisible: true
anchors.top: true
anchors.right: true
margins.top: barVisible ? -20 : 10
margins.right: 6
ListModel {
id: notificationModel
}
property int maxVisible: 5
property int spacing: 5
function addNotification(notification) {
notificationModel.insert(0, {
id: notification.id,
appName: notification.appName || "Notification",
summary: notification.summary || "",
body: notification.body || "",
rawNotification: notification,
appeared: false,
dismissed: false
});
while (notificationModel.count > maxVisible) {
notificationModel.remove(notificationModel.count - 1);
}
}
function dismissNotificationById(id) {
for (var i = 0; i < notificationModel.count; i++) {
if (notificationModel.get(i).id === id) {
dismissNotificationByIndex(i);
break;
}
}
}
function dismissNotificationByIndex(index) {
if (index >= 0 && index < notificationModel.count) {
var notif = notificationModel.get(index);
if (!notif.dismissed) {
notificationModel.set(index, {
id: notif.id,
appName: notif.appName,
summary: notif.summary,
body: notif.body,
rawNotification: notif.rawNotification,
appeared: notif.appeared,
dismissed: true
});
}
}
}
Column {
id: notificationColumn
anchors.right: parent.right
spacing: window.spacing
width: parent.width
clip: false
Repeater {
id: notificationRepeater
model: notificationModel
delegate: Rectangle {
id: notificationDelegate
width: parent.width
color: Theme.backgroundPrimary
radius: 20
property bool appeared: model.appeared
property bool dismissed: model.dismissed
property var rawNotification: model.rawNotification
x: appeared ? 0 : width
opacity: dismissed ? 0 : 1
height: dismissed ? 0 : contentRow.height + 20
Row {
id: contentRow
anchors.centerIn: parent
spacing: 10
width: parent.width - 20
// Circular Icon container with border
Rectangle {
id: iconBackground
width: 36
height: 36
radius: width / 2 // Circular
color: Theme.accentPrimary
anchors.verticalCenter: parent.verticalCenter
border.color: Qt.darker(Theme.accentPrimary, 1.2)
border.width: 1.5
// Get all possible icon sources from notification
property var iconSources: [
rawNotification?.image || "",
rawNotification?.appIcon || "",
rawNotification?.icon || ""
]
// Try to load notification icon
Image {
id: iconImage
anchors.fill: parent
anchors.margins: 4
fillMode: Image.PreserveAspectFit
smooth: true
cache: false
asynchronous: true
sourceSize.width: 36
sourceSize.height: 36
source: {
for (var i = 0; i < iconBackground.iconSources.length; i++) {
var icon = iconBackground.iconSources[i];
if (!icon) continue;
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
if (icon.startsWith('/')) {
return "file://" + icon;
}
return icon;
}
return "";
}
visible: status === Image.Ready && source.toString() !== ""
}
// Fallback to first letter of app name
Text {
anchors.centerIn: parent
visible: !iconImage.visible
text: model.appName ? model.appName.charAt(0).toUpperCase() : "?"
font.pixelSize: 18
font.bold: true
color: Theme.textPrimary
}
}
Column {
width: contentRow.width - iconBackground.width - 10
spacing: 5
Text {
text: model.appName
width: parent.width
color: Theme.textPrimary
font.bold: true
font.pixelSize: 14
elide: Text.ElideRight
}
Text {
text: model.summary
width: parent.width
color: "#eeeeee"
font.pixelSize: 13
wrapMode: Text.Wrap
visible: text !== ""
}
Text {
text: model.body
width: parent.width
color: "#cccccc"
font.pixelSize: 12
wrapMode: Text.Wrap
visible: text !== ""
}
}
}
Timer {
interval: 4000
running: !dismissed
repeat: false
onTriggered: {
dismissAnimation.start();
if (rawNotification) rawNotification.expire();
}
}
MouseArea {
anchors.fill: parent
onClicked: {
dismissAnimation.start();
if (rawNotification) rawNotification.dismiss();
}
}
ParallelAnimation {
id: dismissAnimation
NumberAnimation { target: notificationDelegate; property: "opacity"; to: 0; duration: 150 }
NumberAnimation { target: notificationDelegate; property: "height"; to: 0; duration: 150 }
NumberAnimation { target: notificationDelegate; property: "x"; to: width; duration: 150; easing.type: Easing.InQuad }
onFinished: {
var idx = notificationRepeater.indexOf(notificationDelegate);
if (idx !== -1) {
notificationModel.remove(idx);
}
}
}
ParallelAnimation {
id: appearAnimation
NumberAnimation { target: notificationDelegate; property: "opacity"; to: 1; duration: 150 }
NumberAnimation { target: notificationDelegate; property: "height"; to: contentRow.height + 20; duration: 150 }
NumberAnimation { target: notificationDelegate; property: "x"; to: 0; duration: 150; easing.type: Easing.OutQuad }
}
Component.onCompleted: {
if (!appeared) {
opacity = 0;
height = 0;
x = width;
appearAnimation.start();
var idx = notificationRepeater.indexOf(notificationDelegate);
if (idx !== -1) {
var oldItem = notificationModel.get(idx);
notificationModel.set(idx, {
id: oldItem.id,
appName: oldItem.appName,
summary: oldItem.summary,
body: oldItem.body,
rawNotification: oldItem.rawNotification,
appeared: true,
dismissed: oldItem.dismissed
});
}
}
}
}
}
}
Connections {
target: Quickshell
function onScreensChanged() {
if (window.screen) {
x = window.screen.width - width - 20
}
}
}
}

37
Widgets/Overview.qml Normal file
View file

@ -0,0 +1,37 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Qt5Compat.GraphicalEffects
import qs.Helpers
import qs.Settings
ShellRoot {
property string wallpaperSource: Settings.currentWallpaper !== "" ? Settings.currentWallpaper : "/home/lysec/nixos/assets/wallpapers/lantern.png"
PanelWindow {
anchors {
top: true
bottom: true
right: true
left: true
}
color: "transparent"
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell-overview"
Image {
id: bgImage
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: wallpaperSource
cache: true
smooth: true
visible: true // Show the original for FastBlur input
}
FastBlur {
anchors.fill: parent
source: bgImage
radius: 24 // Adjust blur strength as needed
transparentBorder: true
}
}
}

View file

@ -0,0 +1,66 @@
import QtQuick
import Quickshell
import qs.Settings
import qs.Widgets.Sidebar.Panel
Item {
id: buttonRoot
property Item barBackground
property var screen
width: iconText.implicitWidth + 0
height: iconText.implicitHeight + 0
property color hoverColor: Theme.rippleEffect
property real hoverOpacity: 0.0
property bool isActive: mouseArea.containsMouse || (sidebarPopup && sidebarPopup.visible)
property var sidebarPopup
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (sidebarPopup.visible) {
// Close all modals if open
if (sidebarPopup.settingsModal && sidebarPopup.settingsModal.visible) {
sidebarPopup.settingsModal.visible = false;
}
if (sidebarPopup.wallpaperManagerModal && sidebarPopup.wallpaperManagerModal.visible) {
sidebarPopup.wallpaperManagerModal.visible = false;
}
sidebarPopup.hidePopup();
} else {
sidebarPopup.showAt();
}
}
onEntered: buttonRoot.hoverOpacity = 0.18
onExited: buttonRoot.hoverOpacity = 0.0
}
Rectangle {
anchors.fill: parent
color: hoverColor
opacity: isActive ? 0.18 : hoverOpacity
radius: height / 2
z: 0
visible: (isActive ? 0.18 : hoverOpacity) > 0.01
}
Text {
id: iconText
text: "dashboard"
font.family: isActive ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16
color: sidebarPopup.visible ? Theme.accentPrimary : Theme.textPrimary
anchors.centerIn: parent
z: 1
}
Behavior on hoverOpacity {
NumberAnimation {
duration: 120
easing.type: Easing.OutQuad
}
}
}

486
Widgets/Sidebar/Config.qml Normal file
View file

@ -0,0 +1,486 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Wayland
import qs.Settings
Rectangle {
id: settingsModal
anchors.centerIn: parent
color: Settings.Theme.backgroundPrimary
radius: 20
visible: false
z: 100
// Local properties for editing (not saved until apply)
property string tempWeatherCity: Settings.weatherCity
property bool tempUseFahrenheit: false
property string tempProfileImage: Settings.profileImage
property string tempWallpaperFolder: Settings.wallpaperFolder
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
// Header
RowLayout {
Layout.fillWidth: true
spacing: 16
Text {
text: "settings"
font.family: "Material Symbols Outlined"
font.pixelSize: 28
color: Settings.Theme.accentPrimary
}
Text {
text: "Settings"
font.pixelSize: 22
font.bold: true
color: Settings.Theme.textPrimary
Layout.fillWidth: true
}
Rectangle {
width: 36
height: 36
radius: 18
color: closeButtonArea.containsMouse ? Settings.Theme.accentPrimary : "transparent"
border.color: Settings.Theme.accentPrimary
border.width: 1
Text {
anchors.centerIn: parent
text: "close"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: closeButtonArea.containsMouse ? Settings.Theme.onAccent : Settings.Theme.accentPrimary
}
MouseArea {
id: closeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: settingsModal.closeSettings()
}
}
}
// Weather Settings Card
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 180
color: Settings.Theme.surface
radius: 18
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// Weather Settings Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: "wb_sunny"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Settings.Theme.accentPrimary
}
Text {
text: "Weather Settings"
font.pixelSize: 16
font.bold: true
color: Settings.Theme.textPrimary
Layout.fillWidth: true
}
}
// Weather City Setting
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "City"
font.pixelSize: 13
font.bold: true
color: Settings.Theme.textPrimary
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 8
color: Settings.Theme.surfaceVariant
border.color: cityInput.activeFocus ? Settings.Theme.accentPrimary : Settings.Theme.outline
border.width: 1
TextInput {
id: cityInput
anchors.fill: parent
anchors.margins: 12
text: tempWeatherCity
font.pixelSize: 13
color: Settings.Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
tempWeatherCity = text
}
MouseArea {
anchors.fill: parent
onClicked: {
cityInput.forceActiveFocus()
}
}
}
}
}
// Temperature Unit Setting
RowLayout {
spacing: 12
Layout.fillWidth: true
Text {
text: "Temperature Unit"
font.pixelSize: 13
font.bold: true
color: Settings.Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
// Custom Material 3 Switch
Rectangle {
id: customSwitch
width: 52
height: 32
radius: 16
color: Settings.Theme.accentPrimary
border.color: Settings.Theme.accentPrimary
border.width: 2
Rectangle {
id: thumb
width: 28
height: 28
radius: 14
color: Settings.Theme.surface
border.color: Settings.Theme.outline
border.width: 1
y: 2
x: tempUseFahrenheit ? customSwitch.width - width - 2 : 2
Text {
anchors.centerIn: parent
text: tempUseFahrenheit ? "°F" : "°C"
font.pixelSize: 12
font.bold: true
color: Settings.Theme.textPrimary
}
Behavior on x {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
tempUseFahrenheit = !tempUseFahrenheit
}
}
}
}
}
}
// Profile Image Card
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 140
color: Settings.Theme.surface
radius: 18
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 0
border.color: "transparent"
border.width: 0
Layout.bottomMargin: 16
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// Profile Image Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Settings.Theme.accentPrimary
}
Text {
text: "Profile Image"
font.pixelSize: 16
font.bold: true
color: Settings.Theme.textPrimary
Layout.fillWidth: true
}
}
// Profile Image Input Row
RowLayout {
spacing: 8
Layout.fillWidth: true
Rectangle {
width: 36
height: 36
radius: 18
color: Settings.Theme.surfaceVariant
border.color: profileImageInput.activeFocus ? Settings.Theme.accentPrimary : Settings.Theme.outline
border.width: 1
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 2
source: tempProfileImage
fillMode: Image.PreserveAspectCrop
visible: false
asynchronous: true
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: avatarImage.width / 2
visible: false
}
visible: tempProfileImage !== ""
}
// Fallback icon
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: Settings.Theme.accentPrimary
visible: tempProfileImage === ""
}
}
// Text input styled exactly like weather city
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 8
color: Settings.Theme.surfaceVariant
border.color: profileImageInput.activeFocus ? Settings.Theme.accentPrimary : Settings.Theme.outline
border.width: 1
TextInput {
id: profileImageInput
anchors.fill: parent
anchors.margins: 12
text: tempProfileImage
font.pixelSize: 13
color: Settings.Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
tempProfileImage = text
}
MouseArea {
anchors.fill: parent
onClicked: {
profileImageInput.forceActiveFocus()
}
}
}
}
}
}
}
// Wallpaper Folder Card
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 100
color: Settings.Theme.surface
radius: 18
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: "image"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Settings.Theme.accentPrimary
}
Text {
text: "Wallpaper Folder"
font.pixelSize: 16
font.bold: true
color: Settings.Theme.textPrimary
Layout.fillWidth: true
}
}
// Folder Path Input
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 8
color: Settings.Theme.surfaceVariant
border.color: wallpaperFolderInput.activeFocus ? Settings.Theme.accentPrimary : Settings.Theme.outline
border.width: 1
TextInput {
id: wallpaperFolderInput
anchors.fill: parent
anchors.margins: 12
text: tempWallpaperFolder
font.pixelSize: 13
color: Settings.Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhUrlCharactersOnly
onTextChanged: tempWallpaperFolder = text
MouseArea {
anchors.fill: parent
onClicked: wallpaperFolderInput.forceActiveFocus()
}
}
}
}
}
// Spacer to push content to top
Item {
Layout.fillHeight: true
}
// Apply Button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 44
radius: 12
color: applyButtonArea.containsMouse ? Settings.Theme.accentPrimary : Settings.Theme.accentPrimary
border.color: "transparent"
border.width: 0
opacity: 1.0
Text {
anchors.centerIn: parent
text: "Apply Changes"
font.pixelSize: 15
font.bold: true
color: applyButtonArea.containsMouse ? Settings.Theme.onAccent : Settings.Theme.onAccent
}
MouseArea {
id: applyButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
// Apply the changes
Settings.weatherCity = tempWeatherCity
Settings.useFahrenheit = tempUseFahrenheit
Settings.profileImage = tempProfileImage
Settings.wallpaperFolder = tempWallpaperFolder
// Force save settings
Settings.saveSettings()
// Refresh weather if available
if (typeof weather !== 'undefined' && weather) {
weather.fetchCityWeather()
}
// Close the modal
settingsModal.closeSettings()
}
}
}
}
// Function to open the modal and initialize temp values
function openSettings() {
tempWeatherCity = Settings.weatherCity
tempUseFahrenheit = Settings.useFahrenheit
tempProfileImage = Settings.profileImage
tempWallpaperFolder = Settings.wallpaperFolder
visible = true
// Force focus on the text input after a short delay
focusTimer.start()
}
// Function to close the modal and release focus
function closeSettings() {
visible = false
cityInput.focus = false
profileImageInput.focus = false
wallpaperFolderInput.focus = false
}
Timer {
id: focusTimer
interval: 100
repeat: false
onTriggered: {
if (visible) {
cityInput.forceActiveFocus()
// Optionally, also focus profileImageInput if you want both to get focus:
// profileImageInput.forceActiveFocus()
}
}
}
// Release focus when modal becomes invisible
onVisibleChanged: {
if (!visible) {
cityInput.focus = false
profileImageInput.focus = false
wallpaperFolderInput.focus = false
}
}
}

View file

@ -0,0 +1,54 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import qs.Settings
ColumnLayout {
property alias title: headerText.text
property bool expanded: false // Hidden by default
default property alias content: contentItem.children
Rectangle {
Layout.fillWidth: true
height: 44
radius: 12
color: Theme.surface
border.color: Theme.accentPrimary
border.width: 2
RowLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
Text {
id: headerText
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
}
Item { Layout.fillWidth: true }
Rectangle {
width: 32; height: 32
color: "transparent"
Text {
anchors.centerIn: parent
text: expanded ? "expand_less" : "expand_more"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Theme.accentPrimary
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: expanded = !expanded
}
}
Item { height: 8 }
ColumnLayout {
id: contentItem
Layout.fillWidth: true
visible: expanded
spacing: 0
}
}

View file

@ -0,0 +1,171 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Qt5Compat.GraphicalEffects
import qs.Settings
Rectangle {
id: profileSettingsCard
Layout.fillWidth: true
Layout.preferredHeight: 140
color: Theme.surface
radius: 18
border.color: "transparent"
border.width: 0
Layout.bottomMargin: 16
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// Profile Image Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Theme.accentPrimary
}
Text {
text: "Profile Image"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
}
// Profile Image Input Row
RowLayout {
spacing: 8
Layout.fillWidth: true
Rectangle {
width: 36
height: 36
radius: 18
color: Theme.surfaceVariant
border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 2
source: Settings.profileImage
fillMode: Image.PreserveAspectCrop
visible: false
asynchronous: true
cache: false
sourceSize.width: 64
sourceSize.height: 64
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: avatarImage.width / 2
visible: false
}
visible: Settings.profileImage !== ""
}
// Fallback icon
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: Theme.accentPrimary
visible: Settings.profileImage === ""
}
}
// Text input styled exactly like weather city
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 8
color: Theme.surfaceVariant
border.color: profileImageInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: profileImageInput
anchors.fill: parent
anchors.margins: 12
text: Settings.profileImage
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
Settings.profileImage = text
Settings.saveSettings()
}
MouseArea {
anchors.fill: parent
onClicked: {
profileImageInput.forceActiveFocus()
}
}
}
}
}
// Video Path Input Row
RowLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "Video Path"
font.pixelSize: 14
color: Theme.textPrimary
Layout.alignment: Qt.AlignVCenter
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 8
color: Theme.surfaceVariant
border.color: videoPathInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: videoPathInput
anchors.fill: parent
anchors.margins: 12
text: Settings.videoPath !== undefined ? Settings.videoPath : ""
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhUrlCharactersOnly
onTextChanged: {
Settings.videoPath = text
Settings.saveSettings()
}
MouseArea {
anchors.fill: parent
onClicked: videoPathInput.forceActiveFocus()
}
}
}
}
}
}

View file

@ -0,0 +1,210 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Quickshell
import Quickshell.Wayland
import qs.Settings
PanelWindow {
id: settingsModal
implicitWidth: 480
implicitHeight: 720
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: -24
//z: 100
//border.color: Theme.outline
//border.width: 1
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
// Local properties for editing (not saved until apply)
property string tempWeatherCity: (Settings.weatherCity !== undefined && Settings.weatherCity !== null) ? Settings.weatherCity : ""
property bool tempUseFahrenheit: Settings.useFahrenheit
property string tempProfileImage: (Settings.profileImage !== undefined && Settings.profileImage !== null) ? Settings.profileImage : ""
property string tempWallpaperFolder: (Settings.wallpaperFolder !== undefined && Settings.wallpaperFolder !== null) ? Settings.wallpaperFolder : ""
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 24
//border.color: Theme.outline
//border.width: 1
z: 0
ColumnLayout {
anchors.fill: parent
anchors.margins: 32
spacing: 24
// Header
ColumnLayout {
Layout.fillWidth: true
spacing: 4
RowLayout {
Layout.fillWidth: true
spacing: 20
Text {
text: "settings"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.accentPrimary
}
Text {
text: "Settings"
font.pixelSize: 26
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
Rectangle {
width: 36
height: 36
radius: 18
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
Text {
anchors.centerIn: parent
text: "close"
font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 20
color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: closeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: settingsModal.closeSettings()
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.outline
opacity: 0.12
}
}
// Scrollable settings area
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 520
color: "transparent"
border.width: 0
radius: 20
Flickable {
id: flick
anchors.fill: parent
contentWidth: width
contentHeight: column.implicitHeight
clip: true
interactive: true
boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: column
width: flick.width
spacing: 24
// CollapsibleCategory sections here
CollapsibleCategory {
title: "Weather"
expanded: false
WeatherSettings {
weatherCity: (typeof tempWeatherCity !== 'undefined' && tempWeatherCity !== null) ? tempWeatherCity : ""
useFahrenheit: tempUseFahrenheit
onCityChanged: function(city) { tempWeatherCity = city }
onTemperatureUnitChanged: function(useFahrenheit) { tempUseFahrenheit = useFahrenheit }
}
}
CollapsibleCategory {
title: "System"
expanded: false
ProfileSettings { }
}
CollapsibleCategory {
title: "Wallpaper"
expanded: false
WallpaperSettings {
wallpaperFolder: (typeof tempWallpaperFolder !== 'undefined' && tempWallpaperFolder !== null) ? tempWallpaperFolder : ""
onWallpaperFolderEdited: function(folder) { tempWallpaperFolder = folder }
}
}
}
}
}
// Apply Button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 52
radius: 16
color: applyButtonArea.containsMouse ? Theme.accentPrimary : Theme.accentPrimary
border.color: "transparent"
border.width: 0
opacity: 1.0
Text {
anchors.centerIn: parent
text: "Apply Changes"
font.pixelSize: 17
font.bold: true
color: applyButtonArea.containsMouse ? Theme.onAccent : Theme.onAccent
}
MouseArea {
id: applyButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Settings.weatherCity = (typeof tempWeatherCity !== 'undefined' && tempWeatherCity !== null) ? tempWeatherCity : ""
Settings.useFahrenheit = tempUseFahrenheit
Settings.profileImage = (typeof tempProfileImage !== 'undefined' && tempProfileImage !== null) ? tempProfileImage : ""
Settings.wallpaperFolder = (typeof tempWallpaperFolder !== 'undefined' && tempWallpaperFolder !== null) ? tempWallpaperFolder : ""
Settings.saveSettings()
if (typeof weather !== 'undefined' && weather) {
weather.fetchCityWeather()
}
settingsModal.closeSettings()
}
}
}
}
}
// Function to open the modal and initialize temp values
function openSettings() {
tempWeatherCity = (Settings.weatherCity !== undefined && Settings.weatherCity !== null) ? Settings.weatherCity : ""
tempUseFahrenheit = Settings.useFahrenheit
tempProfileImage = (Settings.profileImage !== undefined && Settings.profileImage !== null) ? Settings.profileImage : ""
tempWallpaperFolder = (Settings.wallpaperFolder !== undefined && Settings.wallpaperFolder !== null) ? Settings.wallpaperFolder : ""
if (tempWallpaperFolder === undefined || tempWallpaperFolder === null) tempWallpaperFolder = ""
visible = true
// Force focus on the text input after a short delay
focusTimer.start()
}
// Function to close the modal and release focus
function closeSettings() {
visible = false
}
Timer {
id: focusTimer
interval: 100
repeat: false
onTriggered: {
if (visible) {
// Focus will be handled by the individual components
}
}
}
// Release focus when modal becomes invisible
onVisibleChanged: {
if (!visible) {
// Focus will be handled by the individual components
}
}
}

View file

@ -0,0 +1,71 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import qs.Settings
Rectangle {
id: wallpaperSettingsCard
Layout.fillWidth: true
Layout.preferredHeight: 100
color: Theme.surface
radius: 18
// Property for binding
property string wallpaperFolder: ""
signal wallpaperFolderEdited(string folder)
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: "image"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Theme.accentPrimary
}
Text {
text: "Wallpaper Folder"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
}
// Folder Path Input
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 8
color: Theme.surfaceVariant
border.color: folderInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: folderInput
anchors.fill: parent
anchors.margins: 12
text: wallpaperFolder
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhUrlCharactersOnly
onTextChanged: {
wallpaperFolderEdited(text)
}
MouseArea {
anchors.fill: parent
onClicked: folderInput.forceActiveFocus()
}
}
}
}
}

View file

@ -0,0 +1,153 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import qs.Settings
Rectangle {
id: weatherSettingsCard
Layout.fillWidth: true
Layout.preferredHeight: 180
color: Theme.surface
radius: 18
// Properties for binding
property string weatherCity: ""
property bool useFahrenheit: false
signal cityChanged(string city)
signal temperatureUnitChanged(bool useFahrenheit)
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// Weather Settings Header
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
text: "wb_sunny"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Theme.accentPrimary
}
Text {
text: "Weather Settings"
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
}
// Weather City Setting
ColumnLayout {
spacing: 8
Layout.fillWidth: true
Text {
text: "City"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 8
color: Theme.surfaceVariant
border.color: cityInput.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: cityInput
anchors.fill: parent
anchors.margins: 12
text: weatherCity
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
cityChanged(text)
}
MouseArea {
anchors.fill: parent
onClicked: {
cityInput.forceActiveFocus()
}
}
}
}
}
// Temperature Unit Setting
RowLayout {
spacing: 12
Layout.fillWidth: true
Text {
text: "Temperature Unit"
font.pixelSize: 13
font.bold: true
color: Theme.textPrimary
}
Item {
Layout.fillWidth: true
}
// Custom Material 3 Switch
Rectangle {
id: customSwitch
width: 52
height: 32
radius: 16
color: Theme.accentPrimary
border.color: Theme.accentPrimary
border.width: 2
Rectangle {
id: thumb
width: 28
height: 28
radius: 14
color: Theme.surface
border.color: Theme.outline
border.width: 1
y: 2
x: useFahrenheit ? customSwitch.width - width - 2 : 2
Text {
anchors.centerIn: parent
text: useFahrenheit ? "°F" : "°C"
font.pixelSize: 12
font.bold: true
color: Theme.textPrimary
}
Behavior on x {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
temperatureUnitChanged(!useFahrenheit)
}
}
}
}
}
}

View file

@ -0,0 +1,343 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Quickshell.Wayland
import Quickshell
import Quickshell.Bluetooth
import qs.Settings
import qs.Components
import qs.Helpers
Item {
id: root
property alias panel: bluetoothPanelModal
// For showing error/status messages
property string statusMessage: ""
property bool statusPopupVisible: false
function showStatus(msg) {
statusMessage = msg
statusPopupVisible = true
}
function hideStatus() {
statusPopupVisible = false
}
function showAt() {
bluetoothLogic.showAt()
}
Rectangle {
id: card
width: 36; height: 36
radius: 18
border.color: Theme.accentPrimary
border.width: 1
color: bluetoothButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
Text {
anchors.centerIn: parent
text: "bluetooth"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: bluetoothButtonArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
}
MouseArea {
id: bluetoothButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: bluetoothLogic.showAt()
}
}
QtObject {
id: bluetoothLogic
function showAt() {
if (Bluetooth.defaultAdapter) {
if (!Bluetooth.defaultAdapter.enabled)
Bluetooth.defaultAdapter.enabled = true
if (!Bluetooth.defaultAdapter.discovering)
Bluetooth.defaultAdapter.discovering = true
}
bluetoothPanelModal.visible = true
}
}
PanelWindow {
id: bluetoothPanelModal
implicitWidth: 480
implicitHeight: 720
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: -24
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
onVisibleChanged: {
if (!visible && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering)
Bluetooth.defaultAdapter.discovering = false
}
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 24
ColumnLayout {
anchors.fill: parent
anchors.margins: 32
spacing: 0
RowLayout {
Layout.fillWidth: true
spacing: 20
Layout.preferredHeight: 48
Layout.leftMargin: 16
Layout.rightMargin: 16
Text {
text: "bluetooth"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.accentPrimary
}
Text {
text: "Bluetooth"
font.pixelSize: 26
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
Rectangle {
width: 36; height: 36; radius: 18
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
Text {
anchors.centerIn: parent
text: "close"
font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 20
color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: closeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: bluetoothPanelModal.visible = false
cursorShape: Qt.PointingHandCursor
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.outline
opacity: 0.12
}
// Content area (centered, in a card)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 520
Layout.alignment: Qt.AlignHCenter
Layout.margins: 0
color: Theme.surfaceVariant
radius: 18
border.color: Theme.outline
border.width: 1
anchors.topMargin: 32
Rectangle {
id: bg
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 12
border.width: 1
border.color: Theme.surfaceVariant
z: 0
}
Rectangle {
id: header
color: "transparent"
}
Rectangle {
id: listContainer
anchors.top: header.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 24
color: "transparent"
clip: true
ListView {
id: deviceListView
anchors.fill: parent
spacing: 4
boundsBehavior: Flickable.StopAtBounds
model: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : []
delegate: Rectangle {
width: parent.width
height: 60
color: "transparent"
radius: 8
property bool userInitiatedDisconnect: false
Rectangle {
anchors.fill: parent
radius: 8
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.18)
: (deviceMouseArea.containsMouse ? Theme.highlight : "transparent")
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 12
// Fixed-width icon for alignment
Text {
width: 28
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: modelData.connected ? "bluetooth" : "bluetooth_disabled"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
// Device name always fills width for alignment
Text {
Layout.fillWidth: true
text: modelData.name || "Unknown Device"
color: modelData.connected ? Theme.accentPrimary : Theme.textPrimary
font.pixelSize: 14
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: modelData.address
color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary
font.pixelSize: 11
elide: Text.ElideRight
}
Text {
text: "Paired: " + modelData.paired + " | Trusted: " + modelData.trusted
font.pixelSize: 10
color: Theme.textSecondary
visible: true
}
// No "Connected" text here!
}
Spinner {
running: modelData.pairing || modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting
color: Theme.textPrimary
size: 16
visible: running
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected) {
userInitiatedDisconnect = true
modelData.disconnect()
} else if (!modelData.paired) {
modelData.pair()
root.showStatus("Pairing... Please check your phone or system for a PIN dialog.")
} else {
modelData.connect()
}
}
}
Connections {
target: modelData
function onPairedChanged() {
if (modelData.paired) {
root.showStatus("Paired! Now connecting...")
modelData.connect()
}
}
function onPairingChanged() {
if (!modelData.pairing && !modelData.paired) {
root.showStatus("Pairing failed or was cancelled.")
}
}
function onConnectedChanged() {
userInitiatedDisconnect = false
}
function onStateChanged() {
// Optionally handle more granular feedback here
}
}
}
}
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: 2
anchors.top: listContainer.top
anchors.bottom: listContainer.bottom
width: 4
radius: 2
color: Theme.textSecondary
opacity: deviceListView.contentHeight > deviceListView.height ? 0.3 : 0
visible: opacity > 0
}
}
}
}
// Status/Info popup
Popup {
id: statusPopup
x: (parent.width - width) / 2
y: 40
width: Math.min(360, parent.width - 40)
visible: root.statusPopupVisible
modal: false
focus: false
background: Rectangle {
color: Theme.accentPrimary // Use your theme's accent color
radius: 8
}
contentItem: Text {
text: root.statusMessage
color: "white"
wrapMode: Text.WordWrap
padding: 12
font.pixelSize: 14
}
onVisibleChanged: {
if (visible) {
// Auto-hide after 3 seconds
statusPopupTimer.restart()
}
}
}
}
}

View file

@ -0,0 +1,410 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import Qt5Compat.GraphicalEffects
import Quickshell.Services.Mpris
import qs.Settings
import qs.Components
import QtQuick
Rectangle {
id: musicCard
width: 360
height: 200
color: "transparent"
property var currentPlayer: null
property real currentPosition: 0
property int selectedPlayerIndex: 0
// Get all available players
function getAvailablePlayers() {
if (!Mpris.players || !Mpris.players.values) {
return []
}
let allPlayers = Mpris.players.values
let controllablePlayers = []
for (let i = 0; i < allPlayers.length; i++) {
let player = allPlayers[i]
if (player && player.canControl) {
controllablePlayers.push(player)
}
}
return controllablePlayers
}
// Find the active player
function findActivePlayer() {
let availablePlayers = getAvailablePlayers()
if (availablePlayers.length === 0) {
return null
}
// Use selected player if valid, otherwise use first available
if (selectedPlayerIndex < availablePlayers.length) {
return availablePlayers[selectedPlayerIndex]
} else {
selectedPlayerIndex = 0
return availablePlayers[0]
}
}
// Update current player
function updateCurrentPlayer() {
let newPlayer = findActivePlayer()
if (newPlayer !== currentPlayer) {
currentPlayer = newPlayer
currentPosition = currentPlayer ? currentPlayer.position : 0
}
}
// Timer to update progress bar position
Timer {
id: positionTimer
interval: 1000
running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0
repeat: true
onTriggered: {
if (currentPlayer && currentPlayer.isPlaying) {
currentPosition = currentPlayer.position
}
}
}
// Monitor for player changes
Connections {
target: Mpris.players
function onValuesChanged() {
updateCurrentPlayer()
}
}
Component.onCompleted: {
updateCurrentPlayer()
}
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
// No music player available state
Item {
width: parent.width
height: parent.height
visible: !currentPlayer
ColumnLayout {
anchors.centerIn: parent
spacing: 16
Text {
text: "music_note"
font.family: "Material Symbols Outlined"
font.pixelSize: 48
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
Layout.alignment: Qt.AlignHCenter
}
Text {
text: getAvailablePlayers().length > 0 ? "No controllable player selected" : "No music player detected"
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
font.family: "Roboto"
font.pixelSize: 14
Layout.alignment: Qt.AlignHCenter
}
}
}
// Music player content
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
visible: currentPlayer
// Album artwork and track info row
RowLayout {
spacing: 12
Layout.fillWidth: true
// Album artwork with circular spectrum visualizer, aligned left
Item {
id: albumArtContainer
width: 96; height: 96 // enough for spectrum and art (will adjust if needed)
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
// Circular spectrum visualizer behind album art
CircularSpectrum {
id: spectrum
anchors.centerIn: parent
innerRadius: 30 // just outside 60x60 album art
outerRadius: 48 // how far bars extend
fillColor: Theme.accentPrimary
strokeColor: Theme.accentPrimary
strokeWidth: 0
z: 0
}
// Album art in the center
Rectangle {
id: albumArtwork
width: 60; height: 60
anchors.centerIn: parent
radius: 30 // circle
color: Qt.darker(Theme.surface, 1.1)
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
border.width: 1
Image {
id: albumArt
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectCrop
smooth: true
cache: false
asynchronous: true
sourceSize.width: 60
sourceSize.height: 60
source: currentPlayer ? (currentPlayer.trackArtUrl || "") : ""
visible: source.toString() !== ""
// Rounded corners using layer
layer.enabled: true
layer.effect: OpacityMask {
cached: true
maskSource: Rectangle {
width: albumArt.width
height: albumArt.height
radius: albumArt.width / 2 // circle
visible: false
}
}
}
// Fallback music icon
Text {
anchors.centerIn: parent
text: "album"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4)
visible: !albumArt.visible
}
}
}
// Track info
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text {
text: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
color: Theme.textPrimary
font.family: "Roboto"
font.pixelSize: 14
font.bold: true
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
Layout.fillWidth: true
}
Text {
text: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.8)
font.family: "Roboto"
font.pixelSize: 12
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
font.family: "Roboto"
font.pixelSize: 10
elide: Text.ElideRight
Layout.fillWidth: true
}
}
}
// Progress bar
Rectangle {
id: progressBarBackground
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.15)
Layout.fillWidth: true
property real progressRatio: currentPlayer && currentPlayer.length > 0 ?
(currentPosition / currentPlayer.length) : 0
Rectangle {
id: progressFill
width: progressBarBackground.progressRatio * parent.width
height: parent.height
radius: parent.radius
color: Theme.accentPrimary
Behavior on width {
NumberAnimation { duration: 200 }
}
}
// Interactive progress handle
Rectangle {
id: progressHandle
width: 12
height: 12
radius: 6
color: Theme.accentPrimary
border.color: Qt.lighter(Theme.accentPrimary, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter
visible: currentPlayer && currentPlayer.length > 0
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
}
// Mouse area for seeking
MouseArea {
id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.length > 0 && currentPlayer.canSeek
onClicked: function(mouse) {
if (currentPlayer && currentPlayer.length > 0) {
let ratio = mouse.x / width
let seekPosition = ratio * currentPlayer.length
currentPlayer.position = seekPosition
currentPosition = seekPosition
}
}
onPositionChanged: function(mouse) {
if (pressed && currentPlayer && currentPlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let seekPosition = ratio * currentPlayer.length
currentPlayer.position = seekPosition
currentPosition = seekPosition
}
}
}
}
// Media controls
RowLayout {
spacing: 4
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
// Previous button
Rectangle {
width: 28
height: 28
radius: 14
color: previousButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
border.width: 1
MouseArea {
id: previousButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canGoPrevious
onClicked: if (currentPlayer) currentPlayer.previous()
}
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: previousButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
}
}
// Play/Pause button
Rectangle {
width: 36
height: 36
radius: 18
color: playButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
border.color: Theme.accentPrimary
border.width: 2
MouseArea {
id: playButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && (currentPlayer.canPlay || currentPlayer.canPause)
onClicked: {
if (currentPlayer) {
if (currentPlayer.isPlaying) {
currentPlayer.pause()
} else {
currentPlayer.play()
}
}
}
}
Text {
anchors.centerIn: parent
text: currentPlayer && currentPlayer.isPlaying ? "pause" : "play_arrow"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: playButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
}
}
// Next button
Rectangle {
width: 28
height: 28
radius: 14
color: nextButton.containsMouse ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.2) : Qt.darker(Theme.surface, 1.1)
border.color: Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.3)
border.width: 1
MouseArea {
id: nextButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canGoNext
onClicked: if (currentPlayer) currentPlayer.next()
}
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: nextButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
}
}
}
}
}
// Audio Visualizer (Cava)
Cava {
id: cava
count: 64
}
}

View file

@ -0,0 +1,394 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Settings
import qs.Widgets.Sidebar.Config
import qs.Components
PanelWindow {
id: panelPopup
implicitWidth: 500
implicitHeight: 750
visible: false
color: "transparent"
screen: modelData
anchors.top: true
anchors.right: true
margins.top: -24
WlrLayershell.keyboardFocus: (settingsModal.visible && mouseArea.containsMouse) ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
// Animation properties
property real slideOffset: width
property bool isAnimating: false
function showAt() {
if (!visible) {
visible = true;
forceActiveFocus();
slideAnim.from = width;
slideAnim.to = 0;
slideAnim.running = true;
// Start system monitoring when sidebar becomes visible
if (systemMonitor) systemMonitor.startMonitoring();
if (weather) weather.startWeatherFetch();
if (systemWidget) systemWidget.panelVisible = true;
if (quickAccessWidget) quickAccessWidget.panelVisible = true;
}
}
function hidePopup() {
if (visible) {
slideAnim.from = 0;
slideAnim.to = width;
slideAnim.running = true;
}
}
NumberAnimation {
id: slideAnim
target: panelPopup
property: "slideOffset"
duration: 300
easing.type: Easing.OutCubic
onStopped: {
if (panelPopup.slideOffset === panelPopup.width) {
panelPopup.visible = false;
// Stop system monitoring when sidebar becomes hidden
if (systemMonitor) systemMonitor.stopMonitoring();
if (weather) weather.stopWeatherFetch();
if (systemWidget) systemWidget.panelVisible = false;
if (quickAccessWidget) quickAccessWidget.panelVisible = false;
}
panelPopup.isAnimating = false;
}
onStarted: {
panelPopup.isAnimating = true;
}
}
property int leftPadding: 20
property int bottomPadding: 20
Rectangle {
id: mainRectangle
width: parent.width - leftPadding
height: parent.height - bottomPadding
anchors.top: parent.top
x: leftPadding + slideOffset
y: 0
color: Theme.backgroundPrimary
bottomLeftRadius: 20
z: 0
Behavior on x {
enabled: !panelPopup.isAnimating
NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
}
}
property alias settingsModal: settingsModal
SettingsModal {
id: settingsModal
}
Item {
anchors.fill: mainRectangle
x: slideOffset
Behavior on x {
enabled: !panelPopup.isAnimating
NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
System {
id: systemWidget
Layout.alignment: Qt.AlignHCenter
z: 3
}
Weather {
id: weather
Layout.alignment: Qt.AlignHCenter
z: 2
}
// Music and System Monitor row
RowLayout {
spacing: 12
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
Music {
z: 2
}
SystemMonitor {
id: systemMonitor
z: 2
}
}
// Power profile, Wifi and Bluetooth row
RowLayout {
Layout.alignment: Qt.AlignLeft
Layout.preferredHeight: 80
spacing: 16
z: 3
PowerProfile {
Layout.alignment: Qt.AlignLeft
Layout.preferredHeight: 80
}
// Network card containing Wifi and Bluetooth
Rectangle {
Layout.preferredHeight: 70
Layout.preferredWidth: 140
Layout.fillWidth: false
color: Theme.surface
radius: 18
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: 20
// Wifi button
Rectangle {
id: wifiButton
width: 36; height: 36
radius: 18
border.color: Theme.accentPrimary
border.width: 1
color: wifiButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
Text {
anchors.centerIn: parent
text: "wifi"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: wifiButtonArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: wifiButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: wifiPanelModal.showAt()
}
}
// Bluetooth button
Rectangle {
id: bluetoothButton
width: 36; height: 36
radius: 18
border.color: Theme.accentPrimary
border.width: 1
color: bluetoothButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
Text {
anchors.centerIn: parent
text: "bluetooth"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: bluetoothButtonArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: bluetoothButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: bluetoothPanelModal.showAt()
}
}
}
}
}
// Hidden panel components for modal functionality
WifiPanel {
id: wifiPanelModal
visible: false
}
BluetoothPanel {
id: bluetoothPanelModal
visible: false
}
Item {
Layout.fillHeight: true
}
// QuickAccess widget
QuickAccess {
id: quickAccessWidget
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -16
z: 2
isRecording: panelPopup.isRecording
onRecordingRequested: {
startRecording()
}
onStopRecordingRequested: {
stopRecording()
}
onRecordingStateMismatch: function(actualState) {
isRecording = actualState
quickAccessWidget.isRecording = actualState
}
onSettingsRequested: {
settingsModal.visible = true
}
onWallpaperRequested: {
wallpaperPanelModal.visible = true
}
}
}
Keys.onEscapePressed: panelPopup.hidePopup()
}
onVisibleChanged: if (!visible) {/* cleanup if needed */}
// Update height when screen changes
onScreenChanged: {
if (screen) {
// Height is now hardcoded to 720, no need to update
}
}
// Recording properties
property bool isRecording: false
property var recordingProcess: null
property var recordingPid: null
// Start screen recording
function startRecording() {
var currentDate = new Date()
var hours = String(currentDate.getHours()).padStart(2, '0')
var minutes = String(currentDate.getMinutes()).padStart(2, '0')
var day = String(currentDate.getDate()).padStart(2, '0')
var month = String(currentDate.getMonth() + 1).padStart(2, '0')
var year = currentDate.getFullYear()
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4"
var outputPath = Settings.videoPath + filename
var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath
var qmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "' + command + '"]; running: true }'
recordingProcess = Qt.createQmlObject(qmlString, panelPopup)
isRecording = true
quickAccessWidget.isRecording = true
}
// Stop recording with cleanup
function stopRecording() {
if (recordingProcess && isRecording) {
var stopQmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -SIGINT -f \'gpu-screen-recorder.*portal\'"]; running: true; onExited: function() { destroy() } }'
var stopProcess = Qt.createQmlObject(stopQmlString, panelPopup)
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', panelPopup)
cleanupTimer.triggered.connect(function() {
if (recordingProcess) {
recordingProcess.running = false
recordingProcess.destroy()
recordingProcess = null
}
var forceKillQml = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f \'gpu-screen-recorder.*portal\' 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }'
var forceKillProcess = Qt.createQmlObject(forceKillQml, panelPopup)
cleanupTimer.destroy()
})
}
isRecording = false
quickAccessWidget.isRecording = false
recordingPid = null
}
// Clean up processes on destruction
Component.onDestruction: {
if (isRecording) {
stopRecording()
}
if (recordingProcess) {
recordingProcess.running = false
recordingProcess.destroy()
recordingProcess = null
}
}
Corners {
id: sidebarCornerLeft
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.top: mainRectangle.top
offsetX: -447 + panelPopup.slideOffset
offsetY: 0
Behavior on offsetX {
enabled: !panelPopup.isAnimating
NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
}
}
Corners {
id: sidebarCornerBottom
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
offsetX: 33 + panelPopup.slideOffset
offsetY: 46
Behavior on offsetX {
enabled: !panelPopup.isAnimating
NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
}
}
WallpaperPanel {
id: wallpaperPanelModal
visible: false
Component.onCompleted: {
if (parent) {
wallpaperPanelModal.anchors.top = parent.top;
wallpaperPanelModal.anchors.right = parent.right;
}
}
// Add a close button inside WallpaperPanel.qml for user to close the modal
}
}

View file

@ -0,0 +1,127 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Quickshell.Services.UPower
import qs.Settings
Rectangle {
id: card
width: 200
height: 70
color: Theme.surface
radius: 18
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: 20
// Performance
Rectangle {
width: 36; height: 36
radius: 18
border.color: Theme.accentPrimary
border.width: 1
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance)
? Theme.accentPrimary
: (perfMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
opacity: (typeof PowerProfiles !== 'undefined' && !PowerProfiles.hasPerformanceProfile) ? 0.4 : 1
Text {
anchors.centerIn: parent
text: "speed"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Performance) || perfMouseArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: perfMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: typeof PowerProfiles !== 'undefined' && PowerProfiles.hasPerformanceProfile
cursorShape: Qt.PointingHandCursor
onClicked: {
if (typeof PowerProfiles !== 'undefined')
PowerProfiles.profile = PowerProfile.Performance;
}
}
}
// Balanced
Rectangle {
width: 36; height: 36
radius: 18
border.color: Theme.accentPrimary
border.width: 1
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced)
? Theme.accentPrimary
: (balMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
opacity: 1
Text {
anchors.centerIn: parent
text: "balance"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.Balanced) || balMouseArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: balMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (typeof PowerProfiles !== 'undefined')
PowerProfiles.profile = PowerProfile.Balanced;
}
}
}
// Power Saver
Rectangle {
width: 36; height: 36
radius: 18
border.color: Theme.accentPrimary
border.width: 1
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver)
? Theme.accentPrimary
: (saveMouseArea.containsMouse ? Theme.accentPrimary : "transparent")
opacity: 1
Text {
anchors.centerIn: parent
text: "eco"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: (typeof PowerProfiles !== 'undefined' && PowerProfiles.profile === PowerProfile.PowerSaver) || saveMouseArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: saveMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (typeof PowerProfiles !== 'undefined')
PowerProfiles.profile = PowerProfile.PowerSaver;
}
}
}
}
}

View file

@ -0,0 +1,196 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import "root:/Settings" as Settings
Rectangle {
id: quickAccessWidget
width: 440
height: 80
color: "transparent"
anchors.horizontalCenterOffset: -2
required property bool isRecording
signal recordingRequested()
signal stopRecordingRequested()
signal recordingStateMismatch(bool actualState)
signal settingsRequested()
signal wallpaperRequested()
Rectangle {
id: card
anchors.fill: parent
color: Settings.Theme.surface
radius: 18
RowLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// Settings Button
Rectangle {
id: settingsButton
Layout.fillWidth: true
Layout.preferredHeight: 44
radius: 12
color: settingsButtonArea.containsMouse ? Settings.Theme.accentPrimary : "transparent"
border.color: Settings.Theme.accentPrimary
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "settings"
font.family: settingsButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16
color: settingsButtonArea.containsMouse ? Settings.Theme.onAccent : Settings.Theme.accentPrimary
}
Text {
text: "Settings"
font.pixelSize: 14
font.bold: true
color: settingsButtonArea.containsMouse ? Settings.Theme.onAccent : Settings.Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: settingsButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
settingsRequested()
}
}
}
// Screen Recorder Button
Rectangle {
id: recorderButton
Layout.fillWidth: true
Layout.preferredHeight: 44
radius: 12
color: isRecording ? Settings.Theme.accentPrimary :
(recorderButtonArea.containsMouse ? Settings.Theme.accentPrimary : "transparent")
border.color: Settings.Theme.accentPrimary
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: isRecording ? "radio_button_checked" : "radio_button_unchecked"
font.family: (isRecording || recorderButtonArea.containsMouse) ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 16
color: isRecording || recorderButtonArea.containsMouse ? Settings.Theme.onAccent : Settings.Theme.accentPrimary
}
Text {
text: isRecording ? "End" : "Record"
font.pixelSize: 14
font.bold: true
color: isRecording || recorderButtonArea.containsMouse ? Settings.Theme.onAccent : Settings.Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: recorderButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (isRecording) {
stopRecordingRequested()
} else {
recordingRequested()
}
}
}
}
// Wallpaper Button
Rectangle {
id: wallpaperButton
Layout.fillWidth: true
Layout.preferredHeight: 44
radius: 12
color: wallpaperButtonArea.containsMouse ? Settings.Theme.accentPrimary : "transparent"
border.color: Settings.Theme.accentPrimary
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "image"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: wallpaperButtonArea.containsMouse ? Settings.Theme.onAccent : Settings.Theme.accentPrimary
}
Text {
text: "Wallpaper"
font.pixelSize: 14
font.bold: true
color: wallpaperButtonArea.containsMouse ? Settings.Theme.onAccent : Settings.Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: wallpaperButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
wallpaperRequested()
}
}
}
}
}
// Properties
property bool panelVisible: false
// Timer to check if recording is active
Timer {
interval: 2000 // Check every 2 seconds
repeat: true
running: panelVisible
onTriggered: checkRecordingStatus()
}
function checkRecordingStatus() {
// Simple check - if we're recording but no process, reset state
if (isRecording) {
checkRecordingProcess.running = true
}
}
// Process to check if gpu-screen-recorder is running
Process {
id: checkRecordingProcess
command: ["pgrep", "-f", "gpu-screen-recorder.*portal"]
onExited: function(exitCode, exitStatus) {
var isActuallyRecording = exitCode === 0
// If we think we're recording but process isn't running, reset state
if (isRecording && !isActuallyRecording) {
recordingStateMismatch(isActuallyRecording)
}
}
}
}

View file

@ -0,0 +1,372 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import qs.Settings
import qs.Widgets
import qs.Helpers
Rectangle {
id: systemWidget
width: 440
height: 80
color: "transparent"
anchors.horizontalCenterOffset: -2
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// User Info Row
RowLayout {
Layout.fillWidth: true
spacing: 12
// Profile Image
Rectangle {
width: 48
height: 48
radius: 24
color: Theme.accentPrimary
// Border overlay
Rectangle {
anchors.fill: parent
color: "transparent"
radius: 24
border.color: Theme.accentPrimary
border.width: 2
z: 2
}
OpacityMask {
anchors.fill: parent
source: Image {
id: avatarImage
anchors.fill: parent
source: Settings.profileImage !== undefined ? Settings.profileImage : ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
sourceSize.width: 44
sourceSize.height: 44
}
maskSource: Rectangle {
width: 44
height: 44
radius: 22
visible: false
}
visible: Settings.profileImage !== undefined && Settings.profileImage !== ""
z: 1
}
// Fallback icon
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: Theme.onAccent
visible: Settings.profileImage === undefined || Settings.profileImage === ""
z: 0
}
}
// User Info
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: Quickshell.env("USER")
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
}
Text {
text: "System Uptime: " + uptimeText
font.pixelSize: 12
color: Theme.textSecondary
}
}
// Spacer to push button to the right
Item {
Layout.fillWidth: true
}
// System Menu Button - positioned all the way to the right
Rectangle {
id: systemButton
width: 32
height: 32
radius: 16
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
Text {
anchors.centerIn: parent
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: systemButtonArea.containsMouse || systemButtonArea.pressed ? Theme.backgroundPrimary : Theme.accentPrimary
}
MouseArea {
id: systemButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
systemMenu.visible = !systemMenu.visible
}
}
}
}
}
}
// System Menu Popup - positioned below the button
Rectangle {
id: systemMenu
width: 160
height: 180
color: Theme.surface
radius: 8
border.color: Theme.outline
border.width: 1
visible: false
z: 9999
// Position relative to the system button using absolute positioning
x: systemButton.x + systemButton.width - width + 12
y: systemButton.y + systemButton.height + 32
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
// Lock Button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: lockButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "lock_outline"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Lock Screen"
font.pixelSize: 14
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: lockButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
lockScreen.locked = true;
systemMenu.visible = false;
}
}
}
// Reboot Button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: rebootButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "refresh"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Reboot"
font.pixelSize: 14
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: rebootButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Processes.reboot()
systemMenu.visible = false
}
}
}
// Logout Button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: logoutButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "exit_to_app"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Logout"
font.pixelSize: 14
color: logoutButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: logoutButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Processes.logout()
systemMenu.visible = false
}
}
}
// Shutdown Button
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: shutdownButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Shutdown"
font.pixelSize: 14
color: shutdownButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: shutdownButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Processes.shutdown()
systemMenu.visible = false
}
}
}
}
// Close menu when clicking outside
MouseArea {
anchors.fill: parent
enabled: systemMenu.visible
onClicked: systemMenu.visible = false
z: -1 // Put this behind other elements
}
}
// Properties
property string uptimeText: "--:--"
// Process to get uptime
Process {
id: uptimeProcess
command: ["sh", "-c", "uptime | awk -F 'up ' '{print $2}' | awk -F ',' '{print $1}' | xargs"]
running: false
stdout: StdioCollector {
onStreamFinished: {
uptimeText = this.text.trim()
uptimeProcess.running = false
}
}
}
property bool panelVisible: false
// Trigger initial update when panel becomes visible
onPanelVisibleChanged: {
if (panelVisible) {
updateSystemInfo()
}
}
// Timer to update uptime - only runs when panel is visible
Timer {
interval: 60000 // Update every minute
repeat: true
running: panelVisible
onTriggered: updateSystemInfo()
}
Component.onCompleted: {
// Don't update system info immediately - wait for panel to be visible
// updateSystemInfo() will be called when panelVisible becomes true
uptimeProcess.running = true
}
function updateSystemInfo() {
uptimeProcess.running = true
}
// Add lockscreen instance (hidden by default)
LockScreen {
id: lockScreen
}
}

View file

@ -0,0 +1,158 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Quickshell.Io
import "root:/Settings" as Settings
import "root:/Components" as Components
Rectangle {
id: systemMonitor
width: 70
height: 200
color: "transparent"
property real cpuUsage: 0
property real memoryUsage: 0
property real diskUsage: 0
property bool isVisible: false
// Timers to control when processes run
Timer {
id: cpuTimer
interval: 2000
repeat: true
running: isVisible
onTriggered: cpuInfo.running = true
}
Timer {
id: memoryTimer
interval: 3000
repeat: true
running: isVisible
onTriggered: memoryInfo.running = true
}
Timer {
id: diskTimer
interval: 5000
repeat: true
running: isVisible
onTriggered: diskInfo.running = true
}
// Process for getting CPU usage
Process {
id: cpuInfo
command: ["sh", "-c", "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | awk -F'%' '{print $1}'"]
running: false
stdout: SplitParser {
onRead: data => {
let usage = parseFloat(data.trim())
if (!isNaN(usage)) {
systemMonitor.cpuUsage = usage
}
cpuInfo.running = false
}
}
}
// Process for getting memory usage
Process {
id: memoryInfo
command: ["sh", "-c", "free | grep Mem | awk '{print int($3/$2 * 100)}'"]
running: false
stdout: SplitParser {
onRead: data => {
let usage = parseFloat(data.trim())
if (!isNaN(usage)) {
systemMonitor.memoryUsage = usage
}
memoryInfo.running = false
}
}
}
// Process for getting disk usage
Process {
id: diskInfo
command: ["sh", "-c", "df / | tail -1 | awk '{print int($5)}'"]
running: false
stdout: SplitParser {
onRead: data => {
let usage = parseFloat(data.trim())
if (!isNaN(usage)) {
systemMonitor.diskUsage = usage
}
diskInfo.running = false
}
}
}
// Function to start monitoring
function startMonitoring() {
isVisible = true
// Trigger initial readings
cpuInfo.running = true
memoryInfo.running = true
diskInfo.running = true
}
// Function to stop monitoring
function stopMonitoring() {
isVisible = false
cpuInfo.running = false
memoryInfo.running = false
diskInfo.running = false
}
Rectangle {
id: card
anchors.fill: parent
color: Settings.Theme.surface
radius: 18
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 12
Layout.alignment: Qt.AlignVCenter
// CPU Usage
Components.CircularProgressBar {
progress: cpuUsage / 100
size: 50
strokeWidth: 4
hasNotch: true
notchIcon: "speed"
notchIconSize: 14
Layout.alignment: Qt.AlignHCenter
}
// Memory Usage
Components.CircularProgressBar {
progress: memoryUsage / 100
size: 50
strokeWidth: 4
hasNotch: true
notchIcon: "memory"
notchIconSize: 14
Layout.alignment: Qt.AlignHCenter
}
// Disk Usage
Components.CircularProgressBar {
progress: diskUsage / 100
size: 50
strokeWidth: 4
hasNotch: true
notchIcon: "storage"
notchIconSize: 14
Layout.alignment: Qt.AlignHCenter
}
}
}
}

View file

@ -0,0 +1,150 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import Quickshell
import Quickshell.Io
import qs.Settings
PanelWindow {
id: wallpaperPanelModal
implicitWidth: 480
implicitHeight: 720
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: -24
property var wallpapers: []
Process {
id: listWallpapersProcess
running: visible
command: ["ls", Settings.wallpaperFolder !== undefined ? Settings.wallpaperFolder : ""]
stdout: StdioCollector {
onStreamFinished: {
// Split by newlines and filter out empty lines
wallpaperPanelModal.wallpapers = this.text.split("\n").filter(function(x){return x.length > 0})
}
}
}
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 24
ColumnLayout {
anchors.fill: parent
anchors.margins: 32
spacing: 0
RowLayout {
Layout.fillWidth: true
spacing: 20
Layout.preferredHeight: 48
Text {
text: "image"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.accentPrimary
}
Text {
text: "Wallpapers"
font.pixelSize: 26
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
Rectangle {
width: 36; height: 36; radius: 18
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
Text {
anchors.centerIn: parent
text: "close"
font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 20
color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: closeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: wallpaperPanelModal.visible = false
cursorShape: Qt.PointingHandCursor
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.outline
opacity: 0.12
}
// Wallpaper grid area
Item {
Layout.fillWidth: true
Layout.fillHeight: true
anchors.topMargin: 16
anchors.bottomMargin: 16
anchors.leftMargin: 0
anchors.rightMargin: 0
anchors.margins: 0
clip: true
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
GridView {
id: wallpaperGrid
anchors.fill: parent
cellWidth: Math.max(120, (scrollView.width / 3) - 12)
cellHeight: cellWidth * 0.6
model: wallpapers
cacheBuffer: 0
leftMargin: 8
rightMargin: 8
topMargin: 8
bottomMargin: 8
delegate: Item {
width: wallpaperGrid.cellWidth - 8
height: wallpaperGrid.cellHeight - 8
Rectangle {
id: wallpaperItem
anchors.fill: parent
anchors.margins: 4
color: Qt.darker(Theme.backgroundPrimary, 1.1)
radius: 12
border.color: Settings.currentWallpaper === (Settings.wallpaperFolder !== undefined ? Settings.wallpaperFolder : "") + "/" + modelData ? Theme.accentPrimary : Theme.outline
border.width: Settings.currentWallpaper === (Settings.wallpaperFolder !== undefined ? Settings.wallpaperFolder : "") + "/" + modelData ? 3 : 1
Image {
id: wallpaperImage
anchors.fill: parent
anchors.margins: 4
source: (Settings.wallpaperFolder !== undefined ? Settings.wallpaperFolder : "") + "/" + modelData
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
sourceSize.width: Math.min(width, 150)
sourceSize.height: Math.min(height, 90)
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
var selectedPath = (Settings.wallpaperFolder !== undefined ? Settings.wallpaperFolder : "") + "/" + modelData;
Settings.currentWallpaper = selectedPath;
Settings.saveSettings();
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,197 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import qs.Settings
import "root:/Helpers/Weather.js" as WeatherHelper
Rectangle {
id: weatherRoot
width: 440
height: 180
color: "transparent"
anchors.horizontalCenterOffset: -2
property string city: Settings.weatherCity !== undefined ? Settings.weatherCity : ""
property var weatherData: null
property string errorString: ""
property bool isVisible: false
Component.onCompleted: {
if (isVisible) {
fetchCityWeather()
}
}
function fetchCityWeather() {
WeatherHelper.fetchCityWeather(city,
function(result) {
weatherData = result.weather;
errorString = "";
},
function(err) {
errorString = err;
}
);
}
function startWeatherFetch() {
isVisible = true
fetchCityWeather()
}
function stopWeatherFetch() {
isVisible = false
}
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
// Current weather row
RowLayout {
spacing: 12
Layout.fillWidth: true
// Weather icon and basic info
RowLayout {
spacing: 12
Layout.preferredWidth: 140
// Material Symbol icon
Text {
id: weatherIcon
text: weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud"
font.family: "Material Symbols Outlined"
font.pixelSize: 28
verticalAlignment: Text.AlignVCenter
color: Theme.accentPrimary
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
spacing: 2
RowLayout {
spacing: 4
Text {
text: city
font.pixelSize: 14
font.bold: true
color: Theme.textPrimary
}
Text {
text: weatherData && weatherData.timezone_abbreviation ? `(${weatherData.timezone_abbreviation})` : ""
font.pixelSize: 10
color: Theme.textSecondary
leftPadding: 2
}
}
Text {
text: weatherData && weatherData.current_weather ? ((Settings.useFahrenheit !== undefined ? Settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.useFahrenheit !== undefined ? Settings.useFahrenheit : false) ? "--°F" : "--°C")
font.pixelSize: 24
font.bold: true
color: Theme.textPrimary
}
}
}
// Spacer to push content to the right
Item {
Layout.fillWidth: true
}
}
// Separator line
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.textSecondary.g, Theme.textSecondary.g, Theme.textSecondary.b, 0.12)
Layout.fillWidth: true
Layout.topMargin: 2
Layout.bottomMargin: 2
}
// 5-day forecast row (smaller)
RowLayout {
spacing: 12
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
visible: weatherData && weatherData.daily && weatherData.daily.time
Repeater {
model: weatherData && weatherData.daily && weatherData.daily.time ? 5 : 0
delegate: ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignHCenter
Text {
// Day name (e.g., Mon)
text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd")
font.pixelSize: 12
color: Theme.textSecondary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
Text {
// Material Symbol icon
text: materialSymbolForCode(weatherData.daily.weathercode[index])
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: Theme.accentPrimary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
Text {
// High/low temp
text: weatherData && weatherData.daily ? ((Settings.useFahrenheit !== undefined ? Settings.useFahrenheit : false) ? `${Math.round(weatherData.daily.temperature_2m_max[index] * 9/5 + 32)}° / ${Math.round(weatherData.daily.temperature_2m_min[index] * 9/5 + 32)}°` : `${Math.round(weatherData.daily.temperature_2m_max[index])}° / ${Math.round(weatherData.daily.temperature_2m_min[index])}°`) : ((Settings.useFahrenheit !== undefined ? Settings.useFahrenheit : false) ? "--° / --°" : "--° / --°")
font.pixelSize: 12
color: Theme.textPrimary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
}
}
// Error message (if any)
Text {
text: errorString
color: Theme.error
visible: errorString !== ""
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
}
// Weather code to Material Symbol ligature
function materialSymbolForCode(code) {
// Open-Meteo WMO code mapping
if (code === 0) return "sunny"; // Clear
if (code === 1 || code === 2) return "partly_cloudy_day"; // Mainly clear/partly cloudy
if (code === 3) return "cloud"; // Overcast
if (code >= 45 && code <= 48) return "foggy"; // Fog
if (code >= 51 && code <= 67) return "rainy"; // Drizzle
if (code >= 71 && code <= 77) return "weather_snowy"; // Snow
if (code >= 80 && code <= 82) return "rainy"; // Rain showers
if (code >= 95 && code <= 99) return "thunderstorm"; // Thunderstorm
return "cloud";
}
function weatherDescriptionForCode(code) {
if (code === 0) return "Clear sky";
if (code === 1) return "Mainly clear";
if (code === 2) return "Partly cloudy";
if (code === 3) return "Overcast";
if (code === 45 || code === 48) return "Fog";
if (code >= 51 && code <= 67) return "Drizzle";
if (code >= 71 && code <= 77) return "Snow";
if (code >= 80 && code <= 82) return "Rain showers";
if (code >= 95 && code <= 99) return "Thunderstorm";
return "Unknown";
}
}

View file

@ -0,0 +1,637 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell.Wayland
import Quickshell
import Quickshell.Io
import Quickshell.Bluetooth
import qs.Settings
import qs.Components
import qs.Helpers
Item {
property alias panel: wifiPanelModal
function showAt() {
wifiPanelModal.visible = true;
wifiLogic.refreshNetworks();
}
function signalIcon(signal) {
if (signal >= 80) return "network_wifi_4_bar";
if (signal >= 60) return "network_wifi_3_bar";
if (signal >= 40) return "network_wifi_2_bar";
if (signal >= 20) return "network_wifi_1_bar";
return "wifi_0_bar";
}
Process {
id: scanProcess
running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
onRunningChanged: {
// Removed debug log
}
stdout: StdioCollector {
onStreamFinished: {
var lines = text.split("\n");
var nets = [];
var seen = {};
for (var i = 0; i < lines.length; ++i) {
var line = lines[i].trim();
if (!line) continue;
var parts = line.split(":");
var ssid = parts[0];
var security = parts[1];
var signal = parseInt(parts[2]);
var inUse = parts[3] === "*";
if (ssid && !seen[ssid]) {
nets.push({ ssid: ssid, security: security, signal: signal, connected: inUse });
seen[ssid] = true;
}
}
wifiLogic.networks = nets;
}
}
}
QtObject {
id: wifiLogic
property var networks: []
property var anchorItem: null
property real anchorX
property real anchorY
property string passwordPromptSsid: ""
property string passwordInput: ""
property bool showPasswordPrompt: false
property string connectingSsid: ""
property string connectStatus: ""
property string connectStatusSsid: ""
property string connectError: ""
property string connectSecurity: ""
property var pendingConnect: null // store connect params for after delete
property string detectedInterface: ""
property var connectionsToDelete: []
function profileNameForSsid(ssid) {
return "quickshell-" + ssid.replace(/[^a-zA-Z0-9]/g, "_");
}
function disconnectAndDeleteNetwork(ssid) {
var profileName = wifiLogic.profileNameForSsid(ssid);
console.log('WifiPanel: disconnectAndDeleteNetwork called for SSID', ssid, 'profile', profileName);
disconnectProfileProcess.connectionName = profileName;
disconnectProfileProcess.running = true;
}
function refreshNetworks() {
scanProcess.running = true;
}
function showAt() {
wifiPanelModal.visible = true;
wifiLogic.refreshNetworks();
}
function connectNetwork(ssid, security) {
wifiLogic.pendingConnect = {ssid: ssid, security: security, password: ""};
listConnectionsProcess.running = true;
}
function submitPassword() {
wifiLogic.pendingConnect = {ssid: wifiLogic.passwordPromptSsid, security: wifiLogic.connectSecurity, password: wifiLogic.passwordInput};
listConnectionsProcess.running = true;
}
function doConnect() {
var params = wifiLogic.pendingConnect;
wifiLogic.connectingSsid = params.ssid;
if (params.security && params.security !== "--") {
getInterfaceProcess.running = true;
} else {
connectProcess.security = params.security;
connectProcess.ssid = params.ssid;
connectProcess.password = params.password;
connectProcess.running = true;
wifiLogic.pendingConnect = null;
}
}
}
// Disconnect, then delete the profile. This chain is triggered by clicking the row.
Process {
id: disconnectProfileProcess
property string connectionName: ""
running: false
command: ["nmcli", "connection", "down", "id", connectionName]
onRunningChanged: {
if (!running) {
// After disconnect, delete the profile
deleteProfileProcess.connectionName = connectionName;
deleteProfileProcess.running = true;
}
}
}
Process {
id: deleteProfileProcess
property string connectionName: ""
running: false
command: ["nmcli", "connection", "delete", "id", connectionName]
onRunningChanged: {
if (!running) {
wifiLogic.refreshNetworks();
}
}
}
Process {
id: listConnectionsProcess
running: false
command: ["nmcli", "-t", "-f", "NAME,SSID", "connection", "show"]
stdout: StdioCollector {
onStreamFinished: {
var params = wifiLogic.pendingConnect;
var lines = text.split("\n");
var toDelete = [];
for (var i = 0; i < lines.length; ++i) {
var parts = lines[i].split(":");
if (parts.length === 2 && parts[1] === params.ssid) {
toDelete.push(parts[0]);
}
}
wifiLogic.connectionsToDelete = toDelete;
if (toDelete.length > 0) {
deleteProfileProcess.connectionName = toDelete[0];
deleteProfileProcess.running = true;
} else {
wifiLogic.doConnect();
}
}
}
}
Process {
id: connectProcess
property string ssid: ""
property string password: ""
property string security: ""
running: false
command: {
if (password) {
return ["nmcli", "device", "wifi", "connect", ssid, "password", password]
} else {
return ["nmcli", "device", "wifi", "connect", ssid]
}
}
stdout: StdioCollector {
onStreamFinished: {
wifiLogic.connectingSsid = "";
wifiLogic.showPasswordPrompt = false;
wifiLogic.passwordPromptSsid = "";
wifiLogic.passwordInput = "";
wifiLogic.connectStatus = "success";
wifiLogic.connectStatusSsid = connectProcess.ssid;
wifiLogic.connectError = "";
wifiLogic.refreshNetworks();
}
}
stderr: StdioCollector {
onStreamFinished: {
wifiLogic.connectingSsid = "";
wifiLogic.showPasswordPrompt = false;
wifiLogic.passwordPromptSsid = "";
wifiLogic.passwordInput = "";
wifiLogic.connectStatus = "error";
wifiLogic.connectStatusSsid = connectProcess.ssid;
wifiLogic.connectError = text;
}
}
}
Process {
id: getInterfaceProcess
running: false
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
stdout: StdioCollector {
onStreamFinished: {
var lines = text.split("\n");
for (var i = 0; i < lines.length; ++i) {
var parts = lines[i].split(":");
if (parts[1] === "wifi" && parts[2] !== "unavailable") {
wifiLogic.detectedInterface = parts[0];
break;
}
}
if (wifiLogic.detectedInterface) {
var params = wifiLogic.pendingConnect;
addConnectionProcess.ifname = wifiLogic.detectedInterface;
addConnectionProcess.ssid = params.ssid;
addConnectionProcess.password = params.password;
addConnectionProcess.profileName = wifiLogic.profileNameForSsid(params.ssid);
addConnectionProcess.security = params.security;
addConnectionProcess.running = true;
} else {
wifiLogic.connectStatus = "error";
wifiLogic.connectStatusSsid = wifiLogic.pendingConnect.ssid;
wifiLogic.connectError = "No Wi-Fi interface found.";
wifiLogic.connectingSsid = "";
wifiLogic.pendingConnect = null;
}
}
}
}
Process {
id: addConnectionProcess
property string ifname: ""
property string ssid: ""
property string password: ""
property string profileName: ""
property string security: ""
running: false
command: {
var cmd = ["nmcli", "connection", "add", "type", "wifi", "ifname", ifname, "con-name", profileName, "ssid", ssid];
if (security && security !== "--") {
cmd.push("wifi-sec.key-mgmt");
cmd.push("wpa-psk");
cmd.push("wifi-sec.psk");
cmd.push(password);
}
return cmd;
}
stdout: StdioCollector {
onStreamFinished: {
upConnectionProcess.profileName = addConnectionProcess.profileName;
upConnectionProcess.running = true;
}
}
stderr: StdioCollector {
onStreamFinished: {
upConnectionProcess.profileName = addConnectionProcess.profileName;
upConnectionProcess.running = true;
}
}
}
Process {
id: upConnectionProcess
property string profileName: ""
running: false
command: ["nmcli", "connection", "up", "id", profileName]
stdout: StdioCollector {
onStreamFinished: {
wifiLogic.connectingSsid = "";
wifiLogic.showPasswordPrompt = false;
wifiLogic.passwordPromptSsid = "";
wifiLogic.passwordInput = "";
wifiLogic.connectStatus = "success";
wifiLogic.connectStatusSsid = wifiLogic.pendingConnect ? wifiLogic.pendingConnect.ssid : "";
wifiLogic.connectError = "";
wifiLogic.refreshNetworks();
wifiLogic.pendingConnect = null;
}
}
stderr: StdioCollector {
onStreamFinished: {
wifiLogic.connectingSsid = "";
wifiLogic.showPasswordPrompt = false;
wifiLogic.passwordPromptSsid = "";
wifiLogic.passwordInput = "";
wifiLogic.connectStatus = "error";
wifiLogic.connectStatusSsid = wifiLogic.pendingConnect ? wifiLogic.pendingConnect.ssid : "";
wifiLogic.connectError = text;
wifiLogic.pendingConnect = null;
}
}
}
// Wifi button (no background card)
Rectangle {
id: wifiButton
width: 36; height: 36
radius: 18
border.color: Theme.accentPrimary
border.width: 1
color: wifiButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
Text {
anchors.centerIn: parent
text: "wifi"
font.family: "Material Symbols Outlined"
font.pixelSize: 22
color: wifiButtonArea.containsMouse
? Theme.backgroundPrimary
: Theme.accentPrimary
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
MouseArea {
id: wifiButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: wifiLogic.showAt()
}
}
PanelWindow {
id: wifiPanelModal
implicitWidth: 480
implicitHeight: 720
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: -24
WlrLayershell.keyboardFocus: visible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
Component.onCompleted: {
wifiLogic.refreshNetworks()
}
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 24
ColumnLayout {
anchors.fill: parent
anchors.margins: 32
spacing: 0
RowLayout {
Layout.fillWidth: true
spacing: 20
Layout.preferredHeight: 48
Layout.leftMargin: 16
Layout.rightMargin: 16
Text {
text: "wifi"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Theme.accentPrimary
}
Text {
text: "Wi-Fi"
font.pixelSize: 26
font.bold: true
color: Theme.textPrimary
Layout.fillWidth: true
}
Rectangle {
width: 36; height: 36; radius: 18
color: closeButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: Theme.accentPrimary
border.width: 1
Text {
anchors.centerIn: parent
text: "close"
font.family: closeButtonArea.containsMouse ? "Material Symbols Rounded" : "Material Symbols Outlined"
font.pixelSize: 20
color: closeButtonArea.containsMouse ? Theme.onAccent : Theme.accentPrimary
}
MouseArea {
id: closeButtonArea
anchors.fill: parent
hoverEnabled: true
onClicked: wifiPanelModal.visible = false
cursorShape: Qt.PointingHandCursor
}
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.outline
opacity: 0.12
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 520
Layout.alignment: Qt.AlignHCenter
Layout.margins: 0
color: Theme.surfaceVariant
radius: 18
border.color: Theme.outline
border.width: 1
Rectangle {
id: bg
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 12
border.width: 1
border.color: Theme.surfaceVariant
z: 0
}
Rectangle {
id: header
}
Rectangle {
id: listContainer
anchors.top: header.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 24
color: "transparent"
clip: true
ListView {
id: networkListView
anchors.fill: parent
spacing: 4
boundsBehavior: Flickable.StopAtBounds
model: wifiLogic.networks
delegate: Item {
id: networkEntry
width: parent.width
height: modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42
ColumnLayout {
anchors.fill: parent
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 42
radius: 8
color: modelData.connected ? Qt.rgba(Theme.accentPrimary.r, Theme.accentPrimary.g, Theme.accentPrimary.b, 0.18) : (networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.highlight : "transparent")
RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 12
Text {
text: signalIcon(modelData.signal)
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
RowLayout {
Layout.fillWidth: true
spacing: 6
Text {
text: modelData.ssid || "Unknown Network"
color: networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textPrimary)
font.pixelSize: 14
elide: Text.ElideRight
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Item {
width: 22; height: 22
visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus !== ""
RowLayout {
anchors.fill: parent
spacing: 2
Text {
visible: wifiLogic.connectStatus === "success"
text: "check_circle"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: "#43a047"
verticalAlignment: Text.AlignVCenter
}
Text {
visible: wifiLogic.connectStatus === "error"
text: "error"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: Theme.error
verticalAlignment: Text.AlignVCenter
}
}
}
Spinner {
visible: wifiLogic.connectingSsid === modelData.ssid
running: wifiLogic.connectingSsid === modelData.ssid
color: Theme.textPrimary
size: 18
}
}
Text {
text: modelData.security && modelData.security !== "--" ? modelData.security : "Open"
color: networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.backgroundPrimary : (modelData.connected ? Theme.accentPrimary : Theme.textSecondary)
font.pixelSize: 11
elide: Text.ElideRight
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
Text {
visible: wifiLogic.connectStatusSsid === modelData.ssid && wifiLogic.connectStatus === "error" && wifiLogic.connectError.length > 0
text: wifiLogic.connectError
color: Theme.error
font.pixelSize: 11
elide: Text.ElideRight
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
}
Text {
visible: modelData.connected
text: "connected"
color: networkMouseArea.containsMouse || (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt) ? Theme.backgroundPrimary : Theme.accentPrimary
font.pixelSize: 11
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
}
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (modelData.connected) {
wifiLogic.disconnectAndDeleteNetwork(modelData.ssid);
} else if (modelData.security && modelData.security !== "--") {
wifiLogic.passwordPromptSsid = modelData.ssid;
wifiLogic.passwordInput = "";
wifiLogic.showPasswordPrompt = true;
wifiLogic.connectStatus = "";
wifiLogic.connectStatusSsid = "";
wifiLogic.connectError = "";
wifiLogic.connectSecurity = modelData.security;
} else {
wifiLogic.connectNetwork(modelData.ssid, modelData.security)
}
}
}
}
Rectangle {
visible: modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: 8
color: "transparent"
anchors.leftMargin: 32
anchors.rightMargin: 32
z: 2
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Item {
Layout.fillWidth: true
Layout.preferredHeight: 36
Rectangle {
anchors.fill: parent
radius: 8
color: "transparent"
border.color: passwordField.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: passwordField
anchors.fill: parent
anchors.margins: 12
text: wifiLogic.passwordInput
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
focus: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
echoMode: TextInput.Password
onTextChanged: wifiLogic.passwordInput = text
onAccepted: wifiLogic.submitPassword()
MouseArea {
id: passwordMouseArea
anchors.fill: parent
onClicked: passwordField.forceActiveFocus()
}
}
}
}
Rectangle {
width: 80
height: 36
radius: 18
color: Theme.accentPrimary
border.color: Theme.accentPrimary
border.width: 0
opacity: 1.0
Behavior on color { ColorAnimation { duration: 100 } }
MouseArea {
anchors.fill: parent
onClicked: wifiLogic.submitPassword()
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.color = Qt.darker(Theme.accentPrimary, 1.1)
onExited: parent.color = Theme.accentPrimary
}
Text {
anchors.centerIn: parent
text: "Connect"
color: Theme.backgroundPrimary
font.pixelSize: 14
font.bold: true
}
}
}
}
}
}
}
}
}
}
}
}
}