Merge branch 'noctalia-dev:main' into powermenu-ipc

This commit is contained in:
Dillon Johnson 2025-08-22 10:14:29 -10:00 committed by GitHub
commit 5cee4c234a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 815 additions and 260 deletions

View file

@ -1,19 +1,19 @@
{ {
"dark": { "dark": {
"mPrimary": "#ebbcba", "mPrimary": "#ebbcba",
"mOnPrimary": "#191724", "mOnPrimary": "#1f1d2e",
"mSecondary": "#9ccfd8", "mSecondary": "#9ccfd8",
"mOnSecondary": "#191724", "mOnSecondary": "#1f1d2e",
"mTertiary": "#f6c177", "mTertiary": "#f6c177",
"mOnTertiary": "#191724", "mOnTertiary": "#1f1d2e",
"mError": "#eb6f92", "mError": "#eb6f92",
"mOnError": "#1f1d2e", "mOnError": "#1f1d2e",
"mSurface": "#191724", "mSurface": "#1f1d2e",
"mOnSurface": "#e0def4", "mOnSurface": "#e0def4",
"mSurfaceVariant": "#26233a", "mSurfaceVariant": "#26233a",
"mOnSurfaceVariant": "#908caa", "mOnSurfaceVariant": "#908caa",
"mOutline": "#403d52", "mOutline": "#403d52",
"mShadow": "#191724" "mShadow": "#1f1d2e"
}, },
"light": { "light": {
"mPrimary": "#d46e6b", "mPrimary": "#d46e6b",

View file

@ -47,6 +47,8 @@ Singleton {
// ----------- // -----------
function applyOpacity(color, opacity) { function applyOpacity(color, opacity) {
// Convert color to string and apply opacity // Convert color to string and apply opacity
if (!color)
return "transparent"
return color.toString().replace("#", "#" + opacity) return color.toString().replace("#", "#" + opacity)
} }

View file

@ -120,16 +120,18 @@ Singleton {
bar: JsonObject { bar: JsonObject {
property string position: "top" // Possible values: "top", "bottom" property string position: "top" // Possible values: "top", "bottom"
property bool showActiveWindow: true
property bool showActiveWindowIcon: true property bool showActiveWindowIcon: true
property bool showSystemInfo: false
property bool showMedia: false
property bool showBrightness: true
property bool showNotificationsHistory: true
property bool showTray: true
property bool alwaysShowBatteryPercentage: false property bool alwaysShowBatteryPercentage: false
property real backgroundOpacity: 1.0 property real backgroundOpacity: 1.0
property list<string> monitors: [] property list<string> monitors: []
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
property list<string> center: ["Workspace"]
property list<string> right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"]
}
} }
// general // general

88
Commons/WidgetLoader.qml Normal file
View file

@ -0,0 +1,88 @@
import QtQuick
import qs.Commons
QtObject {
id: root
// Signal emitted when widget loading status changes
signal widgetLoaded(string widgetName)
signal widgetFailed(string widgetName, string error)
signal loadingComplete(int total, int loaded, int failed)
// Properties to track loading status
property int totalWidgets: 0
property int loadedWidgets: 0
property int failedWidgets: 0
// Auto-discover widget components
function getWidgetComponent(widgetName) {
if (!widgetName || widgetName.trim() === "") {
return null
}
const widgetPath = `../Modules/Bar/Widgets/${widgetName}.qml`
// Try to load the widget directly from file
const component = Qt.createComponent(widgetPath)
if (component.status === Component.Ready) {
return component
}
const errorMsg = `Failed to load ${widgetName}.qml widget, status: ${component.status}, error: ${component.errorString(
)}`
Logger.error("WidgetLoader", errorMsg)
return null
}
// Initialize loading tracking
function initializeLoading(widgetList) {
totalWidgets = widgetList.length
loadedWidgets = 0
failedWidgets = 0
}
// Track widget loading success
function onWidgetLoaded(widgetName) {
loadedWidgets++
widgetLoaded(widgetName)
if (loadedWidgets + failedWidgets === totalWidgets) {
Logger.log("WidgetLoader", `Loaded ${loadedWidgets} widgets`)
loadingComplete(totalWidgets, loadedWidgets, failedWidgets)
}
}
// Track widget loading failure
function onWidgetFailed(widgetName, error) {
failedWidgets++
widgetFailed(widgetName, error)
if (loadedWidgets + failedWidgets === totalWidgets) {
loadingComplete(totalWidgets, loadedWidgets, failedWidgets)
}
}
// This is where you should add your Modules/Bar/Widgets/
// so it gets registered in the BarTab
function discoverAvailableWidgets() {
const widgetFiles = ["ActiveWindow", "Battery", "Bluetooth", "Brightness", "Clock", "MediaMini", "NotificationHistory", "ScreenRecorderIndicator", "SidePanelToggle", "SystemMonitor", "Tray", "Volume", "WiFi", "Workspace"]
const availableWidgets = []
widgetFiles.forEach(widgetName => {
// Test if the widget can be loaded
const component = getWidgetComponent(widgetName)
if (component) {
availableWidgets.push({
"key": widgetName,
"name": widgetName
})
}
})
// Sort alphabetically
availableWidgets.sort((a, b) => a.name.localeCompare(b.name))
return availableWidgets
}
}

View file

@ -144,6 +144,7 @@ Loader {
maskEnabled: true maskEnabled: true
maskSource: maskTexture maskSource: maskTexture
maskInverted: false maskInverted: false
maskSpreadAtMax: 0.75
} }
mask: Region {} mask: Region {}

View file

@ -47,7 +47,7 @@ Variants {
layer.enabled: true layer.enabled: true
} }
// Left // Left Section - Dynamic Widgets
Row { Row {
id: leftSection id: leftSection
@ -57,14 +57,25 @@ Variants {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
SystemMonitor {} Repeater {
model: Settings.data.bar.widgets.left
ActiveWindow {} delegate: Loader {
id: leftWidgetLoader
MediaMini {} sourceComponent: widgetLoader.getWidgetComponent(modelData)
active: true
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Loader.Error) {
widgetLoader.onWidgetFailed(modelData, "Loader error")
} else if (status === Loader.Ready) {
widgetLoader.onWidgetLoaded(modelData)
}
}
}
}
} }
// Center // Center Section - Dynamic Widgets
Row { Row {
id: centerSection id: centerSection
@ -73,10 +84,25 @@ Variants {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
Workspace {} Repeater {
model: Settings.data.bar.widgets.center
delegate: Loader {
id: centerWidgetLoader
sourceComponent: widgetLoader.getWidgetComponent(modelData)
active: true
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Loader.Error) {
widgetLoader.onWidgetFailed(modelData, "Loader error")
} else if (status === Loader.Ready) {
widgetLoader.onWidgetLoaded(modelData)
}
}
}
}
} }
// Right // Right Section - Dynamic Widgets
Row { Row {
id: rightSection id: rightSection
@ -86,44 +112,38 @@ Variants {
anchors.verticalCenter: bar.verticalCenter anchors.verticalCenter: bar.verticalCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
ScreenRecorderIndicator { Repeater {
anchors.verticalCenter: parent.verticalCenter model: Settings.data.bar.widgets.right
delegate: Loader {
id: rightWidgetLoader
sourceComponent: widgetLoader.getWidgetComponent(modelData)
active: true
anchors.verticalCenter: parent.verticalCenter
onStatusChanged: {
if (status === Loader.Error) {
widgetLoader.onWidgetFailed(modelData, "Loader error")
} else if (status === Loader.Ready) {
widgetLoader.onWidgetLoaded(modelData)
}
}
}
} }
Tray {
anchors.verticalCenter: parent.verticalCenter
}
NotificationHistory {
anchors.verticalCenter: parent.verticalCenter
}
WiFi {
anchors.verticalCenter: parent.verticalCenter
}
Bluetooth {
anchors.verticalCenter: parent.verticalCenter
}
Battery {
anchors.verticalCenter: parent.verticalCenter
}
Volume {
anchors.verticalCenter: parent.verticalCenter
}
Brightness {
anchors.verticalCenter: parent.verticalCenter
}
Clock {
anchors.verticalCenter: parent.verticalCenter
}
SidePanelToggle {}
} }
} }
// Widget loader instance
WidgetLoader {
id: widgetLoader
onWidgetFailed: function (widgetName, error) {
Logger.error("Bar", `Widget failed: ${widgetName} - ${error}`)
}
}
// Initialize widget loading tracking
Component.onCompleted: {
const allWidgets = [...Settings.data.bar.widgets.left, ...Settings.data.bar.widgets.center, ...Settings.data.bar.widgets.right]
widgetLoader.initializeLoading(allWidgets)
}
} }
} }

View file

@ -1,43 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
readonly property bool wifiEnabled: Settings.data.network.wifiEnabled
sizeMultiplier: 0.8
visible: wifiEnabled
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: {
let connected = false
let signalStrength = 0
for (const net in NetworkService.networks) {
if (NetworkService.networks[net].connected) {
connected = true
signalStrength = NetworkService.networks[net].signal
break
}
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi"
}
tooltipText: "WiFi Networks"
onClicked: {
wifiPanel.toggle(screen)
}
WiFiPanel {
id: wifiPanel
}
}

View file

@ -11,7 +11,7 @@ Row {
id: root id: root
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
visible: (Settings.data.bar.showActiveWindow && getTitle() !== "") visible: getTitle() !== ""
property bool showingFullTitle: false property bool showingFullTitle: false
property int lastWindowIndex: -1 property int lastWindowIndex: -1

View file

@ -22,8 +22,6 @@ NPill {
// Choose icon based on charge and charging state // Choose icon based on charge and charging state
function batteryIcon() { function batteryIcon() {
if (!show)
return ""
if (charging) if (charging)
return "battery_android_bolt" return "battery_android_bolt"

View file

@ -10,9 +10,7 @@ import qs.Widgets
NIconButton { NIconButton {
id: root id: root
readonly property bool bluetoothEnabled: Settings.data.network.bluetoothEnabled
sizeMultiplier: 0.8 sizeMultiplier: 0.8
visible: bluetoothEnabled
colorBg: Color.mSurfaceVariant colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface colorFg: Color.mOnSurface
@ -33,8 +31,4 @@ NIconButton {
onClicked: { onClicked: {
bluetoothPanel.toggle(screen) bluetoothPanel.toggle(screen)
} }
BluetoothPanel {
id: bluetoothPanel
}
} }

View file

@ -10,7 +10,7 @@ Item {
width: pill.width width: pill.width
height: pill.height height: pill.height
visible: Settings.data.bar.showBrightness && firstBrightnessReceived && getMonitor() !== null visible: getMonitor() !== null
// Used to avoid opening the pill on Quickshell startup // Used to avoid opening the pill on Quickshell startup
property bool firstBrightnessReceived: false property bool firstBrightnessReceived: false

View file

@ -11,7 +11,8 @@ Row {
id: root id: root
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
visible: Settings.data.bar.showMedia && (MediaService.canPlay || MediaService.canPause) visible: MediaService.currentPlayer !== null
width: MediaService.currentPlayer !== null ? implicitWidth : 0
function getTitle() { function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
@ -109,14 +110,14 @@ Row {
visible: Settings.data.audio.showMiniplayerAlbumArt visible: Settings.data.audio.showMiniplayerAlbumArt
Rectangle { Rectangle {
width: 16 * scaling width: 18 * scaling
height: 16 * scaling height: 18 * scaling
radius: width * 0.5 radius: width * 0.5
color: Color.transparent color: Color.transparent
antialiasing: true antialiasing: true
clip: true clip: true
NImageRounded { NImageCircled {
id: trackArt id: trackArt
visible: MediaService.trackArtUrl.toString() !== "" visible: MediaService.trackArtUrl.toString() !== ""
anchors.fill: parent anchors.fill: parent
@ -126,8 +127,6 @@ Row {
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow" fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow"
borderWidth: 0 borderWidth: 0
border.color: Color.transparent border.color: Color.transparent
imageRadius: width
antialiasing: true
} }
// Fallback icon when no album art available // Fallback icon when no album art available

View file

@ -10,7 +10,6 @@ import qs.Widgets
NIconButton { NIconButton {
id: root id: root
visible: Settings.data.bar.showNotificationsHistory
sizeMultiplier: 0.8 sizeMultiplier: 0.8
icon: "notifications" icon: "notifications"
tooltipText: "Notification History" tooltipText: "Notification History"

View file

@ -8,7 +8,6 @@ Row {
id: root id: root
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
visible: (Settings.data.bar.showSystemInfo)
Rectangle { Rectangle {
// Let the Rectangle size itself based on its content (the Row) // Let the Rectangle size itself based on its content (the Row)

View file

@ -12,9 +12,8 @@ import qs.Widgets
Rectangle { Rectangle {
readonly property real itemSize: 24 * scaling readonly property real itemSize: 24 * scaling
visible: Settings.data.bar.showTray && (SystemTray.items.values.length > 0) visible: SystemTray.items.values.length > 0
width: tray.width + Style.marginM * scaling * 2 width: tray.width + Style.marginM * scaling * 2
height: Math.round(Style.capsuleHeight * scaling) height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
@ -95,14 +94,14 @@ Rectangle {
return return
} }
if (modelData.hasMenu && modelData.menu && trayMenu) { if (modelData.hasMenu && modelData.menu && trayMenu.item) {
trayPanel.open() trayPanel.open()
// Anchor the menu to the tray icon item (parent) and position it below the icon // Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.width / 2) const menuX = (width / 2) - (trayMenu.item.width / 2)
const menuY = (Style.barHeight * scaling) const menuY = (Style.barHeight * scaling)
trayMenu.menu = modelData.menu trayMenu.item.menu = modelData.menu
trayMenu.showAt(parent, menuX, menuY) trayMenu.item.showAt(parent, menuX, menuY)
} else { } else {
Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set") Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set")
} }
@ -142,7 +141,7 @@ Rectangle {
function close() { function close() {
visible = false visible = false
trayMenu.hideMenu() trayMenu.item.hideMenu()
} }
// Clicking outside of the rectangle to close // Clicking outside of the rectangle to close
@ -151,8 +150,9 @@ Rectangle {
onClicked: trayPanel.close() onClicked: trayPanel.close()
} }
TrayMenu { Loader {
id: trayMenu id: trayMenu
source: "TrayMenu.qml"
} }
} }
} }

View file

@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
sizeMultiplier: 0.8
Component.onCompleted: {
Logger.log("WiFi", "Widget component completed")
Logger.log("WiFi", "NetworkService available:", !!NetworkService)
if (NetworkService) {
Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks)
}
}
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: {
try {
let connected = false
let signalStrength = 0
for (const net in NetworkService.networks) {
if (NetworkService.networks[net].connected) {
connected = true
signalStrength = NetworkService.networks[net].signal
break
}
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi"
} catch (error) {
Logger.error("WiFi", "Error getting icon:", error)
return "wifi"
}
}
tooltipText: "WiFi Networks"
onClicked: {
try {
Logger.log("WiFi", "Button clicked, toggling panel")
wifiPanel.toggle(screen)
} catch (error) {
Logger.error("WiFi", "Error toggling panel:", error)
}
}
}

View file

@ -225,7 +225,7 @@ NPanel {
Layout.bottomMargin: Style.marginM * scaling Layout.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
color: Color.mSurface color: Color.mSurface
border.color: searchInput.activeFocus ? Color.mTertiary : Color.mOutline border.color: searchInput.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, searchInput.activeFocus ? Style.borderM * scaling : Style.borderS * scaling) border.width: Math.max(1, searchInput.activeFocus ? Style.borderM * scaling : Style.borderS * scaling)
Item { Item {
@ -355,7 +355,7 @@ NPanel {
height: 65 * scaling height: 65 * scaling
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
property bool isSelected: index === selectedIndex property bool isSelected: index === selectedIndex
color: (appCardArea.containsMouse || isSelected) ? Color.mTertiary : Color.mSurface color: (appCardArea.containsMouse || isSelected) ? Color.mSecondary : Color.mSurface
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {

View file

@ -294,13 +294,14 @@ Loader {
// Animated avatar with glow effect or audio visualizer // Animated avatar with glow effect or audio visualizer
Rectangle { Rectangle {
width: 120 * scaling width: 108 * scaling
height: 120 * scaling height: 108 * scaling
radius: width * 0.5 radius: width * 0.5
color: Color.transparent color: Color.transparent
border.color: Color.mPrimary border.color: Color.mPrimary
border.width: Math.max(1, Style.borderL * scaling) border.width: Math.max(1, Style.borderL * scaling)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
z: 10
// Circular audio visualizer when music is playing // Circular audio visualizer when music is playing
Loader { Loader {
@ -464,13 +465,12 @@ Loader {
} }
} }
NImageRounded { NImageCircled {
anchors.centerIn: parent anchors.centerIn: parent
width: 100 * scaling width: 100 * scaling
height: 100 * scaling height: 100 * scaling
imagePath: Settings.data.general.avatarImage imagePath: Settings.data.general.avatarImage
fallbackIcon: "person" fallbackIcon: "person"
imageRadius: width * 0.5
} }
// Hover animation // Hover animation

View file

@ -111,7 +111,7 @@ NPanel {
width: notificationList ? notificationList.width : 380 * scaling width: notificationList ? notificationList.width : 380 * scaling
height: Math.max(80, notificationContent.height + 30) height: Math.max(80, notificationContent.height + 30)
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
color: notificationMouseArea.containsMouse ? Color.mTertiary : Color.mSurfaceVariant color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant
RowLayout { RowLayout {
anchors { anchors {

View file

@ -215,7 +215,7 @@ NPanel {
if (pending) if (pending)
return Color.applyOpacity(Color.mPrimary, "20") return Color.applyOpacity(Color.mPrimary, "20")
if (mouseArea.containsMouse) if (mouseArea.containsMouse)
return Color.mTertiary return Color.mSecondary
return Color.transparent return Color.transparent
} }

View file

@ -47,6 +47,7 @@ NPanel {
id: barTab id: barTab
Tabs.BarTab {} Tabs.BarTab {}
} }
Component { Component {
id: audioTab id: audioTab
Tabs.AudioTab {} Tabs.AudioTab {}

View file

@ -199,7 +199,7 @@ ColumnLayout {
width: contributorsGrid.cellWidth - Style.marginL * scaling width: contributorsGrid.cellWidth - Style.marginL * scaling
height: contributorsGrid.cellHeight - Style.marginXS * scaling height: contributorsGrid.cellHeight - Style.marginXS * scaling
radius: Style.radiusL * scaling radius: Style.radiusL * scaling
color: contributorArea.containsMouse ? Color.mTertiary : Color.transparent color: contributorArea.containsMouse ? Color.mSecondary : Color.transparent
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
@ -211,14 +211,13 @@ ColumnLayout {
Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling
NImageRounded { NImageCircled {
imagePath: modelData.avatar_url || "" imagePath: modelData.avatar_url || ""
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginXS * scaling anchors.margins: Style.marginXS * scaling
fallbackIcon: "person" fallbackIcon: "person"
borderColor: Color.mPrimary borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM * scaling) borderWidth: Math.max(1, Style.borderM * scaling)
imageRadius: width * 0.5
} }
} }

View file

@ -33,12 +33,6 @@ ColumnLayout {
spacing: Style.marginL * scaling spacing: Style.marginL * scaling
Layout.fillWidth: true Layout.fillWidth: true
NText {
text: "Bar Components"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
ColumnLayout { ColumnLayout {
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
@ -78,70 +72,7 @@ ColumnLayout {
} }
} }
NToggle { ColumnLayout {
label: "Show Active Window"
description: "Display the title of the currently focused window."
checked: Settings.data.bar.showActiveWindow
onToggled: checked => {
Settings.data.bar.showActiveWindow = checked
}
}
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => {
Settings.data.bar.showActiveWindowIcon = checked
}
}
NToggle {
label: "Show System Info"
description: "Display system statistics (CPU, RAM, Temperature)."
checked: Settings.data.bar.showSystemInfo
onToggled: checked => {
Settings.data.bar.showSystemInfo = checked
}
}
NToggle {
label: "Show Media"
description: "Display media controls and information."
checked: Settings.data.bar.showMedia
onToggled: checked => {
Settings.data.bar.showMedia = checked
}
}
NToggle {
label: "Show Notifications History"
description: "Display a shortcut to the notifications history."
checked: Settings.data.bar.showNotificationsHistory
onToggled: checked => {
Settings.data.bar.showNotificationsHistory = checked
}
}
NToggle {
label: "Show Applications Tray"
description: "Display the applications tray."
checked: Settings.data.bar.showTray
onToggled: checked => {
Settings.data.bar.showTray = checked
}
}
NToggle {
label: "Show Battery Percentage"
description: "Show battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => {
Settings.data.bar.alwaysShowBatteryPercentage = checked
}
}
ColumnLayout {
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.fillWidth: true Layout.fillWidth: true
@ -179,7 +110,168 @@ ColumnLayout {
} }
} }
} }
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => {
Settings.data.bar.showActiveWindowIcon = checked
}
}
NToggle {
label: "Show Battery Percentage"
description: "Show battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => {
Settings.data.bar.alwaysShowBatteryPercentage = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginL * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Widgets Management Section
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Widgets Positioning"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Add, remove, or reorder widgets in each section of the bar using the control buttons."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
// Bar Sections
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.topMargin: Style.marginM * scaling
spacing: Style.marginM * scaling
// Left Section
NWidgetCard {
sectionName: "Left"
widgetModel: Settings.data.bar.widgets.left
availableWidgets: availableWidgets
scrollView: scrollView
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
// Center Section
NWidgetCard {
sectionName: "Center"
widgetModel: Settings.data.bar.widgets.center
availableWidgets: availableWidgets
scrollView: scrollView
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
// Right Section
NWidgetCard {
sectionName: "Right"
widgetModel: Settings.data.bar.widgets.right
availableWidgets: availableWidgets
scrollView: scrollView
onAddWidget: (widgetName, section) => addWidgetToSection(widgetName, section)
onRemoveWidget: (section, index) => removeWidgetFromSection(section, index)
onReorderWidget: (section, fromIndex, toIndex) => reorderWidgetInSection(section, fromIndex, toIndex)
}
}
}
} }
} }
} }
// Helper functions
function addWidgetToSection(widgetName, section) {
console.log("Adding widget", widgetName, "to section", section)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()
newArray.push(widgetName)
console.log("Widget added. New array:", JSON.stringify(newArray))
// Assign the new array
Settings.data.bar.widgets[section] = newArray
}
}
function removeWidgetFromSection(section, index) {
console.log("Removing widget from section", section, "at index", index)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray && index >= 0 && index < sectionArray.length) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()
newArray.splice(index, 1)
console.log("Widget removed. New array:", JSON.stringify(newArray))
// Assign the new array
Settings.data.bar.widgets[section] = newArray
}
}
function reorderWidgetInSection(section, fromIndex, toIndex) {
console.log("Reordering widget in section", section, "from", fromIndex, "to", toIndex)
var sectionArray = Settings.data.bar.widgets[section]
if (sectionArray && fromIndex >= 0 && fromIndex < sectionArray.length && toIndex >= 0
&& toIndex < sectionArray.length) {
// Create a new array to avoid modifying the original
var newArray = sectionArray.slice()
var item = newArray[fromIndex]
newArray.splice(fromIndex, 1)
newArray.splice(toIndex, 0, item)
console.log("Widget reordered. New array:", JSON.stringify(newArray))
// Assign the new array
Settings.data.bar.widgets[section] = newArray
}
}
// Widget loader for discovering available widgets
WidgetLoader {
id: widgetLoader
}
ListModel {
id: availableWidgets
}
Component.onCompleted: {
discoverWidgets()
}
// Automatically discover available widgets using WidgetLoader
function discoverWidgets() {
availableWidgets.clear()
// Use WidgetLoader to discover available widgets
const discoveredWidgets = widgetLoader.discoverAvailableWidgets()
// Add discovered widgets to the ListModel
discoveredWidgets.forEach(widget => {
availableWidgets.append(widget)
})
}
} }

View file

@ -51,13 +51,13 @@ ColumnLayout {
spacing: Style.marginL * scaling spacing: Style.marginL * scaling
// Avatar preview // Avatar preview
NImageRounded { NImageCircled {
width: 64 * scaling width: 64 * scaling
height: 64 * scaling height: 64 * scaling
imagePath: Settings.data.general.avatarImage imagePath: Settings.data.general.avatarImage
fallbackIcon: "person" fallbackIcon: "person"
borderColor: Color.mPrimary borderColor: Color.mPrimary
borderWidth: Math.max(1, Style.borderM) borderWidth: Math.max(1, Style.borderM * scaling)
} }
NTextInput { NTextInput {

View file

@ -76,7 +76,7 @@ NBox {
// implicitWidth: 120 * scaling // implicitWidth: 120 * scaling
// implicitHeight: 30 * scaling // implicitHeight: 30 * scaling
color: Color.transparent color: Color.transparent
border.color: playerSelector.activeFocus ? Color.mTertiary : Color.mOutline border.color: playerSelector.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
} }
@ -138,7 +138,7 @@ NBox {
background: Rectangle { background: Rectangle {
width: popup.width - Style.marginS * scaling * 2 width: popup.width - Style.marginS * scaling * 2
color: highlighted ? Color.mTertiary : Color.transparent color: highlighted ? Color.mSecondary : Color.transparent
radius: Style.radiusXS * scaling radius: Style.radiusXS * scaling
} }
} }
@ -164,7 +164,7 @@ NBox {
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
clip: true clip: true
NImageRounded { NImageCircled {
id: trackArt id: trackArt
visible: MediaService.trackArtUrl.toString() !== "" visible: MediaService.trackArtUrl.toString() !== ""
@ -174,7 +174,6 @@ NBox {
fallbackIcon: "music_note" fallbackIcon: "music_note"
borderColor: Color.mOutline borderColor: Color.mOutline
borderWidth: Math.max(1, Style.borderS * scaling) borderWidth: Math.max(1, Style.borderS * scaling)
imageRadius: width * 0.5
} }
// Fallback icon when no album art available // Fallback icon when no album art available

View file

@ -28,7 +28,7 @@ NBox {
anchors.margins: Style.marginM * scaling anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NImageRounded { NImageCircled {
width: Style.baseWidgetSize * 1.25 * scaling width: Style.baseWidgetSize * 1.25 * scaling
height: Style.baseWidgetSize * 1.25 * scaling height: Style.baseWidgetSize * 1.25 * scaling
imagePath: Settings.data.general.avatarImage imagePath: Settings.data.general.avatarImage

View file

@ -78,7 +78,7 @@ Singleton {
} }
} }
writeColorsToDisk(variant) writeColorsToDisk(variant)
Logger.log("ColorScheme", "Applied color scheme:", path) Logger.log("ColorScheme", "Applying color scheme:", path)
} catch (e) { } catch (e) {
Logger.error("ColorScheme", "Failed to parse scheme JSON:", e) Logger.error("ColorScheme", "Failed to parse scheme JSON:", e)
} }
@ -90,7 +90,8 @@ Singleton {
id: colorsWriter id: colorsWriter
path: colorsJsonFilePath path: colorsJsonFilePath
onSaved: { onSaved: {
Logger.log("ColorScheme", "Colors saved")
// Logger.log("ColorScheme", "Colors saved")
} }
JsonAdapter { JsonAdapter {
id: out id: out
@ -130,6 +131,9 @@ Singleton {
out.mOutline = pick(obj, "mOutline", "outline", out.mOutline) out.mOutline = pick(obj, "mOutline", "outline", out.mOutline)
out.mShadow = pick(obj, "mShadow", "shadow", out.mShadow) out.mShadow = pick(obj, "mShadow", "shadow", out.mShadow)
// Force a rewrite by updating the path
colorsWriter.path = ""
colorsWriter.path = colorsJsonFilePath
colorsWriter.writeAdapter() colorsWriter.writeAdapter()
} }

View file

@ -0,0 +1,30 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(binding = 1) uniform sampler2D source;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float imageOpacity;
} ubuf;
void main() {
// Center coordinates around (0, 0)
vec2 uv = qt_TexCoord0 - 0.5;
// Calculate distance from center
float distance = length(uv);
// Create circular mask - anything beyond radius 0.5 is transparent
float mask = 1.0 - smoothstep(0.48, 0.52, distance);
// Sample the texture
vec4 color = texture(source, qt_TexCoord0);
// Apply the circular mask and opacity
float finalAlpha = color.a * mask * ubuf.imageOpacity * ubuf.qt_Opacity;
fragColor = vec4(color.rgb * finalAlpha, finalAlpha);
}

View file

@ -0,0 +1,56 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(binding = 1) uniform sampler2D source;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
// Custom properties with non-conflicting names
float itemWidth;
float itemHeight;
float cornerRadius;
float imageOpacity;
} ubuf;
// Function to calculate the signed distance from a point to a rounded box
float roundedBoxSDF(vec2 centerPos, vec2 boxSize, float radius) {
vec2 d = abs(centerPos) - boxSize + radius;
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius;
}
void main() {
// Get size from uniforms
vec2 itemSize = vec2(ubuf.itemWidth, ubuf.itemHeight);
float cornerRadius = ubuf.cornerRadius;
float itemOpacity = ubuf.imageOpacity;
// Normalize coordinates to [-0.5, 0.5] range
vec2 uv = qt_TexCoord0 - 0.5;
// Scale by aspect ratio to maintain uniform rounding
vec2 aspectRatio = itemSize / max(itemSize.x, itemSize.y);
uv *= aspectRatio;
// Calculate half size in normalized space
vec2 halfSize = 0.5 * aspectRatio;
// Normalize the corner radius
float normalizedRadius = cornerRadius / max(itemSize.x, itemSize.y);
// Calculate distance to rounded rectangle
float distance = roundedBoxSDF(uv, halfSize, normalizedRadius);
// Create smooth alpha mask
float smoothedAlpha = 1.0 - smoothstep(0.0, fwidth(distance), distance);
// Sample the texture
vec4 color = texture(source, qt_TexCoord0);
// Apply the rounded mask and opacity
// Make sure areas outside the rounded rect are completely transparent
float finalAlpha = color.a * smoothedAlpha * itemOpacity * ubuf.qt_Opacity;
fragColor = vec4(color.rgb * finalAlpha, finalAlpha);
}

Binary file not shown.

Binary file not shown.

View file

@ -16,6 +16,7 @@ ColumnLayout {
} }
property string currentKey: '' property string currentKey: ''
property string placeholder: ""
signal selected(string key) signal selected(string key)
@ -50,7 +51,7 @@ ColumnLayout {
implicitWidth: Style.baseWidgetSize * 3.75 * scaling implicitWidth: Style.baseWidgetSize * 3.75 * scaling
implicitHeight: preferredHeight implicitHeight: preferredHeight
color: Color.mSurface color: Color.mSurface
border.color: combo.activeFocus ? Color.mTertiary : Color.mOutline border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, Style.borderS * scaling) border.width: Math.max(1, Style.borderS * scaling)
radius: Style.radiusM * scaling radius: Style.radiusM * scaling
} }
@ -61,8 +62,10 @@ ColumnLayout {
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
text: (combo.currentIndex >= 0 && combo.currentIndex < root.model.count) ? root.model.get( color: (combo.currentIndex >= 0
combo.currentIndex).name : "" && combo.currentIndex < root.model.count) ? Color.mOnSurface : Color.mOnSurfaceVariant
text: (combo.currentIndex >= 0
&& combo.currentIndex < root.model.count) ? root.model.get(combo.currentIndex).name : root.placeholder
} }
indicator: NIcon { indicator: NIcon {
@ -112,7 +115,7 @@ ColumnLayout {
background: Rectangle { background: Rectangle {
width: combo.width - Style.marginM * scaling * 3 width: combo.width - Style.marginM * scaling * 3
color: highlighted ? Color.mTertiary : Color.transparent color: highlighted ? Color.mSecondary : Color.transparent
radius: Style.radiusS * scaling radius: Style.radiusS * scaling
} }
} }

73
Widgets/NImageCircled.qml Normal file
View file

@ -0,0 +1,73 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Commons
import qs.Services
Rectangle {
id: root
property string imagePath: ""
property string fallbackIcon: ""
property color borderColor: Color.transparent
property real borderWidth: 0
color: Color.transparent
radius: parent.width * 0.5
anchors.margins: Style.marginXXS * scaling
Rectangle {
color: Color.transparent
anchors.fill: parent
Image {
id: img
anchors.fill: parent
source: imagePath
visible: false // Hide since we're using it as shader source
mipmap: true
smooth: true
asynchronous: true
antialiasing: true
fillMode: Image.PreserveAspectCrop
}
ShaderEffect {
anchors.fill: parent
property var source: ShaderEffectSource {
sourceItem: img
hideSource: true
live: true
recursive: false
format: ShaderEffectSource.RGBA
}
property real imageOpacity: root.opacity
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/circled_image.frag.qsb")
supportsAtlasTextures: false
blending: true
}
// Fallback icon
NIcon {
anchors.centerIn: parent
text: fallbackIcon
font.pointSize: Style.fontSizeXXL * scaling
visible: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
z: 0
}
}
//Border
Rectangle {
anchors.fill: parent
radius: parent.radius
color: Color.transparent
border.color: parent.borderColor
border.width: parent.borderWidth
antialiasing: true
z: 10
}
}

View file

@ -20,6 +20,62 @@ Rectangle {
radius: scaledRadius radius: scaledRadius
anchors.margins: Style.marginXXS * scaling anchors.margins: Style.marginXXS * scaling
Rectangle {
color: Color.transparent
anchors.fill: parent
Image {
id: img
anchors.fill: parent
source: imagePath
visible: false // Hide since we're using it as shader source
mipmap: true
smooth: true
asynchronous: true
antialiasing: true
fillMode: Image.PreserveAspectCrop
}
ShaderEffect {
anchors.fill: parent
property var source: ShaderEffectSource {
sourceItem: img
hideSource: true
live: true
recursive: false
format: ShaderEffectSource.RGBA
}
// Use custom property names to avoid conflicts with final properties
property real itemWidth: root.width
property real itemHeight: root.height
property real cornerRadius: root.radius
property real imageOpacity: root.opacity
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/rounded_image.frag.qsb")
// Qt6 specific properties - ensure proper blending
supportsAtlasTextures: false
blending: true
// Make sure the background is transparent
Rectangle {
id: background
anchors.fill: parent
color: "transparent"
z: -1
}
}
// Fallback icon
NIcon {
anchors.centerIn: parent
text: fallbackIcon
font.pointSize: Style.fontSizeXXL * scaling
visible: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
z: 0
}
}
// Border // Border
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@ -27,45 +83,7 @@ Rectangle {
color: Color.transparent color: Color.transparent
border.color: parent.borderColor border.color: parent.borderColor
border.width: parent.borderWidth border.width: parent.borderWidth
antialiasing: true
z: 10 z: 10
} }
Image {
id: img
anchors.fill: parent
source: imagePath
visible: false
mipmap: true
smooth: true
asynchronous: true
fillMode: Image.PreserveAspectCrop
}
MultiEffect {
anchors.fill: parent
source: img
maskEnabled: true
maskSource: mask
visible: imagePath !== ""
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
radius: scaledRadius
}
}
// Fallback icon
NIcon {
anchors.centerIn: parent
text: fallbackIcon
font.pointSize: Style.fontSizeXXL * scaling
visible: fallbackIcon !== undefined && fallbackIcon !== "" && (imagePath === undefined || imagePath === "")
z: 0
}
} }

View file

@ -46,7 +46,7 @@ Item {
anchors.fill: parent anchors.fill: parent
radius: frame.radius radius: frame.radius
color: Color.transparent color: Color.transparent
border.color: input.activeFocus ? Color.mTertiary : Color.transparent border.color: input.activeFocus ? Color.mSecondary : Color.transparent
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0 border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0
} }

158
Widgets/NWidgetCard.qml Normal file
View file

@ -0,0 +1,158 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
NCard {
id: root
property string sectionName: ""
property var widgetModel: []
property var availableWidgets: []
property var scrollView: null
signal addWidget(string widgetName, string section)
signal removeWidget(string section, int index)
signal reorderWidget(string section, int fromIndex, int toIndex)
Layout.fillWidth: true
Layout.minimumHeight: {
var widgetCount = widgetModel.length
if (widgetCount === 0)
return 140 * scaling
var availableWidth = scrollView ? scrollView.availableWidth - (Style.marginM * scaling * 2) : 400 * scaling
var avgWidgetWidth = 150 * scaling
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
var rows = Math.ceil(widgetCount / widgetsPerRow)
return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
NText {
text: sectionName + " Section"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NComboBox {
id: comboBox
width: 120 * scaling
model: availableWidgets
label: ""
description: ""
placeholder: "Add widget to " + sectionName.toLowerCase() + " section"
onSelected: key => {
comboBox.selectedKey = key
}
}
NIconButton {
icon: "add"
size: 24 * scaling
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
colorBgHover: Color.mPrimaryContainer
colorFgHover: Color.mOnPrimaryContainer
enabled: comboBox.selectedKey !== ""
Layout.alignment: Qt.AlignVCenter
onClicked: {
if (comboBox.selectedKey !== "") {
addWidget(comboBox.selectedKey, sectionName.toLowerCase())
comboBox.reset()
}
}
}
}
Flow {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 65 * scaling
spacing: Style.marginS * scaling
flow: Flow.LeftToRight
Repeater {
model: widgetModel
delegate: Rectangle {
width: widgetContent.implicitWidth + 16 * scaling
height: 48 * scaling
radius: Style.radiusS * scaling
color: Color.mPrimary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
id: widgetContent
anchors.centerIn: parent
spacing: Style.marginXS * scaling
NIconButton {
icon: "chevron_left"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
enabled: index > 0
onClicked: {
if (index > 0) {
reorderWidget(sectionName.toLowerCase(), index, index - 1)
}
}
}
NText {
text: modelData
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnPrimary
horizontalAlignment: Text.AlignHCenter
}
NIconButton {
icon: "chevron_right"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
enabled: index < widgetModel.length - 1
onClicked: {
if (index < widgetModel.length - 1) {
reorderWidget(sectionName.toLowerCase(), index, index + 1)
}
}
}
NIconButton {
icon: "close"
size: 20 * scaling
colorBg: Color.applyOpacity(Color.mOnPrimary, "20")
colorFg: Color.mOnPrimary
colorBgHover: Color.applyOpacity(Color.mOnPrimary, "40")
colorFgHover: Color.mOnPrimary
onClicked: {
removeWidget(sectionName.toLowerCase(), index)
}
}
}
}
}
}
}
}

View file

@ -16,6 +16,7 @@ import qs.Commons
import qs.Modules.Launcher import qs.Modules.Launcher
import qs.Modules.Background import qs.Modules.Background
import qs.Modules.Bar import qs.Modules.Bar
import qs.Modules.BluetoothPanel
import qs.Modules.Calendar import qs.Modules.Calendar
import qs.Modules.Dock import qs.Modules.Dock
import qs.Modules.IPC import qs.Modules.IPC
@ -25,7 +26,7 @@ import qs.Modules.SettingsPanel
import qs.Modules.PowerPanel import qs.Modules.PowerPanel
import qs.Modules.SidePanel import qs.Modules.SidePanel
import qs.Modules.Toast import qs.Modules.Toast
import qs.Modules.WiFiPanel
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@ -70,6 +71,14 @@ ShellRoot {
id: powerPanel id: powerPanel
} }
WiFiPanel {
id: wifiPanel
}
BluetoothPanel {
id: bluetoothPanel
}
ToastManager {} ToastManager {}
IPCManager {} IPCManager {}