diff --git a/Assets/ColorScheme/Rosepine.json b/Assets/ColorScheme/Rosepine.json index 891b544..37e9b3a 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 221716f..dc1d554 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 23fa432..6797201 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 0000000..ce44431 --- /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/Bar/Bar.qml b/Modules/Bar/Bar.qml index 6cc30a7..ae0ad9f 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 da749ff..0000000 --- a/Modules/Bar/WiFi.qml +++ /dev/null @@ -1,44 +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: { - if (NetworkService.ethernet) return "lan" - 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_find" - } - 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 ec7b42b..25af960 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 ef09f18..8349da5 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 adc76e0..bbe35f1 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 39c0fb8..58c8cc6 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 a061031..cc0da68 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 ba794b4..8cf0502 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 860adf3..18bb8b6 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 c6ba8c9..553e56b 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 99% rename from Modules/Bar/TrayMenu.qml rename to Modules/Bar/Widgets/TrayMenu.qml index 699d8d1..a3a24fd 100644 --- a/Modules/Bar/TrayMenu.qml +++ b/Modules/Bar/Widgets/TrayMenu.qml @@ -134,7 +134,7 @@ PopupWindow { Rectangle { anchors.fill: parent - color: mouseArea.containsMouse ? Color.mSecondary : Color.transparent + color: mouseArea.containsMouse ? Color.mTertiary : Color.transparent radius: Style.radiusS * scaling visible: !(modelData?.isSeparator ?? false) 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 0000000..193eae7 --- /dev/null +++ b/Modules/Bar/Widgets/WiFi.qml @@ -0,0 +1,55 @@ +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 { + if (NetworkService.ethernet) return "lan" + 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_find" + } catch (error) { + Logger.error("WiFi", "Error getting icon:", error) + return "signal_wifi_bad" + } + } + 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 99% rename from Modules/Bar/BluetoothPanel.qml rename to Modules/BluetoothPanel/BluetoothPanel.qml index b583897..eeb4c7f 100644 --- a/Modules/Bar/BluetoothPanel.qml +++ b/Modules/BluetoothPanel/BluetoothPanel.qml @@ -118,7 +118,7 @@ NPanel { radius: Style.radiusM * scaling color: { if (availableDeviceArea.containsMouse && !isBusy) - return Color.mSecondary + return Color.mTertiary if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) return Color.mPrimary diff --git a/Modules/LockScreen/LockScreen.qml b/Modules/LockScreen/LockScreen.qml index b980fa3..5f41bd7 100644 --- a/Modules/LockScreen/LockScreen.qml +++ b/Modules/LockScreen/LockScreen.qml @@ -465,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/SettingsPanel/SettingsPanel.qml b/Modules/SettingsPanel/SettingsPanel.qml index 0224d89..b5667f9 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 {} @@ -202,7 +203,7 @@ NPanel { width: parent.width height: 32 * scaling radius: Style.radiusS * scaling - color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mSecondary : Color.transparent) + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mTertiary : Color.transparent) readonly property bool selected: index === currentTabIndex property bool hovering: false property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnTertiary : Color.mOnSurface) @@ -265,7 +266,7 @@ NPanel { // Tab label on the main right side NText { text: root.tabsModel[currentTabIndex].label - font.pointSize: Style.fontSizeXL * scaling + font.pointSize: Style.fontSizeL * scaling font.weight: Style.fontWeightBold color: Color.mPrimary Layout.fillWidth: true diff --git a/Modules/SettingsPanel/Tabs/AboutTab.qml b/Modules/SettingsPanel/Tabs/AboutTab.qml index a7b7b3d..b76c3d9 100644 --- a/Modules/SettingsPanel/Tabs/AboutTab.qml +++ b/Modules/SettingsPanel/Tabs/AboutTab.qml @@ -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.borderL * scaling) - imageRadius: width * 0.5 + borderWidth: Math.max(1, Style.borderM * scaling) } } diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index fe5cb42..9e4bf9d 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -33,6 +33,7 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true + ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true @@ -71,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 @@ -172,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/BrightnessTab.qml b/Modules/SettingsPanel/Tabs/BrightnessTab.qml index e4758c3..3b61e7b 100644 --- a/Modules/SettingsPanel/Tabs/BrightnessTab.qml +++ b/Modules/SettingsPanel/Tabs/BrightnessTab.qml @@ -6,33 +6,34 @@ import qs.Commons import qs.Services import qs.Widgets -ColumnLayout { - id: root - spacing: 0 - +Item { + property real scaling: 1 readonly property string tabIcon: "brightness_6" readonly property string tabLabel: "Brightness" + Layout.fillWidth: true + Layout.fillHeight: true ScrollView { - id: scrollView - Layout.fillWidth: true - Layout.fillHeight: true - padding: Style.marginM * scaling + anchors.fill: parent clip: true - - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.vertical.policy: ScrollBar.AsNeeded + ScrollBar.horizontal.policy: ScrollBar.AsNeeded + contentWidth: parent.width ColumnLayout { - width: scrollView.availableWidth - spacing: 0 - + width: parent.width ColumnLayout { - - width: scrollView.availableWidth spacing: Style.marginL * scaling + Layout.margins: Style.marginL * scaling Layout.fillWidth: true + NText { + text: "Brightness Settings" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + // Bar Visibility Section NToggle { label: "Show Bar Icon" diff --git a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml index 80da36a..c7343b6 100644 --- a/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml +++ b/Modules/SettingsPanel/Tabs/ColorSchemeTab.qml @@ -163,28 +163,22 @@ ColumnLayout { Layout.bottomMargin: Style.marginL * scaling } - NText { - text: "Predefined Color Schemes" - font.pointSize: Style.fontSizeXXL * scaling - font.weight: Style.fontWeightBold - color: Color.mOnSurface - } - ColumnLayout { spacing: Style.marginXXS * scaling Layout.fillWidth: true - // NText { - // text: "Predefined Color Schemes" - // font.pointSize: Style.fontSizeL * scaling - // font.weight: Style.fontWeightBold - // color: Color.mOnSurface - // Layout.fillWidth: true - // } + NText { + text: "Predefined Color Schemes" + font.pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + NText { text: "These color schemes only apply when 'Use Matugen' is disabled. When enabled, Matugen will generate colors based on your wallpaper instead. You can toggle between light and dark themes when using Matugen." font.pointSize: Style.fontSizeXS * scaling - color: Color.mOnSurfaceVariant + color: Color.mOnSurface Layout.fillWidth: true wrapMode: Text.WordWrap } diff --git a/Modules/SettingsPanel/Tabs/GeneralTab.qml b/Modules/SettingsPanel/Tabs/GeneralTab.qml index 19da93f..5c80c31 100644 --- a/Modules/SettingsPanel/Tabs/GeneralTab.qml +++ b/Modules/SettingsPanel/Tabs/GeneralTab.qml @@ -33,6 +33,13 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true + NText { + text: "General Settings" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + // Profile section ColumnLayout { spacing: Style.marginS * scaling @@ -44,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/SettingsPanel/Tabs/LauncherTab.qml b/Modules/SettingsPanel/Tabs/LauncherTab.qml index 5d9d6aa..ae10ade 100644 --- a/Modules/SettingsPanel/Tabs/LauncherTab.qml +++ b/Modules/SettingsPanel/Tabs/LauncherTab.qml @@ -34,7 +34,7 @@ ColumnLayout { Layout.fillWidth: true NText { - text: "Launcer Options" + text: "Launcher" font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold color: Color.mOnSurface @@ -56,7 +56,7 @@ ColumnLayout { } NText { - text: "Launcer Anchoring" + text: "Launcher Position" font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold color: Color.mOnSurface diff --git a/Modules/SettingsPanel/Tabs/NetworkTab.qml b/Modules/SettingsPanel/Tabs/NetworkTab.qml index f621f79..7029ecb 100644 --- a/Modules/SettingsPanel/Tabs/NetworkTab.qml +++ b/Modules/SettingsPanel/Tabs/NetworkTab.qml @@ -34,6 +34,13 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true + NText { + text: "Interfaces" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + NToggle { label: "WiFi Enabled" description: "Enable WiFi connectivity." diff --git a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml index a02b3a3..836ca6c 100644 --- a/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml +++ b/Modules/SettingsPanel/Tabs/ScreenRecorderTab.qml @@ -33,6 +33,14 @@ ColumnLayout { spacing: Style.marginXS * scaling Layout.fillWidth: true + NText { + text: "Recordings" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginS * scaling + } + // Output Directory ColumnLayout { spacing: Style.marginS * scaling diff --git a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml index f3783a5..4e7cd52 100644 --- a/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml +++ b/Modules/SettingsPanel/Tabs/TimeWeatherTab.qml @@ -33,6 +33,14 @@ ColumnLayout { spacing: Style.marginXS * scaling Layout.fillWidth: true + NText { + text: "Location" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginS * scaling + } + // Location section ColumnLayout { spacing: Style.marginM * scaling diff --git a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml index b3112e8..79b7fa2 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperSelectorTab.qml @@ -31,7 +31,7 @@ Item { // Current wallpaper display NText { text: "Current Wallpaper" - font.pointSize: Style.fontSizeL * scaling + font.pointSize: Style.fontSizeXXL * scaling font.weight: Style.fontWeightBold color: Color.mOnSurface } diff --git a/Modules/SettingsPanel/Tabs/WallpaperTab.qml b/Modules/SettingsPanel/Tabs/WallpaperTab.qml index 8fe156d..f8da98d 100644 --- a/Modules/SettingsPanel/Tabs/WallpaperTab.qml +++ b/Modules/SettingsPanel/Tabs/WallpaperTab.qml @@ -34,6 +34,14 @@ ColumnLayout { spacing: Style.marginL * scaling Layout.fillWidth: true + NText { + text: "Directory" + font.pointSize: Style.fontSizeXXL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.bottomMargin: Style.marginS * scaling + } + // Wallpaper Settings Category ColumnLayout { spacing: Style.marginS * scaling diff --git a/Modules/SidePanel/Cards/MediaCard.qml b/Modules/SidePanel/Cards/MediaCard.qml index 7809a5f..aad0839 100644 --- a/Modules/SidePanel/Cards/MediaCard.qml +++ b/Modules/SidePanel/Cards/MediaCard.qml @@ -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 b9b7df9..7374159 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 99% rename from Modules/Bar/WiFiPanel.qml rename to Modules/WiFiPanel/WiFiPanel.qml index ce5acef..8d88cd9 100644 --- a/Modules/Bar/WiFiPanel.qml +++ b/Modules/WiFiPanel/WiFiPanel.qml @@ -147,7 +147,7 @@ NPanel { Layout.fillWidth: true Layout.preferredHeight: Style.baseWidgetSize * 1.5 * scaling radius: Style.radiusS * scaling - color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mSecondary : Color.transparent) + color: modelData.connected ? Color.mPrimary : (networkMouseArea.containsMouse ? Color.mTertiary : Color.transparent) RowLayout { anchors.fill: parent diff --git a/Shaders/frag/circled_image.frag b/Shaders/frag/circled_image.frag new file mode 100644 index 0000000..308a9c5 --- /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 0000000..9d493b2 --- /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 0000000..37a99ef 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 0000000..c404fc7 Binary files /dev/null and b/Shaders/qsb/rounded_image.frag.qsb differ diff --git a/Widgets/NComboBox.qml b/Widgets/NComboBox.qml index 9ec2aaf..1cb33bb 100644 --- a/Widgets/NComboBox.qml +++ b/Widgets/NComboBox.qml @@ -16,6 +16,7 @@ ColumnLayout { } property string currentKey: '' + property string placeholder: "" signal selected(string key) @@ -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 { diff --git a/Widgets/NImageCircled.qml b/Widgets/NImageCircled.qml new file mode 100644 index 0000000..7279c08 --- /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 c7fbde9..14ff263 100644 --- a/Widgets/NImageRounded.qml +++ b/Widgets/NImageRounded.qml @@ -23,13 +23,12 @@ Rectangle { Rectangle { color: Color.transparent anchors.fill: parent - anchors.margins: borderWidth Image { id: img anchors.fill: parent source: imagePath - visible: false + visible: false // Hide since we're using it as shader source mipmap: true smooth: true asynchronous: true @@ -37,24 +36,33 @@ Rectangle { fillMode: Image.PreserveAspectCrop } - MultiEffect { + ShaderEffect { anchors.fill: parent - source: img - maskEnabled: true - maskSource: mask - maskSpreadAtMax: 0.75 - visible: imagePath !== "" - } - Item { - id: mask - anchors.fill: parent - layer.enabled: true - visible: false + 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 - radius: scaledRadius - antialiasing: true + color: "transparent" + z: -1 } } diff --git a/Widgets/NWidgetCard.qml b/Widgets/NWidgetCard.qml new file mode 100644 index 0000000..2808465 --- /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 bd52508..8ce8361 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 {}