diff --git a/Common/SettingsData.qml b/Common/SettingsData.qml index 159c727b..2cba9e78 100644 --- a/Common/SettingsData.qml +++ b/Common/SettingsData.qml @@ -45,6 +45,16 @@ Singleton { property bool controlCenterShowNetworkIcon: true property bool controlCenterShowBluetoothIcon: true property bool controlCenterShowAudioIcon: true + property var controlCenterWidgets: [ + {"id": "volumeSlider", "enabled": true, "width": 50}, + {"id": "brightnessSlider", "enabled": true, "width": 50}, + {"id": "wifi", "enabled": true, "width": 50}, + {"id": "bluetooth", "enabled": true, "width": 50}, + {"id": "audioOutput", "enabled": true, "width": 50}, + {"id": "audioInput", "enabled": true, "width": 50}, + {"id": "nightMode", "enabled": true, "width": 50}, + {"id": "darkMode", "enabled": true, "width": 50} + ] property bool showWorkspaceIndex: false property bool showWorkspacePadding: false property bool showWorkspaceApps: false @@ -226,6 +236,16 @@ Singleton { controlCenterShowNetworkIcon = settings.controlCenterShowNetworkIcon !== undefined ? settings.controlCenterShowNetworkIcon : true controlCenterShowBluetoothIcon = settings.controlCenterShowBluetoothIcon !== undefined ? settings.controlCenterShowBluetoothIcon : true controlCenterShowAudioIcon = settings.controlCenterShowAudioIcon !== undefined ? settings.controlCenterShowAudioIcon : true + controlCenterWidgets = settings.controlCenterWidgets !== undefined ? settings.controlCenterWidgets : [ + {"id": "volumeSlider", "enabled": true, "width": 50}, + {"id": "brightnessSlider", "enabled": true, "width": 50}, + {"id": "wifi", "enabled": true, "width": 50}, + {"id": "bluetooth", "enabled": true, "width": 50}, + {"id": "audioOutput", "enabled": true, "width": 50}, + {"id": "audioInput", "enabled": true, "width": 50}, + {"id": "nightMode", "enabled": true, "width": 50}, + {"id": "darkMode", "enabled": true, "width": 50} + ] showWorkspaceIndex = settings.showWorkspaceIndex !== undefined ? settings.showWorkspaceIndex : false showWorkspacePadding = settings.showWorkspacePadding !== undefined ? settings.showWorkspacePadding : false showWorkspaceApps = settings.showWorkspaceApps !== undefined ? settings.showWorkspaceApps : false @@ -354,6 +374,7 @@ Singleton { "controlCenterShowNetworkIcon": controlCenterShowNetworkIcon, "controlCenterShowBluetoothIcon": controlCenterShowBluetoothIcon, "controlCenterShowAudioIcon": controlCenterShowAudioIcon, + "controlCenterWidgets": controlCenterWidgets, "showWorkspaceIndex": showWorkspaceIndex, "showWorkspacePadding": showWorkspacePadding, "showWorkspaceApps": showWorkspaceApps, @@ -681,6 +702,10 @@ Singleton { controlCenterShowAudioIcon = enabled saveSettings() } + function setControlCenterWidgets(widgets) { + controlCenterWidgets = widgets + saveSettings() + } function setTopBarWidgetOrder(order) { topBarWidgetOrder = order diff --git a/Modules/ControlCenter/Components/ActionTile.qml b/Modules/ControlCenter/Components/ActionTile.qml new file mode 100644 index 00000000..e8cbc12f --- /dev/null +++ b/Modules/ControlCenter/Components/ActionTile.qml @@ -0,0 +1,121 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string iconName: "" + property string text: "" + property string secondaryText: "" + property bool isActive: false + property bool enabled: true + property int widgetIndex: 0 + property var widgetData: null + property bool editMode: false + + signal clicked() + + width: parent ? parent.width : 200 + height: 60 + radius: { + if (Theme.cornerRadius === 0) return 0 + return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4 + } + + readonly property color _tileBgActive: Theme.primary + readonly property color _tileBgInactive: + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, + Theme.getContentBackgroundAlpha() * 0.60) + readonly property color _tileRingActive: + Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22) + + color: isActive ? _tileBgActive : _tileBgInactive + border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: isActive ? 1 : 1 + opacity: enabled ? 1.0 : 0.6 + + function hoverTint(base) { + const factor = 1.2 + return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor) + } + + Rectangle { + anchors.fill: parent + radius: Theme.cornerRadius + color: mouseArea.containsMouse ? hoverTint(root.color) : "transparent" + opacity: mouseArea.containsMouse ? 0.08 : 0.0 + + Behavior on opacity { + NumberAnimation { duration: Theme.shortDuration } + } + } + + Row { + anchors.fill: parent + anchors.leftMargin: Theme.spacingL + 2 + anchors.rightMargin: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: root.iconName + size: Theme.iconSize + color: isActive ? Theme.primaryContainer : Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Item { + width: parent.width - Theme.iconSize - parent.spacing + height: parent.height + + Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Typography { + width: parent.width + text: root.text + style: Typography.Style.Body + color: isActive ? Theme.primaryContainer : Theme.surfaceText + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + + Typography { + width: parent.width + text: root.secondaryText + style: Typography.Style.Caption + color: isActive ? Theme.primaryContainer : Theme.surfaceVariantText + visible: text.length > 0 + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled + onClicked: root.clicked() + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on radius { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Components/DetailHost.qml b/Modules/ControlCenter/Components/DetailHost.qml new file mode 100644 index 00000000..cfcc32b6 --- /dev/null +++ b/Modules/ControlCenter/Components/DetailHost.qml @@ -0,0 +1,52 @@ +import QtQuick +import qs.Common +import qs.Modules.ControlCenter.Details + +Item { + id: root + + property string expandedSection: "" + + Loader { + width: parent.width + height: 250 + y: Theme.spacingS + active: parent.height > 0 + sourceComponent: { + switch (root.expandedSection) { + case "network": + case "wifi": return networkDetailComponent + case "bluetooth": return bluetoothDetailComponent + case "audioOutput": return audioOutputDetailComponent + case "audioInput": return audioInputDetailComponent + case "battery": return batteryDetailComponent + default: return null + } + } + } + + Component { + id: networkDetailComponent + NetworkDetail {} + } + + Component { + id: bluetoothDetailComponent + BluetoothDetail {} + } + + Component { + id: audioOutputDetailComponent + AudioOutputDetail {} + } + + Component { + id: audioInputDetailComponent + AudioInputDetail {} + } + + Component { + id: batteryDetailComponent + BatteryDetail {} + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Components/EditControls.qml b/Modules/ControlCenter/Components/EditControls.qml new file mode 100644 index 00000000..8c79c02f --- /dev/null +++ b/Modules/ControlCenter/Components/EditControls.qml @@ -0,0 +1,236 @@ +import QtQuick +import QtQuick.Controls +import qs.Common +import qs.Widgets + +Row { + id: root + + property var availableWidgets: [] + + signal addWidget(string widgetId) + signal resetToDefault() + signal clearAll() + + height: 48 + spacing: Theme.spacingS + + onAddWidget: addWidgetPopup.close() + + Popup { + id: addWidgetPopup + anchors.centerIn: parent + width: 400 + height: 300 + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: Theme.surfaceContainer + border.color: Theme.primarySelected + border.width: 1 + radius: Theme.cornerRadius + } + + contentItem: Item { + anchors.fill: parent + anchors.margins: Theme.spacingL + + Row { + id: headerRow + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + spacing: Theme.spacingM + + DankIcon { + name: "add_circle" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Typography { + text: "Add Widget" + style: Typography.Style.Subtitle + color: Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + DankListView { + anchors.top: headerRow.bottom + anchors.topMargin: Theme.spacingM + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + spacing: Theme.spacingS + model: root.availableWidgets + + delegate: Rectangle { + width: 400 - Theme.spacingL * 2 + height: 50 + radius: Theme.cornerRadius + color: widgetMouseArea.containsMouse ? Theme.primaryHover : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) + border.width: 1 + + Row { + anchors.fill: parent + anchors.margins: Theme.spacingM + spacing: Theme.spacingM + + DankIcon { + name: modelData.icon + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + width: 400 - Theme.spacingL * 2 - Theme.iconSize - Theme.spacingM * 3 - Theme.iconSize + + Typography { + text: modelData.text + style: Typography.Style.Body + color: Theme.surfaceText + elide: Text.ElideRight + width: parent.width + } + + Typography { + text: modelData.description + style: Typography.Style.Caption + color: Theme.outline + elide: Text.ElideRight + width: parent.width + } + } + + DankIcon { + name: "add" + size: Theme.iconSize - 4 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: widgetMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.addWidget(modelData.id) + } + } + } + } + } + } + + Rectangle { + width: (parent.width - Theme.spacingS * 2) / 3 + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) + border.color: Theme.primary + border.width: 1 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "add" + size: Theme.iconSize - 2 + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Typography { + text: "Add Widget" + style: Typography.Style.Button + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: addWidgetPopup.open() + } + } + + Rectangle { + width: (parent.width - Theme.spacingS * 2) / 3 + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) + border.color: Theme.warning + border.width: 1 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "settings_backup_restore" + size: Theme.iconSize - 2 + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + + Typography { + text: "Defaults" + style: Typography.Style.Button + color: Theme.warning + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.resetToDefault() + } + } + + Rectangle { + width: (parent.width - Theme.spacingS * 2) / 3 + height: 48 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + border.color: Theme.error + border.width: 1 + + Row { + anchors.centerIn: parent + spacing: Theme.spacingS + + DankIcon { + name: "clear_all" + size: Theme.iconSize - 2 + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + + Typography { + text: "Reset" + style: Typography.Style.Button + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: root.clearAll() + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Components/EditModeOverlay.qml b/Modules/ControlCenter/Components/EditModeOverlay.qml new file mode 100644 index 00000000..bf1c7b4d --- /dev/null +++ b/Modules/ControlCenter/Components/EditModeOverlay.qml @@ -0,0 +1,241 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Item { + id: root + + property bool editMode: false + property var widgetData: null + property int widgetIndex: -1 + property bool showSizeControls: true + property bool isSlider: false + + signal removeWidget(int index) + signal toggleWidgetSize(int index) + signal moveWidget(int fromIndex, int toIndex) + + // Delete button in top-right + Rectangle { + width: 16 + height: 16 + radius: 8 + color: Theme.error + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: -4 + visible: editMode + z: 10 + + DankIcon { + anchors.centerIn: parent + name: "close" + size: 12 + color: Theme.primaryText + } + + MouseArea { + anchors.fill: parent + onClicked: root.removeWidget(widgetIndex) + } + } + + // Size control buttons in bottom-right + Row { + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: -8 + spacing: 4 + visible: editMode && showSizeControls + z: 10 + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: (widgetData?.width || 50) === 25 ? Theme.primary : Theme.primaryContainer + border.color: Theme.primary + border.width: 1 + visible: !isSlider + + StyledText { + anchors.centerIn: parent + text: "25" + font.pixelSize: 10 + font.weight: Font.Medium + color: (widgetData?.width || 50) === 25 ? Theme.primaryText : Theme.primary + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + var widgets = SettingsData.controlCenterWidgets.slice() + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + widgets[widgetIndex].width = 25 + SettingsData.setControlCenterWidgets(widgets) + } + } + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: (widgetData?.width || 50) === 50 ? Theme.primary : Theme.primaryContainer + border.color: Theme.primary + border.width: 1 + + StyledText { + anchors.centerIn: parent + text: "50" + font.pixelSize: 10 + font.weight: Font.Medium + color: (widgetData?.width || 50) === 50 ? Theme.primaryText : Theme.primary + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + var widgets = SettingsData.controlCenterWidgets.slice() + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + widgets[widgetIndex].width = 50 + SettingsData.setControlCenterWidgets(widgets) + } + } + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: (widgetData?.width || 50) === 75 ? Theme.primary : Theme.primaryContainer + border.color: Theme.primary + border.width: 1 + visible: !isSlider + + StyledText { + anchors.centerIn: parent + text: "75" + font.pixelSize: 10 + font.weight: Font.Medium + color: (widgetData?.width || 50) === 75 ? Theme.primaryText : Theme.primary + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + var widgets = SettingsData.controlCenterWidgets.slice() + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + widgets[widgetIndex].width = 75 + SettingsData.setControlCenterWidgets(widgets) + } + } + } + } + + Rectangle { + width: 24 + height: 24 + radius: 12 + color: (widgetData?.width || 50) === 100 ? Theme.primary : Theme.primaryContainer + border.color: Theme.primary + border.width: 1 + + StyledText { + anchors.centerIn: parent + text: "100" + font.pixelSize: 9 + font.weight: Font.Medium + color: (widgetData?.width || 50) === 100 ? Theme.primaryText : Theme.primary + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + var widgets = SettingsData.controlCenterWidgets.slice() + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + widgets[widgetIndex].width = 100 + SettingsData.setControlCenterWidgets(widgets) + } + } + } + } + } + + // Arrow buttons for reordering in top-left + Row { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: 4 + spacing: 2 + visible: editMode + z: 20 + + Rectangle { + width: 16 + height: 16 + radius: 8 + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + + DankIcon { + anchors.centerIn: parent + name: "keyboard_arrow_left" + size: 12 + color: Theme.surfaceText + } + + MouseArea { + anchors.fill: parent + enabled: widgetIndex > 0 + opacity: enabled ? 1.0 : 0.5 + onClicked: root.moveWidget(widgetIndex, widgetIndex - 1) + } + } + + Rectangle { + width: 16 + height: 16 + radius: 8 + color: Theme.surfaceContainer + border.color: Theme.outline + border.width: 1 + + DankIcon { + anchors.centerIn: parent + name: "keyboard_arrow_right" + size: 12 + color: Theme.surfaceText + } + + MouseArea { + anchors.fill: parent + enabled: widgetIndex < ((SettingsData.controlCenterWidgets?.length ?? 0) - 1) + opacity: enabled ? 1.0 : 0.5 + onClicked: root.moveWidget(widgetIndex, widgetIndex + 1) + } + } + } + + // Border highlight + Rectangle { + anchors.fill: parent + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) + radius: Theme.cornerRadius + border.color: Theme.primary + border.width: editMode ? 1 : 0 + visible: editMode + z: -1 + + Behavior on border.width { + NumberAnimation { duration: Theme.shortDuration } + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Components/HeaderPane.qml b/Modules/ControlCenter/Components/HeaderPane.qml new file mode 100644 index 00000000..5507bd50 --- /dev/null +++ b/Modules/ControlCenter/Components/HeaderPane.qml @@ -0,0 +1,122 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool powerOptionsExpanded: false + property bool editMode: false + + signal powerActionRequested(string action, string title, string message) + signal lockRequested() + signal editModeToggled() + + implicitHeight: 90 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + Theme.getContentBackgroundAlpha() * 0.4) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.08) + border.width: 1 + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Theme.spacingL + anchors.rightMargin: Theme.spacingL + spacing: Theme.spacingM + + DankCircularImage { + id: avatarContainer + + width: 64 + height: 64 + imageSource: { + if (PortalService.profileImage === "") + return "" + + if (PortalService.profileImage.startsWith("/")) + return "file://" + PortalService.profileImage + + return PortalService.profileImage + } + fallbackIcon: "person" + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 2 + + Typography { + text: UserInfoService.fullName + || UserInfoService.username || "User" + style: Typography.Style.Subtitle + color: Theme.surfaceText + } + + Typography { + text: (UserInfoService.uptime || "Unknown") + style: Typography.Style.Caption + color: Theme.surfaceVariantText + } + } + } + + Row { + id: actionButtonsRow + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: Theme.spacingXS + anchors.topMargin: Theme.spacingXS + spacing: Theme.spacingXS + + DankActionButton { + buttonSize: 36 + iconName: "lock" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + backgroundColor: "transparent" + onClicked: { + root.lockRequested() + } + } + + DankActionButton { + buttonSize: 36 + iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new" + iconSize: Theme.iconSize - 4 + iconColor: root.powerOptionsExpanded ? Theme.primary : Theme.surfaceText + backgroundColor: "transparent" + onClicked: { + root.powerOptionsExpanded = !root.powerOptionsExpanded + } + } + + DankActionButton { + buttonSize: 36 + iconName: "settings" + iconSize: Theme.iconSize - 4 + iconColor: Theme.surfaceText + backgroundColor: "transparent" + onClicked: { + settingsModal.show() + } + } + } + + DankActionButton { + buttonSize: 24 + iconName: editMode ? "done" : "edit" + iconSize: 14 + iconColor: editMode ? Theme.primary : Theme.outline + backgroundColor: "transparent" + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: Theme.spacingXS + onClicked: root.editModeToggled() + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Components/PowerButton.qml b/Modules/ControlCenter/Components/PowerButton.qml new file mode 100644 index 00000000..a0c83fb2 --- /dev/null +++ b/Modules/ControlCenter/Components/PowerButton.qml @@ -0,0 +1,52 @@ +import QtQuick +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string iconName: "" + property string text: "" + + signal pressed() + + height: 34 + radius: Theme.cornerRadius + color: mouseArea.containsMouse ? Qt.rgba( + Theme.primary.r, + Theme.primary.g, + Theme.primary.b, + 0.12) : Qt.rgba( + Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + 0.5) + + Row { + anchors.centerIn: parent + spacing: Theme.spacingXS + + DankIcon { + name: root.iconName + size: Theme.fontSizeSmall + color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + + Typography { + text: root.text + style: Typography.Style.Button + color: mouseArea.containsMouse ? Theme.primary : Theme.surfaceText + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + id: mouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onPressed: root.pressed() + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Components/PowerOptionsPane.qml b/Modules/ControlCenter/Components/PowerOptionsPane.qml new file mode 100644 index 00000000..1b6f43ab --- /dev/null +++ b/Modules/ControlCenter/Components/PowerOptionsPane.qml @@ -0,0 +1,73 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +Item { + id: root + + property bool expanded: false + + signal powerActionRequested(string action, string title, string message) + + implicitHeight: expanded ? 60 : 0 + height: implicitHeight + clip: true + + Rectangle { + width: parent.width + height: 60 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, + Theme.surfaceVariant.g, + Theme.surfaceVariant.b, + Theme.getContentBackgroundAlpha() * 0.4) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, + Theme.outline.b, 0.08) + border.width: root.expanded ? 1 : 0 + opacity: root.expanded ? 1 : 0 + clip: true + + Row { + anchors.centerIn: parent + spacing: SessionService.hibernateSupported ? Theme.spacingS : Theme.spacingL + visible: root.expanded + + PowerButton { + width: SessionService.hibernateSupported ? 85 : 100 + iconName: "logout" + text: "Logout" + onPressed: root.powerActionRequested("logout", "Logout", "Are you sure you want to logout?") + } + + PowerButton { + width: SessionService.hibernateSupported ? 85 : 100 + iconName: "restart_alt" + text: "Restart" + onPressed: root.powerActionRequested("reboot", "Restart", "Are you sure you want to restart?") + } + + PowerButton { + width: SessionService.hibernateSupported ? 85 : 100 + iconName: "bedtime" + text: "Suspend" + onPressed: root.powerActionRequested("suspend", "Suspend", "Are you sure you want to suspend?") + } + + PowerButton { + width: SessionService.hibernateSupported ? 85 : 100 + iconName: "ac_unit" + text: "Hibernate" + visible: SessionService.hibernateSupported + onPressed: root.powerActionRequested("hibernate", "Hibernate", "Are you sure you want to hibernate?") + } + + PowerButton { + width: SessionService.hibernateSupported ? 85 : 100 + iconName: "power_settings_new" + text: "Shutdown" + onPressed: root.powerActionRequested("poweroff", "Shutdown", "Are you sure you want to shutdown?") + } + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Components/Typography.qml b/Modules/ControlCenter/Components/Typography.qml new file mode 100644 index 00000000..c2c739b9 --- /dev/null +++ b/Modules/ControlCenter/Components/Typography.qml @@ -0,0 +1,46 @@ +import QtQuick +import qs.Common +import qs.Widgets + +StyledText { + id: root + + enum Style { + Title, + Subtitle, + Body, + Caption, + Button + } + + property int style: Typography.Style.Body + + font.pixelSize: { + switch (style) { + case Typography.Style.Title: return Theme.fontSizeXLarge + case Typography.Style.Subtitle: return Theme.fontSizeLarge + case Typography.Style.Body: return Theme.fontSizeMedium + case Typography.Style.Caption: return Theme.fontSizeSmall + case Typography.Style.Button: return Theme.fontSizeSmall + default: return Theme.fontSizeMedium + } + } + + font.weight: { + switch (style) { + case Typography.Style.Title: return Font.Bold + case Typography.Style.Subtitle: return Font.Medium + case Typography.Style.Body: return Font.Normal + case Typography.Style.Caption: return Font.Normal + case Typography.Style.Button: return Font.Medium + default: return Font.Normal + } + } + + color: { + switch (style) { + case Typography.Style.Caption: return Theme.surfaceVariantText + default: return Theme.surfaceText + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Components/WidgetGrid.qml b/Modules/ControlCenter/Components/WidgetGrid.qml new file mode 100644 index 00000000..73ba7439 --- /dev/null +++ b/Modules/ControlCenter/Components/WidgetGrid.qml @@ -0,0 +1,679 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Modules.ControlCenter.Widgets +import qs.Modules.ControlCenter.Components +import "../utils/layout.js" as LayoutUtils + +Column { + id: root + + property bool editMode: false + property string expandedSection: "" + property int expandedWidgetIndex: -1 + property var model: null + + signal expandClicked(var widgetData, int globalIndex) + signal removeWidget(int index) + signal moveWidget(int fromIndex, int toIndex) + signal toggleWidgetSize(int index) + + spacing: editMode ? Theme.spacingL : Theme.spacingS + + property var currentRowWidgets: [] + property real currentRowWidth: 0 + property int expandedRowIndex: -1 + + function calculateRowsAndWidgets() { + return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex) + } + + Repeater { + model: { + const result = root.calculateRowsAndWidgets() + root.expandedRowIndex = result.expandedRowIndex + return result.rows + } + + Column { + width: root.width + spacing: 0 + property int rowIndex: index + property var rowWidgets: modelData + property bool isSliderOnlyRow: { + const widgets = rowWidgets || [] + if (widgets.length === 0) return false + return widgets.every(w => w.id === "volumeSlider" || w.id === "brightnessSlider" || w.id === "inputVolumeSlider") + } + topPadding: isSliderOnlyRow ? (root.editMode ? 4 : -12) : 0 + bottomPadding: isSliderOnlyRow ? (root.editMode ? 4 : -12) : 0 + + Flow { + width: parent.width + spacing: Theme.spacingS + + Repeater { + model: rowWidgets || [] + + Item { + property var widgetData: modelData + property int globalWidgetIndex: { + const widgets = SettingsData.controlCenterWidgets || [] + for (var i = 0; i < widgets.length; i++) { + if (widgets[i].id === modelData.id) { + return i + } + } + return -1 + } + property int widgetWidth: modelData.width || 50 + width: { + const baseWidth = root.width + const spacing = Theme.spacingS + if (widgetWidth <= 25) { + return (baseWidth - spacing * 3) / 4 + } else if (widgetWidth <= 50) { + return (baseWidth - spacing) / 2 + } else if (widgetWidth <= 75) { + return (baseWidth - spacing * 2) * 0.75 + } else { + return baseWidth + } + } + height: 60 + + Loader { + id: widgetLoader + anchors.fill: parent + property var widgetData: parent.widgetData + property int widgetIndex: parent.globalWidgetIndex + property int globalWidgetIndex: parent.globalWidgetIndex + property int widgetWidth: parent.widgetWidth + + sourceComponent: { + const id = modelData.id || "" + if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") { + return compoundPillComponent + } else if (id === "volumeSlider") { + return audioSliderComponent + } else if (id === "brightnessSlider") { + return brightnessSliderComponent + } else if (id === "inputVolumeSlider") { + return inputAudioSliderComponent + } else if (id === "battery") { + return widgetWidth <= 25 ? smallBatteryComponent : batteryPillComponent + } else { + return widgetWidth <= 25 ? smallToggleComponent : toggleButtonComponent + } + } + + } + } + } + } + + DetailHost { + width: parent.width + height: active ? (250 + Theme.spacingS) : 0 + property bool active: root.expandedSection !== "" && rowIndex === root.expandedRowIndex + visible: active + expandedSection: root.expandedSection + } + } + } + + Component { + id: compoundPillComponent + CompoundPill { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") + width: parent.width + height: 60 + iconName: { + switch (widgetData.id || "") { + case "wifi": { + if (NetworkService.wifiToggling) { + return "sync" + } + if (NetworkService.networkStatus === "ethernet") { + return "settings_ethernet" + } + if (NetworkService.networkStatus === "wifi") { + return NetworkService.wifiSignalIcon + } + if (NetworkService.wifiEnabled) { + return "wifi_off" + } + return "wifi_off" + } + case "bluetooth": { + if (!BluetoothService.available) { + return "bluetooth_disabled" + } + if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) { + return "bluetooth_disabled" + } + const primaryDevice = (() => { + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) { + return null + } + let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))] + for (let device of devices) { + if (device && device.connected) { + return device + } + } + return null + })() + if (primaryDevice) { + return BluetoothService.getDeviceIcon(primaryDevice) + } + return "bluetooth" + } + case "audioOutput": { + if (!AudioService.sink) return "volume_off" + let volume = AudioService.sink.audio.volume + let muted = AudioService.sink.audio.muted + if (muted || volume === 0.0) return "volume_off" + if (volume <= 0.33) return "volume_down" + if (volume <= 0.66) return "volume_up" + return "volume_up" + } + case "audioInput": { + if (!AudioService.source) return "mic_off" + let muted = AudioService.source.audio.muted + return muted ? "mic_off" : "mic" + } + default: return widgetDef?.icon || "help" + } + } + primaryText: { + switch (widgetData.id || "") { + case "wifi": { + if (NetworkService.wifiToggling) { + return NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..." + } + if (NetworkService.networkStatus === "ethernet") { + return "Ethernet" + } + if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) { + return NetworkService.currentWifiSSID + } + if (NetworkService.wifiEnabled) { + return "Not connected" + } + return "WiFi off" + } + case "bluetooth": { + if (!BluetoothService.available) { + return "Bluetooth" + } + if (!BluetoothService.adapter) { + return "No adapter" + } + if (!BluetoothService.adapter.enabled) { + return "Disabled" + } + return "Enabled" + } + case "audioOutput": return AudioService.sink?.description || "No output device" + case "audioInput": return AudioService.source?.description || "No input device" + default: return widgetDef?.text || "Unknown" + } + } + secondaryText: { + switch (widgetData.id || "") { + case "wifi": { + if (NetworkService.wifiToggling) { + return "Please wait..." + } + if (NetworkService.networkStatus === "ethernet") { + return "Connected" + } + if (NetworkService.networkStatus === "wifi") { + return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected" + } + if (NetworkService.wifiEnabled) { + return "Select network" + } + return "" + } + case "bluetooth": { + if (!BluetoothService.available) { + return "No adapters" + } + if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) { + return "Off" + } + const primaryDevice = (() => { + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) { + return null + } + let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))] + for (let device of devices) { + if (device && device.connected) { + return device + } + } + return null + })() + if (primaryDevice) { + return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device" + } + return "No devices" + } + case "audioOutput": { + if (!AudioService.sink) { + return "Select device" + } + if (AudioService.sink.audio.muted) { + return "Muted" + } + return Math.round(AudioService.sink.audio.volume * 100) + "%" + } + case "audioInput": { + if (!AudioService.source) { + return "Select device" + } + if (AudioService.source.audio.muted) { + return "Muted" + } + return Math.round(AudioService.source.audio.volume * 100) + "%" + } + default: return widgetDef?.description || "" + } + } + isActive: { + switch (widgetData.id || "") { + case "wifi": { + if (NetworkService.wifiToggling) { + return false + } + if (NetworkService.networkStatus === "ethernet") { + return true + } + if (NetworkService.networkStatus === "wifi") { + return true + } + return NetworkService.wifiEnabled + } + case "bluetooth": return !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled) + case "audioOutput": return !!(AudioService.sink && !AudioService.sink.audio.muted) + case "audioInput": return !!(AudioService.source && !AudioService.source.audio.muted) + default: return false + } + } + enabled: (widgetDef?.enabled ?? true) + onToggled: { + if (root.editMode) return + switch (widgetData.id || "") { + case "wifi": { + if (NetworkService.networkStatus !== "ethernet" && !NetworkService.wifiToggling) { + NetworkService.toggleWifiRadio() + } + break + } + case "bluetooth": { + if (BluetoothService.available && BluetoothService.adapter) { + BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled + } + break + } + case "audioOutput": { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.muted = !AudioService.sink.audio.muted + } + break + } + case "audioInput": { + if (AudioService.source && AudioService.source.audio) { + AudioService.source.audio.muted = !AudioService.source.audio.muted + } + break + } + } + } + onExpandClicked: { + if (root.editMode) return + root.expandClicked(widgetData, widgetIndex) + } + onWheelEvent: function (wheelEvent) { + const id = widgetData.id || "" + if (id === "audioOutput") { + if (!AudioService.sink || !AudioService.sink.audio) return + let delta = wheelEvent.angleDelta.y + let currentVolume = AudioService.sink.audio.volume * 100 + let newVolume + if (delta > 0) + newVolume = Math.min(100, currentVolume + 5) + else + newVolume = Math.max(0, currentVolume - 5) + AudioService.sink.audio.muted = false + AudioService.sink.audio.volume = newVolume / 100 + wheelEvent.accepted = true + } else if (id === "audioInput") { + if (!AudioService.source || !AudioService.source.audio) return + let delta = wheelEvent.angleDelta.y + let currentVolume = AudioService.source.audio.volume * 100 + let newVolume + if (delta > 0) + newVolume = Math.min(100, currentVolume + 5) + else + newVolume = Math.max(0, currentVolume - 5) + AudioService.source.audio.muted = false + AudioService.source.audio.volume = newVolume / 100 + wheelEvent.accepted = true + } + } + + EditModeOverlay { + anchors.fill: parent + editMode: root.editMode + widgetData: parent.widgetData + widgetIndex: parent.widgetIndex + showSizeControls: true + isSlider: false + onRemoveWidget: (index) => root.removeWidget(index) + onToggleWidgetSize: (index) => root.toggleWidgetSize(index) + onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + } + } + } + + Component { + id: audioSliderComponent + Item { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") + width: parent.width + height: 16 + + AudioSliderRow { + anchors.centerIn: parent + width: parent.width + height: 14 + property color sliderTrackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60) + } + + EditModeOverlay { + anchors.fill: parent + editMode: root.editMode + widgetData: parent.widgetData + widgetIndex: parent.widgetIndex + showSizeControls: true + isSlider: true + onRemoveWidget: (index) => root.removeWidget(index) + onToggleWidgetSize: (index) => root.toggleWidgetSize(index) + onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + } + } + } + + Component { + id: brightnessSliderComponent + Item { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + width: parent.width + height: 16 + + BrightnessSliderRow { + anchors.centerIn: parent + width: parent.width + height: 14 + property color sliderTrackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60) + } + + EditModeOverlay { + anchors.fill: parent + editMode: root.editMode + widgetData: parent.widgetData + widgetIndex: parent.widgetIndex + showSizeControls: true + isSlider: true + onRemoveWidget: (index) => root.removeWidget(index) + onToggleWidgetSize: (index) => root.toggleWidgetSize(index) + onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + } + } + } + + Component { + id: inputAudioSliderComponent + Item { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + width: parent.width + height: 16 + + InputAudioSliderRow { + anchors.centerIn: parent + width: parent.width + height: 14 + property color sliderTrackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60) + } + + EditModeOverlay { + anchors.fill: parent + editMode: root.editMode + widgetData: parent.widgetData + widgetIndex: parent.widgetIndex + showSizeControls: true + isSlider: true + onRemoveWidget: (index) => root.removeWidget(index) + onToggleWidgetSize: (index) => root.toggleWidgetSize(index) + onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + } + } + } + + Component { + id: batteryPillComponent + BatteryPill { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + width: parent.width + height: 60 + + onExpandClicked: { + if (!root.editMode) { + root.expandClicked(widgetData, widgetIndex) + } + } + + EditModeOverlay { + anchors.fill: parent + editMode: root.editMode + widgetData: parent.widgetData + widgetIndex: parent.widgetIndex + showSizeControls: true + isSlider: false + onRemoveWidget: (index) => root.removeWidget(index) + onToggleWidgetSize: (index) => root.toggleWidgetSize(index) + onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + } + } + } + + Component { + id: smallBatteryComponent + SmallBatteryButton { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + width: parent.width + height: 48 + + onClicked: { + if (!root.editMode) { + root.expandClicked(widgetData, widgetIndex) + } + } + + EditModeOverlay { + anchors.fill: parent + editMode: root.editMode + widgetData: parent.widgetData + widgetIndex: parent.widgetIndex + showSizeControls: true + isSlider: false + onRemoveWidget: (index) => root.removeWidget(index) + onToggleWidgetSize: (index) => root.toggleWidgetSize(index) + onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + } + } + } + + Component { + id: toggleButtonComponent + ToggleButton { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") + width: parent.width + height: 60 + + iconName: { + switch (widgetData.id || "") { + case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode" + case "darkMode": return "contrast" + case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off" + case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" + default: return widgetDef?.icon || "help" + } + } + + text: { + switch (widgetData.id || "") { + case "nightMode": return "Night Mode" + case "darkMode": return SessionData.isLightMode ? "Light Mode" : "Dark Mode" + case "doNotDisturb": return "Do Not Disturb" + case "idleInhibitor": return SessionService.idleInhibited ? "Keeping Awake" : "Keep Awake" + default: return widgetDef?.text || "Unknown" + } + } + + secondaryText: "" + + iconRotation: widgetData.id === "darkMode" && SessionData.isLightMode ? 180 : 0 + + isActive: { + switch (widgetData.id || "") { + case "nightMode": return DisplayService.nightModeEnabled || false + case "darkMode": return !SessionData.isLightMode + case "doNotDisturb": return SessionData.doNotDisturb || false + case "idleInhibitor": return SessionService.idleInhibited || false + default: return false + } + } + + enabled: (widgetDef?.enabled ?? true) && !root.editMode + + onClicked: { + switch (widgetData.id || "") { + case "nightMode": { + if (DisplayService.automationAvailable) { + DisplayService.toggleNightMode() + } + break + } + case "darkMode": { + Theme.toggleLightMode() + break + } + case "doNotDisturb": { + SessionData.setDoNotDisturb(!SessionData.doNotDisturb) + break + } + case "idleInhibitor": { + SessionService.toggleIdleInhibit() + break + } + } + } + + EditModeOverlay { + anchors.fill: parent + editMode: root.editMode + widgetData: parent.widgetData + widgetIndex: parent.widgetIndex + showSizeControls: true + isSlider: false + onRemoveWidget: (index) => root.removeWidget(index) + onToggleWidgetSize: (index) => root.toggleWidgetSize(index) + onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + } + } + } + + Component { + id: smallToggleComponent + SmallToggleButton { + property var widgetData: parent.widgetData || {} + property int widgetIndex: parent.widgetIndex || 0 + property var widgetDef: root.model?.getWidgetForId(widgetData.id || "") + width: parent.width + height: 48 + + iconName: { + switch (widgetData.id || "") { + case "nightMode": return DisplayService.nightModeEnabled ? "nightlight" : "dark_mode" + case "darkMode": return "contrast" + case "doNotDisturb": return SessionData.doNotDisturb ? "do_not_disturb_on" : "do_not_disturb_off" + case "idleInhibitor": return SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle" + default: return widgetDef?.icon || "help" + } + } + + iconRotation: widgetData.id === "darkMode" && SessionData.isLightMode ? 180 : 0 + + isActive: { + switch (widgetData.id || "") { + case "nightMode": return DisplayService.nightModeEnabled || false + case "darkMode": return !SessionData.isLightMode + case "doNotDisturb": return SessionData.doNotDisturb || false + case "idleInhibitor": return SessionService.idleInhibited || false + default: return false + } + } + + enabled: (widgetDef?.enabled ?? true) && !root.editMode + + onClicked: { + switch (widgetData.id || "") { + case "nightMode": { + if (DisplayService.automationAvailable) { + DisplayService.toggleNightMode() + } + break + } + case "darkMode": { + Theme.toggleLightMode() + break + } + case "doNotDisturb": { + SessionData.setDoNotDisturb(!SessionData.doNotDisturb) + break + } + case "idleInhibitor": { + SessionService.toggleIdleInhibit() + break + } + } + } + + EditModeOverlay { + anchors.fill: parent + editMode: root.editMode + widgetData: parent.widgetData + widgetIndex: parent.widgetIndex + showSizeControls: true + isSlider: false + onRemoveWidget: (index) => root.removeWidget(index) + onToggleWidgetSize: (index) => root.toggleWidgetSize(index) + onMoveWidget: (fromIndex, toIndex) => root.moveWidget(fromIndex, toIndex) + } + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/ControlCenterPopout.qml b/Modules/ControlCenter/ControlCenterPopout.qml index cf4b349f..2fb442f1 100644 --- a/Modules/ControlCenter/ControlCenterPopout.qml +++ b/Modules/ControlCenter/ControlCenterPopout.qml @@ -10,10 +10,12 @@ import qs.Common import qs.Modules.ControlCenter import qs.Modules.ControlCenter.Widgets import qs.Modules.ControlCenter.Details -import qs.Modules.ControlCenter.Details 1.0 as Details import qs.Modules.TopBar import qs.Services import qs.Widgets +import qs.Modules.ControlCenter.Components +import qs.Modules.ControlCenter.Models +import "./utils/state.js" as StateUtils DankPopout { id: root @@ -22,37 +24,26 @@ DankPopout { property bool powerOptionsExpanded: false property string triggerSection: "right" property var triggerScreen: null + property bool editMode: false + property int expandedWidgetIndex: -1 + + signal powerActionRequested(string action, string title, string message) + signal lockRequested readonly property color _containerBg: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60) function setTriggerPosition(x, y, width, section, screen) { - triggerX = x - triggerY = y - triggerWidth = width - triggerSection = section - triggerScreen = screen + StateUtils.setTriggerPosition(root, x, y, width, section, screen) } function openWithSection(section) { - if (shouldBeVisible) { - close() - } else { - expandedSection = section - open() - } + StateUtils.openWithSection(root, section) } function toggleSection(section) { - if (expandedSection === section) { - expandedSection = "" - } else { - expandedSection = section - } + StateUtils.toggleSection(root, section) } - signal powerActionRequested(string action, string title, string message) - signal lockRequested - popupWidth: 550 popupHeight: Math.min((triggerScreen?.height ?? 1080) - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400) triggerX: (triggerScreen?.width ?? 1920) - 600 - Theme.spacingL @@ -73,20 +64,24 @@ DankPopout { } else { Qt.callLater(() => { NetworkService.autoRefreshEnabled = false - if (BluetoothService.adapter - && BluetoothService.adapter.discovering) + if (BluetoothService.adapter && BluetoothService.adapter.discovering) BluetoothService.adapter.discovering = false + editMode = false }) } } + WidgetModel { + id: widgetModel + } + content: Component { Rectangle { id: controlContent - + implicitHeight: mainColumn.implicitHeight + Theme.spacingM property alias bluetoothCodecSelector: bluetoothCodecSelector - + color: { const transparency = Theme.popupTransparency || 0.92 const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1) @@ -106,621 +101,61 @@ DankPopout { y: Theme.spacingL spacing: Theme.spacingS - Rectangle { + HeaderPane { + id: headerPane width: parent.width - height: 90 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, - Theme.getContentBackgroundAlpha() * 0.4) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, - Theme.outline.b, 0.08) - border.width: 1 - - Row { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Theme.spacingL - anchors.rightMargin: Theme.spacingL - spacing: Theme.spacingM - - DankCircularImage { - id: avatarContainer - - width: 64 - height: 64 - imageSource: { - if (PortalService.profileImage === "") - return "" - - if (PortalService.profileImage.startsWith("/")) - return "file://" + PortalService.profileImage - - return PortalService.profileImage - } - fallbackIcon: "person" - } - - Column { - anchors.verticalCenter: parent.verticalCenter - spacing: 2 - - StyledText { - text: UserInfoService.fullName - || UserInfoService.username || "User" - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - } - - StyledText { - text: (UserInfoService.uptime - || "Unknown") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - font.weight: Font.Normal - } - } - } - - Rectangle { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.rightMargin: Theme.spacingM - width: actionButtonsRow.implicitWidth + Theme.spacingM * 2 - height: 48 - radius: Theme.cornerRadius + 2 - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.6) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) - border.width: 1 - - Row { - id: actionButtonsRow - anchors.centerIn: parent - spacing: Theme.spacingXS - - Item { - width: batteryContentRow.implicitWidth - height: 36 - visible: BatteryService.batteryAvailable - - Row { - id: batteryContentRow - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable) - size: Theme.iconSize - 4 - color: { - if (batteryMouseArea.containsMouse) { - return Theme.primary - } - if (BatteryService.isLowBattery && !BatteryService.isCharging) { - return Theme.error - } - if (BatteryService.isCharging || BatteryService.isPluggedIn) { - return Theme.primary - } - return Theme.surfaceText - } - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: `${BatteryService.batteryLevel}%` - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - color: { - if (batteryMouseArea.containsMouse) { - return Theme.primary - } - if (BatteryService.isLowBattery && !BatteryService.isCharging) { - return Theme.error - } - if (BatteryService.isCharging) { - return Theme.primary - } - return Theme.surfaceText - } - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: batteryMouseArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: { - const globalPos = mapToGlobal(0, 0) - const currentScreen = root.triggerScreen || Screen - const screenX = currentScreen.x || 0 - const relativeX = globalPos.x - screenX - controlCenterBatteryPopout.setTriggerPosition(relativeX, 123 + Theme.spacingXS, width, "right", currentScreen) - - if (controlCenterBatteryPopout.shouldBeVisible) { - controlCenterBatteryPopout.close() - } else { - controlCenterBatteryPopout.open() - } - } - } - } - - DankActionButton { - buttonSize: 36 - iconName: "lock" - iconSize: Theme.iconSize - 4 - iconColor: Theme.surfaceText - backgroundColor: "transparent" - onClicked: { - root.close() - root.lockRequested() - } - } - - DankActionButton { - buttonSize: 36 - iconName: root.powerOptionsExpanded ? "expand_less" : "power_settings_new" - iconSize: Theme.iconSize - 4 - iconColor: root.powerOptionsExpanded ? Theme.primary : Theme.surfaceText - backgroundColor: "transparent" - onClicked: { - root.powerOptionsExpanded = !root.powerOptionsExpanded - } - } - - DankActionButton { - buttonSize: 36 - iconName: "settings" - iconSize: Theme.iconSize - 4 - iconColor: Theme.surfaceText - backgroundColor: "transparent" - onClicked: { - root.close() - settingsModal.show() - } - } - } + powerOptionsExpanded: root.powerOptionsExpanded + editMode: root.editMode + onPowerOptionsExpandedChanged: root.powerOptionsExpanded = powerOptionsExpanded + onEditModeToggled: root.editMode = !root.editMode + onPowerActionRequested: (action, title, message) => root.powerActionRequested(action, title, message) + onLockRequested: { + root.close() + root.lockRequested() } } - Item { + PowerOptionsPane { + id: powerOptionsPane width: parent.width - implicitHeight: root.powerOptionsExpanded ? 60 : 0 - height: implicitHeight - clip: true - - Rectangle { - width: parent.width - height: 60 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, - Theme.getContentBackgroundAlpha() * 0.4) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, - Theme.outline.b, 0.08) - border.width: root.powerOptionsExpanded ? 1 : 0 - opacity: root.powerOptionsExpanded ? 1 : 0 - clip: true - - Row { - anchors.centerIn: parent - spacing: SessionService.hibernateSupported ? Theme.spacingS : Theme.spacingL - visible: root.powerOptionsExpanded - - Rectangle { - width: SessionService.hibernateSupported ? 85 : 100 - height: 34 - radius: Theme.cornerRadius - color: logoutButton.containsMouse ? Qt.rgba( - Theme.primary.r, - Theme.primary.g, - Theme.primary.b, - 0.12) : Qt.rgba( - Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, - 0.5) - - Row { - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: "logout" - size: Theme.fontSizeSmall - color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: "Logout" - font.pixelSize: Theme.fontSizeSmall - color: logoutButton.containsMouse ? Theme.primary : Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: logoutButton - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: { - root.powerOptionsExpanded = false - root.close() - root.powerActionRequested( - "logout", "Logout", - "Are you sure you want to logout?") - } - } - } - - Rectangle { - width: SessionService.hibernateSupported ? 85 : 100 - height: 34 - radius: Theme.cornerRadius - color: rebootButton.containsMouse ? Qt.rgba( - Theme.primary.r, - Theme.primary.g, - Theme.primary.b, - 0.12) : Qt.rgba( - Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, - 0.5) - - Row { - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: "restart_alt" - size: Theme.fontSizeSmall - color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: "Restart" - font.pixelSize: Theme.fontSizeSmall - color: rebootButton.containsMouse ? Theme.primary : Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: rebootButton - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: { - root.powerOptionsExpanded = false - root.close() - root.powerActionRequested( - "reboot", "Restart", - "Are you sure you want to restart?") - } - } - } - - Rectangle { - width: SessionService.hibernateSupported ? 85 : 100 - height: 34 - radius: Theme.cornerRadius - color: suspendButton.containsMouse ? Qt.rgba( - Theme.primary.r, - Theme.primary.g, - Theme.primary.b, - 0.12) : Qt.rgba( - Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, - 0.5) - - Row { - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: "bedtime" - size: Theme.fontSizeSmall - color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: "Suspend" - font.pixelSize: Theme.fontSizeSmall - color: suspendButton.containsMouse ? Theme.primary : Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: suspendButton - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: { - root.powerOptionsExpanded = false - root.close() - root.powerActionRequested( - "suspend", "Suspend", - "Are you sure you want to suspend?") - } - } - } - - Rectangle { - width: SessionService.hibernateSupported ? 85 : 100 - height: 34 - radius: Theme.cornerRadius - color: hibernateButton.containsMouse ? Qt.rgba( - Theme.primary.r, - Theme.primary.g, - Theme.primary.b, - 0.12) : Qt.rgba( - Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, - 0.5) - visible: SessionService.hibernateSupported - - Row { - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: "ac_unit" - size: Theme.fontSizeSmall - color: hibernateButton.containsMouse ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: "Hibernate" - font.pixelSize: Theme.fontSizeSmall - color: hibernateButton.containsMouse ? Theme.primary : Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: hibernateButton - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: { - root.powerOptionsExpanded = false - root.close() - root.powerActionRequested( - "hibernate", "Hibernate", - "Are you sure you want to hibernate?") - } - } - } - - Rectangle { - width: SessionService.hibernateSupported ? 85 : 100 - height: 34 - radius: Theme.cornerRadius - color: shutdownButton.containsMouse ? Qt.rgba( - Theme.primary.r, - Theme.primary.g, - Theme.primary.b, - 0.12) : Qt.rgba( - Theme.surfaceVariant.r, - Theme.surfaceVariant.g, - Theme.surfaceVariant.b, - 0.5) - - Row { - anchors.centerIn: parent - spacing: Theme.spacingXS - - DankIcon { - name: "power_settings_new" - size: Theme.fontSizeSmall - color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter - } - - StyledText { - text: "Shutdown" - font.pixelSize: Theme.fontSizeSmall - color: shutdownButton.containsMouse ? Theme.primary : Theme.surfaceText - font.weight: Font.Medium - anchors.verticalCenter: parent.verticalCenter - } - } - - MouseArea { - id: shutdownButton - - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onPressed: { - root.powerOptionsExpanded = false - root.close() - root.powerActionRequested( - "poweroff", "Shutdown", - "Are you sure you want to shutdown?") - } - } - } + expanded: root.powerOptionsExpanded + onPowerActionRequested: (action, title, message) => { + root.powerOptionsExpanded = false + root.close() + root.powerActionRequested(action, title, message) } } + WidgetGrid { + id: widgetGrid + width: parent.width + editMode: root.editMode + expandedSection: root.expandedSection + expandedWidgetIndex: root.expandedWidgetIndex + model: widgetModel + onExpandClicked: (widgetData, globalIndex) => { + root.expandedWidgetIndex = globalIndex + root.toggleSection(widgetData.id) + } + onRemoveWidget: (index) => widgetModel.removeWidget(index) + onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex) + onToggleWidgetSize: (index) => widgetModel.toggleWidgetSize(index) } - Item { + EditControls { width: parent.width - height: audioSliderRow.implicitHeight - - Row { - id: audioSliderRow - x: -Theme.spacingS - width: parent.width + Theme.spacingS * 2 - spacing: Theme.spacingM - - AudioSliderRow { - width: SettingsData.hideBrightnessSlider ? parent.width - Theme.spacingS : (parent.width - Theme.spacingM) / 2 - property color sliderTrackColor: root._containerBg - } - - Item { - width: (parent.width - Theme.spacingM) / 2 - height: parent.height - visible: !SettingsData.hideBrightnessSlider - - BrightnessSliderRow { - width: parent.width - height: parent.height - x: -Theme.spacingS - } - } - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - - NetworkPill { - width: (parent.width - Theme.spacingM) / 2 - expanded: root.expandedSection === "network" - onExpandClicked: root.toggleSection("network") - } - - BluetoothPill { - width: (parent.width - Theme.spacingM) / 2 - expanded: root.expandedSection === "bluetooth" - onExpandClicked: { - if (!BluetoothService.available) return - root.toggleSection("bluetooth") - } - } - } - - Loader { - width: parent.width - active: root.expandedSection === "network" || root.expandedSection === "bluetooth" - visible: active - sourceComponent: DetailView { - width: parent.width - isVisible: true - title: { - switch (root.expandedSection) { - case "network": return "Network Settings" - case "bluetooth": return "Bluetooth Settings" - default: return "" - } - } - content: { - switch (root.expandedSection) { - case "network": return networkDetailComponent - case "bluetooth": return bluetoothDetailComponent - default: return null - } - } - contentHeight: 250 - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - - AudioOutputPill { - width: (parent.width - Theme.spacingM) / 2 - expanded: root.expandedSection === "audio_output" - onExpandClicked: root.toggleSection("audio_output") - } - - AudioInputPill { - width: (parent.width - Theme.spacingM) / 2 - expanded: root.expandedSection === "audio_input" - onExpandClicked: root.toggleSection("audio_input") - } - } - - Loader { - width: parent.width - active: root.expandedSection === "audio_output" || root.expandedSection === "audio_input" - visible: active - sourceComponent: DetailView { - width: parent.width - isVisible: true - title: { - switch (root.expandedSection) { - case "audio_output": return "Audio Output" - case "audio_input": return "Audio Input" - default: return "" - } - } - content: { - switch (root.expandedSection) { - case "audio_output": return audioOutputDetailComponent - case "audio_input": return audioInputDetailComponent - default: return null - } - } - contentHeight: 250 - } - } - - Row { - width: parent.width - spacing: Theme.spacingM - - ToggleButton { - width: (parent.width - Theme.spacingM) / 2 - iconName: DisplayService.nightModeEnabled ? "nightlight" : "dark_mode" - text: "Night Mode" - secondaryText: SessionData.nightModeAutoEnabled ? "Auto" : (DisplayService.nightModeEnabled ? "On" : "Off") - isActive: DisplayService.nightModeEnabled - enabled: DisplayService.automationAvailable - onClicked: DisplayService.toggleNightMode() - - DankIcon { - anchors.top: parent.top - anchors.right: parent.right - anchors.topMargin: Theme.spacingS - anchors.rightMargin: Theme.spacingS - name: "schedule" - size: 12 - color: Theme.primary - visible: SessionData.nightModeAutoEnabled - opacity: 0.8 - } - } - - ToggleButton { - width: (parent.width - Theme.spacingM) / 2 - iconName: SessionData.isLightMode ? "light_mode" : "palette" - text: "Theme" - secondaryText: SessionData.isLightMode ? "Light" : "Dark" - isActive: true - onClicked: Theme.toggleLightMode() + visible: editMode + availableWidgets: { + const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id) + return widgetModel.baseWidgetDefinitions.filter(w => !existingIds.includes(w.id)) } + onAddWidget: (widgetId) => widgetModel.addWidget(widgetId) + onResetToDefault: () => widgetModel.resetToDefault() + onClearAll: () => widgetModel.clearAll() } } - - Details.BluetoothCodecSelector { + + BluetoothCodecSelector { id: bluetoothCodecSelector anchors.fill: parent z: 10000 @@ -728,10 +163,6 @@ DankPopout { } } - BatteryPopout { - id: controlCenterBatteryPopout - } - Component { id: networkDetailComponent NetworkDetail {} @@ -761,4 +192,9 @@ DankPopout { id: audioInputDetailComponent AudioInputDetail {} } -} + + Component { + id: batteryDetailComponent + BatteryDetail {} + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Details/AudioInputDetail.qml b/Modules/ControlCenter/Details/AudioInputDetail.qml index d36000e0..84161b92 100644 --- a/Modules/ControlCenter/Details/AudioInputDetail.qml +++ b/Modules/ControlCenter/Details/AudioInputDetail.qml @@ -7,7 +7,12 @@ import qs.Services import qs.Widgets Rectangle { - implicitHeight: headerRow.height + volumeSlider.height + audioContent.height + Theme.spacingM + property bool hasInputVolumeSliderInCC: { + const widgets = SettingsData.controlCenterWidgets || [] + return widgets.some(widget => widget.id === "inputVolumeSlider") + } + + implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM radius: Theme.cornerRadius color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) @@ -43,6 +48,7 @@ Rectangle { anchors.topMargin: Theme.spacingXS height: 35 spacing: 0 + visible: !hasInputVolumeSliderInCC Rectangle { width: Theme.iconSize + Theme.spacingS * 2 @@ -106,12 +112,12 @@ Rectangle { DankFlickable { id: audioContent - anchors.top: volumeSlider.bottom + anchors.top: hasInputVolumeSliderInCC ? headerRow.bottom : volumeSlider.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Theme.spacingM - anchors.topMargin: Theme.spacingS + anchors.topMargin: hasInputVolumeSliderInCC ? Theme.spacingM : Theme.spacingS contentHeight: audioColumn.height clip: true diff --git a/Modules/ControlCenter/Details/AudioOutputDetail.qml b/Modules/ControlCenter/Details/AudioOutputDetail.qml index 77556203..53178ef5 100644 --- a/Modules/ControlCenter/Details/AudioOutputDetail.qml +++ b/Modules/ControlCenter/Details/AudioOutputDetail.qml @@ -7,7 +7,12 @@ import qs.Services import qs.Widgets Rectangle { - implicitHeight: headerRow.height + audioContent.height + Theme.spacingM + property bool hasVolumeSliderInCC: { + const widgets = SettingsData.controlCenterWidgets || [] + return widgets.some(widget => widget.id === "volumeSlider") + } + + implicitHeight: headerRow.height + (!hasVolumeSliderInCC ? volumeSlider.height : 0) + audioContent.height + Theme.spacingM radius: Theme.cornerRadius color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) @@ -32,15 +37,92 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter } } - + + Row { + id: volumeSlider + anchors.left: parent.left + anchors.right: parent.right + anchors.top: headerRow.bottom + anchors.leftMargin: Theme.spacingM + anchors.rightMargin: Theme.spacingM + anchors.topMargin: Theme.spacingXS + height: 35 + spacing: 0 + visible: !hasVolumeSliderInCC + + Rectangle { + width: Theme.iconSize + Theme.spacingS * 2 + height: Theme.iconSize + Theme.spacingS * 2 + anchors.verticalCenter: parent.verticalCenter + radius: (Theme.iconSize + Theme.spacingS * 2) / 2 + color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + MouseArea { + id: iconArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.muted = !AudioService.sink.audio.muted + } + } + } + + DankIcon { + anchors.centerIn: parent + name: { + if (!AudioService.sink || !AudioService.sink.audio) return "volume_off" + let muted = AudioService.sink.audio.muted + let volume = AudioService.sink.audio.volume + if (muted || volume === 0.0) return "volume_off" + if (volume <= 0.33) return "volume_down" + if (volume <= 0.66) return "volume_up" + return "volume_up" + } + size: Theme.iconSize + color: AudioService.sink && AudioService.sink.audio && !AudioService.sink.audio.muted && AudioService.sink.audio.volume > 0 ? Theme.primary : Theme.surfaceText + } + } + + DankSlider { + readonly property real actualVolumePercent: AudioService.sink && AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0 + + anchors.verticalCenter: parent.verticalCenter + width: parent.width - (Theme.iconSize + Theme.spacingS * 2) + enabled: AudioService.sink && AudioService.sink.audio + minimum: 0 + maximum: 100 + value: AudioService.sink && AudioService.sink.audio ? Math.min(100, Math.round(AudioService.sink.audio.volume * 100)) : 0 + showValue: true + unit: "%" + valueOverride: actualVolumePercent + thumbOutlineColor: Theme.surfaceVariant + + onSliderValueChanged: function(newValue) { + if (AudioService.sink && AudioService.sink.audio) { + AudioService.sink.audio.volume = newValue / 100 + if (newValue > 0 && AudioService.sink.audio.muted) { + AudioService.sink.audio.muted = false + } + AudioService.volumeChanged() + } + } + } + } + DankFlickable { id: audioContent - anchors.top: headerRow.bottom + anchors.top: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Theme.spacingM - anchors.topMargin: Theme.spacingM + anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM contentHeight: audioColumn.height clip: true diff --git a/Modules/ControlCenter/Details/BatteryDetail.qml b/Modules/ControlCenter/Details/BatteryDetail.qml new file mode 100644 index 00000000..c6cbd0d8 --- /dev/null +++ b/Modules/ControlCenter/Details/BatteryDetail.qml @@ -0,0 +1,260 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.UPower +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) + border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: 1 + + function isActiveProfile(profile) { + if (typeof PowerProfiles === "undefined") { + return false + } + return PowerProfiles.profile === profile + } + + function setProfile(profile) { + if (typeof PowerProfiles === "undefined") { + ToastService.showError("power-profiles-daemon not available") + return + } + PowerProfiles.profile = profile + if (PowerProfiles.profile !== profile) { + ToastService.showError("Failed to set power profile") + } + } + + Column { + id: contentColumn + width: parent.width - Theme.spacingL * 2 + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingL + + Row { + id: headerRow + width: parent.width + height: 48 + spacing: Theme.spacingM + + DankIcon { + name: BatteryService.getBatteryIcon() + size: Theme.iconSizeLarge + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) + return Theme.error + if (BatteryService.isCharging || BatteryService.isPluggedIn) + return Theme.primary + return Theme.surfaceText + } + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Theme.iconSizeLarge - Theme.spacingM + + Row { + spacing: Theme.spacingS + + StyledText { + text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : "Power" + font.pixelSize: Theme.fontSizeXLarge + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error + } + if (BatteryService.isCharging) { + return Theme.primary + } + return Theme.surfaceText + } + font.weight: Font.Bold + } + + StyledText { + text: BatteryService.batteryAvailable ? BatteryService.batteryStatus : "Management" + font.pixelSize: Theme.fontSizeLarge + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error + } + if (BatteryService.isCharging) { + return Theme.primary + } + return Theme.surfaceText + } + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + + StyledText { + text: { + if (!BatteryService.batteryAvailable) return "Power profile management available" + const time = BatteryService.formatTimeRemaining() + if (time !== "Unknown") { + return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}` + } + return "" + } + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceTextMedium + visible: text.length > 0 + elide: Text.ElideRight + width: parent.width + } + } + } + + Row { + width: parent.width + spacing: Theme.spacingM + visible: BatteryService.batteryAvailable + + StyledRect { + width: (parent.width - Theme.spacingM) / 2 + height: 64 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) + border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) + border.width: 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: "Health" + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: BatteryService.batteryHealth + font.pixelSize: Theme.fontSizeLarge + color: { + if (BatteryService.batteryHealth === "N/A") { + return Theme.surfaceText + } + const healthNum = parseInt(BatteryService.batteryHealth) + return healthNum < 80 ? Theme.error : Theme.surfaceText + } + font.weight: Font.Bold + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + StyledRect { + width: (parent.width - Theme.spacingM) / 2 + height: 64 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) + border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) + border.width: 1 + + Column { + anchors.centerIn: parent + spacing: Theme.spacingXS + + StyledText { + text: "Capacity" + font.pixelSize: Theme.fontSizeSmall + color: Theme.primary + font.weight: Font.Medium + anchors.horizontalCenter: parent.horizontalCenter + } + + StyledText { + text: BatteryService.batteryCapacity > 0 ? `${BatteryService.batteryCapacity.toFixed(1)} Wh` : "Unknown" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText + font.weight: Font.Bold + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + + DankButtonGroup { + property var profileModel: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] + property int currentProfileIndex: { + if (typeof PowerProfiles === "undefined") return 1 + return profileModel.findIndex(profile => isActiveProfile(profile)) + } + + model: profileModel.map(profile => Theme.getPowerProfileLabel(profile)) + currentIndex: currentProfileIndex + selectionMode: "single" + anchors.horizontalCenter: parent.horizontalCenter + onSelectionChanged: (index, selected) => { + if (!selected) return + setProfile(profileModel[index]) + } + } + + StyledRect { + width: parent.width + height: degradationContent.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) + border.color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3) + border.width: 1 + visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None + + Column { + id: degradationContent + width: parent.width - Theme.spacingL * 2 + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Theme.spacingL + spacing: Theme.spacingS + + Row { + width: parent.width + spacing: Theme.spacingM + + DankIcon { + name: "warning" + size: Theme.iconSize + color: Theme.error + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + width: parent.width - Theme.iconSize - Theme.spacingM + + StyledText { + text: "Power Profile Degradation" + font.pixelSize: Theme.fontSizeLarge + color: Theme.error + font.weight: Font.Medium + } + + StyledText { + text: (typeof PowerProfiles !== "undefined") ? PerformanceDegradationReason.toString(PowerProfiles.degradationReason) : "" + font.pixelSize: Theme.fontSizeSmall + color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8) + wrapMode: Text.WordWrap + width: parent.width + } + } + } + } + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Models/WidgetModel.qml b/Modules/ControlCenter/Models/WidgetModel.qml new file mode 100644 index 00000000..6daf7b5f --- /dev/null +++ b/Modules/ControlCenter/Models/WidgetModel.qml @@ -0,0 +1,139 @@ +import QtQuick +import qs.Common +import qs.Services +import "../utils/widgets.js" as WidgetUtils + +QtObject { + id: root + + readonly property var baseWidgetDefinitions: [ + { + "id": "nightMode", + "text": "Night Mode", + "description": "Blue light filter", + "icon": "nightlight", + "type": "toggle", + "enabled": DisplayService.automationAvailable, + "warning": !DisplayService.automationAvailable ? "Requires night mode support" : undefined + }, + { + "id": "darkMode", + "text": "Dark Mode", + "description": "System theme toggle", + "icon": "contrast", + "type": "toggle", + "enabled": true + }, + { + "id": "doNotDisturb", + "text": "Do Not Disturb", + "description": "Block notifications", + "icon": "do_not_disturb_on", + "type": "toggle", + "enabled": true + }, + { + "id": "idleInhibitor", + "text": "Keep Awake", + "description": "Prevent screen timeout", + "icon": "motion_sensor_active", + "type": "toggle", + "enabled": true + }, + { + "id": "wifi", + "text": "Network", + "description": "Wi-Fi and Ethernet connection", + "icon": "wifi", + "type": "connection", + "enabled": NetworkService.wifiAvailable, + "warning": !NetworkService.wifiAvailable ? "Wi-Fi not available" : undefined + }, + { + "id": "bluetooth", + "text": "Bluetooth", + "description": "Device connections", + "icon": "bluetooth", + "type": "connection", + "enabled": BluetoothService.available, + "warning": !BluetoothService.available ? "Bluetooth not available" : undefined + }, + { + "id": "audioOutput", + "text": "Audio Output", + "description": "Speaker settings", + "icon": "volume_up", + "type": "connection", + "enabled": true + }, + { + "id": "audioInput", + "text": "Audio Input", + "description": "Microphone settings", + "icon": "mic", + "type": "connection", + "enabled": true + }, + { + "id": "volumeSlider", + "text": "Volume Slider", + "description": "Audio volume control", + "icon": "volume_up", + "type": "slider", + "enabled": true + }, + { + "id": "brightnessSlider", + "text": "Brightness Slider", + "description": "Display brightness control", + "icon": "brightness_6", + "type": "slider", + "enabled": DisplayService.brightnessAvailable, + "warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined + }, + { + "id": "inputVolumeSlider", + "text": "Input Volume Slider", + "description": "Microphone volume control", + "icon": "mic", + "type": "slider", + "enabled": true + }, + { + "id": "battery", + "text": "Battery", + "description": "Battery and power management", + "icon": "battery_std", + "type": "action", + "enabled": true + } + ] + + function getWidgetForId(widgetId) { + return WidgetUtils.getWidgetForId(baseWidgetDefinitions, widgetId) + } + + function addWidget(widgetId) { + WidgetUtils.addWidget(widgetId) + } + + function removeWidget(index) { + WidgetUtils.removeWidget(index) + } + + function toggleWidgetSize(index) { + WidgetUtils.toggleWidgetSize(index) + } + + function moveWidget(fromIndex, toIndex) { + WidgetUtils.moveWidget(fromIndex, toIndex) + } + + function resetToDefault() { + WidgetUtils.resetToDefault() + } + + function clearAll() { + WidgetUtils.clearAll() + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Widgets/BatteryPill.qml b/Modules/ControlCenter/Widgets/BatteryPill.qml new file mode 100644 index 00000000..5c73dc14 --- /dev/null +++ b/Modules/ControlCenter/Widgets/BatteryPill.qml @@ -0,0 +1,48 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.ControlCenter.Widgets + +CompoundPill { + id: root + + iconName: BatteryService.getBatteryIcon() + + isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn) + + primaryText: { + if (!BatteryService.batteryAvailable) { + return "No battery" + } + return "Battery" + } + + secondaryText: { + if (!BatteryService.batteryAvailable) { + return "Not available" + } + if (BatteryService.isCharging) { + return `${BatteryService.batteryLevel}% • Charging` + } + if (BatteryService.isPluggedIn) { + return `${BatteryService.batteryLevel}% • Plugged in` + } + return `${BatteryService.batteryLevel}%` + } + + iconColor: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error + } + if (BatteryService.isCharging || BatteryService.isPluggedIn) { + return Theme.primary + } + return Theme.surfaceText + } + + onToggled: { + expandClicked() + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Widgets/CompoundPill.qml b/Modules/ControlCenter/Widgets/CompoundPill.qml index 03eb0441..c1170fc9 100644 --- a/Modules/ControlCenter/Widgets/CompoundPill.qml +++ b/Modules/ControlCenter/Widgets/CompoundPill.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Controls import Quickshell import qs.Common import qs.Widgets @@ -40,8 +39,11 @@ Rectangle { readonly property color _labelPrimary: Theme.surfaceText readonly property color _labelSecondary: Theme.surfaceVariantText readonly property color _tileBgActive: Theme.primary - readonly property color _tileBgInactive: - Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, 0.85) + readonly property color _tileBgInactive: { + const transparency = Theme.popupTransparency || 0.92 + const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1) + return Qt.rgba(surface.r, surface.g, surface.b, transparency) + } readonly property color _tileRingActive: Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22) readonly property color _tileRingInactive: diff --git a/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml b/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml new file mode 100644 index 00000000..edd96cbe --- /dev/null +++ b/Modules/ControlCenter/Widgets/InputAudioSliderRow.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.Pipewire +import qs.Common +import qs.Services +import qs.Widgets + +Row { + id: root + + property var defaultSource: AudioService.source + property color sliderTrackColor: "transparent" + + height: 40 + spacing: 0 + + Rectangle { + width: Theme.iconSize + Theme.spacingS * 2 + height: Theme.iconSize + Theme.spacingS * 2 + anchors.verticalCenter: parent.verticalCenter + radius: (Theme.iconSize + Theme.spacingS * 2) / 2 + color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" + + Behavior on color { + ColorAnimation { duration: Theme.shortDuration } + } + + MouseArea { + id: iconArea + anchors.fill: parent + visible: defaultSource !== null + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (defaultSource) { + defaultSource.audio.muted = !defaultSource.audio.muted + } + } + } + + DankIcon { + anchors.centerIn: parent + name: { + if (!defaultSource) return "mic_off" + + let volume = defaultSource.audio.volume + let muted = defaultSource.audio.muted + + if (muted || volume === 0.0) return "mic_off" + return "mic" + } + size: Theme.iconSize + color: defaultSource && !defaultSource.audio.muted && defaultSource.audio.volume > 0 ? Theme.primary : Theme.surfaceText + } + } + + DankSlider { + readonly property real actualVolumePercent: defaultSource ? Math.round(defaultSource.audio.volume * 100) : 0 + + anchors.verticalCenter: parent.verticalCenter + width: parent.width - (Theme.iconSize + Theme.spacingS * 2) + enabled: defaultSource !== null + minimum: 0 + maximum: 100 + value: defaultSource ? Math.min(100, Math.round(defaultSource.audio.volume * 100)) : 0 + showValue: true + unit: "%" + valueOverride: actualVolumePercent + thumbOutlineColor: Theme.surfaceContainer + trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.60) + onSliderValueChanged: function(newValue) { + if (defaultSource) { + defaultSource.audio.volume = newValue / 100.0 + if (newValue > 0 && defaultSource.audio.muted) { + defaultSource.audio.muted = false + } + } + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Widgets/SmallBatteryButton.qml b/Modules/ControlCenter/Widgets/SmallBatteryButton.qml new file mode 100644 index 00000000..6acd4a02 --- /dev/null +++ b/Modules/ControlCenter/Widgets/SmallBatteryButton.qml @@ -0,0 +1,105 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets + +Rectangle { + id: root + + property bool isActive: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn) + property bool enabled: BatteryService.batteryAvailable + + signal clicked() + + width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48 + height: 48 + radius: { + if (Theme.cornerRadius === 0) return 0 + return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4 + } + + function hoverTint(base) { + const factor = 1.2 + return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor) + } + + readonly property color _tileBgActive: Theme.primary + readonly property color _tileBgInactive: + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, + Theme.getContentBackgroundAlpha() * 0.60) + readonly property color _tileRingActive: + Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22) + readonly property color _tileIconActive: Theme.primaryContainer + readonly property color _tileIconInactive: Theme.primary + + color: isActive ? _tileBgActive : _tileBgInactive + border.color: isActive ? _tileRingActive : "transparent" + border.width: isActive ? 1 : 0 + antialiasing: true + opacity: enabled ? 1.0 : 0.6 + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: hoverTint(root.color) + opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0) + visible: opacity > 0 + antialiasing: true + Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } } + } + + Row { + anchors.centerIn: parent + spacing: 4 + + DankIcon { + name: BatteryService.getBatteryIcon() + size: parent.parent.width * 0.25 + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error + } + return isActive ? _tileIconActive : _tileIconInactive + } + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: BatteryService.batteryAvailable ? `${BatteryService.batteryLevel}%` : "" + font.pixelSize: parent.parent.width * 0.15 + font.weight: Font.Medium + color: { + if (BatteryService.isLowBattery && !BatteryService.isCharging) { + return Theme.error + } + return isActive ? _tileIconActive : _tileIconInactive + } + anchors.verticalCenter: parent.verticalCenter + visible: BatteryService.batteryAvailable + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled + onClicked: root.clicked() + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on radius { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Widgets/SmallToggleButton.qml b/Modules/ControlCenter/Widgets/SmallToggleButton.qml new file mode 100644 index 00000000..e2bb7f9e --- /dev/null +++ b/Modules/ControlCenter/Widgets/SmallToggleButton.qml @@ -0,0 +1,83 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Widgets + +Rectangle { + id: root + + property string iconName: "" + property bool isActive: false + property bool enabled: true + property real iconRotation: 0 + + signal clicked() + + width: parent ? ((parent.width - parent.spacing * 3) / 4) : 48 + height: 48 + radius: { + if (Theme.cornerRadius === 0) return 0 + return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4 + } + + function hoverTint(base) { + const factor = 1.2 + return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor) + } + + readonly property color _tileBgActive: Theme.primary + readonly property color _tileBgInactive: + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, + Theme.getContentBackgroundAlpha() * 0.60) + readonly property color _tileRingActive: + Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22) + readonly property color _tileIconActive: Theme.primaryContainer + readonly property color _tileIconInactive: Theme.primary + + color: isActive ? _tileBgActive : _tileBgInactive + border.color: isActive ? _tileRingActive : "transparent" + border.width: isActive ? 1 : 0 + antialiasing: true + opacity: enabled ? 1.0 : 0.6 + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: hoverTint(root.color) + opacity: mouseArea.pressed ? 0.3 : (mouseArea.containsMouse ? 0.2 : 0.0) + visible: opacity > 0 + antialiasing: true + Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } } + } + + DankIcon { + anchors.centerIn: parent + name: iconName + size: Theme.iconSize + color: isActive ? _tileIconActive : _tileIconInactive + rotation: iconRotation + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled + onClicked: root.clicked() + } + + Behavior on color { + ColorAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on radius { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/Widgets/ToggleButton.qml b/Modules/ControlCenter/Widgets/ToggleButton.qml index 58d35fbc..8b39bd2b 100644 --- a/Modules/ControlCenter/Widgets/ToggleButton.qml +++ b/Modules/ControlCenter/Widgets/ToggleButton.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Controls import Quickshell import qs.Common import qs.Widgets @@ -12,15 +11,27 @@ Rectangle { property bool isActive: false property bool enabled: true property string secondaryText: "" + property real iconRotation: 0 signal clicked() width: parent ? parent.width : 200 height: 60 - radius: Theme.cornerRadius - color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6) - border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) - border.width: 1 + radius: { + if (Theme.cornerRadius === 0) return 0 + return isActive ? Theme.cornerRadius : Theme.cornerRadius + 4 + } + + readonly property color _tileBgActive: Theme.primary + readonly property color _tileBgInactive: + Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, + Theme.getContentBackgroundAlpha() * 0.60) + readonly property color _tileRingActive: + Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22) + + color: isActive ? _tileBgActive : _tileBgInactive + border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) + border.width: isActive ? 1 : 1 opacity: enabled ? 1.0 : 0.6 function hoverTint(base) { @@ -52,8 +63,9 @@ Rectangle { DankIcon { name: root.iconName size: Theme.iconSize - color: Theme.primary + color: isActive ? Theme.primaryContainer : Theme.primary anchors.verticalCenter: parent.verticalCenter + rotation: root.iconRotation } Item { @@ -70,7 +82,7 @@ Rectangle { width: parent.width text: root.text font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText + color: isActive ? Theme.primaryContainer : Theme.surfaceText font.weight: Font.Medium elide: Text.ElideRight wrapMode: Text.NoWrap @@ -80,7 +92,7 @@ Rectangle { width: parent.width text: root.secondaryText font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText + color: isActive ? Theme.primaryContainer : Theme.surfaceVariantText visible: text.length > 0 elide: Text.ElideRight wrapMode: Text.NoWrap @@ -104,4 +116,11 @@ Rectangle { easing.type: Theme.standardEasing } } + + Behavior on radius { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } } \ No newline at end of file diff --git a/Modules/ControlCenter/utils/layout.js b/Modules/ControlCenter/utils/layout.js new file mode 100644 index 00000000..187a107e --- /dev/null +++ b/Modules/ControlCenter/utils/layout.js @@ -0,0 +1,45 @@ +function calculateRowsAndWidgets(controlCenterColumn, expandedSection, expandedWidgetIndex) { + var rows = [] + var currentRow = [] + var currentWidth = 0 + var expandedRow = -1 + + const widgets = SettingsData.controlCenterWidgets || [] + const baseWidth = controlCenterColumn.width + const spacing = Theme.spacingS + + for (var i = 0; i < widgets.length; i++) { + const widget = widgets[i] + const widgetWidth = widget.width || 50 + + var itemWidth + if (widgetWidth <= 25) { + itemWidth = (baseWidth - spacing * 3) / 4 + } else if (widgetWidth <= 50) { + itemWidth = (baseWidth - spacing) / 2 + } else if (widgetWidth <= 75) { + itemWidth = (baseWidth - spacing * 2) * 0.75 + } else { + itemWidth = baseWidth + } + + if (currentRow.length > 0 && (currentWidth + spacing + itemWidth > baseWidth)) { + rows.push([...currentRow]) + currentRow = [widget] + currentWidth = itemWidth + } else { + currentRow.push(widget) + currentWidth += (currentRow.length > 1 ? spacing : 0) + itemWidth + } + + if (widget.id === expandedSection && expandedWidgetIndex === i) { + expandedRow = rows.length + } + } + + if (currentRow.length > 0) { + rows.push(currentRow) + } + + return { rows: rows, expandedRowIndex: expandedRow } +} \ No newline at end of file diff --git a/Modules/ControlCenter/utils/state.js b/Modules/ControlCenter/utils/state.js new file mode 100644 index 00000000..b7240db3 --- /dev/null +++ b/Modules/ControlCenter/utils/state.js @@ -0,0 +1,25 @@ +function setTriggerPosition(root, x, y, width, section, screen) { + root.triggerX = x + root.triggerY = y + root.triggerWidth = width + root.triggerSection = section + root.triggerScreen = screen +} + +function openWithSection(root, section) { + if (root.shouldBeVisible) { + root.close() + } else { + root.expandedSection = section + root.open() + } +} + +function toggleSection(root, section) { + if (root.expandedSection === section) { + root.expandedSection = "" + root.expandedWidgetIndex = -1 + } else { + root.expandedSection = section + } +} \ No newline at end of file diff --git a/Modules/ControlCenter/utils/widgets.js b/Modules/ControlCenter/utils/widgets.js new file mode 100644 index 00000000..611de131 --- /dev/null +++ b/Modules/ControlCenter/utils/widgets.js @@ -0,0 +1,75 @@ +function getWidgetForId(baseWidgetDefinitions, widgetId) { + return baseWidgetDefinitions.find(w => w.id === widgetId) +} + +function addWidget(widgetId) { + var widgets = SettingsData.controlCenterWidgets.slice() + var widget = { + "id": widgetId, + "enabled": true, + "width": 50 + } + widgets.push(widget) + SettingsData.setControlCenterWidgets(widgets) +} + +function removeWidget(index) { + var widgets = SettingsData.controlCenterWidgets.slice() + if (index >= 0 && index < widgets.length) { + widgets.splice(index, 1) + SettingsData.setControlCenterWidgets(widgets) + } +} + +function toggleWidgetSize(index) { + var widgets = SettingsData.controlCenterWidgets.slice() + if (index >= 0 && index < widgets.length) { + const currentWidth = widgets[index].width || 50 + const id = widgets[index].id || "" + + if (id === "wifi" || id === "bluetooth" || id === "audioOutput" || id === "audioInput") { + widgets[index].width = currentWidth <= 50 ? 100 : 50 + } else { + if (currentWidth <= 25) { + widgets[index].width = 50 + } else if (currentWidth <= 50) { + widgets[index].width = 100 + } else { + widgets[index].width = 25 + } + } + + SettingsData.setControlCenterWidgets(widgets) + } +} + +function reorderWidgets(newOrder) { + SettingsData.setControlCenterWidgets(newOrder) +} + +function moveWidget(fromIndex, toIndex) { + let widgets = [...(SettingsData.controlCenterWidgets || [])] + if (fromIndex >= 0 && fromIndex < widgets.length && toIndex >= 0 && toIndex < widgets.length) { + const movedWidget = widgets.splice(fromIndex, 1)[0] + widgets.splice(toIndex, 0, movedWidget) + SettingsData.setControlCenterWidgets(widgets) + } +} + +function resetToDefault() { + const defaultWidgets = [ + {"id": "volumeSlider", "enabled": true, "width": 50}, + {"id": "brightnessSlider", "enabled": true, "width": 50}, + {"id": "wifi", "enabled": true, "width": 50}, + {"id": "bluetooth", "enabled": true, "width": 50}, + {"id": "audioOutput", "enabled": true, "width": 50}, + {"id": "audioInput", "enabled": true, "width": 50}, + {"id": "nightMode", "enabled": true, "width": 50}, + {"id": "darkMode", "enabled": true, "width": 50} + ] + SettingsData.setControlCenterWidgets(defaultWidgets) +} + +function clearAll() { + SettingsData.setControlCenterWidgets([]) +} \ No newline at end of file diff --git a/Modules/DankDash/MediaPlayerTab.qml b/Modules/DankDash/MediaPlayerTab.qml index 81e723f8..7dc1f728 100644 --- a/Modules/DankDash/MediaPlayerTab.qml +++ b/Modules/DankDash/MediaPlayerTab.qml @@ -677,6 +677,7 @@ Item { Item { width: parent.width height: 200 + clip: false DankAlbumArt { width: Math.min(parent.width * 0.8, parent.height * 0.9) diff --git a/Modules/DankDash/Overview/MediaOverviewCard.qml b/Modules/DankDash/Overview/MediaOverviewCard.qml index 92f9315a..8865dde6 100644 --- a/Modules/DankDash/Overview/MediaOverviewCard.qml +++ b/Modules/DankDash/Overview/MediaOverviewCard.qml @@ -8,6 +8,7 @@ import qs.Widgets Card { id: root + clip: false signal clicked() @@ -65,13 +66,20 @@ Card { spacing: Theme.spacingL visible: activePlayer - DankAlbumArt { - width: 110 - height: 80 + Item { + width: 140 + height: 110 anchors.horizontalCenter: parent.horizontalCenter - activePlayer: root.activePlayer - albumSize: 76 - animationScale: 1.05 + clip: false + + DankAlbumArt { + width: 110 + height: 80 + anchors.centerIn: parent + activePlayer: root.activePlayer + albumSize: 76 + animationScale: 1.05 + } } Column { diff --git a/Modules/Lock/LockScreenContent.qml b/Modules/Lock/LockScreenContent.qml index 37c62070..ebfba7f6 100644 --- a/Modules/Lock/LockScreenContent.qml +++ b/Modules/Lock/LockScreenContent.qml @@ -352,14 +352,22 @@ Item { } onActiveFocusChanged: { - if (!activeFocus && !demoMode && visible) { - Qt.callLater(() => forceActiveFocus()) + if (!activeFocus && !demoMode && visible && passwordField) { + Qt.callLater(() => { + if (passwordField && passwordField.forceActiveFocus) { + passwordField.forceActiveFocus() + } + }) } } onEnabledChanged: { - if (enabled && !demoMode && visible) { - Qt.callLater(() => forceActiveFocus()) + if (enabled && !demoMode && visible && passwordField) { + Qt.callLater(() => { + if (passwordField && passwordField.forceActiveFocus) { + passwordField.forceActiveFocus() + } + }) } } } diff --git a/Modules/Settings/PersonalizationTab.qml b/Modules/Settings/PersonalizationTab.qml index 025e315e..b6666ff8 100644 --- a/Modules/Settings/PersonalizationTab.qml +++ b/Modules/Settings/PersonalizationTab.qml @@ -946,15 +946,6 @@ Item { } } - DankToggle { - width: parent.width - text: "Hide Brightness Slider" - description: "Hide the brightness slider in Control Center and make audio slider full width" - checked: SettingsData.hideBrightnessSlider - onToggled: checked => { - SettingsData.setHideBrightnessSlider(checked) - } - } Rectangle { width: parent.width diff --git a/Modules/TopBar/Battery.qml b/Modules/TopBar/Battery.qml index f59858c8..75372e52 100644 --- a/Modules/TopBar/Battery.qml +++ b/Modules/TopBar/Battery.qml @@ -37,93 +37,7 @@ Rectangle { spacing: SettingsData.topBarNoBackground ? 1 : 2 DankIcon { - name: { - if (!BatteryService.batteryAvailable) { - return "power"; - } - - if (BatteryService.isCharging) { - if (BatteryService.batteryLevel >= 90) { - return "battery_charging_full"; - } - - if (BatteryService.batteryLevel >= 80) { - return "battery_charging_90"; - } - - if (BatteryService.batteryLevel >= 60) { - return "battery_charging_80"; - } - - if (BatteryService.batteryLevel >= 50) { - return "battery_charging_60"; - } - - if (BatteryService.batteryLevel >= 30) { - return "battery_charging_50"; - } - - if (BatteryService.batteryLevel >= 20) { - return "battery_charging_30"; - } - - return "battery_charging_20"; - } - // Check if plugged in but not charging (like at 80% charge limit) - if (BatteryService.isPluggedIn) { - if (BatteryService.batteryLevel >= 90) { - return "battery_charging_full"; - } - - if (BatteryService.batteryLevel >= 80) { - return "battery_charging_90"; - } - - if (BatteryService.batteryLevel >= 60) { - return "battery_charging_80"; - } - - if (BatteryService.batteryLevel >= 50) { - return "battery_charging_60"; - } - - if (BatteryService.batteryLevel >= 30) { - return "battery_charging_50"; - } - - if (BatteryService.batteryLevel >= 20) { - return "battery_charging_30"; - } - - return "battery_charging_20"; - } - // On battery power - if (BatteryService.batteryLevel >= 95) { - return "battery_full"; - } - - if (BatteryService.batteryLevel >= 85) { - return "battery_6_bar"; - } - - if (BatteryService.batteryLevel >= 70) { - return "battery_5_bar"; - } - - if (BatteryService.batteryLevel >= 55) { - return "battery_4_bar"; - } - - if (BatteryService.batteryLevel >= 40) { - return "battery_3_bar"; - } - - if (BatteryService.batteryLevel >= 25) { - return "battery_2_bar"; - } - - return "battery_1_bar"; - } + name: BatteryService.getBatteryIcon() size: Theme.iconSize - 6 color: { if (!BatteryService.batteryAvailable) { diff --git a/Modules/TopBar/TopBar.qml b/Modules/TopBar/TopBar.qml index 6d742a8b..3112ccda 100644 --- a/Modules/TopBar/TopBar.qml +++ b/Modules/TopBar/TopBar.qml @@ -546,47 +546,82 @@ PanelWindow { totalWidgets = 0 totalWidth = 0 + let configuredWidgets = 0 for (var i = 0; i < centerRepeater.count; i++) { const item = centerRepeater.itemAt(i) - if (item?.active && item.item) { - centerWidgets.push(item.item) - totalWidgets++ - totalWidth += item.item.width + if (item && topBarContent.getWidgetVisible(item.widgetId)) { + configuredWidgets++ + if (item.active && item.item) { + centerWidgets.push(item.item) + totalWidgets++ + totalWidth += item.item.width + } } } if (totalWidgets > 1) { totalWidth += spacing * (totalWidgets - 1) } - positionWidgets() + positionWidgets(configuredWidgets) } - function positionWidgets() { + function positionWidgets(configuredWidgets) { if (totalWidgets === 0 || width <= 0) { return } const parentCenterX = width / 2 - const isOdd = totalWidgets % 2 === 1 + const isOdd = configuredWidgets % 2 === 1 centerWidgets.forEach(widget => widget.anchors.horizontalCenter = undefined) if (isOdd) { - const middleIndex = Math.floor(totalWidgets / 2) - const middleWidget = centerWidgets[middleIndex] - middleWidget.x = parentCenterX - (middleWidget.width / 2) + const middleIndex = Math.floor(configuredWidgets / 2) + let currentActiveIndex = 0 + let middleWidget = null - let currentX = middleWidget.x - for (var i = middleIndex - 1; i >= 0; i--) { - currentX -= (spacing + centerWidgets[i].width) - centerWidgets[i].x = currentX + for (var i = 0; i < centerRepeater.count; i++) { + const item = centerRepeater.itemAt(i) + if (item && topBarContent.getWidgetVisible(item.widgetId)) { + if (currentActiveIndex === middleIndex && item.active && item.item) { + middleWidget = item.item + break + } + currentActiveIndex++ + } } - currentX = middleWidget.x + middleWidget.width - for (var i = middleIndex + 1; i < totalWidgets; i++) { - currentX += spacing - centerWidgets[i].x = currentX - currentX += centerWidgets[i].width + if (middleWidget) { + middleWidget.x = parentCenterX - (middleWidget.width / 2) + + let leftWidgets = [] + let rightWidgets = [] + let foundMiddle = false + + for (var i = 0; i < centerWidgets.length; i++) { + if (centerWidgets[i] === middleWidget) { + foundMiddle = true + continue + } + if (!foundMiddle) { + leftWidgets.push(centerWidgets[i]) + } else { + rightWidgets.push(centerWidgets[i]) + } + } + + let currentX = middleWidget.x + for (var i = leftWidgets.length - 1; i >= 0; i--) { + currentX -= (spacing + leftWidgets[i].width) + leftWidgets[i].x = currentX + } + + currentX = middleWidget.x + middleWidget.width + for (var i = 0; i < rightWidgets.length; i++) { + currentX += spacing + rightWidgets[i].x = currentX + currentX += rightWidgets[i].width + } } } else { const leftIndex = (totalWidgets / 2) - 1 diff --git a/Services/BatteryService.qml b/Services/BatteryService.qml index 2df75cf8..4023ca8b 100644 --- a/Services/BatteryService.qml +++ b/Services/BatteryService.qml @@ -80,4 +80,72 @@ Singleton { return `${minutes}m` } + + function getBatteryIcon() { + if (!batteryAvailable) { + return "power" + } + + if (isCharging) { + if (batteryLevel >= 90) { + return "battery_charging_full" + } + if (batteryLevel >= 80) { + return "battery_charging_90" + } + if (batteryLevel >= 60) { + return "battery_charging_80" + } + if (batteryLevel >= 50) { + return "battery_charging_60" + } + if (batteryLevel >= 30) { + return "battery_charging_50" + } + if (batteryLevel >= 20) { + return "battery_charging_30" + } + return "battery_charging_20" + } + if (isPluggedIn) { + if (batteryLevel >= 90) { + return "battery_charging_full" + } + if (batteryLevel >= 80) { + return "battery_charging_90" + } + if (batteryLevel >= 60) { + return "battery_charging_80" + } + if (batteryLevel >= 50) { + return "battery_charging_60" + } + if (batteryLevel >= 30) { + return "battery_charging_50" + } + if (batteryLevel >= 20) { + return "battery_charging_30" + } + return "battery_charging_20" + } + if (batteryLevel >= 95) { + return "battery_full" + } + if (batteryLevel >= 85) { + return "battery_6_bar" + } + if (batteryLevel >= 70) { + return "battery_5_bar" + } + if (batteryLevel >= 55) { + return "battery_4_bar" + } + if (batteryLevel >= 40) { + return "battery_3_bar" + } + if (batteryLevel >= 25) { + return "battery_2_bar" + } + return "battery_1_bar" + } } diff --git a/Shaders/frag/wp_iris_bloom.frag b/Shaders/frag/wp_iris_bloom.frag index e06e44b5..56275b07 100644 --- a/Shaders/frag/wp_iris_bloom.frag +++ b/Shaders/frag/wp_iris_bloom.frag @@ -4,20 +4,19 @@ layout(location = 0) in vec2 qt_TexCoord0; layout(location = 0) out vec4 fragColor; -layout(binding = 1) uniform sampler2D source1; // Current wallpaper -layout(binding = 2) uniform sampler2D source2; // Next wallpaper +layout(binding = 1) uniform sampler2D source1; +layout(binding = 2) uniform sampler2D source2; layout(std140, binding = 0) uniform buf { mat4 qt_Matrix; float qt_Opacity; - float progress; // 0.0 -> 1.0 - float centerX; // 0..1 - float centerY; // 0..1 - float smoothness; // 0..1 (edge softness) - float aspectRatio; // width / height + float progress; + float centerX; + float centerY; + float smoothness; + float aspectRatio; - // Fill mode parameters - float fillMode; // 0=no(center), 1=crop(fill), 2=fit(contain), 3=stretch + float fillMode; float imageWidth1; float imageHeight1; float imageWidth2; @@ -29,7 +28,6 @@ layout(std140, binding = 0) uniform buf { vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { vec2 transformedUV = uv; - if (ubuf.fillMode < 0.5) { vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; @@ -50,8 +48,6 @@ vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { vec2 imagePixel = (screenPixel - offset) / scale; transformedUV = imagePixel / vec2(imgWidth, imgHeight); } - // else stretch - return transformedUV; } @@ -65,34 +61,34 @@ vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight) void main() { vec2 uv = qt_TexCoord0; - vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1); vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2); - // Edge softness mapping float edgeSoft = mix(0.001, 0.45, ubuf.smoothness * ubuf.smoothness); - // Aspect-corrected coordinates so the iris stays circular vec2 center = vec2(ubuf.centerX, ubuf.centerY); vec2 acUv = vec2(uv.x * ubuf.aspectRatio, uv.y); vec2 acCenter = vec2(center.x * ubuf.aspectRatio, center.y); - float dist = length(acUv - acCenter); + vec2 q = acUv - acCenter; - // Max radius needed to cover the screen from the chosen center - float maxDistX = max(center.x * ubuf.aspectRatio, (1.0 - center.x) * ubuf.aspectRatio); - float maxDistY = max(center.y, 1.0 - center.y); - float maxDist = length(vec2(maxDistX, maxDistY)); + float maxX = max(center.x * ubuf.aspectRatio, (1.0 - center.x) * ubuf.aspectRatio); + float maxY = max(center.y, 1.0 - center.y); + float maxDist = length(vec2(maxX, maxY)); float p = ubuf.progress; p = p * p * (3.0 - 2.0 * p); - float radius = p * maxDist - edgeSoft; + float radius = p * maxDist; - // Soft circular edge: inside -> color2 (new), outside -> color1 (old) + // squash factor for the "eye" slit + float squash = mix(0.2, 1.0, p); + q.y /= squash; + + float dist = length(q); float t = smoothstep(radius - edgeSoft, radius + edgeSoft, dist); + vec4 col = mix(color2, color1, t); - // Exact snaps at ends if (ubuf.progress <= 0.0) col = color1; if (ubuf.progress >= 1.0) col = color2;