1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00

ControlCenter: Implement edit mode for customizing widgets

This commit is contained in:
bbedward
2025-09-23 14:38:01 -04:00
parent b9b1737639
commit c04177e45d
32 changed files with 2870 additions and 796 deletions

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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 {}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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 }
}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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?")
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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,36 +24,25 @@ 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)
@@ -73,13 +64,17 @@ 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
@@ -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: {
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()
}
}
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()
}
}
}
}
}
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: {
expanded: root.powerOptionsExpanded
onPowerActionRequested: (action, title, message) => {
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
root.powerActionRequested(action, title, message)
}
}
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?")
}
}
}
}
}
}
Item {
WidgetGrid {
id: widgetGrid
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
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 {
width: (parent.width - Theme.spacingM) / 2
height: parent.height
visible: !SettingsData.hideBrightnessSlider
BrightnessSliderRow {
EditControls {
width: parent.width
height: parent.height
x: -Theme.spacingS
}
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()
}
}
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()
}
}
}
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 {}
}
}

View File

@@ -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

View File

@@ -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)
@@ -33,14 +38,91 @@ Rectangle {
}
}
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

View File

@@ -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
}
}
}
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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:

View File

@@ -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
}
}
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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 }
}

View File

@@ -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
}
}

View File

@@ -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([])
}

View File

@@ -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)

View File

@@ -8,6 +8,7 @@ import qs.Widgets
Card {
id: root
clip: false
signal clicked()
@@ -65,14 +66,21 @@ Card {
spacing: Theme.spacingL
visible: activePlayer
Item {
width: 140
height: 110
anchors.horizontalCenter: parent.horizontalCenter
clip: false
DankAlbumArt {
width: 110
height: 80
anchors.horizontalCenter: parent.horizontalCenter
anchors.centerIn: parent
activePlayer: root.activePlayer
albumSize: 76
animationScale: 1.05
}
}
Column {
width: parent.width

View File

@@ -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()
}
})
}
}
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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) {
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]
const middleIndex = Math.floor(configuredWidgets / 2)
let currentActiveIndex = 0
let middleWidget = null
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++
}
}
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 = middleIndex - 1; i >= 0; i--) {
currentX -= (spacing + centerWidgets[i].width)
centerWidgets[i].x = currentX
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 = middleIndex + 1; i < totalWidgets; i++) {
for (var i = 0; i < rightWidgets.length; i++) {
currentX += spacing
centerWidgets[i].x = currentX
currentX += centerWidgets[i].width
rightWidgets[i].x = currentX
currentX += rightWidgets[i].width
}
}
} else {
const leftIndex = (totalWidgets / 2) - 1

View File

@@ -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"
}
}

View File

@@ -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;