diff --git a/Assets/ColorScheme/Rosepine.json b/Assets/ColorScheme/Rosepine.json index 891b5448..37e9b3a9 100644 --- a/Assets/ColorScheme/Rosepine.json +++ b/Assets/ColorScheme/Rosepine.json @@ -1,19 +1,19 @@ { "dark": { "mPrimary": "#ebbcba", - "mOnPrimary": "#191724", + "mOnPrimary": "#1f1d2e", "mSecondary": "#9ccfd8", - "mOnSecondary": "#191724", + "mOnSecondary": "#1f1d2e", "mTertiary": "#f6c177", - "mOnTertiary": "#191724", + "mOnTertiary": "#1f1d2e", "mError": "#eb6f92", "mOnError": "#1f1d2e", - "mSurface": "#191724", + "mSurface": "#1f1d2e", "mOnSurface": "#e0def4", "mSurfaceVariant": "#26233a", "mOnSurfaceVariant": "#908caa", "mOutline": "#403d52", - "mShadow": "#191724" + "mShadow": "#1f1d2e" }, "light": { "mPrimary": "#d46e6b", diff --git a/Commons/Color.qml b/Commons/Color.qml index 221716fe..dc1d5541 100644 --- a/Commons/Color.qml +++ b/Commons/Color.qml @@ -47,6 +47,8 @@ Singleton { // ----------- function applyOpacity(color, opacity) { // Convert color to string and apply opacity + if (!color) + return "transparent" return color.toString().replace("#", "#" + opacity) } diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 23fa4322..67972014 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -120,16 +120,18 @@ Singleton { bar: JsonObject { property string position: "top" // Possible values: "top", "bottom" - property bool showActiveWindow: 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 real backgroundOpacity: 1.0 property list monitors: [] + + // Widget configuration for modular bar system + property JsonObject widgets + widgets: JsonObject { + property list left: ["SystemMonitor", "ActiveWindow", "MediaMini"] + property list center: ["Workspace"] + property list right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "Clock", "SidePanelToggle"] + } } // general diff --git a/Commons/WidgetLoader.qml b/Commons/WidgetLoader.qml new file mode 100644 index 00000000..ce444318 --- /dev/null +++ b/Commons/WidgetLoader.qml @@ -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 + } +} diff --git a/Modules/Background/ScreenCorners.qml b/Modules/Background/ScreenCorners.qml index 3dd596a2..0d2e8e07 100644 --- a/Modules/Background/ScreenCorners.qml +++ b/Modules/Background/ScreenCorners.qml @@ -144,6 +144,7 @@ Loader { maskEnabled: true maskSource: maskTexture maskInverted: false + maskSpreadAtMax: 0.75 } mask: Region {} diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 6cc30a71..ae0ad9f4 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -47,7 +47,7 @@ Variants { layer.enabled: true } - // Left + // Left Section - Dynamic Widgets Row { id: leftSection @@ -57,14 +57,25 @@ Variants { anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling - SystemMonitor {} - - ActiveWindow {} - - MediaMini {} + Repeater { + model: Settings.data.bar.widgets.left + delegate: Loader { + id: leftWidgetLoader + 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 { id: centerSection @@ -73,10 +84,25 @@ Variants { anchors.verticalCenter: parent.verticalCenter 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 { id: rightSection @@ -86,44 +112,38 @@ Variants { anchors.verticalCenter: bar.verticalCenter spacing: Style.marginS * scaling - ScreenRecorderIndicator { - anchors.verticalCenter: parent.verticalCenter + Repeater { + 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) + } } } diff --git a/Modules/Bar/WiFi.qml b/Modules/Bar/WiFi.qml deleted file mode 100644 index 6b9b9215..00000000 --- a/Modules/Bar/WiFi.qml +++ /dev/null @@ -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 - } -} diff --git a/Modules/Bar/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml similarity index 98% rename from Modules/Bar/ActiveWindow.qml rename to Modules/Bar/Widgets/ActiveWindow.qml index ec7b42be..25af9604 100644 --- a/Modules/Bar/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -11,7 +11,7 @@ Row { id: root anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling - visible: (Settings.data.bar.showActiveWindow && getTitle() !== "") + visible: getTitle() !== "" property bool showingFullTitle: false property int lastWindowIndex: -1 diff --git a/Modules/Bar/Battery.qml b/Modules/Bar/Widgets/Battery.qml similarity index 98% rename from Modules/Bar/Battery.qml rename to Modules/Bar/Widgets/Battery.qml index ef09f18b..8349da50 100644 --- a/Modules/Bar/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -22,8 +22,6 @@ NPill { // Choose icon based on charge and charging state function batteryIcon() { - if (!show) - return "" if (charging) return "battery_android_bolt" diff --git a/Modules/Bar/Bluetooth.qml b/Modules/Bar/Widgets/Bluetooth.qml similarity index 82% rename from Modules/Bar/Bluetooth.qml rename to Modules/Bar/Widgets/Bluetooth.qml index adc76e0c..bbe35f1e 100644 --- a/Modules/Bar/Bluetooth.qml +++ b/Modules/Bar/Widgets/Bluetooth.qml @@ -10,9 +10,7 @@ import qs.Widgets NIconButton { id: root - readonly property bool bluetoothEnabled: Settings.data.network.bluetoothEnabled sizeMultiplier: 0.8 - visible: bluetoothEnabled colorBg: Color.mSurfaceVariant colorFg: Color.mOnSurface @@ -33,8 +31,4 @@ NIconButton { onClicked: { bluetoothPanel.toggle(screen) } - - BluetoothPanel { - id: bluetoothPanel - } } diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Widgets/Brightness.qml similarity index 96% rename from Modules/Bar/Brightness.qml rename to Modules/Bar/Widgets/Brightness.qml index 39c0fb8b..58c8cc66 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Widgets/Brightness.qml @@ -10,7 +10,7 @@ Item { width: pill.width height: pill.height - visible: Settings.data.bar.showBrightness && firstBrightnessReceived && getMonitor() !== null + visible: getMonitor() !== null // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false diff --git a/Modules/Bar/Clock.qml b/Modules/Bar/Widgets/Clock.qml similarity index 100% rename from Modules/Bar/Clock.qml rename to Modules/Bar/Widgets/Clock.qml diff --git a/Modules/Bar/MediaMini.qml b/Modules/Bar/Widgets/MediaMini.qml similarity index 95% rename from Modules/Bar/MediaMini.qml rename to Modules/Bar/Widgets/MediaMini.qml index a061031f..cc0da68c 100644 --- a/Modules/Bar/MediaMini.qml +++ b/Modules/Bar/Widgets/MediaMini.qml @@ -11,7 +11,8 @@ Row { id: root anchors.verticalCenter: parent.verticalCenter 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() { return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "") @@ -109,14 +110,14 @@ Row { visible: Settings.data.audio.showMiniplayerAlbumArt Rectangle { - width: 16 * scaling - height: 16 * scaling + width: 18 * scaling + height: 18 * scaling radius: width * 0.5 color: Color.transparent antialiasing: true clip: true - NImageRounded { + NImageCircled { id: trackArt visible: MediaService.trackArtUrl.toString() !== "" anchors.fill: parent @@ -126,8 +127,6 @@ Row { fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow" borderWidth: 0 border.color: Color.transparent - imageRadius: width - antialiasing: true } // Fallback icon when no album art available diff --git a/Modules/Bar/NotificationHistory.qml b/Modules/Bar/Widgets/NotificationHistory.qml similarity index 89% rename from Modules/Bar/NotificationHistory.qml rename to Modules/Bar/Widgets/NotificationHistory.qml index ba794b4e..8cf0502b 100644 --- a/Modules/Bar/NotificationHistory.qml +++ b/Modules/Bar/Widgets/NotificationHistory.qml @@ -10,7 +10,6 @@ import qs.Widgets NIconButton { id: root - visible: Settings.data.bar.showNotificationsHistory sizeMultiplier: 0.8 icon: "notifications" tooltipText: "Notification History" diff --git a/Modules/Bar/ScreenRecorderIndicator.qml b/Modules/Bar/Widgets/ScreenRecorderIndicator.qml similarity index 100% rename from Modules/Bar/ScreenRecorderIndicator.qml rename to Modules/Bar/Widgets/ScreenRecorderIndicator.qml diff --git a/Modules/Bar/SidePanelToggle.qml b/Modules/Bar/Widgets/SidePanelToggle.qml similarity index 100% rename from Modules/Bar/SidePanelToggle.qml rename to Modules/Bar/Widgets/SidePanelToggle.qml diff --git a/Modules/Bar/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml similarity index 98% rename from Modules/Bar/SystemMonitor.qml rename to Modules/Bar/Widgets/SystemMonitor.qml index 860adf36..18bb8b6e 100644 --- a/Modules/Bar/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -8,7 +8,6 @@ Row { id: root anchors.verticalCenter: parent.verticalCenter spacing: Style.marginS * scaling - visible: (Settings.data.bar.showSystemInfo) Rectangle { // Let the Rectangle size itself based on its content (the Row) diff --git a/Modules/Bar/Tray.qml b/Modules/Bar/Widgets/Tray.qml similarity index 93% rename from Modules/Bar/Tray.qml rename to Modules/Bar/Widgets/Tray.qml index c6ba8c94..553e56ba 100644 --- a/Modules/Bar/Tray.qml +++ b/Modules/Bar/Widgets/Tray.qml @@ -12,9 +12,8 @@ import qs.Widgets Rectangle { 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 - height: Math.round(Style.capsuleHeight * scaling) radius: Math.round(Style.radiusM * scaling) color: Color.mSurfaceVariant @@ -95,14 +94,14 @@ Rectangle { return } - if (modelData.hasMenu && modelData.menu && trayMenu) { + if (modelData.hasMenu && modelData.menu && trayMenu.item) { trayPanel.open() // 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) - trayMenu.menu = modelData.menu - trayMenu.showAt(parent, menuX, menuY) + trayMenu.item.menu = modelData.menu + trayMenu.item.showAt(parent, menuX, menuY) } else { Logger.log("Tray", "No menu available for", modelData.id, "or trayMenu not set") } @@ -142,7 +141,7 @@ Rectangle { function close() { visible = false - trayMenu.hideMenu() + trayMenu.item.hideMenu() } // Clicking outside of the rectangle to close @@ -151,8 +150,9 @@ Rectangle { onClicked: trayPanel.close() } - TrayMenu { + Loader { id: trayMenu + source: "TrayMenu.qml" } } } diff --git a/Modules/Bar/TrayMenu.qml b/Modules/Bar/Widgets/TrayMenu.qml similarity index 100% rename from Modules/Bar/TrayMenu.qml rename to Modules/Bar/Widgets/TrayMenu.qml diff --git a/Modules/Bar/Volume.qml b/Modules/Bar/Widgets/Volume.qml similarity index 100% rename from Modules/Bar/Volume.qml rename to Modules/Bar/Widgets/Volume.qml diff --git a/Modules/Bar/Widgets/WiFi.qml b/Modules/Bar/Widgets/WiFi.qml new file mode 100644 index 00000000..83b4689e --- /dev/null +++ b/Modules/Bar/Widgets/WiFi.qml @@ -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) + } + } +} diff --git a/Modules/Bar/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml similarity index 100% rename from Modules/Bar/Workspace.qml rename to Modules/Bar/Widgets/Workspace.qml diff --git a/Modules/Bar/BluetoothPanel.qml b/Modules/BluetoothPanel/BluetoothPanel.qml similarity index 100% rename from Modules/Bar/BluetoothPanel.qml rename to Modules/BluetoothPanel/BluetoothPanel.qml diff --git a/Modules/Launcher/Launcher.qml b/Modules/Launcher/Launcher.qml index 39ebc6b3..46792007 100644 --- a/Modules/Launcher/Launcher.qml +++ b/Modules/Launcher/Launcher.qml @@ -225,7 +225,7 @@ NPanel { Layout.bottomMargin: Style.marginM * scaling radius: Style.radiusM * scaling 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) Item { @@ -355,7 +355,7 @@ NPanel { height: 65 * scaling radius: Style.radiusM * scaling property bool isSelected: index === selectedIndex - color: (appCardArea.containsMouse || isSelected) ? Color.mTertiary : Color.mSurface + color: (appCardArea.containsMouse || isSelected) ? Color.mSecondary : Color.mSurface Behavior on color { ColorAnimation { diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index 1b5e7dc0..5f41bd79 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -294,13 +294,14 @@ Loader { // Animated avatar with glow effect or audio visualizer Rectangle { - width: 120 * scaling - height: 120 * scaling + width: 108 * scaling + height: 108 * scaling radius: width * 0.5 color: Color.transparent border.color: Color.mPrimary border.width: Math.max(1, Style.borderL * scaling) anchors.horizontalCenter: parent.horizontalCenter + z: 10 // Circular audio visualizer when music is playing Loader { @@ -464,13 +465,12 @@ Loader { } } - NImageRounded { + NImageCircled { anchors.centerIn: parent width: 100 * scaling height: 100 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" - imageRadius: width * 0.5 } // Hover animation diff --git a/Modules/Notification/NotificationHistoryPanel.qml b/Modules/Notification/NotificationHistoryPanel.qml index 915c1a4b..74eb0755 100644 --- a/Modules/Notification/NotificationHistoryPanel.qml +++ b/Modules/Notification/NotificationHistoryPanel.qml @@ -111,7 +111,7 @@ NPanel { width: notificationList ? notificationList.width : 380 * scaling height: Math.max(80, notificationContent.height + 30) radius: Style.radiusM * scaling - color: notificationMouseArea.containsMouse ? Color.mTertiary : Color.mSurfaceVariant + color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant RowLayout { anchors { diff --git a/Modules/PowerPanel/PowerPanel.qml b/Modules/PowerPanel/PowerPanel.qml index a32211e4..9066444a 100644 --- a/Modules/PowerPanel/PowerPanel.qml +++ b/Modules/PowerPanel/PowerPanel.qml @@ -215,7 +215,7 @@ NPanel { if (pending) return Color.applyOpacity(Color.mPrimary, "20") if (mouseArea.containsMouse) - return Color.mTertiary + return Color.mSecondary return Color.transparent } diff --git a/Modules/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 2df15d94..b5667f97 100644 --- a/Modules/SettingsPanel/SettingsPanel.qml +++ b/Modules/SettingsPanel/SettingsPanel.qml @@ -47,6 +47,7 @@ NPanel { id: barTab Tabs.BarTab {} } + Component { id: audioTab Tabs.AudioTab {} diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml index 405ed1b3..b76c3d95 100644 --- a/Modules/SettingsPanel/Tabs/AboutTab.qml +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -199,7 +199,7 @@ ColumnLayout { width: contributorsGrid.cellWidth - Style.marginL * scaling height: contributorsGrid.cellHeight - Style.marginXS * scaling radius: Style.radiusL * scaling - color: contributorArea.containsMouse ? Color.mTertiary : Color.transparent + color: contributorArea.containsMouse ? Color.mSecondary : Color.transparent RowLayout { anchors.fill: parent @@ -211,14 +211,13 @@ ColumnLayout { Layout.preferredWidth: Style.baseWidgetSize * 2 * scaling Layout.preferredHeight: Style.baseWidgetSize * 2 * scaling - NImageRounded { + NImageCircled { imagePath: modelData.avatar_url || "" anchors.fill: parent anchors.margins: Style.marginXS * scaling fallbackIcon: "person" borderColor: Color.mPrimary borderWidth: Math.max(1, Style.borderM * scaling) - imageRadius: width * 0.5 } } diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index 5ab810cb..9e4bf9dd 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -33,12 +33,6 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true - NText { - text: "Bar Components" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - } ColumnLayout { spacing: Style.marginXXS * scaling @@ -78,70 +72,7 @@ ColumnLayout { } } - NToggle { - 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 { + ColumnLayout { spacing: Style.marginXXS * scaling 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) + }) + } } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 1cb1878a..5c80c315 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -51,13 +51,13 @@ ColumnLayout { spacing: Style.marginL * scaling // Avatar preview - NImageRounded { + NImageCircled { width: 64 * scaling height: 64 * scaling imagePath: Settings.data.general.avatarImage fallbackIcon: "person" borderColor: Color.mPrimary - borderWidth: Math.max(1, Style.borderM) + borderWidth: Math.max(1, Style.borderM * scaling) } NTextInput { diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 3c3d2ce7..aad0839d 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -76,7 +76,7 @@ NBox { // implicitWidth: 120 * scaling // implicitHeight: 30 * scaling 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) radius: Style.radiusM * scaling } @@ -138,7 +138,7 @@ NBox { background: Rectangle { width: popup.width - Style.marginS * scaling * 2 - color: highlighted ? Color.mTertiary : Color.transparent + color: highlighted ? Color.mSecondary : Color.transparent radius: Style.radiusXS * scaling } } @@ -164,7 +164,7 @@ NBox { border.width: Math.max(1, Style.borderS * scaling) clip: true - NImageRounded { + NImageCircled { id: trackArt visible: MediaService.trackArtUrl.toString() !== "" @@ -174,7 +174,6 @@ NBox { fallbackIcon: "music_note" borderColor: Color.mOutline borderWidth: Math.max(1, Style.borderS * scaling) - imageRadius: width * 0.5 } // Fallback icon when no album art available diff --git a/Modules/SidePanel/Cards/ProfileCard.qml b/Modules/SidePanel/Cards/ProfileCard.qml index b9b7df9b..7374159f 100644 --- a/Modules/SidePanel/Cards/ProfileCard.qml +++ b/Modules/SidePanel/Cards/ProfileCard.qml @@ -28,7 +28,7 @@ NBox { anchors.margins: Style.marginM * scaling spacing: Style.marginM * scaling - NImageRounded { + NImageCircled { width: Style.baseWidgetSize * 1.25 * scaling height: Style.baseWidgetSize * 1.25 * scaling imagePath: Settings.data.general.avatarImage diff --git a/Modules/Bar/WiFiPanel.qml b/Modules/WiFiPanel/WiFiPanel.qml similarity index 100% rename from Modules/Bar/WiFiPanel.qml rename to Modules/WiFiPanel/WiFiPanel.qml diff --git a/Services/ColorSchemeService.qml b/Services/ColorSchemeService.qml index 7a0e930a..0847f072 100644 --- a/Services/ColorSchemeService.qml +++ b/Services/ColorSchemeService.qml @@ -78,7 +78,7 @@ Singleton { } } writeColorsToDisk(variant) - Logger.log("ColorScheme", "Applied color scheme:", path) + Logger.log("ColorScheme", "Applying color scheme:", path) } catch (e) { Logger.error("ColorScheme", "Failed to parse scheme JSON:", e) } @@ -90,7 +90,8 @@ Singleton { id: colorsWriter path: colorsJsonFilePath onSaved: { - Logger.log("ColorScheme", "Colors saved") + + // Logger.log("ColorScheme", "Colors saved") } JsonAdapter { id: out @@ -130,6 +131,9 @@ Singleton { out.mOutline = pick(obj, "mOutline", "outline", out.mOutline) out.mShadow = pick(obj, "mShadow", "shadow", out.mShadow) + // Force a rewrite by updating the path + colorsWriter.path = "" + colorsWriter.path = colorsJsonFilePath colorsWriter.writeAdapter() } diff --git a/Shaders/frag/circled_image.frag b/Shaders/frag/circled_image.frag new file mode 100644 index 00000000..308a9c5b --- /dev/null +++ b/Shaders/frag/circled_image.frag @@ -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); +} \ No newline at end of file diff --git a/Shaders/frag/rounded_image.frag b/Shaders/frag/rounded_image.frag new file mode 100644 index 00000000..9d493b21 --- /dev/null +++ b/Shaders/frag/rounded_image.frag @@ -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); +} \ No newline at end of file diff --git a/Shaders/qsb/circled_image.frag.qsb b/Shaders/qsb/circled_image.frag.qsb new file mode 100644 index 00000000..37a99ef0 Binary files /dev/null and b/Shaders/qsb/circled_image.frag.qsb differ diff --git a/Shaders/qsb/rounded_image.frag.qsb b/Shaders/qsb/rounded_image.frag.qsb new file mode 100644 index 00000000..c404fc76 Binary files /dev/null and b/Shaders/qsb/rounded_image.frag.qsb differ diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 29fa6e60..1cb33bb7 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -16,6 +16,7 @@ ColumnLayout { } property string currentKey: '' + property string placeholder: "" signal selected(string key) @@ -50,7 +51,7 @@ ColumnLayout { implicitWidth: Style.baseWidgetSize * 3.75 * scaling implicitHeight: preferredHeight 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) radius: Style.radiusM * scaling } @@ -61,8 +62,10 @@ ColumnLayout { font.pointSize: Style.fontSizeM * scaling verticalAlignment: Text.AlignVCenter elide: Text.ElideRight - text: (combo.currentIndex >= 0 && combo.currentIndex < root.model.count) ? root.model.get( - combo.currentIndex).name : "" + color: (combo.currentIndex >= 0 + && 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 { @@ -112,7 +115,7 @@ ColumnLayout { background: Rectangle { width: combo.width - Style.marginM * scaling * 3 - color: highlighted ? Color.mTertiary : Color.transparent + color: highlighted ? Color.mSecondary : Color.transparent radius: Style.radiusS * scaling } } diff --git a/Widgets/NImageCircled.qml b/Widgets/NImageCircled.qml new file mode 100644 index 00000000..7279c084 --- /dev/null +++ b/Widgets/NImageCircled.qml @@ -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 + } +} diff --git a/Widgets/NImageRounded.qml b/Widgets/NImageRounded.qml index bd6daf08..14ff2636 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -20,6 +20,62 @@ Rectangle { radius: scaledRadius 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 Rectangle { anchors.fill: parent @@ -27,45 +83,7 @@ Rectangle { color: Color.transparent border.color: parent.borderColor border.width: parent.borderWidth + antialiasing: true 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 - } } diff --git a/Widgets/NTextInput.qml b/Widgets/NTextInput.qml index 1d9a138d..bc1a9555 100644 --- a/Widgets/NTextInput.qml +++ b/Widgets/NTextInput.qml @@ -46,7 +46,7 @@ Item { anchors.fill: parent radius: frame.radius 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 } diff --git a/Widgets/NWidgetCard.qml b/Widgets/NWidgetCard.qml new file mode 100644 index 00000000..28084653 --- /dev/null +++ b/Widgets/NWidgetCard.qml @@ -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) + } + } + } + } + } + } + } +} diff --git a/shell.qml b/shell.qml index bd525083..8ce83617 100644 --- a/shell.qml +++ b/shell.qml @@ -16,6 +16,7 @@ import qs.Commons import qs.Modules.Launcher import qs.Modules.Background import qs.Modules.Bar +import qs.Modules.BluetoothPanel import qs.Modules.Calendar import qs.Modules.Dock import qs.Modules.IPC @@ -25,7 +26,7 @@ import qs.Modules.SettingsPanel import qs.Modules.PowerPanel import qs.Modules.SidePanel import qs.Modules.Toast - +import qs.Modules.WiFiPanel import qs.Services import qs.Widgets @@ -70,6 +71,14 @@ ShellRoot { id: powerPanel } + WiFiPanel { + id: wifiPanel + } + + BluetoothPanel { + id: bluetoothPanel + } + ToastManager {} IPCManager {}