Reworked the sidepanel file structure

This commit is contained in:
quadbyte 2025-08-06 20:25:26 -04:00
parent 5388260020
commit 0b5f1cd9e5
20 changed files with 4 additions and 1952 deletions

View file

@ -0,0 +1,347 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell.Wayland
import Quickshell
import Quickshell.Bluetooth
import qs.Settings
import qs.Components
import qs.Helpers
Item {
id: root
property alias panel: bluetoothPanelModal
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: 780
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
onVisibleChanged: {
if (!visible && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering)
Bluetooth.defaultAdapter.discovering = false
}
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 20
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.family: Theme.fontFamily
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
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 640
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"
font.family: Theme.fontFamily
color: modelData.connected ? Theme.accentPrimary : Theme.textPrimary
font.pixelSize: 14
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: modelData.address
font.family: Theme.fontFamily
color: modelData.connected ? Theme.accentPrimary : Theme.textSecondary
font.pixelSize: 11
elide: Text.ElideRight
}
Text {
text: "Paired: " + modelData.paired + " | Trusted: " + modelData.trusted
font.family: Theme.fontFamily
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,59 @@
import QtQuick
import Quickshell
import qs.Settings
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
cursorShape: Qt.PointingHandCursor
onClicked: {
if (sidebarPopup.visible) {
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
}
}
}

424
Widgets/SidePanel/Music.qml Normal file
View file

@ -0,0 +1,424 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import qs.Settings
import qs.Components
import qs.Services
Rectangle {
id: musicCard
width: 360
height: 250
color: "transparent"
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
// Show fallback UI if no player is available
Item {
width: parent.width
height: parent.height
visible: !MusicManager.currentPlayer
ColumnLayout {
anchors.centerIn: parent
spacing: 16
Text {
text: "music_note"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeHeader
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
Layout.alignment: Qt.AlignHCenter
}
Text {
text: MusicManager.hasPlayer ? "No controllable player selected" : "No music player detected"
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
Layout.alignment: Qt.AlignHCenter
}
}
}
// Main player UI
ColumnLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
visible: !!MusicManager.currentPlayer
// Player selector
ComboBox {
id: playerSelector
Layout.fillWidth: true
Layout.preferredHeight: 40
visible: MusicManager.getAvailablePlayers().length > 1
model: MusicManager.getAvailablePlayers()
textRole: "identity"
currentIndex: MusicManager.selectedPlayerIndex
background: Rectangle {
implicitWidth: 120
implicitHeight: 40
color: Theme.surfaceVariant
border.color: playerSelector.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
radius: 16
}
contentItem: Text {
leftPadding: 12
rightPadding: playerSelector.indicator.width + playerSelector.spacing
text: playerSelector.displayText
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
indicator: Text {
x: playerSelector.width - width - 12
y: playerSelector.topPadding + (playerSelector.availableHeight - height) / 2
text: "arrow_drop_down"
font.family: "Material Symbols Outlined"
font.pixelSize: 24
color: Theme.textPrimary
}
popup: Popup {
y: playerSelector.height
width: playerSelector.width
implicitHeight: contentItem.implicitHeight
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: playerSelector.popup.visible ? playerSelector.delegateModel : null
currentIndex: playerSelector.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator {}
}
background: Rectangle {
color: Theme.surfaceVariant
border.color: Theme.outline
border.width: 1
radius: 16
}
}
delegate: ItemDelegate {
width: playerSelector.width
contentItem: Text {
text: modelData.identity
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
highlighted: playerSelector.highlightedIndex === index
background: Rectangle {
color: highlighted ? Theme.accentPrimary.toString().replace(/#/, "#1A") : "transparent"
}
}
onActivated: {
MusicManager.selectedPlayerIndex = index;
MusicManager.updateCurrentPlayer();
}
}
// Album art with spectrum visualizer
RowLayout {
spacing: 12
Layout.fillWidth: true
// Album art container with circular spectrum overlay
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 around album art
CircularSpectrum {
id: spectrum
values: MusicManager.cavaValues
anchors.centerIn: parent
innerRadius: 30 // Position just outside 60x60 album art
outerRadius: 48 // Extend bars outward from album art
fillColor: Theme.accentPrimary
strokeColor: Theme.accentPrimary
strokeWidth: 0
z: 0
}
// Album art image
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
mipmap: true
cache: false
asynchronous: true
sourceSize.width: 60
sourceSize.height: 60
source: MusicManager.trackArtUrl
visible: source.toString() !== ""
// Apply circular mask for rounded corners
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskSource: mask
}
}
Item {
id: mask
anchors.fill: albumArt
layer.enabled: true
visible: false
Rectangle {
width: albumArt.width
height: albumArt.height
radius: albumArt.width / 2 // circle
}
}
// Fallback icon when no album art available
Text {
anchors.centerIn: parent
text: "album"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.4)
visible: !albumArt.visible
}
}
}
// Track metadata
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Text {
text: MusicManager.trackTitle
color: Theme.textPrimary
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.bold: true
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
Layout.fillWidth: true
}
Text {
text: MusicManager.trackArtist
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.8)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption
elide: Text.ElideRight
Layout.fillWidth: true
}
Text {
text: MusicManager.trackAlbum
color: Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.6)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeCaption
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: {
if (!MusicManager.currentPlayer || !MusicManager.isPlaying || MusicManager.trackLength <= 0) {
return 0;
}
return Math.min(1, MusicManager.currentPosition / MusicManager.trackLength);
}
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: MusicManager.trackLength > 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
cursorShape: Qt.PointingHandCursor
enabled: MusicManager.trackLength > 0 && MusicManager.canSeek
onClicked: function (mouse) {
let ratio = mouse.x / width;
MusicManager.seekByRatio(ratio);
}
onPositionChanged: function (mouse) {
if (pressed) {
let ratio = Math.max(0, Math.min(1, mouse.x / width));
MusicManager.seekByRatio(ratio);
}
}
}
}
// 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
cursorShape: Qt.PointingHandCursor
enabled: MusicManager.canGoPrevious
onClicked: MusicManager.previous()
}
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeCaption
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
cursorShape: Qt.PointingHandCursor
enabled: MusicManager.canPlay || MusicManager.canPause
onClicked: MusicManager.playPause()
}
Text {
anchors.centerIn: parent
text: MusicManager.isPlaying ? "pause" : "play_arrow"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeBody
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
cursorShape: Qt.PointingHandCursor
enabled: MusicManager.canGoNext
onClicked: MusicManager.next()
}
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: "Material Symbols Outlined"
font.pixelSize: Theme.fontSizeCaption
color: nextButton.enabled ? Theme.accentPrimary : Qt.rgba(Theme.textPrimary.r, Theme.textPrimary.g, Theme.textPrimary.b, 0.3)
}
}
}
}
}
}

View file

@ -0,0 +1,460 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Settings
import qs.Widgets.SettingsWindow
import qs.Components
PanelWithOverlay {
id: sidebarPopup
property var shell: null
// Trigger initial weather loading when component is completed
Component.onCompleted: {
// Load initial weather data after a short delay to ensure all components are ready
Qt.callLater(function() {
if (weather && weather.fetchCityWeather) {
weather.fetchCityWeather();
}
});
}
function showAt() {
sidebarPopupRect.showAt();
}
function hidePopup() {
sidebarPopupRect.hidePopup();
}
function show() {
sidebarPopupRect.showAt();
}
function dismiss() {
sidebarPopupRect.hidePopup();
}
Rectangle {
id: sidebarPopupRect
implicitWidth: 500
implicitHeight: 800
visible: parent.visible
color: "transparent"
anchors.top: parent.top
anchors.right: parent.right
property real slideOffset: width
property bool isAnimating: false
function showAt() {
if (!sidebarPopup.visible) {
sidebarPopup.visible = true;
forceActiveFocus();
slideAnim.from = width;
slideAnim.to = 0;
slideAnim.running = true;
if (weather)
weather.startWeatherFetch();
if (systemWidget)
systemWidget.panelVisible = true;
if (quickAccessWidget)
quickAccessWidget.panelVisible = true;
}
}
function hidePopup() {
if (shell && shell.settingsWindow && shell.settingsWindow.visible) {
shell.settingsWindow.visible = false;
}
if (wifiPanelLoader.active && wifiPanelLoader.item && wifiPanelLoader.item.visible) {
wifiPanelLoader.item.visible = false;
}
if (bluetoothPanelLoader.active && bluetoothPanelLoader.item && bluetoothPanelLoader.item.visible) {
bluetoothPanelLoader.item.visible = false;
}
if (sidebarPopup.visible) {
slideAnim.from = 0;
slideAnim.to = width;
slideAnim.running = true;
}
}
NumberAnimation {
id: slideAnim
target: sidebarPopupRect
property: "slideOffset"
duration: 300
easing.type: Easing.OutCubic
onStopped: {
if (sidebarPopupRect.slideOffset === sidebarPopupRect.width) {
sidebarPopup.visible = false;
if (weather)
weather.stopWeatherFetch();
if (systemWidget)
systemWidget.panelVisible = false;
if (quickAccessWidget)
quickAccessWidget.panelVisible = false;
}
sidebarPopupRect.isAnimating = false;
}
onStarted: {
sidebarPopupRect.isAnimating = true;
}
}
property int leftPadding: 20
property int bottomPadding: 20
Rectangle {
id: mainRectangle
width: sidebarPopupRect.width - sidebarPopupRect.leftPadding
height: sidebarPopupRect.height - sidebarPopupRect.bottomPadding
anchors.top: sidebarPopupRect.top
x: sidebarPopupRect.leftPadding + sidebarPopupRect.slideOffset
y: 0
color: Theme.backgroundPrimary
bottomLeftRadius: 20
z: 0
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
// Access the shell's SettingsWindow instead of creating a new one
// LazyLoader for WifiPanel
LazyLoader {
id: wifiPanelLoader
loading: false
component: WifiPanel {}
}
// LazyLoader for BluetoothPanel
LazyLoader {
id: bluetoothPanelLoader
loading: false
component: BluetoothPanel {}
}
// SettingsIcon component
SettingsIcon {
id: settingsModal
onWeatherRefreshRequested: {
if (weather && weather.fetchCityWeather) {
weather.fetchCityWeather();
}
}
}
Item {
anchors.fill: mainRectangle
x: sidebarPopupRect.slideOffset
Behavior on x {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
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: 80
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: {
if (!wifiPanelLoader.active) {
wifiPanelLoader.loading = true;
}
if (wifiPanelLoader.item) {
wifiPanelLoader.item.showAt();
}
}
}
StyledTooltip {
text: "Wifi"
targetItem: wifiButtonArea
tooltipVisible: wifiButtonArea.containsMouse
}
}
// 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: {
if (!bluetoothPanelLoader.active) {
bluetoothPanelLoader.loading = true;
}
if (bluetoothPanelLoader.item) {
bluetoothPanelLoader.item.showAt();
}
}
}
StyledTooltip {
text: "Bluetooth"
targetItem: bluetoothButtonArea
tooltipVisible: bluetoothButtonArea.containsMouse
}
}
}
}
}
Item {
Layout.fillHeight: true
}
// QuickAccess widget
QuickAccess {
id: quickAccessWidget
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -16
z: 2
isRecording: sidebarPopupRect.isRecording
onRecordingRequested: {
sidebarPopupRect.startRecording();
}
onStopRecordingRequested: {
sidebarPopupRect.stopRecording();
}
onRecordingStateMismatch: function (actualState) {
isRecording = actualState;
quickAccessWidget.isRecording = actualState;
}
onSettingsRequested: {
// Use the SettingsModal's openSettings function
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) {
settingsModal.openSettings();
}
}
onWallpaperSelectorRequested: {
// Use the SettingsModal's openSettings function with wallpaper tab (index 6)
if (typeof settingsModal !== 'undefined' && settingsModal && settingsModal.openSettings) {
settingsModal.openSettings(6); // 6 is the wallpaper tab index
}
}
}
}
Keys.onEscapePressed: sidebarPopupRect.hidePopup()
}
// Recording properties
property bool isRecording: false
// Start screen recording using Quickshell.execDetached
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 videoPath = Settings.settings.videoPath;
if (videoPath && !videoPath.endsWith("/")) {
videoPath += "/";
}
var outputPath = videoPath + filename;
var command = "gpu-screen-recorder -w portal" +
" -f " + Settings.settings.recordingFrameRate +
" -a default_output" +
" -k " + Settings.settings.recordingCodec +
" -ac " + Settings.settings.audioCodec +
" -q " + Settings.settings.recordingQuality +
" -cursor " + (Settings.settings.showCursor ? "yes" : "no") +
" -cr " + Settings.settings.colorRange +
" -o " + outputPath;
Quickshell.execDetached(["sh", "-c", command]);
isRecording = true;
quickAccessWidget.isRecording = true;
}
// Stop recording using Quickshell.execDetached
function stopRecording() {
Quickshell.execDetached(["sh", "-c", "pkill -SIGINT -f 'gpu-screen-recorder.*portal'"]);
// Optionally, force kill after a delay
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', sidebarPopupRect);
cleanupTimer.triggered.connect(function () {
Quickshell.execDetached(["sh", "-c", "pkill -9 -f 'gpu-screen-recorder.*portal' 2>/dev/null || true"]);
cleanupTimer.destroy();
});
isRecording = false;
quickAccessWidget.isRecording = false;
}
// Clean up processes on destruction
Component.onDestruction: {
if (isRecording) {
stopRecording();
}
}
Loader {
active: Settings.settings.showCorners
anchors.fill: parent
sourceComponent: Item {
Corners {
id: sidebarCornerLeft
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.top: parent.top
offsetX: -447 + sidebarPopupRect.slideOffset
offsetY: 0
Behavior on offsetX {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
Corners {
id: sidebarCornerBottom
position: "bottomright"
size: 1.1
fillColor: Theme.backgroundPrimary
anchors.bottom: sidebarPopupRect.bottom
offsetX: 33 + sidebarPopupRect.slideOffset
offsetY: 46
Behavior on offsetX {
enabled: !sidebarPopupRect.isAnimating
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
}
}
}
}

View file

@ -0,0 +1,158 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell.Services.UPower
import qs.Settings
import qs.Components
Rectangle {
id: card
width: 200
height: 70
color: Theme.surface
radius: 18
Row {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: 20
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 {
id: perfIcon
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;
}
onEntered: perfTooltip.tooltipVisible = true
onExited: perfTooltip.tooltipVisible = false
}
StyledTooltip {
id: perfTooltip
text: "Performance Profile"
tooltipVisible: false
targetItem: perfIcon
delay: 200
}
}
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 {
id: balIcon
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;
}
onEntered: balTooltip.tooltipVisible = true
onExited: balTooltip.tooltipVisible = false
}
StyledTooltip {
id: balTooltip
text: "Balanced Profile"
tooltipVisible: false
targetItem: balIcon
delay: 200
}
}
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 {
id: saveIcon
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;
}
onEntered: saveTooltip.tooltipVisible = true
onExited: saveTooltip.tooltipVisible = false
}
StyledTooltip {
id: saveTooltip
text: "Power Saver Profile"
tooltipVisible: false
targetItem: saveIcon
delay: 200
}
}
}
}

View file

@ -0,0 +1,200 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import qs.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()
signal wallpaperSelectorRequested()
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
RowLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 12
Rectangle {
id: settingsButton
Layout.fillWidth: true
Layout.preferredHeight: 44
radius: 12
color: settingsButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: 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 ? Theme.onAccent : Theme.accentPrimary
}
Text {
text: "Settings"
font.family: Theme.fontFamily
font.pixelSize: 14
font.bold: true
color: settingsButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: settingsButtonArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
settingsRequested()
}
}
}
Rectangle {
id: recorderButton
Layout.fillWidth: true
Layout.preferredHeight: 44
radius: 12
color: isRecording ? Theme.accentPrimary :
(recorderButtonArea.containsMouse ? Theme.accentPrimary : "transparent")
border.color: 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 ? Theme.onAccent : Theme.accentPrimary
}
Text {
text: isRecording ? "End" : "Record"
font.family: Theme.fontFamily
font.pixelSize: 14
font.bold: true
color: isRecording || recorderButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: recorderButtonArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
if (isRecording) {
stopRecordingRequested()
} else {
recordingRequested()
}
}
}
}
Rectangle {
id: wallpaperButton
Layout.fillWidth: true
Layout.preferredHeight: 44
radius: 12
color: wallpaperButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
border.color: 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 ? Theme.onAccent : Theme.accentPrimary
}
Text {
text: "Wallpaper"
font.family: Theme.fontFamily
font.pixelSize: 14
font.bold: true
color: wallpaperButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: wallpaperButtonArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
wallpaperSelectorRequested()
}
}
}
}
}
property bool panelVisible: false
Timer {
interval: 2000
repeat: true
running: panelVisible
onTriggered: checkRecordingStatus()
}
function checkRecordingStatus() {
if (isRecording) {
checkRecordingProcess.running = true
}
}
Process {
id: checkRecordingProcess
command: ["pgrep", "-f", "gpu-screen-recorder.*portal"]
onExited: function(exitCode, exitStatus) {
var isActuallyRecording = exitCode === 0
if (isRecording && !isActuallyRecording) {
recordingStateMismatch(isActuallyRecording)
}
}
}
}

View file

@ -0,0 +1,93 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Settings
import qs.Services
import qs.Widgets.SettingsWindow
import qs.Components
PanelWindow {
id: settingsModal
implicitWidth: 480
implicitHeight: 780
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
// Signal to request weather refresh
signal weatherRefreshRequested()
// Property to track the settings window instance
property var settingsWindow: null
// Function to open the modal and initialize temp values
function openSettings(initialTabIndex) {
if (!settingsWindow) {
// Create new window
settingsWindow = settingsComponent.createObject(null); // No parent to avoid dependency issues
if (settingsWindow) {
// Set the initial tab if provided
if (typeof initialTabIndex === 'number' && initialTabIndex >= 0 && initialTabIndex <= 8) {
settingsWindow.activeTabIndex = initialTabIndex;
}
settingsWindow.visible = true;
// Show wallpaper selector if opening wallpaper tab (after window is visible)
if (typeof initialTabIndex === 'number' && initialTabIndex === 6) {
Qt.callLater(function() {
if (settingsWindow && settingsWindow.showWallpaperSelector) {
settingsWindow.showWallpaperSelector();
}
}, 100); // Small delay to ensure window is fully loaded
}
// Handle window closure
settingsWindow.visibleChanged.connect(function() {
if (settingsWindow && !settingsWindow.visible) {
// Trigger weather refresh when settings close
weatherRefreshRequested();
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.destroy();
}
});
}
} else if (settingsWindow.visible) {
// Close and destroy window
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.visible = false;
windowToDestroy.destroy();
}
}
// Function to close the modal and release focus
function closeSettings() {
if (settingsWindow) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.visible = false;
windowToDestroy.destroy();
}
}
Component {
id: settingsComponent
SettingsWindow {}
}
// Clean up on destruction
Component.onDestruction: {
if (settingsWindow) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.destroy();
}
}
}

View file

@ -0,0 +1,81 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Settings
import qs.Services
import qs.Widgets.SettingsWindow
import qs.Components
PanelWindow {
id: settingsModal
implicitWidth: 480
implicitHeight: 780
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
// Property to track the settings window instance
property var settingsWindow: null
// Function to open the modal and initialize temp values
function openSettings() {
if (!settingsWindow) {
// Create new window
settingsWindow = settingsComponent.createObject(null); // No parent to avoid dependency issues
if (settingsWindow) {
settingsWindow.visible = true;
// Handle window closure
settingsWindow.visibleChanged.connect(function() {
if (settingsWindow && !settingsWindow.visible) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.destroy();
}
});
}
} else if (settingsWindow.visible) {
// Close and destroy window
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.visible = false;
windowToDestroy.destroy();
}
}
// Function to close the modal and release focus
function closeSettings() {
if (settingsWindow) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.visible = false;
windowToDestroy.destroy();
}
}
Component {
id: settingsComponent
SettingsWindow {}
}
// Clean up on destruction
Component.onDestruction: {
if (settingsWindow) {
var windowToDestroy = settingsWindow;
settingsWindow = null;
windowToDestroy.destroy();
}
}
// Refresh weather data when hidden
onVisibleChanged: {
if (!visible && typeof weather !== 'undefined' && weather !== null && weather.fetchCityWeather) {
weather.fetchCityWeather();
}
}
}

View file

@ -0,0 +1,453 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Settings
import qs.Widgets
import qs.Widgets.LockScreen
import qs.Helpers
import qs.Services
import qs.Components
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
RowLayout {
Layout.fillWidth: true
spacing: 12
Rectangle {
width: 48
height: 48
radius: 24
color: Theme.accentPrimary
Rectangle {
anchors.fill: parent
color: "transparent"
radius: 24
border.color: Theme.accentPrimary
border.width: 2
z: 2
}
Avatar {}
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Text {
text: Quickshell.env("USER")
font.family: Theme.fontFamily
font.pixelSize: 16
font.bold: true
color: Theme.textPrimary
}
Text {
text: "System Uptime: " + uptimeText
font.family: Theme.fontFamily
font.pixelSize: 12
color: Theme.textSecondary
}
}
Item {
Layout.fillWidth: true
}
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
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onClicked: {
systemMenu.visible = !systemMenu.visible;
}
}
StyledTooltip {
id: systemTooltip
text: "System"
targetItem: systemButton
tooltipVisible: systemButtonArea.containsMouse
}
}
}
}
}
PanelWithOverlay {
id: systemMenu
anchors.top: systemButton.bottom
anchors.right: systemButton.right
Rectangle {
width: 160
height: 220
color: Theme.surface
radius: 8
border.color: Theme.outline
border.width: 1
visible: true
z: 9999
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: 32
anchors.topMargin: systemButton.y + systemButton.height + 48
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
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.family: Theme.fontFamily
font.pixelSize: 14
color: lockButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: lockButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
lockScreen.locked = true;
systemMenu.visible = false;
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 36
radius: 6
color: suspendButtonArea.containsMouse ? Theme.accentPrimary : "transparent"
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
Text {
text: "bedtime"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
}
Text {
text: "Suspend"
font.pixelSize: 14
color: suspendButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: suspendButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
suspend();
systemMenu.visible = false;
}
}
}
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.family: Theme.fontFamily
font.pixelSize: 14
color: rebootButtonArea.containsMouse ? Theme.onAccent : Theme.textPrimary
Layout.fillWidth: true
}
}
MouseArea {
id: rebootButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
reboot();
systemMenu.visible = false;
}
}
}
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
cursorShape: Qt.PointingHandCursor
onClicked: {
logout();
systemMenu.visible = false;
}
}
}
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
cursorShape: Qt.PointingHandCursor
onClicked: {
shutdown();
systemMenu.visible = false;
}
}
}
}
}
}
property string uptimeText: "--:--"
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;
}
}
}
Process {
id: shutdownProcess
command: ["shutdown", "-h", "now"]
running: false
}
Process {
id: rebootProcess
command: ["reboot"]
running: false
}
Process {
id: suspendProcess
command: ["systemctl", "suspend"]
running: false
}
Process {
id: logoutProcessNiri
command: ["niri", "msg", "action", "quit", "--skip-confirmation"]
running: false
}
Process {
id: logoutProcessHyprland
command: ["hyprctl", "dispatch", "exit"]
running: false
}
Process {
id: logoutProcess
command: ["loginctl", "terminate-user", Quickshell.env("USER")]
running: false
}
function logout() {
if (WorkspaceManager.isNiri) {
logoutProcessNiri.running = true;
} else if (WorkspaceManager.isHyprland) {
logoutProcessHyprland.running = true;
} else {
console.warn("No supported compositor detected for logout");
}
}
function suspend() {
suspendProcess.running = true;
}
function shutdown() {
shutdownProcess.running = true;
}
function reboot() {
rebootProcess.running = true;
}
property bool panelVisible: false
onPanelVisibleChanged: {
if (panelVisible) {
updateSystemInfo();
}
}
Timer {
interval: 60000
repeat: true
running: panelVisible
onTriggered: updateSystemInfo()
}
Component.onCompleted: {
uptimeProcess.running = true;
}
function updateSystemInfo() {
uptimeProcess.running = true;
}
LockScreen {
id: lockScreen
}
}

View file

@ -0,0 +1,152 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell.Io
import qs.Components
import qs.Services
import qs.Settings
Rectangle {
id: systemMonitor
width: 70
height: 250
color: "transparent"
// Track visibility state for panel integration
property bool isVisible: false
Rectangle {
id: card
anchors.fill: parent
color: Theme.surface
radius: 18
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 12
Layout.alignment: Qt.AlignVCenter
// CPU usage indicator with circular progress bar
Item {
width: 50; height: 50
CircularProgressBar {
id: cpuBar
progress: Sysinfo.cpuUsage / 100
size: 50
strokeWidth: 4
hasNotch: true
notchIcon: "speed"
notchIconSize: 14
Layout.alignment: Qt.AlignHCenter
}
MouseArea {
id: cpuBarMouse
anchors.fill: parent
hoverEnabled: true
onEntered: cpuTooltip.tooltipVisible = true
onExited: cpuTooltip.tooltipVisible = false
}
StyledTooltip {
id: cpuTooltip
text: 'CPU Usage: ' + Sysinfo.cpuUsage + '%'
tooltipVisible: false
targetItem: cpuBar
delay: 200
}
}
// CPU temperature indicator with circular progress bar
Item {
width: 50; height: 50
CircularProgressBar {
id: tempBar
progress: Sysinfo.cpuTemp / 100
size: 50
strokeWidth: 4
hasNotch: true
units: "°C"
notchIcon: "thermometer"
notchIconSize: 14
Layout.alignment: Qt.AlignHCenter
}
MouseArea {
id: tempBarMouse
anchors.fill: parent
hoverEnabled: true
onEntered: tempTooltip.tooltipVisible = true
onExited: tempTooltip.tooltipVisible = false
}
StyledTooltip {
id: tempTooltip
text: 'CPU Temp: ' + Sysinfo.cpuTemp + '°C'
tooltipVisible: false
targetItem: tempBar
delay: 200
}
}
// Memory usage indicator with circular progress bar
Item {
width: 50; height: 50
CircularProgressBar {
id: memBar
progress: Sysinfo.memoryUsagePer / 100
size: 50
strokeWidth: 4
hasNotch: true
notchIcon: "memory"
notchIconSize: 14
Layout.alignment: Qt.AlignHCenter
}
MouseArea {
id: memBarMouse
anchors.fill: parent
hoverEnabled: true
onEntered: memTooltip.tooltipVisible = true
onExited: memTooltip.tooltipVisible = false
}
StyledTooltip {
id: memTooltip
text: 'Memory Usage: ' + Sysinfo.memoryUsagePer + '% (' + Sysinfo.memoryUsageStr + ' used)'
tooltipVisible: false
targetItem: memBar
delay: 200
}
}
// Disk usage indicator with circular progress bar
Item {
width: 50; height: 50
CircularProgressBar {
id: diskBar
progress: Sysinfo.diskUsage / 100
size: 50
strokeWidth: 4
hasNotch: true
notchIcon: "storage"
notchIconSize: 14
Layout.alignment: Qt.AlignHCenter
}
MouseArea {
id: diskBarMouse
anchors.fill: parent
hoverEnabled: true
onEntered: diskTooltip.tooltipVisible = true
onExited: diskTooltip.tooltipVisible = false
}
StyledTooltip {
id: diskTooltip
text: 'Disk Usage: ' + Sysinfo.diskUsage + '%'
tooltipVisible: false
targetItem: diskBar
delay: 200
}
}
}
}
}

View file

@ -0,0 +1,247 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import qs.Settings
import "../../Helpers/Weather.js" as WeatherHelper
Rectangle {
id: weatherRoot
width: 440
height: 180
color: "transparent"
anchors.horizontalCenterOffset: -2
property string city: Settings.settings.weatherCity !== undefined ? Settings.settings.weatherCity : ""
property var weatherData: null
property string errorString: ""
property bool isVisible: false
property int lastFetchTime: 0
property bool isLoading: false
// Auto-refetch weather when city changes
Connections {
target: Settings.settings
function onWeatherCityChanged() {
if (isVisible && city !== "") {
// Force refresh when city changes
lastFetchTime = 0;
fetchCityWeather();
}
}
}
Component.onCompleted: {
if (isVisible) {
fetchCityWeather()
}
}
function fetchCityWeather() {
if (!city || city.trim() === "") {
errorString = "No city configured";
return;
}
// Check if we should fetch new data (avoid fetching too frequently)
var currentTime = Date.now();
var timeSinceLastFetch = currentTime - lastFetchTime;
// Only skip if we have recent data AND lastFetchTime is not 0 (initial state)
if (lastFetchTime > 0 && timeSinceLastFetch < 60000) { // 1 minute
return; // Skip if last fetch was less than 1 minute ago
}
isLoading = true;
errorString = "";
WeatherHelper.fetchCityWeather(city,
function(result) {
weatherData = result.weather;
lastFetchTime = currentTime;
errorString = "";
isLoading = false;
},
function(err) {
errorString = err;
isLoading = false;
}
);
}
function startWeatherFetch() {
isVisible = true
// Force refresh when panel opens, regardless of time check
lastFetchTime = 0;
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
RowLayout {
spacing: 12
Layout.fillWidth: true
RowLayout {
spacing: 12
Layout.preferredWidth: 140
Text {
id: weatherIcon
text: isLoading ? "sync" : (weatherData && weatherData.current_weather ? materialSymbolForCode(weatherData.current_weather.weathercode) : "cloud")
font.family: "Material Symbols Outlined"
font.pixelSize: 28
verticalAlignment: Text.AlignVCenter
color: isLoading ? Theme.accentPrimary : Theme.accentPrimary
Layout.alignment: Qt.AlignVCenter
// Add rotation animation for loading state
RotationAnimation on rotation {
running: isLoading
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
ColumnLayout {
spacing: 2
RowLayout {
spacing: 4
Text {
text: city
font.family: Theme.fontFamily
font.pixelSize: 14
font.bold: true
color: Theme.textPrimary
}
Text {
text: weatherData && weatherData.timezone_abbreviation ? `(${weatherData.timezone_abbreviation})` : ""
font.family: Theme.fontFamily
font.pixelSize: 10
color: Theme.textSecondary
leftPadding: 2
}
}
Text {
text: weatherData && weatherData.current_weather ? ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? `${Math.round(weatherData.current_weather.temperature * 9/5 + 32)}°F` : `${Math.round(weatherData.current_weather.temperature)}°C`) : ((Settings.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--°F" : "--°C")
font.family: Theme.fontFamily
font.pixelSize: 24
font.bold: true
color: Theme.textPrimary
}
}
}
Item {
Layout.fillWidth: true
}
}
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
}
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 {
text: Qt.formatDateTime(new Date(weatherData.daily.time[index]), "ddd")
font.family: Theme.fontFamily
font.pixelSize: 12
color: Theme.textSecondary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
Text {
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 {
text: weatherData && weatherData.daily ? ((Settings.settings.useFahrenheit !== undefined ? Settings.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.settings.useFahrenheit !== undefined ? Settings.settings.useFahrenheit : false) ? "--° / --°" : "--° / --°")
font.family: Theme.fontFamily
font.pixelSize: 12
color: Theme.textPrimary
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
}
}
Text {
text: errorString
color: Theme.error
visible: errorString !== ""
font.family: Theme.fontFamily
font.pixelSize: 10
horizontalAlignment: Text.AlignHCenter
Layout.alignment: Qt.AlignHCenter
}
}
}
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";
}
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,909 @@
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();
}
Component.onCompleted: {
existingNetwork.running = true;
}
function signalIcon(signal) {
if (signal >= 80)
return "network_wifi";
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: existingNetwork
running: false
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n");
const networksMap = {};
refreshIndicator.running = true;
refreshIndicator.visible = true;
for (let i = 0; i < lines.length; ++i) {
const line = lines[i].trim();
if (!line)
continue;
const parts = line.split(":");
if (parts.length < 2) {
console.warn("Malformed nmcli output line:", line);
continue;
}
const ssid = wifiLogic.replaceQuickshell(parts[0]);
const type = parts[1];
if (ssid) {
networksMap[ssid] = {
ssid: ssid,
type: type
};
}
}
scanProcess.existingNetwork = networksMap;
scanProcess.running = true;
}
}
}
Process {
id: scanProcess
running: false
command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list"]
property var existingNetwork
stdout: StdioCollector {
onStreamFinished: {
const lines = text.split("\n");
const networksMap = {};
for (let i = 0; i < lines.length; ++i) {
const line = lines[i].trim();
if (!line)
continue;
const parts = line.split(":");
if (parts.length < 4) {
console.warn("Malformed nmcli output line:", line);
continue;
}
const ssid = parts[0];
const security = parts[1];
const signal = parseInt(parts[2]);
const inUse = parts[3] === "*";
if (ssid) {
if (!networksMap[ssid]) {
networksMap[ssid] = {
ssid: ssid,
security: security,
signal: signal,
connected: inUse,
existing: ssid in scanProcess.existingNetwork
};
} else {
const existingNet = networksMap[ssid];
if (inUse) {
existingNet.connected = true;
}
if (signal > existingNet.signal) {
existingNet.signal = signal;
existingNet.security = security;
}
}
}
}
wifiLogic.networks = networksMap;
scanProcess.existingNetwork = {};
refreshIndicator.running = false;
refreshIndicator.visible = false;
}
}
}
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
property string detectedInterface: ""
property string actionPanelSsid: ""
function replaceQuickshell(ssid: string): string {
const newName = ssid.replace("quickshell-", "");
if (!ssid.startsWith("quickshell-")) {
return newName;
}
if (wifiLogic.networks && newName in wifiLogic.networks) {
console.log(`Quickshell ${newName} already exists, deleting old profile`)
deleteProfileProcess.connName = ssid;
deleteProfileProcess.running = true;
}
console.log(`Changing from ${ssid} to ${newName}`)
renameConnectionProcess.oldName = ssid;
renameConnectionProcess.newName = newName;
renameConnectionProcess.running = true;
return newName;
}
function disconnectNetwork(ssid) {
const profileName = ssid;
disconnectProfileProcess.connectionName = profileName;
disconnectProfileProcess.running = true;
}
function refreshNetworks() {
existingNetwork.running = true;
}
function showAt() {
wifiPanelModal.visible = true;
wifiLogic.refreshNetworks();
}
function connectNetwork(ssid, security) {
wifiLogic.pendingConnect = {
ssid: ssid,
security: security,
password: ""
};
wifiLogic.doConnect();
}
function submitPassword() {
wifiLogic.pendingConnect = {
ssid: wifiLogic.passwordPromptSsid,
security: wifiLogic.connectSecurity,
password: wifiLogic.passwordInput
};
wifiLogic.doConnect();
}
function doConnect() {
const params = wifiLogic.pendingConnect;
if (!params)
return;
wifiLogic.connectingSsid = params.ssid;
const targetNetwork = wifiLogic.networks[params.ssid];
if (targetNetwork && targetNetwork.existing) {
upConnectionProcess.profileName = params.ssid;
upConnectionProcess.running = true;
wifiLogic.pendingConnect = null;
return;
}
if (params.security && params.security !== "--") {
getInterfaceProcess.running = true;
return;
}
connectProcess.security = params.security;
connectProcess.ssid = params.ssid;
connectProcess.password = params.password;
connectProcess.running = true;
wifiLogic.pendingConnect = null;
}
function isSecured(security) {
return security && security.trim() !== "" && security.trim() !== "--";
}
}
Process {
id: disconnectProfileProcess
property string connectionName: ""
running: false
command: ["nmcli", "connection", "down", connectionName]
onRunningChanged: {
if (!running) {
wifiLogic.refreshNetworks();
}
}
}
// Process to rename a connection
Process {
id: renameConnectionProcess
running: false
property string oldName: ""
property string newName: ""
command: ["nmcli", "connection", "modify", oldName, "connection.id", newName]
stdout: StdioCollector {
onStreamFinished: {
console.log("Successfully renamed connection '" +
renameConnectionProcess.oldName + "' to '" +
renameConnectionProcess.newName + "'");
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text.trim() !== "" && !text.toLowerCase().includes("warning")) {
console.error("Error renaming connection:", text);
}
}
}
}
// Process to rename a connection
Process {
id: deleteProfileProcess
running: false
property string connName: ""
command: ["nmcli", "connection", "delete", `'${connName}'`]
stdout: StdioCollector {
onStreamFinished: {
console.log("Deleted connection '" + deleteProfileProcess.connName + "'");
}
}
stderr: StdioCollector {
onStreamFinished: {
console.error("Error deleting connection '" + deleteProfileProcess.connName + "':", text);
}
}
}
Process {
id: connectProcess
property string ssid: ""
property string password: ""
property string security: ""
running: false
onStarted: {
refreshIndicator.running = true;
}
onExited: (exitCode, exitStatus) => {
refreshIndicator.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 = 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;
}
}
}
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: 780
visible: false
color: "transparent"
anchors.top: true
anchors.right: true
margins.right: 0
margins.top: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
Component.onCompleted: {
wifiLogic.refreshNetworks();
}
Rectangle {
anchors.fill: parent
color: Theme.backgroundPrimary
radius: 20
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
}
Item {
Layout.fillWidth: true
}
Spinner {
id: refreshIndicator
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignVCenter
visible: false
running: false
color: Theme.accentPrimary
size: 22
}
IconButton {
id: refreshButton
icon: "refresh"
onClicked: wifiLogic.refreshNetworks()
}
Rectangle {
implicitWidth: 36
implicitHeight: 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: 640
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 ? Object.values(wifiLogic.networks) : null
delegate: Item {
id: networkEntry
required property var modelData
property var signalIcon: wifiPanel.signalIcon
width: parent.width
height: (modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt ? 102 : 42) + (modelData.ssid === wifiLogic.actionPanelSsid ? 60 : 0)
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.44) : (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
}
}
}
}
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
}
Item {
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: 22
Layout.preferredWidth: 22
Spinner {
visible: wifiLogic.connectingSsid === modelData.ssid
running: wifiLogic.connectingSsid === modelData.ssid
color: Theme.accentPrimary
anchors.centerIn: parent
size: 22
}
}
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (wifiLogic.actionPanelSsid === modelData.ssid) {
wifiLogic.actionPanelSsid = ""; // Close if already open
} else {
wifiLogic.actionPanelSsid = modelData.ssid; // Open for this network
}
}
}
}
Rectangle {
visible: modelData.ssid === wifiLogic.passwordPromptSsid && wifiLogic.showPasswordPrompt
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: 8
color: "transparent"
Layout.alignment: Qt.AlignLeft
Layout.leftMargin: 32
Layout.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 {
Layout.preferredWidth: 80
Layout.preferredHeight: 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
}
}
}
}
Rectangle {
visible: modelData.ssid === wifiLogic.actionPanelSsid
Layout.fillWidth: true
Layout.preferredHeight: 60
radius: 8
color: "transparent"
Layout.alignment: Qt.AlignLeft
Layout.leftMargin: 32
Layout.rightMargin: 32
z: 2
RowLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 10
Item {
Layout.fillWidth: true
Layout.preferredHeight: 36
visible: wifiLogic.isSecured(modelData.security) && !modelData.connected && !modelData.existing
Rectangle {
anchors.fill: parent
radius: 8
color: "transparent"
border.color: actionPanelPasswordField.activeFocus ? Theme.accentPrimary : Theme.outline
border.width: 1
TextInput {
id: actionPanelPasswordField
anchors.fill: parent
anchors.margins: 12
font.pixelSize: 13
color: Theme.textPrimary
verticalAlignment: TextInput.AlignVCenter
clip: true
selectByMouse: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
echoMode: TextInput.Password
onAccepted: {
wifiLogic.pendingConnect = {
ssid: modelData.ssid,
security: modelData.security,
password: text
};
wifiLogic.doConnect();
wifiLogic.actionPanelSsid = ""; // Close the panel
}
}
}
}
Rectangle {
Layout.preferredWidth: 80
Layout.preferredHeight: 36
radius: 18
color: modelData.connected ? Theme.error : Theme.accentPrimary
border.color: modelData.connected ? Theme.error : Theme.accentPrimary
border.width: 0
opacity: 1.0
Behavior on color {
ColorAnimation {
duration: 100
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (modelData.connected) {
wifiLogic.disconnectNetwork(modelData.ssid);
} else {
if (wifiLogic.isSecured(modelData.security) && !modelData.existing) {
if (actionPanelPasswordField.text.length > 0) {
wifiLogic.pendingConnect = {
ssid: modelData.ssid,
security: modelData.security,
password: actionPanelPasswordField.text
};
wifiLogic.doConnect();
}
} else {
wifiLogic.connectNetwork(modelData.ssid, modelData.security);
}
}
wifiLogic.actionPanelSsid = ""; // Close the panel
}
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: parent.color = modelData.connected ? Qt.darker(Theme.error, 1.1) : Qt.darker(Theme.accentPrimary, 1.1)
onExited: parent.color = modelData.connected ? Theme.error : Theme.accentPrimary
}
Text {
anchors.centerIn: parent
text: modelData.connected ? "wifi_off" : "check"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Theme.backgroundPrimary
}
}
}
}
}
}
}
}
}
}
}
}
}