1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 21:42:51 -05:00

switch hto monorepo structure

This commit is contained in:
bbedward
2025-11-12 17:18:45 -05:00
parent 6013c994a6
commit 24e800501a
768 changed files with 76284 additions and 221 deletions

View File

@@ -0,0 +1,853 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
DankPopout {
id: appDrawerPopout
layerNamespace: "dms:app-launcher"
property var triggerScreen: null
// Setting to Exclusive, so virtual keyboards can send input to app drawer
WlrLayershell.keyboardFocus: shouldBeVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
function show() {
open()
}
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x
triggerY = y
triggerWidth = width
triggerSection = section
triggerScreen = screen
}
popupWidth: 520
popupHeight: 600
triggerX: Theme.spacingL
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
triggerWidth: 40
positioning: ""
screen: triggerScreen
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
appLauncher.searchQuery = ""
appLauncher.selectedIndex = 0
appLauncher.setCategory(I18n.tr("All"))
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""
contentLoader.item.searchField.forceActiveFocus()
}
})
}
}
AppLauncher {
id: appLauncher
viewMode: SettingsData.appLauncherViewMode
gridColumns: 4
onAppLaunched: appDrawerPopout.close()
onViewModeSelected: function (mode) {
SettingsData.set("appLauncherViewMode", mode)
}
}
content: Component {
Rectangle {
id: launcherPanel
property alias searchField: searchField
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
antialiasing: true
smooth: true
// Multi-layer border effect
Repeater {
model: [{
"margin": -3,
"color": Qt.rgba(0, 0, 0, 0.05),
"z": -3
}, {
"margin": -2,
"color": Qt.rgba(0, 0, 0, 0.08),
"z": -2
}, {
"margin": 0,
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
"z": -1
}]
Rectangle {
anchors.fill: parent
anchors.margins: modelData.margin
color: "transparent"
radius: parent.radius + Math.abs(modelData.margin)
border.color: modelData.color
border.width: 0
z: modelData.z
}
}
Item {
id: keyHandler
anchors.fill: parent
focus: true
readonly property var keyMappings: {
const mappings = {}
mappings[Qt.Key_Escape] = () => appDrawerPopout.close()
mappings[Qt.Key_Down] = () => appLauncher.selectNext()
mappings[Qt.Key_Up] = () => appLauncher.selectPrevious()
mappings[Qt.Key_Return] = () => appLauncher.launchSelected()
mappings[Qt.Key_Enter] = () => appLauncher.launchSelected()
mappings[Qt.Key_Tab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectNextInRow() : appLauncher.selectNext()
mappings[Qt.Key_Backtab] = () => appLauncher.viewMode === "grid" ? appLauncher.selectPreviousInRow() : appLauncher.selectPrevious()
if (appLauncher.viewMode === "grid") {
mappings[Qt.Key_Right] = () => appLauncher.selectNextInRow()
mappings[Qt.Key_Left] = () => appLauncher.selectPreviousInRow()
}
return mappings
}
Keys.onPressed: function (event) {
if (keyMappings[event.key]) {
keyMappings[event.key]()
event.accepted = true
return
}
if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext()
event.accepted = true
return
}
if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious()
event.accepted = true
return
}
if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNext()
event.accepted = true
return
}
if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPrevious()
event.accepted = true
return
}
if (appLauncher.viewMode === "grid") {
if (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier) {
appLauncher.selectNextInRow()
event.accepted = true
return
}
if (event.key === Qt.Key_H && event.modifiers & Qt.ControlModifier) {
appLauncher.selectPreviousInRow()
event.accepted = true
return
}
}
}
Column {
width: parent.width - Theme.spacingS * 2
height: parent.height - Theme.spacingS * 2
x: Theme.spacingS
y: Theme.spacingS
spacing: Theme.spacingS
Item {
width: parent.width
height: 40
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Applications")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: appLauncher.model.count + " apps"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
DankTextField {
id: searchField
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
height: 52
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeLarge
enabled: appDrawerPopout.shouldBeVisible
ignoreLeftRightKeys: appLauncher.viewMode !== "list"
ignoreTabKeys: true
keyForwardTargets: [keyHandler]
onTextEdited: {
appLauncher.searchQuery = text
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
appDrawerPopout.close()
event.accepted = true
return
}
const isEnterKey = [Qt.Key_Return, Qt.Key_Enter].includes(event.key)
const hasText = text.length > 0
if (isEnterKey && hasText) {
if (appLauncher.keyboardNavigationActive && appLauncher.model.count > 0) {
appLauncher.launchSelected()
} else if (appLauncher.model.count > 0) {
appLauncher.launchApp(appLauncher.model.get(0))
}
event.accepted = true
return
}
const navigationKeys = [Qt.Key_Down, Qt.Key_Up, Qt.Key_Left, Qt.Key_Right, Qt.Key_Tab, Qt.Key_Backtab]
const isNavigationKey = navigationKeys.includes(event.key)
const isEmptyEnter = isEnterKey && !hasText
event.accepted = !(isNavigationKey || isEmptyEnter)
}
Connections {
function onShouldBeVisibleChanged() {
if (!appDrawerPopout.shouldBeVisible) {
searchField.focus = false
}
}
target: appDrawerPopout
}
}
Row {
width: parent.width
height: 40
spacing: Theme.spacingM
visible: searchField.text.length === 0
leftPadding: Theme.spacingS
Rectangle {
width: 180
height: 40
radius: Theme.cornerRadius
color: "transparent"
DankDropdown {
anchors.fill: parent
text: ""
dropdownWidth: 180
currentValue: appLauncher.selectedCategory
options: appLauncher.categories
optionIcons: appLauncher.categoryIcons
onValueChanged: function (value) {
appLauncher.setCategory(value)
}
}
}
Item {
width: parent.width - 290
height: 1
}
Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
DankActionButton {
buttonSize: 36
circular: false
iconName: "view_list"
iconSize: 20
iconColor: appLauncher.viewMode === "list" ? Theme.primary : Theme.surfaceText
backgroundColor: appLauncher.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("list")
}
}
DankActionButton {
buttonSize: 36
circular: false
iconName: "grid_view"
iconSize: 20
iconColor: appLauncher.viewMode === "grid" ? Theme.primary : Theme.surfaceText
backgroundColor: appLauncher.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
onClicked: {
appLauncher.setViewMode("grid")
}
}
}
}
Rectangle {
width: parent.width
height: {
let usedHeight = 40 + Theme.spacingS
usedHeight += 52 + Theme.spacingS
usedHeight += (searchField.text.length === 0 ? 40 : 0)
return parent.height - usedHeight
}
radius: Theme.cornerRadius
color: "transparent"
DankListView {
id: appList
property int itemHeight: 72
property int iconSize: 56
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
var itemY = index * (itemHeight + itemSpacing)
var itemBottom = itemY + itemHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData)
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false
}
delegate: AppLauncherListDelegate {
listView: appList
itemHeight: appList.itemHeight
iconSize: appList.iconSize
showDescription: appList.showDescription
hoverUpdatesSelection: appList.hoverUpdatesSelection
keyboardNavigationActive: appList.keyboardNavigationActive
isCurrentItem: ListView.isCurrentItem
mouseAreaLeftMargin: Theme.spacingS
mouseAreaRightMargin: Theme.spacingS
mouseAreaBottomMargin: Theme.spacingM
iconMargins: Theme.spacingXS
iconFallbackLeftMargin: Theme.spacingS
iconFallbackRightMargin: Theme.spacingS
iconFallbackBottomMargin: Theme.spacingM
onItemClicked: (idx, modelData) => appList.itemClicked(idx, modelData)
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
const panelPos = contextMenu.parent.mapFromItem(null, mouseX, mouseY)
appList.itemRightClicked(idx, modelData, panelPos.x, panelPos.y)
}
onKeyboardNavigationReset: appList.keyboardNavigationReset
}
}
DankGridView {
id: appGrid
property int currentIndex: appLauncher.selectedIndex
property int columns: 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property int cellPadding: 8
property real iconSizeRatio: 0.6
property int maxIconSize: 56
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset
signal itemClicked(int index, var modelData)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return
var itemY = Math.floor(index / actualColumns) * cellHeight
var itemBottom = itemY + cellHeight
if (itemY < contentY)
contentY = itemY
else if (itemBottom > contentY + height)
contentY = itemBottom - height
}
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
rightMargin: leftMargin
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex)
}
onItemClicked: function (index, modelData) {
appLauncher.launchApp(modelData)
}
onItemRightClicked: function (index, modelData, mouseX, mouseY) {
contextMenu.show(mouseX, mouseY, modelData)
}
onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false
}
delegate: AppLauncherGridDelegate {
gridView: appGrid
cellWidth: appGrid.cellWidth
cellHeight: appGrid.cellHeight
cellPadding: appGrid.cellPadding
minIconSize: appGrid.minIconSize
maxIconSize: appGrid.maxIconSize
iconSizeRatio: appGrid.iconSizeRatio
hoverUpdatesSelection: appGrid.hoverUpdatesSelection
keyboardNavigationActive: appGrid.keyboardNavigationActive
currentIndex: appGrid.currentIndex
mouseAreaLeftMargin: Theme.spacingS
mouseAreaRightMargin: Theme.spacingS
mouseAreaBottomMargin: Theme.spacingS
iconFallbackLeftMargin: Theme.spacingS
iconFallbackRightMargin: Theme.spacingS
iconFallbackBottomMargin: Theme.spacingS
iconMaterialSizeAdjustment: Theme.spacingL
onItemClicked: (idx, modelData) => appGrid.itemClicked(idx, modelData)
onItemRightClicked: (idx, modelData, mouseX, mouseY) => {
const panelPos = contextMenu.parent.mapFromItem(null, mouseX, mouseY)
appGrid.itemRightClicked(idx, modelData, panelPos.x, panelPos.y)
}
onKeyboardNavigationReset: appGrid.keyboardNavigationReset
}
}
}
}
}
}
}
Popup {
id: contextMenu
property var currentApp: null
readonly property var desktopEntry: (currentApp && !currentApp.isPlugin && appLauncher && appLauncher._uniqueApps && currentApp.appIndex >= 0 && currentApp.appIndex < appLauncher._uniqueApps.length) ? appLauncher._uniqueApps[currentApp.appIndex] : null
readonly property string appId: desktopEntry ? (desktopEntry.id || desktopEntry.execString || "") : ""
readonly property bool isPinned: appId && SessionData.isPinnedApp(appId)
function show(x, y, app) {
currentApp = app
contextMenu.x = x + 4
contextMenu.y = y + 4
contextMenu.open()
}
function hide() {
contextMenu.close()
}
width: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
closePolicy: Popup.CloseOnPressOutside
modal: false
dim: false
background: Rectangle {
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: pinMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: contextMenu.isPinned ? "keep_off" : "push_pin"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: contextMenu.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: pinMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!contextMenu.desktopEntry) {
return
}
if (contextMenu.isPinned) {
SessionData.removePinnedApp(contextMenu.appId)
} else {
SessionData.addPinnedApp(contextMenu.appId)
}
contextMenu.hide()
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Repeater {
model: contextMenu.desktopEntry && contextMenu.desktopEntry.actions ? contextMenu.desktopEntry.actions : []
Rectangle {
width: Math.max(parent.width, actionRow.implicitWidth + Theme.spacingS * 2)
height: 32
radius: Theme.cornerRadius
color: actionMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
id: actionRow
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize - 2
height: Theme.iconSize - 2
visible: modelData.icon && modelData.icon !== ""
IconImage {
anchors.fill: parent
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
}
StyledText {
text: modelData.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: actionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && contextMenu.desktopEntry) {
SessionService.launchDesktopAction(contextMenu.desktopEntry, modelData)
if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}
contextMenu.hide()
}
}
}
}
Rectangle {
visible: contextMenu.desktopEntry && contextMenu.desktopEntry.actions && contextMenu.desktopEntry.actions.length > 0
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: launchMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "launch"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: launchMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.currentApp)
appLauncher.launchApp(contextMenu.currentApp)
contextMenu.hide()
}
}
}
Rectangle {
visible: SessionService.hasPrimeRun
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
visible: SessionService.hasPrimeRun
width: parent.width
height: 32
radius: Theme.cornerRadius
color: primeRunMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "memory"
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Launch on dGPU")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: primeRunMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenu.desktopEntry) {
SessionService.launchDesktopEntry(contextMenu.desktopEntry, true)
if (contextMenu.currentApp) {
appLauncher.appLaunched(contextMenu.currentApp)
}
}
contextMenu.hide()
}
}
}
}
}
MouseArea {
anchors.fill: parent
visible: contextMenu.visible
z: 999
onClicked: {
contextMenu.hide()
}
MouseArea {
x: contextMenu.x
y: contextMenu.y
width: contextMenu.width
height: contextMenu.height
onClicked: {
// Prevent closing when clicking on the menu itself
}
}
}
}

View File

@@ -0,0 +1,343 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
// DEVELOPER NOTE: This component manages the AppDrawer launcher (accessed via DankBar icon).
// Changes to launcher behavior, especially item rendering, filtering, or model structure,
// likely require corresponding updates in Modals/Spotlight/SpotlightResults.qml and vice versa.
property string searchQuery: ""
property string selectedCategory: I18n.tr("All")
property string viewMode: "list" // "list" or "grid"
property int selectedIndex: 0
property int maxResults: 50
property int gridColumns: 4
property bool debounceSearch: true
property int debounceInterval: 50
property bool keyboardNavigationActive: false
property bool suppressUpdatesWhileLaunching: false
property var categories: {
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
const result = [I18n.tr("All")]
return result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
}
readonly property var categoryIcons: categories.map(category => AppSearchService.getCategoryIcon(category))
property var appUsageRanking: AppUsageHistoryData.appUsageRanking || {}
property alias model: filteredModel
property var _watchApplications: AppSearchService.applications
property var _uniqueApps: []
property bool _isTriggered: false
property string _triggeredCategory: ""
property bool _updatingFromTrigger: false
signal appLaunched(var app)
signal categorySelected(string category)
signal viewModeSelected(string mode)
function updateCategories() {
const allCategories = AppSearchService.getAllCategories().filter(cat => cat !== "Education" && cat !== "Science")
const result = [I18n.tr("All")]
categories = result.concat(allCategories.filter(cat => cat !== I18n.tr("All")))
}
Connections {
target: PluginService
function onPluginLoaded() { updateCategories() }
function onPluginUnloaded() { updateCategories() }
function onPluginListUpdated() { updateCategories() }
}
Connections {
target: SettingsData
function onSortAppsAlphabeticallyChanged() {
updateFilteredModel()
}
}
function updateFilteredModel() {
if (suppressUpdatesWhileLaunching) {
suppressUpdatesWhileLaunching = false
return
}
filteredModel.clear()
selectedIndex = 0
keyboardNavigationActive = false
const triggerResult = checkPluginTriggers(searchQuery)
if (triggerResult.triggered) {
console.log("AppLauncher: Plugin trigger detected:", triggerResult.trigger, "for plugin:", triggerResult.pluginId)
}
let apps = []
const allCategory = I18n.tr("All")
const emptyTriggerPlugins = typeof PluginService !== "undefined" ? PluginService.getPluginsWithEmptyTrigger() : []
if (triggerResult.triggered) {
_isTriggered = true
_triggeredCategory = triggerResult.pluginCategory
_updatingFromTrigger = true
selectedCategory = triggerResult.pluginCategory
_updatingFromTrigger = false
apps = AppSearchService.getPluginItems(triggerResult.pluginCategory, triggerResult.query)
} else {
if (_isTriggered) {
_updatingFromTrigger = true
selectedCategory = allCategory
_updatingFromTrigger = false
_isTriggered = false
_triggeredCategory = ""
}
if (searchQuery.length === 0) {
if (selectedCategory === allCategory) {
let emptyTriggerItems = []
emptyTriggerPlugins.forEach(pluginId => {
const plugin = PluginService.getLauncherPlugin(pluginId)
const pluginCategory = plugin.name || pluginId
const items = AppSearchService.getPluginItems(pluginCategory, "")
emptyTriggerItems = emptyTriggerItems.concat(items)
})
apps = AppSearchService.applications.concat(emptyTriggerItems)
} else {
apps = AppSearchService.getAppsInCategory(selectedCategory).slice(0, maxResults)
}
} else {
if (selectedCategory === allCategory) {
apps = AppSearchService.searchApplications(searchQuery)
let emptyTriggerItems = []
emptyTriggerPlugins.forEach(pluginId => {
const plugin = PluginService.getLauncherPlugin(pluginId)
const pluginCategory = plugin.name || pluginId
const items = AppSearchService.getPluginItems(pluginCategory, searchQuery)
emptyTriggerItems = emptyTriggerItems.concat(items)
})
apps = apps.concat(emptyTriggerItems)
} else {
const categoryApps = AppSearchService.getAppsInCategory(selectedCategory)
if (categoryApps.length > 0) {
const allSearchResults = AppSearchService.searchApplications(searchQuery)
const categoryNames = new Set(categoryApps.map(app => app.name))
apps = allSearchResults.filter(searchApp => categoryNames.has(searchApp.name)).slice(0, maxResults)
} else {
apps = []
}
}
}
}
if (searchQuery.length === 0) {
if (SettingsData.sortAppsAlphabetically) {
apps = apps.sort((a, b) => {
return (a.name || "").localeCompare(b.name || "")
})
} else {
apps = apps.sort((a, b) => {
const aId = a.id || a.execString || a.exec || ""
const bId = b.id || b.execString || b.exec || ""
const aUsage = appUsageRanking[aId] ? appUsageRanking[aId].usageCount : 0
const bUsage = appUsageRanking[bId] ? appUsageRanking[bId].usageCount : 0
if (aUsage !== bUsage) {
return bUsage - aUsage
}
return (a.name || "").localeCompare(b.name || "")
})
}
}
const seenNames = new Set()
const uniqueApps = []
apps.forEach(app => {
if (app) {
const itemKey = app.name + "|" + (app.execString || app.exec || app.action || "")
if (seenNames.has(itemKey)) {
return
}
seenNames.add(itemKey)
uniqueApps.push(app)
const isPluginItem = app.action !== undefined
filteredModel.append({
"name": app.name || "",
"exec": app.execString || app.exec || app.action || "",
"icon": app.icon !== undefined ? app.icon : (isPluginItem ? "" : "application-x-executable"),
"comment": app.comment || "",
"categories": app.categories || [],
"isPlugin": isPluginItem,
"appIndex": uniqueApps.length - 1
})
}
})
root._uniqueApps = uniqueApps
}
function selectNext() {
if (filteredModel.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = viewMode === "grid" ? Math.min(selectedIndex + gridColumns, filteredModel.count - 1) : Math.min(selectedIndex + 1, filteredModel.count - 1)
}
function selectPrevious() {
if (filteredModel.count === 0) {
return
}
keyboardNavigationActive = true
selectedIndex = viewMode === "grid" ? Math.max(selectedIndex - gridColumns, 0) : Math.max(selectedIndex - 1, 0)
}
function selectNextInRow() {
if (filteredModel.count === 0 || viewMode !== "grid") {
return
}
keyboardNavigationActive = true
selectedIndex = Math.min(selectedIndex + 1, filteredModel.count - 1)
}
function selectPreviousInRow() {
if (filteredModel.count === 0 || viewMode !== "grid") {
return
}
keyboardNavigationActive = true
selectedIndex = Math.max(selectedIndex - 1, 0)
}
function launchSelected() {
if (filteredModel.count === 0 || selectedIndex < 0 || selectedIndex >= filteredModel.count) {
return
}
const selectedApp = filteredModel.get(selectedIndex)
launchApp(selectedApp)
}
function launchApp(appData) {
if (!appData || typeof appData.appIndex === "undefined" || appData.appIndex < 0 || appData.appIndex >= _uniqueApps.length) {
return
}
suppressUpdatesWhileLaunching = true
const actualApp = _uniqueApps[appData.appIndex]
if (appData.isPlugin) {
const pluginId = getPluginIdForItem(actualApp)
if (pluginId) {
AppSearchService.executePluginItem(actualApp, pluginId)
appLaunched(appData)
return
}
} else {
SessionService.launchDesktopEntry(actualApp)
appLaunched(appData)
AppUsageHistoryData.addAppUsage(actualApp)
}
}
function setCategory(category) {
selectedCategory = category
categorySelected(category)
}
function setViewMode(mode) {
viewMode = mode
viewModeSelected(mode)
}
onSearchQueryChanged: {
if (debounceSearch) {
searchDebounceTimer.restart()
} else {
updateFilteredModel()
}
}
onSelectedCategoryChanged: {
if (_updatingFromTrigger) {
return
}
updateFilteredModel()
}
onAppUsageRankingChanged: updateFilteredModel()
on_WatchApplicationsChanged: updateFilteredModel()
Component.onCompleted: {
updateFilteredModel()
}
ListModel {
id: filteredModel
}
Timer {
id: searchDebounceTimer
interval: root.debounceInterval
repeat: false
onTriggered: updateFilteredModel()
}
// Plugin trigger system functions
function checkPluginTriggers(query) {
if (!query || typeof PluginService === "undefined") {
return { triggered: false, pluginCategory: "", query: "" }
}
const triggers = PluginService.getAllPluginTriggers()
for (const trigger in triggers) {
if (query.startsWith(trigger)) {
const pluginId = triggers[trigger]
const plugin = PluginService.getLauncherPlugin(pluginId)
if (plugin) {
const remainingQuery = query.substring(trigger.length).trim()
const result = {
triggered: true,
pluginId: pluginId,
pluginCategory: plugin.name || pluginId,
query: remainingQuery,
trigger: trigger
}
return result
}
}
}
return { triggered: false, pluginCategory: "", query: "" }
}
function getPluginIdForItem(item) {
if (!item || !item.categories || typeof PluginService === "undefined") {
return null
}
const launchers = PluginService.getLauncherPlugins()
for (const pluginId in launchers) {
const plugin = launchers[pluginId]
const pluginCategory = plugin.name || pluginId
let hasCategory = false
if (Array.isArray(item.categories)) {
hasCategory = item.categories.includes(pluginCategory)
} else if (item.categories && typeof item.categories.count !== "undefined") {
for (let i = 0; i < item.categories.count; i++) {
if (item.categories.get(i) === pluginCategory) {
hasCategory = true
break
}
}
}
if (hasCategory) {
return pluginId
}
}
return null
}
}

View File

@@ -0,0 +1,142 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var categories: []
property string selectedCategory: I18n.tr("All")
property bool compact: false
signal categorySelected(string category)
readonly property int maxCompactItems: 8
readonly property int itemHeight: 36
readonly property color selectedBorderColor: "transparent"
readonly property color unselectedBorderColor: "transparent"
function handleCategoryClick(category) {
categorySelected(category)
}
function getButtonWidth(itemCount, containerWidth) {
return itemCount > 0 ? (containerWidth - (itemCount - 1) * Theme.spacingS) / itemCount : 0
}
height: compact ? itemHeight : (itemHeight * 2 + Theme.spacingS)
Row {
visible: compact
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories ? categories.slice(0, Math.min(categories.length || 0, maxCompactItems)) : []
Rectangle {
property int itemCount: Math.min(categories ? categories.length || 0 : 0, maxCompactItems)
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.handleCategoryClick(modelData)
}
}
}
}
Column {
visible: !compact
width: parent.width
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
Repeater {
model: categories ? categories.slice(0, Math.min(4, categories.length || 0)) : []
Rectangle {
property int itemCount: Math.min(4, categories ? categories.length || 0 : 0)
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.handleCategoryClick(modelData)
}
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: categories && categories.length > 4
Repeater {
model: categories && categories.length > 4 ? categories.slice(4) : []
Rectangle {
property int itemCount: categories && categories.length > 4 ? categories.length - 4 : 0
height: root.itemHeight
width: root.getButtonWidth(itemCount, parent.width)
radius: Theme.cornerRadius
color: selectedCategory === modelData ? Theme.primary : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: selectedCategory === modelData ? selectedBorderColor : unselectedBorderColor
StyledText {
anchors.centerIn: parent
text: modelData
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
font.pixelSize: Theme.fontSizeMedium
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
elide: Text.ElideRight
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.handleCategoryClick(modelData)
}
}
}
}
}
}

View File

@@ -0,0 +1,233 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Io
import qs.Common
import qs.Widgets
import qs.Modules
import qs.Services
Variants {
model: {
if (SessionData.isGreeterMode) {
return Quickshell.screens
}
return SettingsData.getFilteredScreens("wallpaper")
}
PanelWindow {
id: blurWallpaperWindow
required property var modelData
screen: modelData
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.namespace: "dms:blurwallpaper"
WlrLayershell.exclusionMode: ExclusionMode.Ignore
anchors.top: true
anchors.bottom: true
anchors.left: true
anchors.right: true
color: "transparent"
Item {
id: root
anchors.fill: parent
property string source: SessionData.getMonitorWallpaper(modelData.name) || ""
property bool isColorSource: source.startsWith("#")
Connections {
target: SessionData
function onIsLightModeChanged() {
if (SessionData.perModeWallpaper) {
var newSource = SessionData.getMonitorWallpaper(modelData.name) || ""
if (newSource !== root.source) {
root.source = newSource
}
}
}
}
function getFillMode(modeName) {
switch (modeName) {
case "Stretch":
return Image.Stretch
case "Fit":
case "PreserveAspectFit":
return Image.PreserveAspectFit
case "Fill":
case "PreserveAspectCrop":
return Image.PreserveAspectCrop
case "Tile":
return Image.Tile
case "TileVertically":
return Image.TileVertically
case "TileHorizontally":
return Image.TileHorizontally
case "Pad":
return Image.Pad
default:
return Image.PreserveAspectCrop
}
}
Component.onCompleted: {
if (source) {
const formattedSource = source.startsWith("file://") ? source : "file://" + source
setWallpaperImmediate(formattedSource)
}
isInitialized = true
}
property bool isInitialized: false
property real transitionProgress: 0
readonly property bool transitioning: transitionAnimation.running
onSourceChanged: {
const isColor = source.startsWith("#")
if (!source) {
setWallpaperImmediate("")
} else if (isColor) {
setWallpaperImmediate("")
} else {
if (!isInitialized || !currentWallpaper.source) {
setWallpaperImmediate(source.startsWith("file://") ? source : "file://" + source)
isInitialized = true
} else if (CompositorService.isNiri && SessionData.isSwitchingMode) {
setWallpaperImmediate(source.startsWith("file://") ? source : "file://" + source)
} else {
changeWallpaper(source.startsWith("file://") ? source : "file://" + source)
}
}
}
function setWallpaperImmediate(newSource) {
transitionAnimation.stop()
root.transitionProgress = 0.0
currentWallpaper.source = newSource
nextWallpaper.source = ""
currentWallpaper.opacity = 1
nextWallpaper.opacity = 0
}
function changeWallpaper(newPath) {
if (newPath === currentWallpaper.source)
return
if (!newPath || newPath.startsWith("#"))
return
if (root.transitioning) {
transitionAnimation.stop()
root.transitionProgress = 0
currentWallpaper.source = nextWallpaper.source
nextWallpaper.source = ""
}
if (!currentWallpaper.source) {
setWallpaperImmediate(newPath)
return
}
nextWallpaper.source = newPath
if (nextWallpaper.status === Image.Ready) {
transitionAnimation.start()
}
}
Loader {
anchors.fill: parent
active: !root.source || root.isColorSource
asynchronous: true
sourceComponent: DankBackdrop {
screenName: modelData.name
}
}
Image {
id: currentWallpaper
anchors.fill: parent
visible: false
opacity: 1
asynchronous: true
smooth: true
cache: true
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SettingsData.wallpaperFillMode)
}
Image {
id: nextWallpaper
anchors.fill: parent
visible: false
opacity: 0
asynchronous: true
smooth: true
cache: true
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SettingsData.wallpaperFillMode)
onStatusChanged: {
if (status !== Image.Ready)
return
if (!root.transitioning) {
transitionAnimation.start()
}
}
}
Item {
id: blurredLayer
anchors.fill: parent
MultiEffect {
anchors.fill: parent
source: currentWallpaper
blurEnabled: true
blur: 0.8
blurMax: 75
opacity: 1 - root.transitionProgress
autoPaddingEnabled: false
}
MultiEffect {
anchors.fill: parent
source: nextWallpaper
blurEnabled: true
blur: 0.8
blurMax: 75
opacity: root.transitionProgress
autoPaddingEnabled: false
}
}
NumberAnimation {
id: transitionAnimation
target: root
property: "transitionProgress"
from: 0.0
to: 1.0
duration: 1000
easing.type: Easing.InOutCubic
onFinished: {
Qt.callLater(() => {
if (nextWallpaper.source && nextWallpaper.status === Image.Ready && !nextWallpaper.source.toString().startsWith("#")) {
currentWallpaper.source = nextWallpaper.source
}
nextWallpaper.source = ""
currentWallpaper.opacity = 1
nextWallpaper.opacity = 0
root.transitionProgress = 0.0
})
}
}
}
}
}

View File

@@ -0,0 +1,309 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
Ref {
service: CupsService
}
ccWidgetIcon: CupsService.cupsAvailable && CupsService.getPrintersNum() > 0 ? "print" : "print_disabled"
ccWidgetPrimaryText: I18n.tr("Printers")
ccWidgetSecondaryText: {
if (CupsService.cupsAvailable && CupsService.getPrintersNum() > 0) {
return I18n.tr("Printers: ") + CupsService.getPrintersNum() + " - " + I18n.tr("Jobs: ") + CupsService.getTotalJobsNum()
} else {
if (!CupsService.cupsAvailable) {
return I18n.tr("Print Server not available")
} else {
return I18n.tr("No printer found")
}
}
}
ccWidgetIsActive: CupsService.cupsAvailable && CupsService.getTotalJobsNum() > 0
onCcWidgetToggled: {
}
ccDetailContent: Component {
Rectangle {
id: detailRoot
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
Column {
visible: !CupsService.cupsAvailable || CupsService.getPrintersNum() == 0
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "print_disabled"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: !CupsService.cupsAvailable ? I18n.tr("Print Server not available") : I18n.tr("No printer found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
id: detailColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
visible: CupsService.cupsAvailable && CupsService.getPrintersNum() > 0
height: visible ? 120 : 0
RowLayout {
spacing: Theme.spacingS
width: parent.width
DankDropdown {
id: printerDropdown
text: ""
Layout.fillWidth: true
Layout.maximumWidth: parent.width - 180
description: ""
currentValue: {
CupsService.getSelectedPrinter()
}
options: CupsService.getPrintersNames()
onValueChanged: value => {
CupsService.setSelectedPrinter(value)
}
}
Column {
spacing: Theme.spacingS
StyledText {
text: CupsService.getCurrentPrinterStatePrettyShort()
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingM
Rectangle {
height: 24
width: 80
radius: 14
color: printerStatusToggle.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: true
opacity: 1.0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: CupsService.getCurrentPrinterState() === "stopped" ? "play_arrow" : "pause"
size: Theme.fontSizeSmall + 4
color: Theme.surfaceText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: CupsService.getCurrentPrinterState() === "stopped" ? I18n.tr("Resume") : I18n.tr("Pause")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: printerStatusToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: true
onClicked: {
const selected = CupsService.getSelectedPrinter()
if (CupsService.getCurrentPrinterState() === "stopped") {
CupsService.resumePrinter(selected)
} else {
CupsService.pausePrinter(selected)
}
}
}
}
Rectangle {
height: 24
width: 80
radius: 14
color: clearJobsToggle.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: true
opacity: 1.0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "delete_forever"
size: Theme.fontSizeSmall + 4
color: Theme.surfaceText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Jobs")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: clearJobsToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: true
onClicked: {
const selected = CupsService.getSelectedPrinter()
CupsService.purgeJobs(selected)
}
}
}
}
}
}
Rectangle {
height: 1
width: parent.width
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
DankFlickable {
width: parent.width
height: 160
contentHeight: listCol.height
clip: true
Column {
id: listCol
width: parent.width
spacing: Theme.spacingXS
Item {
width: parent.width
height: 120
visible: CupsService.getCurrentPrinterJobs().length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "work"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("The job queue of this printer is empty")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Repeater {
model: CupsService.getCurrentPrinterJobs()
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 300
height: 50
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
border.width: 1
border.color: Theme.outlineLight
opacity: 1.0
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "docs"
size: Theme.iconSize + 2
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Column {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText {
text: "[" + modelData.id + "] " + modelData.state + " (" + (modelData.size / 1024) + "kb)"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: {
var date = new Date(modelData.timeCreated)
return date.toLocaleString(Qt.locale(), Locale.ShortFormat)
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Item {
Layout.fillWidth: true
}
}
DankActionButton {
id: cancelJobButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
iconName: "delete"
buttonSize: 36
onClicked: {
CupsService.cancelJob(CupsService.getSelectedPrinter(), modelData.id)
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,251 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.Plugins
PluginComponent {
id: root
Ref {
service: DMSNetworkService
}
ccWidgetIcon: DMSNetworkService.isBusy ? "sync" : (DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off")
ccWidgetPrimaryText: "VPN"
ccWidgetSecondaryText: {
if (!DMSNetworkService.connected)
return "Disconnected"
const names = DMSNetworkService.activeNames || []
if (names.length <= 1)
return names[0] || "Connected"
return names[0] + " +" + (names.length - 1)
}
ccWidgetIsActive: DMSNetworkService.connected
onCcWidgetToggled: {
DMSNetworkService.toggleVpn()
}
ccDetailContent: Component {
Rectangle {
id: detailRoot
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
id: detailColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
spacing: Theme.spacingS
width: parent.width
StyledText {
text: {
if (!DMSNetworkService.connected)
return "Active: None"
const names = DMSNetworkService.activeNames || []
if (names.length <= 1)
return "Active: " + (names[0] || "VPN")
return "Active: " + names[0] + " +" + (names.length - 1)
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
Layout.fillWidth: true
Layout.maximumWidth: parent.width - 120
}
Rectangle {
height: 28
radius: 14
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: DMSNetworkService.connected
width: 110
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "link_off"
size: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Disconnect")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: discAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.disconnectAllActive()
}
}
}
Rectangle {
height: 1
width: parent.width
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
DankFlickable {
width: parent.width
height: 160
contentHeight: listCol.height
clip: true
Column {
id: listCol
width: parent.width
spacing: Theme.spacingXS
Item {
width: parent.width
height: DMSNetworkService.profiles.length === 0 ? 120 : 0
visible: height > 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "playlist_remove"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No VPN profiles found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Add a VPN in NetworkManager")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Repeater {
model: DMSNetworkService.profiles
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 300
height: 50
radius: Theme.cornerRadius
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
size: Theme.iconSize - 4
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Column {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: {
if (modelData.type === "wireguard")
return "WireGuard"
const svc = modelData.serviceType || ""
if (svc.indexOf("openvpn") !== -1)
return "OpenVPN"
if (svc.indexOf("wireguard") !== -1)
return "WireGuard (plugin)"
if (svc.indexOf("openconnect") !== -1)
return "OpenConnect"
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1)
return "Fortinet"
if (svc.indexOf("strongswan") !== -1)
return "IPsec (strongSwan)"
if (svc.indexOf("libreswan") !== -1)
return "IPsec (Libreswan)"
if (svc.indexOf("l2tp") !== -1)
return "L2TP/IPsec"
if (svc.indexOf("pptp") !== -1)
return "PPTP"
if (svc.indexOf("vpnc") !== -1)
return "Cisco (vpnc)"
if (svc.indexOf("sstp") !== -1)
return "SSTP"
if (svc)
return svc.split('.').pop()
return "VPN"
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Item {
Layout.fillWidth: true
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(modelData.uuid)
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
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:
Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
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.primaryText : 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.primaryText : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Typography {
width: parent.width
text: root.secondaryText
style: Typography.Style.Caption
color: isActive ? Theme.primaryText : 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,220 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.Details
import qs.Modules.ControlCenter.Models
Item {
id: root
property string expandedSection: ""
property var expandedWidgetData: null
property var bluetoothCodecSelector: null
property string screenName: ""
property var pluginDetailInstance: null
property var widgetModel: null
property var collapseCallback: null
Loader {
id: pluginDetailLoader
width: parent.width
height: 250
y: Theme.spacingS
active: false
sourceComponent: null
}
Loader {
id: coreDetailLoader
width: parent.width
height: 250
y: Theme.spacingS
active: false
sourceComponent: null
}
Connections {
target: coreDetailLoader.item
enabled: root.expandedSection.startsWith("brightnessSlider_")
ignoreUnknownSignals: true
function onDeviceNameChanged(newDeviceName) {
if (root.expandedWidgetData && root.expandedWidgetData.id === "brightnessSlider") {
const widgets = SettingsData.controlCenterWidgets || []
const newWidgets = widgets.map(w => {
if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w)
updatedWidget.deviceName = newDeviceName
return updatedWidget
}
return w
})
SettingsData.set("controlCenterWidgets", newWidgets)
if (root.collapseCallback) {
root.collapseCallback()
}
}
}
}
Connections {
target: coreDetailLoader.item
enabled: root.expandedSection.startsWith("diskUsage_")
ignoreUnknownSignals: true
function onMountPathChanged(newMountPath) {
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
const widgets = SettingsData.controlCenterWidgets || []
const newWidgets = widgets.map(w => {
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w)
updatedWidget.mountPath = newMountPath
return updatedWidget
}
return w
})
SettingsData.set("controlCenterWidgets", newWidgets)
if (root.collapseCallback) {
root.collapseCallback()
}
}
}
}
onExpandedSectionChanged: {
if (pluginDetailInstance) {
pluginDetailInstance.destroy()
pluginDetailInstance = null
}
pluginDetailLoader.active = false
coreDetailLoader.active = false
if (!root.expandedSection) {
return
}
if (root.expandedSection.startsWith("builtin_")) {
const builtinId = root.expandedSection
let builtinInstance = null
if (builtinId === "builtin_vpn") {
if (widgetModel?.vpnLoader) {
widgetModel.vpnLoader.active = true
}
builtinInstance = widgetModel.vpnBuiltinInstance
}
if (builtinId === "builtin_cups") {
if (widgetModel?.cupsLoader) {
widgetModel.cupsLoader.active = true
}
builtinInstance = widgetModel.cupsBuiltinInstance
}
if (!builtinInstance || !builtinInstance.ccDetailContent) {
return
}
pluginDetailLoader.sourceComponent = builtinInstance.ccDetailContent
pluginDetailLoader.active = parent.height > 0
return
}
if (root.expandedSection.startsWith("plugin_")) {
const pluginId = root.expandedSection.replace("plugin_", "")
const pluginComponent = PluginService.pluginWidgetComponents[pluginId]
if (!pluginComponent) {
return
}
pluginDetailInstance = pluginComponent.createObject(null)
if (!pluginDetailInstance || !pluginDetailInstance.ccDetailContent) {
if (pluginDetailInstance) {
pluginDetailInstance.destroy()
pluginDetailInstance = null
}
return
}
pluginDetailLoader.sourceComponent = pluginDetailInstance.ccDetailContent
pluginDetailLoader.active = parent.height > 0
return
}
if (root.expandedSection.startsWith("diskUsage_")) {
coreDetailLoader.sourceComponent = diskUsageDetailComponent
coreDetailLoader.active = parent.height > 0
return
}
if (root.expandedSection.startsWith("brightnessSlider_")) {
coreDetailLoader.sourceComponent = brightnessDetailComponent
coreDetailLoader.active = parent.height > 0
return
}
switch (root.expandedSection) {
case "network":
case "wifi": coreDetailLoader.sourceComponent = networkDetailComponent; break
case "bluetooth": coreDetailLoader.sourceComponent = bluetoothDetailComponent; break
case "audioOutput": coreDetailLoader.sourceComponent = audioOutputDetailComponent; break
case "audioInput": coreDetailLoader.sourceComponent = audioInputDetailComponent; break
case "battery": coreDetailLoader.sourceComponent = batteryDetailComponent; break
default: return
}
coreDetailLoader.active = parent.height > 0
}
Component {
id: networkDetailComponent
NetworkDetail {}
}
Component {
id: bluetoothDetailComponent
BluetoothDetail {
id: bluetoothDetail
onShowCodecSelector: function(device) {
if (root.bluetoothCodecSelector) {
root.bluetoothCodecSelector.show(device)
root.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) {
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
})
}
}
}
}
Component {
id: audioOutputDetailComponent
AudioOutputDetail {}
}
Component {
id: audioInputDetailComponent
AudioInputDetail {}
}
Component {
id: batteryDetailComponent
BatteryDetail {}
}
Component {
id: diskUsageDetailComponent
DiskUsageDetail {
currentMountPath: root.expandedWidgetData?.mountPath || "/"
instanceId: root.expandedWidgetData?.instanceId || ""
}
}
Component {
id: brightnessDetailComponent
BrightnessDetail {
initialDeviceName: root.expandedWidgetData?.deviceName || ""
instanceId: root.expandedWidgetData?.instanceId || ""
screenName: root.screenName
}
}
}

View File

@@ -0,0 +1,91 @@
import QtQuick
import qs.Common
import qs.Modules.ControlCenter.Details
Item {
id: root
property string expandedSection: ""
property var expandedWidgetData: null
height: active ? 250 : 0
visible: active
readonly property bool active: expandedSection !== ""
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Easing.OutCubic
}
}
Loader {
anchors.fill: parent
anchors.topMargin: Theme.spacingS
sourceComponent: {
if (!root.active) return null
if (expandedSection.startsWith("diskUsage_")) {
return diskUsageDetailComponent
}
switch (expandedSection) {
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 {}
}
Component {
id: diskUsageDetailComponent
DiskUsageDetail {
currentMountPath: root.expandedWidgetData?.mountPath || "/"
instanceId: root.expandedWidgetData?.instanceId || ""
onMountPathChanged: (newMountPath) => {
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
const widgets = SettingsData.controlCenterWidgets || []
const newWidgets = widgets.map(w => {
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w)
updatedWidget.mountPath = newMountPath
return updatedWidget
}
return w
})
SettingsData.set("controlCenterWidgets", newWidgets)
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,289 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool editMode: false
property var widgetData: null
property int widgetIndex: -1
property bool isSlider: false
property Component widgetComponent: null
property real gridCellWidth: 100
property real gridCellHeight: 60
property int gridColumns: 4
property var gridLayout: null
z: dragArea.drag.active ? 10000 : 1
signal widgetMoved(int fromIndex, int toIndex)
signal removeWidget(int index)
signal toggleWidgetSize(int index)
width: {
const widgetWidth = widgetData?.width || 50
if (widgetWidth <= 25) return gridCellWidth
else if (widgetWidth <= 50) return gridCellWidth * 2
else if (widgetWidth <= 75) return gridCellWidth * 3
else return gridCellWidth * 4
}
height: isSlider ? 16 : gridCellHeight
Rectangle {
id: dragIndicator
anchors.fill: parent
color: "transparent"
border.color: Theme.primary
border.width: dragArea.drag.active ? 2 : 0
radius: Theme.cornerRadius
opacity: dragArea.drag.active ? 0.8 : 1.0
z: dragArea.drag.active ? 10000 : 1
Behavior on border.width {
NumberAnimation { duration: 150 }
}
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
Loader {
id: widgetLoader
anchors.fill: parent
sourceComponent: widgetComponent
property var widgetData: root.widgetData
property int widgetIndex: root.widgetIndex
property int globalWidgetIndex: root.widgetIndex
property int widgetWidth: root.widgetData?.width || 50
MouseArea {
id: editModeBlocker
anchors.fill: parent
enabled: root.editMode
acceptedButtons: Qt.AllButtons
onPressed: function(mouse) { mouse.accepted = true }
onWheel: function(wheel) { wheel.accepted = true }
z: 100
}
}
MouseArea {
id: dragArea
anchors.fill: parent
enabled: editMode
cursorShape: editMode ? Qt.OpenHandCursor : Qt.PointingHandCursor
drag.target: editMode ? root : null
drag.axis: Drag.XAndYAxis
drag.smoothed: true
onPressed: function(mouse) {
if (editMode) {
cursorShape = Qt.ClosedHandCursor
if (root.gridLayout && root.gridLayout.moveToTop) {
root.gridLayout.moveToTop(root)
}
}
}
onReleased: function(mouse) {
if (editMode) {
cursorShape = Qt.OpenHandCursor
root.snapToGrid()
}
}
}
Drag.active: dragArea.drag.active
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
function swapIndices(i, j) {
if (i === j) return;
const arr = SettingsData.controlCenterWidgets;
if (!arr || i < 0 || j < 0 || i >= arr.length || j >= arr.length) return;
const copy = arr.slice();
const tmp = copy[i];
copy[i] = copy[j];
copy[j] = tmp;
SettingsData.set("controlCenterWidgets", copy);
}
function snapToGrid() {
if (!editMode || !gridLayout) return
const globalPos = root.mapToItem(gridLayout, 0, 0)
const cellWidth = gridLayout.width / gridColumns
const cellHeight = gridCellHeight + Theme.spacingS
const centerX = globalPos.x + (root.width / 2)
const centerY = globalPos.y + (root.height / 2)
let targetCol = Math.max(0, Math.floor(centerX / cellWidth))
let targetRow = Math.max(0, Math.floor(centerY / cellHeight))
targetCol = Math.min(targetCol, gridColumns - 1)
const newIndex = findBestInsertionIndex(targetRow, targetCol)
if (newIndex !== widgetIndex && newIndex >= 0 && newIndex < (SettingsData.controlCenterWidgets?.length || 0)) {
swapIndices(widgetIndex, newIndex)
}
}
function findBestInsertionIndex(targetRow, targetCol) {
const widgets = SettingsData.controlCenterWidgets || [];
const n = widgets.length;
if (!n || widgetIndex < 0 || widgetIndex >= n) return -1;
function spanFor(width) {
const w = width ?? 50;
if (w <= 25) return 1;
if (w <= 50) return 2;
if (w <= 75) return 3;
return 4;
}
const cols = gridColumns || 4;
let row = 0, col = 0;
let draggedOrigKey = null;
const pos = [];
for (let i = 0; i < n; i++) {
const span = Math.min(spanFor(widgets[i].width), cols);
if (col + span > cols) {
row++;
col = 0;
}
const startCol = col;
const centerKey = row * cols + (startCol + (span - 1) / 2);
if (i === widgetIndex) {
draggedOrigKey = centerKey;
} else {
pos.push({ index: i, row, startCol, span, centerKey });
}
col += span;
if (col >= cols) {
row++;
col = 0;
}
}
if (pos.length === 0) return -1;
const centerColCoord = targetCol + 0.5;
const targetKey = targetRow * cols + centerColCoord;
for (let k = 0; k < pos.length; k++) {
const p = pos[k];
if (p.row === targetRow && centerColCoord >= p.startCol && centerColCoord < (p.startCol + p.span)) {
return p.index;
}
}
let lo = 0, hi = pos.length - 1;
if (targetKey <= pos[0].centerKey) return pos[0].index;
if (targetKey >= pos[hi].centerKey) return pos[hi].index;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
const mk = pos[mid].centerKey;
if (targetKey < mk) hi = mid - 1;
else if (targetKey > mk) lo = mid + 1;
else return pos[mid].index;
}
const movingUp = (draggedOrigKey != null) ? (targetKey < draggedOrigKey) : false;
return (movingUp ? pos[lo].index : pos[hi].index);
}
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
cursorShape: Qt.PointingHandCursor
onClicked: removeWidget(widgetIndex)
}
}
SizeControls {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: -6
visible: editMode
z: 10
currentSize: root.widgetData?.width || 50
isSlider: root.isSlider
widgetIndex: root.widgetIndex
onSizeChanged: (newSize) => {
var widgets = SettingsData.controlCenterWidgets.slice()
if (widgetIndex >= 0 && widgetIndex < widgets.length) {
widgets[widgetIndex].width = newSize
SettingsData.set("controlCenterWidgets", widgets)
}
}
}
Rectangle {
id: dragHandle
width: 16
height: 12
radius: 2
color: Theme.primary
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: 4
visible: editMode
z: 15
opacity: dragArea.drag.active ? 1.0 : 0.7
DankIcon {
anchors.centerIn: parent
name: "drag_indicator"
size: 10
color: Theme.primaryText
}
Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
Rectangle {
anchors.fill: parent
color: editMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
border.color: "transparent"
border.width: 0
z: -1
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}

View File

@@ -0,0 +1,240 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Row {
id: root
property var availableWidgets: []
property Item popoutContent: null
signal addWidget(string widgetId)
signal resetToDefault()
signal clearAll()
height: 48
spacing: Theme.spacingS
onAddWidget: addWidgetPopup.close()
Popup {
id: addWidgetPopup
parent: popoutContent
x: parent ? Math.round((parent.width - width) / 2) : 0
y: parent ? Math.round((parent.height - height) / 2) : 0
width: 400
height: 300
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: Theme.surfaceContainer
border.color: Theme.primarySelected
border.width: 0
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: I18n.tr("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
clip: true
model: root.availableWidgets
delegate: Rectangle {
width: 400 - Theme.spacingL * 2
height: 50
radius: Theme.cornerRadius
color: widgetMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0
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: 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "add"
size: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Typography {
text: I18n.tr("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: 0
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: I18n.tr("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: 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "clear_all"
size: Theme.iconSize - 2
color: Theme.error
anchors.verticalCenter: parent.verticalCenter
}
Typography {
text: I18n.tr("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,114 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool editMode: false
signal powerButtonClicked()
signal lockRequested()
signal editModeToggled()
signal settingsButtonClicked()
implicitHeight: 70
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingM
DankCircularImage {
id: avatarContainer
width: 60
height: 60
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.verticalCenter: parent.verticalCenter
anchors.rightMargin: 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: "power_settings_new"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
backgroundColor: "transparent"
onClicked: root.powerButtonClicked()
}
DankActionButton {
buttonSize: 36
iconName: "settings"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
backgroundColor: "transparent"
onClicked: {
root.settingsButtonClicked()
settingsModal.show()
}
}
DankActionButton {
buttonSize: 36
iconName: editMode ? "done" : "edit"
iconSize: Theme.iconSize - 4
iconColor: editMode ? Theme.primary : Theme.surfaceText
backgroundColor: "transparent"
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,52 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
Row {
id: root
property int currentSize: 50
property bool isSlider: false
property int widgetIndex: -1
signal sizeChanged(int newSize)
readonly property var availableSizes: isSlider ? [50, 100] : [25, 50, 75, 100]
spacing: 2
Repeater {
model: root.availableSizes
Rectangle {
width: 16
height: 16
radius: 3
color: modelData === root.currentSize ? Theme.primary : Theme.surfaceContainer
border.color: modelData === root.currentSize ? Theme.primary : Theme.outline
border.width: 1
StyledText {
anchors.centerIn: parent
text: modelData.toString()
font.pixelSize: 8
font.weight: Font.Medium
color: modelData === root.currentSize ? Theme.primaryText : Theme.surfaceText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentSize = modelData
root.sizeChanged(modelData)
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
}
}

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,256 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.Widgets
import qs.Modules.ControlCenter.Details
import qs.Modules.DankBar
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
layerNamespace: "dms:control-center"
property string expandedSection: ""
property var triggerScreen: null
property bool editMode: false
property int expandedWidgetIndex: -1
property var expandedWidgetData: null
property bool powerMenuOpen: powerMenuModalLoader?.item?.shouldBeVisible ?? false
signal lockRequested
function collapseAll() {
expandedSection = ""
expandedWidgetIndex = -1
expandedWidgetData = null
}
onEditModeChanged: {
if (editMode) {
collapseAll()
}
}
onVisibleChanged: {
if (!visible) {
collapseAll()
}
}
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
function setTriggerPosition(x, y, width, section, screen) {
StateUtils.setTriggerPosition(root, x, y, width, section, screen)
}
function openWithSection(section) {
StateUtils.openWithSection(root, section)
}
function toggleSection(section) {
StateUtils.toggleSection(root, section)
}
popupWidth: 550
popupHeight: Math.min((triggerScreen?.height ?? 1080) - 100, contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400)
triggerX: (triggerScreen?.width ?? 1920) - 600 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
triggerWidth: 80
positioning: ""
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
Qt.callLater(() => {
if (NetworkService.activeService) {
NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled
}
if (UserInfoService)
UserInfoService.getUptime()
})
} else {
Qt.callLater(() => {
if (NetworkService.activeService) {
NetworkService.activeService.autoRefreshEnabled = false
}
if (BluetoothService.adapter && BluetoothService.adapter.discovering)
BluetoothService.adapter.discovering = false
editMode = false
})
}
}
WidgetModel {
id: widgetModel
}
content: Component {
Rectangle {
id: controlContent
implicitHeight: mainColumn.implicitHeight + Theme.spacingM
property alias bluetoothCodecSelector: bluetoothCodecSelector
color: {
const transparency = Theme.popupTransparency
const surface = Theme.surfaceContainer || Qt.rgba(0.1, 0.1, 0.1, 1)
return Qt.rgba(surface.r, surface.g, surface.b, transparency)
}
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
antialiasing: true
smooth: true
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.6)
radius: parent.radius
visible: root.powerMenuOpen
z: 5000
Behavior on opacity {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
Column {
id: mainColumn
width: parent.width - Theme.spacingL * 2
x: Theme.spacingL
y: Theme.spacingL
spacing: Theme.spacingS
HeaderPane {
id: headerPane
width: parent.width
editMode: root.editMode
onEditModeToggled: root.editMode = !root.editMode
onPowerButtonClicked: {
if (powerMenuModalLoader) {
powerMenuModalLoader.active = true
if (powerMenuModalLoader.item) {
const popoutPos = controlContent.mapToItem(null, 0, 0)
const bounds = Qt.rect(popoutPos.x, popoutPos.y, controlContent.width, controlContent.height)
powerMenuModalLoader.item.openFromControlCenter(bounds, root.triggerScreen)
}
}
}
onLockRequested: {
root.close()
root.lockRequested()
}
onSettingsButtonClicked: {
root.close()
}
}
DragDropGrid {
id: widgetGrid
width: parent.width
editMode: root.editMode
expandedSection: root.expandedSection
expandedWidgetIndex: root.expandedWidgetIndex
expandedWidgetData: root.expandedWidgetData
model: widgetModel
bluetoothCodecSelector: bluetoothCodecSelector
colorPickerModal: root.colorPickerModal
screenName: root.triggerScreen?.name || ""
parentScreen: root.triggerScreen
onExpandClicked: (widgetData, globalIndex) => {
root.expandedWidgetIndex = globalIndex
root.expandedWidgetData = widgetData
if (widgetData.id === "diskUsage") {
root.toggleSection("diskUsage_" + (widgetData.instanceId || "default"))
} else if (widgetData.id === "brightnessSlider") {
root.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default"))
} else {
root.toggleSection(widgetData.id)
}
}
onRemoveWidget: index => widgetModel.removeWidget(index)
onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex)
onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index)
onCollapseRequested: root.collapseAll()
}
EditControls {
width: parent.width
visible: editMode
popoutContent: controlContent
availableWidgets: {
if (!editMode)
return []
const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id)
const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets())
return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id))
}
onAddWidget: widgetId => widgetModel.addWidget(widgetId)
onResetToDefault: () => widgetModel.resetToDefault()
onClearAll: () => widgetModel.clearAll()
}
}
BluetoothCodecSelector {
id: bluetoothCodecSelector
anchors.fill: parent
z: 10000
}
}
}
Component {
id: networkDetailComponent
NetworkDetail {}
}
Component {
id: bluetoothDetailComponent
BluetoothDetail {
id: bluetoothDetail
onShowCodecSelector: function (device) {
if (contentLoader.item && contentLoader.item.bluetoothCodecSelector) {
contentLoader.item.bluetoothCodecSelector.show(device)
contentLoader.item.bluetoothCodecSelector.codecSelected.connect(function (deviceAddress, codecName) {
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName)
})
}
}
}
}
Component {
id: audioOutputDetailComponent
AudioOutputDetail {}
}
Component {
id: audioInputDetailComponent
AudioInputDetail {}
}
Component {
id: batteryDetailComponent
BatteryDetail {}
}
property var colorPickerModal: null
property var powerMenuModalLoader: null
}

View File

@@ -0,0 +1,203 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Input Devices")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: volumeSlider
anchors.left: parent.left
anchors.right: parent.right
anchors.top: headerRow.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingXS
height: 35
spacing: 0
visible: !hasInputVolumeSliderInCC
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"
MouseArea {
id: iconArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!AudioService.source || !AudioService.source.audio) return "mic_off"
let muted = AudioService.source.audio.muted
return muted ? "mic_off" : "mic"
}
size: Theme.iconSize
color: AudioService.source && AudioService.source.audio && !AudioService.source.audio.muted && AudioService.source.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
readonly property real actualVolumePercent: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: AudioService.source && AudioService.source.audio
minimum: 0
maximum: 100
value: AudioService.source && AudioService.source.audio ? Math.min(100, Math.round(AudioService.source.audio.volume * 100)) : 0
showValue: true
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceVariant
onSliderValueChanged: function(newValue) {
if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.volume = newValue / 100
if (newValue > 0 && AudioService.source.audio.muted) {
AudioService.source.audio.muted = false
}
}
}
}
}
DankFlickable {
id: audioContent
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: hasInputVolumeSliderInCC ? Theme.spacingM : Theme.spacingS
contentHeight: audioColumn.height
clip: true
Column {
id: audioColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return node.audio && !node.isSink && !node.isStream
})
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset"
else if (modelData.name.includes("usb"))
return "headset"
else
return "mic"
}
size: Theme.iconSize - 4
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.source ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSource = modelData
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,210 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Audio Devices")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
id: volumeSlider
anchors.left: parent.left
anchors.right: parent.right
anchors.top: headerRow.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingXS
height: 35
spacing: 0
visible: !hasVolumeSliderInCC
Rectangle {
width: Theme.iconSize + Theme.spacingS * 2
height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
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: volumeSlider.visible ? volumeSlider.bottom : headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: volumeSlider.visible ? Theme.spacingS : Theme.spacingM
contentHeight: audioColumn.height
clip: true
Column {
id: audioColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return node.audio && node.isSink && !node.isStream
})
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
if (modelData.name.includes("bluez"))
return "headset"
else if (modelData.name.includes("hdmi"))
return "tv"
else if (modelData.name.includes("usb"))
return "headset"
else
return "speaker"
}
size: Theme.iconSize - 4
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,258 @@
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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
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: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("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: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("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: 0
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: I18n.tr("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,301 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var device: null
property bool modalVisible: false
property var parentItem
property var availableCodecs: []
property string currentCodec: ""
property bool isLoading: false
signal codecSelected(string deviceAddress, string codecName)
function show(bluetoothDevice) {
device = bluetoothDevice;
isLoading = true;
availableCodecs = [];
currentCodec = "";
visible = true;
modalVisible = true;
queryCodecs();
Qt.callLater(() => {
focusScope.forceActiveFocus();
});
}
function hide() {
modalVisible = false;
Qt.callLater(() => {
visible = false;
});
}
function queryCodecs() {
if (!device)
return;
BluetoothService.getAvailableCodecs(device, function(codecs, current) {
availableCodecs = codecs;
currentCodec = current;
isLoading = false;
});
}
function selectCodec(profileName) {
if (!device || isLoading)
return;
let selectedCodec = availableCodecs.find(c => c.profile === profileName);
if (selectedCodec && device) {
BluetoothService.updateDeviceCodec(device.address, selectedCodec.name);
codecSelected(device.address, selectedCodec.name);
}
isLoading = true;
BluetoothService.switchCodec(device, profileName, function(success, message) {
isLoading = false;
if (success) {
ToastService.showToast(message, ToastService.levelInfo);
Qt.callLater(root.hide);
} else {
ToastService.showToast(message, ToastService.levelError);
}
});
}
visible: false
anchors.fill: parent
z: 2000
MouseArea {
id: modalBlocker
anchors.fill: parent
visible: modalVisible
enabled: modalVisible
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: root.hide()
onWheel: (wheel) => { wheel.accepted = true }
onPositionChanged: (mouse) => { mouse.accepted = true }
}
Rectangle {
id: modalBackground
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5)
opacity: modalVisible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
FocusScope {
id: focusScope
anchors.fill: parent
focus: root.visible
enabled: root.visible
Keys.onEscapePressed: {
root.hide()
event.accepted = true
}
}
Rectangle {
id: modalContent
anchors.centerIn: parent
width: 320
height: contentColumn.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
opacity: modalVisible ? 1 : 0
scale: modalVisible ? 1 : 0.9
MouseArea {
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: (mouse) => { mouse.accepted = true }
onWheel: (wheel) => { wheel.accepted = true }
onPositionChanged: (mouse) => { mouse.accepted = true }
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: device ? BluetoothService.getDeviceIcon(device) : "headset"
size: Theme.iconSize + 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: device ? (device.name || device.deviceName) : ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: I18n.tr("Audio Codec Selection")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
}
StyledText {
text: isLoading ? "Loading codecs..." : `Current: ${currentCodec}`
font.pixelSize: Theme.fontSizeSmall
color: isLoading ? Theme.primary : Theme.surfaceTextMedium
font.weight: Font.Medium
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: !isLoading
Repeater {
model: availableCodecs
Rectangle {
width: parent.width
height: 48
radius: Theme.cornerRadius
color: {
if (modelData.name === currentCodec)
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency);
else if (codecMouseArea.containsMouse)
return Theme.surfaceHover;
else
return "transparent";
}
border.color: "transparent"
border.width: 0
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Rectangle {
width: 6
height: 6
radius: 3
color: modelData.qualityColor
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.name === currentCodec ? Theme.primary : Theme.surfaceText
font.weight: modelData.name === currentCodec ? Font.Medium : Font.Normal
}
StyledText {
text: modelData.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
}
DankIcon {
name: "check"
size: Theme.iconSize - 4
color: Theme.primary
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: modelData.name === currentCodec
}
MouseArea {
id: codecMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: modelData.name !== currentCodec && !isLoading
onClicked: {
selectCodec(modelData.profile);
}
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}

View File

@@ -0,0 +1,570 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modals
Rectangle {
id: root
implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
property var bluetoothCodecModalRef: null
property var devicesBeingPaired: new Set()
signal showCodecSelector(var device)
function isDeviceBeingPaired(deviceAddress) {
return devicesBeingPaired.has(deviceAddress)
}
function handlePairDevice(device) {
if (!device) return
const deviceAddr = device.address
devicesBeingPaired.add(deviceAddr)
devicesBeingPairedChanged()
BluetoothService.pairDevice(device, function(response) {
devicesBeingPaired.delete(deviceAddr)
devicesBeingPairedChanged()
if (response.error) {
ToastService.showError(I18n.tr("Pairing failed"), response.error)
} else if (!BluetoothService.enhancedPairingAvailable) {
ToastService.showSuccess(I18n.tr("Device paired"))
}
})
}
function updateDeviceCodecDisplay(deviceAddress, codecName) {
for (let i = 0; i < pairedRepeater.count; i++) {
let item = pairedRepeater.itemAt(i)
if (item && item.modelData && item.modelData.address === deviceAddress) {
item.currentCodec = codecName
break
}
}
}
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Bluetooth Settings")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM)
height: parent.height
}
Rectangle {
id: scanButton
width: 100
height: 36
radius: 18
color: {
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
return scanMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
}
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
size: 18
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Scanning" : "Scan"
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: BluetoothService.adapter && BluetoothService.adapter.enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (BluetoothService.adapter)
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
}
}
}
}
DankFlickable {
id: bluetoothContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
contentHeight: bluetoothColumn.height
clip: true
Column {
id: bluetoothColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
id: pairedRepeater
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return []
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
devices.sort((a, b) => {
if (a.connected && !b.connected) return -1
if (!a.connected && b.connected) return 1
return (b.signalStrength || 0) - (a.signalStrength || 0)
})
return devices
}
delegate: Rectangle {
required property var modelData
required property int index
property string currentCodec: BluetoothService.deviceCodecs[modelData.address] || ""
width: parent.width
height: 50
radius: Theme.cornerRadius
Component.onCompleted: {
if (modelData.connected && BluetoothService.isAudioDevice(modelData)) {
BluetoothService.refreshDeviceCodec(modelData)
}
}
color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
if (deviceMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
}
border.color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Theme.warning
if (modelData.connected)
return Theme.primary
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize - 4
color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Theme.warning
if (modelData.connected)
return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.name || modelData.deviceName || "Unknown Device"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: {
if (modelData.state === BluetoothDeviceState.Connecting)
return "Connecting..."
if (modelData.connected) {
let status = "Connected"
if (currentCodec) {
status += " • " + currentCodec
}
return status
}
return "Paired"
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (modelData.state === BluetoothDeviceState.Connecting)
return Theme.warning
return Theme.surfaceVariantText
}
}
StyledText {
text: {
if (modelData.batteryAvailable && modelData.battery > 0)
return "• " + Math.round(modelData.battery * 100) + "%"
var btBattery = BatteryService.bluetoothDevices.find(dev => {
return dev.name === (modelData.name || modelData.deviceName) ||
dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) ||
(modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase())
})
return btBattery ? "• " + btBattery.percentage + "%" : ""
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0
}
StyledText {
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0
}
}
}
}
DankActionButton {
id: pairedOptionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (bluetoothContextMenu.visible) {
bluetoothContextMenu.close()
} else {
bluetoothContextMenu.currentDevice = modelData
bluetoothContextMenu.popup(pairedOptionsButton, -bluetoothContextMenu.width + pairedOptionsButton.width, pairedOptionsButton.height + Theme.spacingXS)
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
anchors.rightMargin: pairedOptionsButton.width + Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.connected) {
modelData.disconnect()
} else {
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
}
Item {
width: parent.width
height: 80
visible: BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
DankIcon {
anchors.centerIn: parent
name: "sync"
size: 24
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
RotationAnimation on rotation {
running: parent.visible && BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
loops: Animation.Infinite
from: 0
to: 360
duration: 1500
}
}
}
Repeater {
id: availableRepeater
model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing && !dev.blocked &&
(dev.signalStrength === undefined || dev.signalStrength > 0)
})
return BluetoothService.sortDevices(filtered)
}
delegate: Rectangle {
required property var modelData
required property int index
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData) || isDeviceBeingPaired(modelData.address)
width: parent.width
height: 50
radius: Theme.cornerRadius
color: availableMouseArea.containsMouse && !isBusy ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
opacity: (canConnect && !isBusy) ? 1 : 0.6
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: BluetoothService.getDeviceIcon(modelData)
size: Theme.iconSize - 4
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.name || modelData.deviceName || "Unknown Device"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: {
if (modelData.pairing || isBusy) return "Pairing..."
if (modelData.blocked) return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: text.length > 0 && !modelData.pairing && !modelData.blocked
}
}
}
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
text: {
if (isBusy) return "Pairing..."
if (!canConnect) return "Cannot pair"
return "Pair"
}
font.pixelSize: Theme.fontSizeSmall
color: (canConnect && !isBusy) ? Theme.primary : Theme.surfaceVariantText
font.weight: Font.Medium
}
MouseArea {
id: availableMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: canConnect && !isBusy
onClicked: {
root.handlePairDevice(modelData)
}
}
}
}
Item {
width: parent.width
height: 60
visible: !BluetoothService.adapter
StyledText {
anchors.centerIn: parent
text: I18n.tr("No Bluetooth adapter found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
}
}
Menu {
id: bluetoothContextMenu
width: 150
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property var currentDevice: null
background: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.width: 0
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
MenuItem {
text: bluetoothContextMenu.currentDevice && bluetoothContextMenu.currentDevice.connected ? "Disconnect" : "Connect"
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (bluetoothContextMenu.currentDevice) {
if (bluetoothContextMenu.currentDevice.connected) {
bluetoothContextMenu.currentDevice.disconnect()
} else {
BluetoothService.connectDeviceWithTrust(bluetoothContextMenu.currentDevice)
}
}
}
}
MenuItem {
text: I18n.tr("Audio Codec")
height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0
visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (bluetoothContextMenu.currentDevice) {
showCodecSelector(bluetoothContextMenu.currentDevice)
}
}
}
MenuItem {
text: I18n.tr("Forget Device")
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (bluetoothContextMenu.currentDevice) {
if (BluetoothService.enhancedPairingAvailable) {
const devicePath = BluetoothService.getDevicePath(bluetoothContextMenu.currentDevice)
DMSService.bluetoothRemove(devicePath, response => {
if (response.error) {
ToastService.showError(I18n.tr("Failed to remove device"), response.error)
}
})
} else {
bluetoothContextMenu.currentDevice.forget()
}
}
}
}
}
BluetoothPairingModal {
id: bluetoothPairingModal
}
Connections {
target: DMSService
function onBluetoothPairingRequest(data) {
bluetoothPairingModal.show(data)
}
}
}

View File

@@ -0,0 +1,460 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property string initialDeviceName: ""
property string instanceId: ""
property string screenName: ""
signal deviceNameChanged(string newDeviceName)
property string currentDeviceName: ""
function resolveDeviceName() {
if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
return ""
}
if (screenName && screenName.length > 0) {
const pins = SettingsData.brightnessDevicePins || {}
const pinnedDevice = pins[screenName]
if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice)
if (found) {
return found.name
}
}
}
if (initialDeviceName && initialDeviceName.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === initialDeviceName)
if (found) {
return found.name
}
}
const currentDeviceNameFromService = DisplayService.currentDevice
if (currentDeviceNameFromService) {
const found = DisplayService.devices.find(dev => dev.name === currentDeviceNameFromService)
if (found) {
return found.name
}
}
const backlight = DisplayService.devices.find(d => d.class === "backlight")
if (backlight) {
return backlight.name
}
const ddc = DisplayService.devices.find(d => d.class === "ddc")
if (ddc) {
return ddc.name
}
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : ""
}
Component.onCompleted: {
currentDeviceName = resolveDeviceName()
}
property bool isPinnedToScreen: {
if (!screenName || screenName.length === 0) {
return false
}
const pins = SettingsData.brightnessDevicePins || {}
return pins[screenName] === currentDeviceName
}
function togglePinToScreen() {
if (!screenName || screenName.length === 0 || !currentDeviceName || currentDeviceName.length === 0) {
return
}
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}))
if (isPinnedToScreen) {
delete pins[screenName]
} else {
pins[screenName] = currentDeviceName
}
SettingsData.set("brightnessDevicePins", pins)
}
implicitHeight: brightnessContent.height + Theme.spacingM
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
DankFlickable {
id: brightnessContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
contentHeight: brightnessColumn.height
clip: true
Column {
id: brightnessColumn
width: parent.width
spacing: Theme.spacingS
Item {
width: parent.width
height: 100
visible: !DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: DisplayService.brightnessAvailable ? "brightness_6" : "error"
size: 32
color: DisplayService.brightnessAvailable ? Theme.primary : Theme.error
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: DisplayService.brightnessAvailable ? "No brightness devices available" : "Brightness control not available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
}
Rectangle {
width: parent.width
height: 40
visible: screenName && screenName.length > 0 && DisplayService.devices && DisplayService.devices.length > 1
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
Item {
anchors.fill: parent
anchors.margins: Theme.spacingM
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "monitor"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: screenName || "Unknown Monitor"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: pinRow.width + Theme.spacingS * 2
height: 28
radius: height / 2
color: isPinnedToScreen ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
Row {
id: pinRow
anchors.centerIn: parent
spacing: 4
DankIcon {
name: isPinnedToScreen ? "push_pin" : "push_pin"
size: 16
color: isPinnedToScreen ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: isPinnedToScreen ? "Pinned" : "Pin"
font.pixelSize: Theme.fontSizeSmall
color: isPinnedToScreen ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.togglePinToScreen()
}
}
}
}
Repeater {
model: DisplayService.devices || []
delegate: Rectangle {
required property var modelData
required property int index
property real deviceBrightness: {
DisplayService.brightnessVersion
return DisplayService.getDeviceBrightness(modelData.name)
}
width: parent.width
height: 100
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData.name === currentDeviceName ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData.name === currentDeviceName ? 2 : 0
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Item {
width: parent.width
height: Math.max(deviceIconColumn.height, deviceInfoColumn.height, exponentControls.height)
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Column {
id: deviceIconColumn
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
name: {
const deviceClass = modelData.class || ""
const deviceName = modelData.name || ""
if (deviceClass === "backlight" || deviceClass === "ddc") {
if (deviceBrightness <= 33)
return "brightness_low"
if (deviceBrightness <= 66)
return "brightness_medium"
return "brightness_high"
} else if (deviceName.includes("kbd")) {
return "keyboard"
} else {
return "lightbulb"
}
}
size: Theme.iconSize
color: modelData.name === currentDeviceName ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: Math.round(deviceBrightness) + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
id: deviceInfoColumn
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - deviceIconColumn.width - exponentControls.width - Theme.spacingM * 3
StyledText {
text: {
const name = modelData.name || ""
const deviceClass = modelData.class || ""
if (deviceClass === "backlight") {
return name.replace("_", " ").replace(/\b\w/g, c => c.toUpperCase())
}
return name
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.name === currentDeviceName ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: {
const deviceClass = modelData.class || ""
if (deviceClass === "backlight")
return "Backlight device"
if (deviceClass === "ddc")
return "DDC/CI monitor"
if (deviceClass === "leds")
return "LED device"
return deviceClass
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
}
}
Row {
id: exponentControls
width: 140
height: 28
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
visible: SessionData.getBrightnessExponential(modelData.name)
z: 1
StyledRect {
width: 28
height: 28
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
opacity: SessionData.getBrightnessExponent(modelData.name) > 1.0 ? 1.0 : 0.4
DankIcon {
anchors.centerIn: parent
name: "remove"
size: 14
color: Theme.surfaceText
}
StateLayer {
stateColor: Theme.primary
cornerRadius: parent.radius
enabled: SessionData.getBrightnessExponent(modelData.name) > 1.0
onClicked: {
const current = SessionData.getBrightnessExponent(modelData.name)
const newValue = Math.max(1.0, Math.round((current - 0.1) * 10) / 10)
SessionData.setBrightnessExponent(modelData.name, newValue)
}
}
}
StyledRect {
width: 50
height: 28
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.width: 0
StyledText {
anchors.centerIn: parent
text: SessionData.getBrightnessExponent(modelData.name).toFixed(1)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
}
}
StyledRect {
width: 28
height: 28
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
opacity: SessionData.getBrightnessExponent(modelData.name) < 2.5 ? 1.0 : 0.4
DankIcon {
anchors.centerIn: parent
name: "add"
size: 14
color: Theme.surfaceText
}
StateLayer {
stateColor: Theme.primary
cornerRadius: parent.radius
enabled: SessionData.getBrightnessExponent(modelData.name) < 2.5
onClicked: {
const current = SessionData.getBrightnessExponent(modelData.name)
const newValue = Math.min(2.5, Math.round((current + 0.1) * 10) / 10)
SessionData.setBrightnessExponent(modelData.name, newValue)
}
}
}
}
}
Rectangle {
width: parent.width
height: 24
radius: height / 2
color: SessionData.getBrightnessExponential(modelData.name) ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
Row {
anchors.centerIn: parent
spacing: 4
DankIcon {
name: "show_chart"
size: 14
color: SessionData.getBrightnessExponential(modelData.name) ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: SessionData.getBrightnessExponential(modelData.name) ? "Exponential" : "Linear"
font.pixelSize: Theme.fontSizeSmall
color: SessionData.getBrightnessExponential(modelData.name) ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
const currentState = SessionData.getBrightnessExponential(modelData.name)
SessionData.setBrightnessExponential(modelData.name, !currentState)
}
}
}
}
MouseArea {
anchors.fill: parent
anchors.bottomMargin: 28
anchors.rightMargin: SessionData.getBrightnessExponential(modelData.name) ? 145 : 0
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (screenName && screenName.length > 0 && modelData.name !== currentDeviceName) {
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}))
if (pins[screenName]) {
delete pins[screenName]
SettingsData.set("brightnessDevicePins", pins)
}
}
currentDeviceName = modelData.name
deviceNameChanged(modelData.name)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,166 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property string currentMountPath: "/"
property string instanceId: ""
signal mountPathChanged(string newMountPath)
implicitHeight: diskContent.height + Theme.spacingM
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
Component.onCompleted: {
DgopService.addRef(["diskmounts"])
}
Component.onDestruction: {
DgopService.removeRef(["diskmounts"])
}
DankFlickable {
id: diskContent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
contentHeight: diskColumn.height
clip: true
Column {
id: diskColumn
width: parent.width
spacing: Theme.spacingS
Item {
width: parent.width
height: 100
visible: !DgopService.dgopAvailable || !DgopService.diskMounts || DgopService.diskMounts.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: DgopService.dgopAvailable ? "storage" : "error"
size: 32
color: DgopService.dgopAvailable ? Theme.primary : Theme.error
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: DgopService.dgopAvailable ? "No disk data available" : "dgop not available"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
}
Repeater {
model: DgopService.diskMounts || []
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 80
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: modelData.mount === currentMountPath ? 2 : 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
name: "storage"
size: Theme.iconSize
color: {
const percentStr = modelData.percent?.replace("%", "") || "0"
const percent = parseFloat(percentStr) || 0
if (percent > 90) return Theme.error
if (percent > 75) return Theme.warning
return modelData.mount === currentMountPath ? Theme.primary : Theme.surfaceText
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
const percentStr = modelData.percent?.replace("%", "") || "0"
const percent = parseFloat(percentStr) || 0
return percent.toFixed(0) + "%"
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - 50 - Theme.spacingM
StyledText {
text: modelData.mount === "/" ? "Root Filesystem" : modelData.mount
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.mount === currentMountPath ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
visible: modelData.mount !== "/"
}
StyledText {
text: `${modelData.used || "?"} / ${modelData.size || "?"}`
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
currentMountPath = modelData.mount
mountPathChanged(modelData.mount)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,678 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modals
Rectangle {
implicitHeight: {
if (NetworkService.wifiToggling) {
return headerRow.height + wifiToggleContent.height + Theme.spacingM
}
if (NetworkService.wifiEnabled) {
return headerRow.height + wifiContent.height + Theme.spacingM
}
return headerRow.height + wifiOffContent.height + Theme.spacingM
}
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
Component.onCompleted: {
NetworkService.addRef()
}
Component.onDestruction: {
NetworkService.removeRef()
}
property int currentPreferenceIndex: {
if (DMSService.apiVersion < 5) {
return 1
}
if (NetworkService.backend !== "networkmanager" || DMSService.apiVersion <= 10) {
return 1
}
const pref = NetworkService.userPreference
const status = NetworkService.networkStatus
let index = 1
if (pref === "ethernet") {
index = 0
} else if (pref === "wifi") {
index = 1
} else {
index = status === "ethernet" ? 0 : 1
}
return index
}
Row {
id: headerRow
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
StyledText {
id: headerText
text: I18n.tr("Network Settings")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM)
height: parent.height
}
DankButtonGroup {
id: preferenceControls
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.backend === "networkmanager" && DMSService.apiVersion > 10
model: ["Ethernet", "WiFi"]
currentIndex: currentPreferenceIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected) return
console.log("NetworkDetail: Setting preference to", index === 0 ? "ethernet" : "wifi")
NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi")
}
}
}
Item {
id: wifiToggleContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 1 && NetworkService.wifiToggling
height: visible ? 80 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "sync"
size: 32
color: Theme.primary
RotationAnimation on rotation {
running: NetworkService.wifiToggling
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: NetworkService.wifiEnabled ? "Disabling WiFi..." : "Enabling WiFi..."
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
horizontalAlignment: Text.AlignHCenter
}
}
}
Item {
id: wifiOffContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 1 && !NetworkService.wifiEnabled && !NetworkService.wifiToggling
height: visible ? 120 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
width: parent.width
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: "wifi_off"
size: 48
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: I18n.tr("WiFi is off")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 120
height: 36
radius: 18
color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.width: 0
border.color: Theme.primary
StyledText {
anchors.centerIn: parent
text: I18n.tr("Enable WiFi")
color: Theme.primary
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
}
MouseArea {
id: enableWifiButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: NetworkService.toggleWifiRadio()
}
}
}
}
DankFlickable {
id: wiredContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 0 && NetworkService.backend === "networkmanager" && DMSService.apiVersion > 10
contentHeight: wiredColumn.height
clip: true
Column {
id: wiredColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: ScriptModel {
values: {
const currentUuid = NetworkService.ethernetConnectionUuid
const networks = NetworkService.wiredConnections
let sorted = [...networks]
sorted.sort((a, b) => {
if (a.isActive && !b.isActive) return -1
if (!a.isActive && b.isActive) return 1
return a.id.localeCompare(b.id)
})
return sorted
}
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: wiredNetworkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: Theme.primary
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: "lan"
size: Theme.iconSize - 4
color: modelData.isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.id || "Unknown Config"
font.pixelSize: Theme.fontSizeMedium
color: modelData.isActive ? Theme.primary : Theme.surfaceText
font.weight: modelData.isActive ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
}
}
DankActionButton {
id: wiredOptionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (wiredNetworkContextMenu.visible) {
wiredNetworkContextMenu.close()
} else {
wiredNetworkContextMenu.currentID = modelData.id
wiredNetworkContextMenu.currentUUID = modelData.uuid
wiredNetworkContextMenu.currentConnected = modelData.isActive
wiredNetworkContextMenu.popup(wiredOptionsButton, -wiredNetworkContextMenu.width + wiredOptionsButton.width, wiredOptionsButton.height + Theme.spacingXS)
}
}
}
MouseArea {
id: wiredNetworkMouseArea
anchors.fill: parent
anchors.rightMargin: wiredOptionsButton.width + Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(event) {
if (modelData.uuid !== NetworkService.ethernetConnectionUuid) {
NetworkService.connectToSpecificWiredConfig(modelData.uuid)
}
event.accepted = true
}
}
}
}
}
}
Menu {
id: wiredNetworkContextMenu
width: 150
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property string currentID: ""
property string currentUUID: ""
property bool currentConnected: false
background: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.width: 0
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
MenuItem {
text: "Activate"
height: !wiredNetworkContextMenu.currentConnected ? 32 : 0
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (!networkContextMenu.currentConnected) {
NetworkService.connectToSpecificWiredConfig(wiredNetworkContextMenu.currentUUID)
}
}
}
MenuItem {
text: I18n.tr("Network Info")
height: wiredNetworkContextMenu.currentConnected ? 32 : 0
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
let networkData = NetworkService.getWiredNetworkInfo(wiredNetworkContextMenu.currentUUID)
networkWiredInfoModal.showNetworkInfo(wiredNetworkContextMenu.currentID, networkData)
}
}
}
DankFlickable {
id: wifiContent
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling
contentHeight: wifiColumn.height
clip: true
property var frozenNetworks: []
property bool menuOpen: false
Column {
id: wifiColumn
width: parent.width
spacing: Theme.spacingS
Item {
width: parent.width
height: 200
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling && NetworkService.isScanning
DankIcon {
anchors.centerIn: parent
name: "refresh"
size: 48
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
RotationAnimation on rotation {
running: NetworkService.isScanning
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
}
}
}
Repeater {
model: ScriptModel {
values: {
const ssid = NetworkService.currentWifiSSID
const networks = NetworkService.wifiNetworks
let sorted = [...networks]
sorted.sort((a, b) => {
if (a.ssid === ssid) return -1
if (b.ssid === ssid) return 1
return b.signal - a.signal
})
if (!wifiContent.menuOpen) {
wifiContent.frozenNetworks = sorted
}
return wifiContent.menuOpen ? wifiContent.frozenNetworks : sorted
}
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 50
radius: Theme.cornerRadius
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
let strength = modelData.signal || 0
if (strength >= 50) return "wifi"
if (strength >= 25) return "wifi_2_bar"
return "wifi_1_bar"
}
size: Theme.iconSize - 4
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: modelData.ssid || "Unknown Network"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected •" : (modelData.secured ? "Secured •" : "Open •")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.saved ? "Saved" : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
visible: text.length > 0
}
StyledText {
text: (modelData.saved ? "• " : "") + modelData.signal + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
DankActionButton {
id: optionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (networkContextMenu.visible) {
networkContextMenu.close()
} else {
wifiContent.menuOpen = true
networkContextMenu.currentSSID = modelData.ssid
networkContextMenu.currentSecured = modelData.secured
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID
networkContextMenu.currentSaved = modelData.saved
networkContextMenu.currentSignal = modelData.signal
networkContextMenu.currentAutoconnect = modelData.autoconnect || false
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS)
}
}
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
anchors.rightMargin: optionsButton.width + Theme.spacingS
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(event) {
if (modelData.ssid !== NetworkService.currentWifiSSID) {
if (modelData.secured && !modelData.saved) {
if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(modelData.ssid)
} else if (PopoutService.wifiPasswordModal) {
PopoutService.wifiPasswordModal.show(modelData.ssid)
}
} else {
NetworkService.connectToWifi(modelData.ssid)
}
}
event.accepted = true
}
}
}
}
}
}
Menu {
id: networkContextMenu
width: 150
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property string currentSSID: ""
property bool currentSecured: false
property bool currentConnected: false
property bool currentSaved: false
property int currentSignal: 0
property bool currentAutoconnect: false
onClosed: {
wifiContent.menuOpen = false
}
background: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.width: 0
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
MenuItem {
text: networkContextMenu.currentConnected ? "Disconnect" : "Connect"
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (networkContextMenu.currentConnected) {
NetworkService.disconnectWifi()
} else {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(networkContextMenu.currentSSID)
} else if (PopoutService.wifiPasswordModal) {
PopoutService.wifiPasswordModal.show(networkContextMenu.currentSSID)
}
} else {
NetworkService.connectToWifi(networkContextMenu.currentSSID)
}
}
}
}
MenuItem {
text: I18n.tr("Network Info")
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData)
}
}
MenuItem {
text: networkContextMenu.currentAutoconnect ? I18n.tr("Disable Autoconnect") : I18n.tr("Enable Autoconnect")
height: (networkContextMenu.currentSaved || networkContextMenu.currentConnected) && DMSService.apiVersion > 13 ? 32 : 0
visible: (networkContextMenu.currentSaved || networkContextMenu.currentConnected) && DMSService.apiVersion > 13
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
NetworkService.setWifiAutoconnect(networkContextMenu.currentSSID, !networkContextMenu.currentAutoconnect)
}
}
MenuItem {
text: I18n.tr("Forget Network")
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID)
}
}
}
NetworkInfoModal {
id: networkInfoModal
}
NetworkWiredInfoModal {
id: networkWiredInfoModal
}
}

View File

@@ -0,0 +1,268 @@
import QtQuick
import qs.Common
import qs.Services
import qs.Modules.ControlCenter.BuiltinPlugins
import "../utils/widgets.js" as WidgetUtils
QtObject {
id: root
property var vpnBuiltinInstance: null
property var cupsBuiltinInstance: null
property var vpnLoader: Loader {
active: false
sourceComponent: Component {
VpnWidget {}
}
onItemChanged: {
root.vpnBuiltinInstance = item
}
Connections {
target: SettingsData
function onControlCenterWidgetsChanged() {
const widgets = SettingsData.controlCenterWidgets || []
const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn")
if (!hasVpnWidget && vpnLoader.active) {
console.log("VpnWidget: No VPN widget in control center, deactivating loader")
vpnLoader.active = false
}
}
}
}
property var cupsLoader: Loader {
active: false
sourceComponent: Component {
CupsWidget {}
}
onItemChanged: {
root.cupsBuiltinInstance = item
if (item && !DMSService.activeSubscriptions.includes("cups") && !DMSService.activeSubscriptions.includes("all")) {
DMSService.addSubscription("cups")
}
}
onActiveChanged: {
if (!active) {
if (DMSService.activeSubscriptions.includes("cups")) {
DMSService.removeSubscription("cups")
}
root.cupsBuiltinInstance = null
}
}
Connections {
target: SettingsData
function onControlCenterWidgetsChanged() {
const widgets = SettingsData.controlCenterWidgets || []
const hasCupsWidget = widgets.some(w => w.id === "builtin_cups")
if (!hasCupsWidget && cupsLoader.active) {
console.log("CupsWidget: No CUPS widget in control center, deactivating loader")
cupsLoader.active = false
}
}
}
}
readonly property var coreWidgetDefinitions: [{
"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,
"allowMultiple": true
}, {
"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
}, {
"id": "diskUsage",
"text": "Disk Usage",
"description": "Filesystem usage monitoring",
"icon": "storage",
"type": "action",
"enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined,
"allowMultiple": true
}, {
"id": "colorPicker",
"text": "Color Picker",
"description": "Choose colors from palette",
"icon": "palette",
"type": "action",
"enabled": true
}, {
"id": "builtin_vpn",
"text": "VPN",
"description": "VPN connections",
"icon": "vpn_key",
"type": "builtin_plugin",
"enabled": DMSNetworkService.available,
"warning": !DMSNetworkService.available ? "VPN not available" : undefined,
"isBuiltinPlugin": true
}, {
"id": "builtin_cups",
"text": "Printers",
"description": "Print Server Management",
"icon": "Print",
"type": "builtin_plugin",
"enabled": CupsService.available,
"warning": !CupsService.available ? "CUPS not available" : undefined,
"isBuiltinPlugin": true
}]
function getPluginWidgets() {
const plugins = []
const loadedPlugins = PluginService.getLoadedPlugins()
for (var i = 0; i < loadedPlugins.length; i++) {
const plugin = loadedPlugins[i]
if (plugin.type === "daemon") {
continue
}
const pluginComponent = PluginService.pluginWidgetComponents[plugin.id]
if (!pluginComponent) {
continue
}
const tempInstance = pluginComponent.createObject(null)
if (!tempInstance) {
continue
}
const hasCCWidget = tempInstance.ccWidgetIcon && tempInstance.ccWidgetIcon.length > 0
tempInstance.destroy()
if (!hasCCWidget) {
continue
}
plugins.push({
"id": "plugin_" + plugin.id,
"pluginId": plugin.id,
"text": plugin.name || "Plugin",
"description": plugin.description || "",
"icon": plugin.icon || "extension",
"type": "plugin",
"enabled": true,
"isPlugin": true
})
}
return plugins
}
readonly property var baseWidgetDefinitions: coreWidgetDefinitions
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,316 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Widgets
PanelWindow {
id: root
readonly property string powerOptionsText: I18n.tr("Power Options")
readonly property string logOutText: I18n.tr("Log Out")
readonly property string suspendText: I18n.tr("Suspend")
readonly property string rebootText: I18n.tr("Reboot")
readonly property string powerOffText: I18n.tr("Power Off")
property bool powerMenuVisible: false
signal powerActionRequested(string action, string title, string message)
visible: powerMenuVisible
implicitWidth: 400
implicitHeight: 320
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: {
powerMenuVisible = false
}
}
Rectangle {
width: Math.min(320, parent.width - Theme.spacingL * 2)
height: 320 // Fixed height to prevent cropping
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
y: Theme.barHeight + Theme.spacingXS
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
Theme.outline.b, 0.08)
border.width: 0
opacity: powerMenuVisible ? 1 : 0
scale: powerMenuVisible ? 1 : 0.85
MouseArea {
anchors.fill: parent
onClicked: {
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Row {
width: parent.width
StyledText {
text: root.powerOptionsText
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 150
height: 1
}
DankActionButton {
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: {
powerMenuVisible = false
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "logout"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.logOutText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: logoutArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"logout", "Log Out",
"Are you sure you want to log out?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r,
Theme.primary.g,
Theme.primary.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "bedtime"
size: Theme.iconSize
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.suspendText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: suspendArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"suspend", "Suspend",
"Are you sure you want to suspend the system?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r,
Theme.warning.g,
Theme.warning.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "restart_alt"
size: Theme.iconSize
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.rebootText
font.pixelSize: Theme.fontSizeMedium
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: rebootArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"reboot", "Reboot",
"Are you sure you want to reboot the system?")
}
}
}
Rectangle {
width: parent.width
height: 50
radius: Theme.cornerRadius
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r,
Theme.error.g,
Theme.error.b,
0.08) : Qt.rgba(
Theme.surfaceVariant.r,
Theme.surfaceVariant.g,
Theme.surfaceVariant.b,
0.08)
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "power_settings_new"
size: Theme.iconSize
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.powerOffText
font.pixelSize: Theme.fontSizeMedium
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: powerOffArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
powerMenuVisible = false
root.powerActionRequested(
"poweroff", "Power Off",
"Are you sure you want to power off the system?")
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}

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 defaultSink: AudioService.sink
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) : Theme.withAlpha(Theme.primary, 0)
MouseArea {
id: iconArea
anchors.fill: parent
visible: defaultSink !== null
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (defaultSink) {
AudioService.suppressOSD = true
defaultSink.audio.muted = !defaultSink.audio.muted
AudioService.suppressOSD = false
}
}
}
DankIcon {
anchors.centerIn: parent
name: {
if (!defaultSink) return "volume_off"
let volume = defaultSink.audio.volume
let muted = defaultSink.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"
}
size: Theme.iconSize
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
}
}
DankSlider {
readonly property real actualVolumePercent: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: defaultSink !== null
minimum: 0
maximum: 100
value: defaultSink ? Math.min(100, Math.round(defaultSink.audio.volume * 100)) : 0
showValue: true
unit: "%"
valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceContainer
trackColor: root.sliderTrackColor.a > 0 ? root.sliderTrackColor : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onSliderValueChanged: function(newValue) {
if (defaultSink) {
defaultSink.audio.volume = newValue / 100.0
if (newValue > 0 && defaultSink.audio.muted) {
defaultSink.audio.muted = false
}
}
}
}
}

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

@@ -0,0 +1,189 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Row {
id: root
property string deviceName: ""
property string instanceId: ""
property string screenName: ""
property var parentScreen: null
signal iconClicked
height: 40
spacing: 0
property string targetDeviceName: {
if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
return ""
}
if (screenName && screenName.length > 0) {
const pins = SettingsData.brightnessDevicePins || {}
const pinnedDevice = pins[screenName]
if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice)
if (found) {
return found.name
}
}
}
if (deviceName && deviceName.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === deviceName)
if (found) {
return found.name
}
}
const currentDeviceName = DisplayService.currentDevice
if (currentDeviceName) {
const found = DisplayService.devices.find(dev => dev.name === currentDeviceName)
if (found) {
return found.name
}
}
const backlight = DisplayService.devices.find(d => d.class === "backlight")
if (backlight) {
return backlight.name
}
const ddc = DisplayService.devices.find(d => d.class === "ddc")
if (ddc) {
return ddc.name
}
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : ""
}
property var targetDevice: {
if (!targetDeviceName || !DisplayService.devices) {
return null
}
return DisplayService.devices.find(dev => dev.name === targetDeviceName) || null
}
property real targetBrightness: {
DisplayService.brightnessVersion
if (!targetDeviceName) {
return 0
}
return DisplayService.getDeviceBrightness(targetDeviceName)
}
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) : Theme.withAlpha(Theme.primary, 0)
MouseArea {
id: iconArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DisplayService.devices && DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (DisplayService.devices && DisplayService.devices.length > 1) {
root.iconClicked()
}
}
onEntered: {
tooltipLoader.active = true
if (tooltipLoader.item) {
const tooltipText = targetDevice ? "bl device: " + targetDevice.name : "Backlight Control"
const globalPos = iconArea.mapToGlobal(iconArea.width / 2, iconArea.height / 2)
const screenY = root.parentScreen?.y ?? 0
const relativeY = globalPos.y - screenY - 55
tooltipLoader.item.show(tooltipText, globalPos.x, relativeY, root.parentScreen)
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
DankIcon {
anchors.centerIn: parent
name: {
if (!DisplayService.brightnessAvailable || !targetDevice) {
return "brightness_low"
}
if (targetDevice.class === "backlight" || targetDevice.class === "ddc") {
const brightness = targetBrightness
if (brightness <= 33)
return "brightness_low"
if (brightness <= 66)
return "brightness_medium"
return "brightness_high"
} else if (targetDevice.name.includes("kbd")) {
return "keyboard"
} else {
return "lightbulb"
}
}
size: Theme.iconSize
color: DisplayService.brightnessAvailable && targetDevice && targetBrightness > 0 ? Theme.primary : Theme.surfaceText
}
}
}
DankSlider {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - (Theme.iconSize + Theme.spacingS * 2)
enabled: DisplayService.brightnessAvailable && targetDeviceName.length > 0
minimum: {
if (!targetDevice) return 1
const isExponential = SessionData.getBrightnessExponential(targetDevice.id)
if (isExponential) {
return 1
}
return (targetDevice.class === "backlight" || targetDevice.class === "ddc") ? 1 : 0
}
maximum: {
if (!targetDevice) return 100
const isExponential = SessionData.getBrightnessExponential(targetDevice.id)
if (isExponential) {
return 100
}
return targetDevice.displayMax || 100
}
value: targetBrightness
showValue: true
unit: {
if (!targetDevice) return "%"
const isExponential = SessionData.getBrightnessExponential(targetDevice.id)
if (isExponential) {
return "%"
}
return targetDevice.class === "ddc" ? "" : "%"
}
onSliderValueChanged: function (newValue) {
if (DisplayService.brightnessAvailable && targetDeviceName) {
DisplayService.setBrightness(newValue, targetDeviceName, true)
}
}
thumbOutlineColor: Theme.surfaceContainer
trackColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
}

View File

@@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
property var colorPickerModal: null
isActive: true
iconName: "palette"
iconColor: Theme.primary
primaryText: "Color Picker"
secondaryText: "Choose a color"
onToggled: {
console.log("ColorPickerPill toggled, modal:", colorPickerModal)
if (colorPickerModal) {
colorPickerModal.show()
}
}
onExpandClicked: {
console.log("ColorPickerPill expandClicked, modal:", colorPickerModal)
if (colorPickerModal) {
colorPickerModal.show()
}
}
}

View File

@@ -0,0 +1,70 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property color iconColor: Theme.surfaceText
property string labelText: ""
property real value: 0.0
property real maximumValue: 1.0
property real minimumValue: 0.0
property bool enabled: true
signal sliderValueChanged(real value)
width: parent ? parent.width : 200
height: 60
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
opacity: enabled ? 1.0 : 0.6
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.right: sliderContainer.left
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: root.iconName
size: Theme.iconSize
color: root.iconColor
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.labelText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Rectangle {
id: sliderContainer
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingM
width: 120
height: parent.height - Theme.spacingS * 2
DankSlider {
anchors.centerIn: parent
width: parent.width
enabled: root.enabled
minimum: Math.round(root.minimumValue * 100)
maximum: Math.round(root.maximumValue * 100)
value: Math.round(root.value * 100)
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
}
}
}

View File

@@ -0,0 +1,170 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property color iconColor: Theme.surfaceText
property string primaryText: ""
property string secondaryText: ""
property bool expanded: false
property bool isActive: false
property bool showExpandArea: true
signal toggled()
signal expandClicked()
signal wheelEvent(var wheelEvent)
width: parent ? parent.width : 220
height: 60
radius: Theme.cornerRadius
function hoverTint(base) {
const factor = 1.2
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
}
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
color: {
const baseColor = bodyMouse.containsMouse ? Theme.widgetBaseHoverColor : _containerBg
return baseColor
}
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.10)
border.width: 0
antialiasing: true
readonly property color _labelPrimary: Theme.surfaceText
readonly property color _labelSecondary: Theme.surfaceVariantText
readonly property color _tileBgActive: Theme.primary
readonly property color _tileBgInactive: {
const transparency = Theme.popupTransparency
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:
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.18)
readonly property color _tileIconActive: Theme.primaryText
readonly property color _tileIconInactive: Theme.primary
property int _padH: Theme.spacingS
property int _tileSize: 48
property int _tileRadius: Theme.cornerRadius
Rectangle {
id: rightHoverOverlay
anchors.fill: parent
radius: root.radius
z: 0
visible: false
color: hoverTint(_containerBg)
opacity: 0.08
antialiasing: true
Behavior on opacity { NumberAnimation { duration: Theme.shortDuration } }
}
Row {
id: row
anchors.fill: parent
anchors.leftMargin: _padH
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
Rectangle {
id: iconTile
z: 1
width: _tileSize
height: _tileSize
anchors.verticalCenter: parent.verticalCenter
radius: _tileRadius
color: isActive ? _tileBgActive : _tileBgInactive
border.color: isActive ? _tileRingActive : "transparent"
border.width: isActive ? 1 : 0
antialiasing: true
Rectangle {
anchors.fill: parent
radius: _tileRadius
color: hoverTint(iconTile.color)
opacity: tileMouse.pressed ? 0.3 : (tileMouse.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
}
MouseArea {
id: tileMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.toggled()
}
}
Item {
id: body
width: row.width - iconTile.width - row.spacing
height: row.height
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
width: parent.width
text: root.primaryText
color: _labelPrimary
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
StyledText {
width: parent.width
text: root.secondaryText
color: _labelSecondary
font.pixelSize: Theme.fontSizeSmall
visible: text.length > 0
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
MouseArea {
id: bodyMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: { rightHoverOverlay.visible = true; rightHoverOverlay.opacity = 0.08 }
onExited: { rightHoverOverlay.opacity = 0.0; rightHoverOverlay.visible = false }
onPressed: rightHoverOverlay.opacity = 0.16
onReleased: rightHoverOverlay.opacity = containsMouse ? 0.08 : 0.0
onClicked: root.expandClicked()
onWheel: function (ev) {
root.wheelEvent(ev)
}
}
}
}
focus: true
Keys.onPressed: function (ev) {
if (ev.key === Qt.Key_Space || ev.key === Qt.Key_Return) { root.toggled(); ev.accepted = true }
else if (ev.key === Qt.Key_Right) { root.expandClicked(); ev.accepted = true }
}
}

View File

@@ -0,0 +1,29 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string title: ""
property Component content: null
property bool isVisible: true
property int contentHeight: 300
width: parent ? parent.width : 400
implicitHeight: isVisible ? contentHeight : 0
height: implicitHeight
color: "transparent"
clip: true
Loader {
id: contentLoader
anchors.fill: parent
sourceComponent: root.content
asynchronous: true
}
}

View File

@@ -0,0 +1,78 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.ControlCenter.Widgets
CompoundPill {
id: root
property string mountPath: "/"
property string instanceId: ""
iconName: "storage"
property var selectedMount: {
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
return null
}
const targetMount = DgopService.diskMounts.find(mount => mount.mount === mountPath)
return targetMount || DgopService.diskMounts.find(mount => mount.mount === "/") || DgopService.diskMounts[0]
}
property real usagePercent: {
if (!selectedMount || !selectedMount.percent) {
return 0
}
const percentStr = selectedMount.percent.replace("%", "")
return parseFloat(percentStr) || 0
}
isActive: DgopService.dgopAvailable && selectedMount !== null
primaryText: {
if (!DgopService.dgopAvailable) {
return "Disk Usage"
}
if (!selectedMount) {
return "No disk data"
}
return selectedMount.mount
}
secondaryText: {
if (!DgopService.dgopAvailable) {
return "dgop not available"
}
if (!selectedMount) {
return "No disk data available"
}
return `${selectedMount.used} / ${selectedMount.size} (${usagePercent.toFixed(0)}%)`
}
iconColor: {
if (!DgopService.dgopAvailable || !selectedMount) {
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
}
if (usagePercent > 90) {
return Theme.error
}
if (usagePercent > 75) {
return Theme.warning
}
return Theme.surfaceText
}
Component.onCompleted: {
DgopService.addRef(["diskmounts"])
}
Component.onDestruction: {
DgopService.removeRef(["diskmounts"])
}
onToggled: {
expandClicked()
}
}

View File

@@ -0,0 +1,51 @@
import QtQuick
import qs.Common
import qs.Widgets
StyledRect {
id: root
property string primaryMessage: ""
property string secondaryMessage: ""
radius: Theme.cornerRadius
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.1)
border.color: Theme.warning
border.width: 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingXS
DankIcon {
name: "warning"
size: 16
color: Theme.warning
anchors.top: parent.top
anchors.topMargin: 2
}
Column {
width: parent.width - 16 - parent.spacing
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: root.primaryMessage
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
font.weight: Font.Medium
wrapMode: Text.WordWrap
}
StyledText {
width: parent.width
text: root.secondaryMessage
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
visible: text.length > 0
}
}
}
}

View File

@@ -0,0 +1,82 @@
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) : Theme.withAlpha(Theme.primary, 0)
MouseArea {
id: iconArea
anchors.fill: parent
visible: defaultSource !== null
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (defaultSource) {
AudioService.suppressOSD = true
defaultSource.audio.muted = !defaultSource.audio.muted
AudioService.suppressOSD = false
}
}
}
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 : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
onIsDraggingChanged: {
AudioService.suppressOSD = isDragging
}
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,100 @@
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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
readonly property color _tileRingActive:
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
readonly property color _tileIconActive: Theme.primaryText
readonly property color _tileIconInactive: Theme.primary
color: {
if (isActive) return _tileBgActive
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : _tileBgInactive
return baseColor
}
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 radius {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -0,0 +1,80 @@
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()
signal iconRotationCompleted()
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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
readonly property color _tileRingActive:
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
readonly property color _tileIconActive: Theme.primaryText
readonly property color _tileIconInactive: Theme.primary
color: {
if (isActive) return _tileBgActive
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : _tileBgInactive
return baseColor
}
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
onRotationCompleted: root.iconRotationCompleted()
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled
onClicked: root.clicked()
}
Behavior on radius {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -0,0 +1,121 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
Rectangle {
id: root
property string iconName: ""
property string text: ""
property bool isActive: false
property bool enabled: true
property string secondaryText: ""
property real iconRotation: 0
signal clicked()
signal iconRotationCompleted()
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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
readonly property color _tileRingActive:
Qt.rgba(Theme.primaryText.r, Theme.primaryText.g, Theme.primaryText.b, 0.22)
color: {
if (isActive) return _tileBgActive
const baseColor = mouseArea.containsMouse ? Theme.widgetBaseHoverColor : _tileBgInactive
return baseColor
}
border.color: isActive ? _tileRingActive : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
opacity: enabled ? 1.0 : 0.6
function hoverTint(base) {
const factor = 1.2
return Theme.isLightMode ? Qt.darker(base, factor) : Qt.lighter(base, factor)
}
readonly property color _containerBg: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? hoverTint(_containerBg) : Theme.withAlpha(_containerBg, 0)
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.primaryText : Theme.primary
anchors.verticalCenter: parent.verticalCenter
rotation: root.iconRotation
onRotationCompleted: root.iconRotationCompleted()
}
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
StyledText {
width: parent.width
text: root.text
font.pixelSize: Theme.fontSizeMedium
color: isActive ? Theme.primaryText : Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
StyledText {
width: parent.width
text: root.secondaryText
font.pixelSize: Theme.fontSizeSmall
color: isActive ? Theme.primaryText : 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 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 (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,90 @@
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
}
if (widgetId === "diskUsage") {
widget.instanceId = generateUniqueId()
widget.mountPath = "/"
}
if (widgetId === "brightnessSlider") {
widget.instanceId = generateUniqueId()
widget.deviceName = ""
}
widgets.push(widget)
SettingsData.set("controlCenterWidgets", widgets)
}
function generateUniqueId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
function removeWidget(index) {
var widgets = SettingsData.controlCenterWidgets.slice()
if (index >= 0 && index < widgets.length) {
widgets.splice(index, 1)
SettingsData.set("controlCenterWidgets", 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.set("controlCenterWidgets", widgets)
}
}
function reorderWidgets(newOrder) {
SettingsData.set("controlCenterWidgets", 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.set("controlCenterWidgets", 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.set("controlCenterWidgets", defaultWidgets)
}
function clearAll() {
SettingsData.set("controlCenterWidgets", [])
}

View File

@@ -0,0 +1,179 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
Item {
id: root
required property var barWindow
required property var axis
required property var appDrawerLoader
required property var dankDashPopoutLoader
required property var processListPopoutLoader
required property var notificationCenterLoader
required property var batteryPopoutLoader
required property var vpnPopoutLoader
required property var controlCenterLoader
required property var clipboardHistoryModalPopup
required property var systemUpdateLoader
required property var notepadInstance
property alias reveal: core.reveal
property alias autoHide: core.autoHide
property alias backgroundTransparency: core.backgroundTransparency
property alias hasActivePopout: core.hasActivePopout
property alias mouseArea: topBarMouseArea
Item {
id: inputMask
readonly property int barThickness: barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing)
readonly property bool showing: SettingsData.dankBarVisible && (core.reveal
|| (CompositorService.isNiri && NiriService.inOverview && SettingsData.dankBarOpenOnOverview)
|| !core.autoHide)
readonly property int maskThickness: showing ? barThickness : 1
x: {
if (!axis.isVertical) {
return 0
} else {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Left: return 0
case SettingsData.Position.Right: return parent.width - maskThickness
default: return 0
}
}
}
y: {
if (axis.isVertical) {
return 0
} else {
switch (SettingsData.dankBarPosition) {
case SettingsData.Position.Top: return 0
case SettingsData.Position.Bottom: return parent.height - maskThickness
default: return 0
}
}
}
width: axis.isVertical ? maskThickness : parent.width
height: axis.isVertical ? parent.height : maskThickness
}
Region {
id: mask
item: inputMask
}
property alias maskRegion: mask
QtObject {
id: core
property real backgroundTransparency: SettingsData.dankBarTransparency
property bool autoHide: SettingsData.dankBarAutoHide
property bool revealSticky: false
property bool notepadInstanceVisible: notepadInstance?.isVisible ?? false
readonly property bool hasActivePopout: {
const loaders = [{
"loader": appDrawerLoader,
"prop": "shouldBeVisible"
}, {
"loader": dankDashPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": processListPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": notificationCenterLoader,
"prop": "shouldBeVisible"
}, {
"loader": batteryPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": vpnPopoutLoader,
"prop": "shouldBeVisible"
}, {
"loader": controlCenterLoader,
"prop": "shouldBeVisible"
}, {
"loader": clipboardHistoryModalPopup,
"prop": "visible"
}, {
"loader": systemUpdateLoader,
"prop": "shouldBeVisible"
}]
return notepadInstanceVisible || loaders.some(item => {
if (item.loader) {
return item.loader?.item?.[item.prop]
}
return false
})
}
property bool reveal: {
if (CompositorService.isNiri && NiriService.inOverview) {
return SettingsData.dankBarOpenOnOverview
}
return SettingsData.dankBarVisible && (!autoHide || topBarMouseArea.containsMouse || hasActivePopout || revealSticky)
}
onHasActivePopoutChanged: {
if (!hasActivePopout && autoHide && !topBarMouseArea.containsMouse) {
revealSticky = true
revealHold.restart()
}
}
}
Timer {
id: revealHold
interval: 250
repeat: false
onTriggered: core.revealSticky = false
}
Connections {
function onDankBarTransparencyChanged() {
core.backgroundTransparency = SettingsData.dankBarTransparency
}
target: SettingsData
}
Connections {
target: topBarMouseArea
function onContainsMouseChanged() {
if (topBarMouseArea.containsMouse) {
core.revealSticky = true
revealHold.stop()
} else {
if (core.autoHide && !core.hasActivePopout) {
revealHold.restart()
}
}
}
}
MouseArea {
id: topBarMouseArea
y: !barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Bottom ? parent.height - height : 0) : 0
x: barWindow.isVertical ? (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.width - width : 0) : 0
height: !barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
width: barWindow.isVertical ? barWindow.px(barWindow.effectiveBarThickness + SettingsData.dankBarSpacing) : undefined
anchors {
left: !barWindow.isVertical ? parent.left : (SettingsData.dankBarPosition === SettingsData.Position.Left ? parent.left : undefined)
right: !barWindow.isVertical ? parent.right : (SettingsData.dankBarPosition === SettingsData.Position.Right ? parent.right : undefined)
top: barWindow.isVertical ? parent.top : undefined
bottom: barWindow.isVertical ? parent.bottom : undefined
}
hoverEnabled: SettingsData.dankBarAutoHide && !core.reveal
acceptedButtons: Qt.NoButton
enabled: SettingsData.dankBarAutoHide && !core.reveal
}
}

View File

@@ -0,0 +1,57 @@
import QtQuick
QtObject {
id: root
property string edge: "top"
readonly property string orientation: isVertical ? "vertical" : "horizontal"
readonly property bool isVertical: edge === "left" || edge === "right"
readonly property bool isHorizontal: !isVertical
function primarySize(item) {
return isVertical ? item.height : item.width
}
function crossSize(item) {
return isVertical ? item.width : item.height
}
function setPrimaryPos(item, value) {
if (isVertical) {
item.y = value
} else {
item.x = value
}
}
function getPrimaryPos(item) {
return isVertical ? item.y : item.x
}
function primaryAnchor(anchors) {
return isVertical ? anchors.verticalCenter : anchors.horizontalCenter
}
function crossAnchor(anchors) {
return isVertical ? anchors.horizontalCenter : anchors.verticalCenter
}
function outerVisualEdge() {
if (edge === "bottom") return "bottom"
if (edge === "left") return "right"
if (edge === "right") return "left"
if (edge === "top") return "top"
return "bottom"
}
signal axisEdgeChanged()
signal axisOrientationChanged()
signal changed() // Single coalesced signal
onEdgeChanged: {
axisEdgeChanged()
axisOrientationChanged()
changed()
}
}

View File

@@ -0,0 +1,410 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Services
Item {
id: root
required property var barWindow
required property var axis
anchors.fill: parent
anchors.left: parent.left
anchors.top: parent.top
anchors.leftMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "right" ? barWindow._wingR : 0)
anchors.rightMargin: -(SettingsData.dankBarGothCornersEnabled && axis.isVertical && axis.edge === "left" ? barWindow._wingR : 0)
anchors.topMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "bottom" ? barWindow._wingR : 0)
anchors.bottomMargin: -(SettingsData.dankBarGothCornersEnabled && !axis.isVertical && axis.edge === "top" ? barWindow._wingR : 0)
readonly property real dpr: CompositorService.getScreenScale(barWindow.screen)
function requestRepaint() {
debounceTimer.restart()
}
Timer {
id: debounceTimer
interval: 50
repeat: false
onTriggered: {
barShape.requestPaint()
barTint.requestPaint()
barBorder.requestPaint()
}
}
Canvas {
id: barShape
anchors.fill: parent
antialiasing: true
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections {
target: barWindow
function on_BgColorChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceContainerChanged() { root.requestRepaint() }
}
Connections {
target: SettingsData
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
}
onPaint: {
const ctx = getContext("2d")
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H)
if (R > 0) {
ctx.lineTo(W, H + R)
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
ctx.lineTo(R, H)
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
ctx.lineTo(0, H + R)
} else {
ctx.lineTo(W, H - RT)
ctx.arcTo(W, H, W - RT, H, RT)
ctx.lineTo(RT, H)
ctx.arcTo(0, H, 0, H - RT, RT)
}
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.save()
if (isBottom) {
ctx.translate(W, H_raw)
ctx.rotate(Math.PI)
} else if (isLeft) {
ctx.translate(0, W)
ctx.rotate(-Math.PI / 2)
} else if (isRight) {
ctx.translate(H_raw, 0)
ctx.rotate(Math.PI / 2)
}
drawTopPath()
ctx.restore()
ctx.fillStyle = barWindow._bgColor
ctx.fill()
}
}
Canvas {
id: barTint
anchors.fill: parent
antialiasing: true
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
property real alphaTint: (barWindow._bgColor?.a ?? 1) < 0.99 ? (Theme.stateLayerOpacity ?? 0) : 0
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onAlphaTintChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections {
target: barWindow
function on_BgColorChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceChanged() { root.requestRepaint() }
}
Connections {
target: SettingsData
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
}
onPaint: {
const ctx = getContext("2d")
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
function drawTopPath() {
ctx.beginPath()
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H)
if (R > 0) {
ctx.lineTo(W, H + R)
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
ctx.lineTo(R, H)
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
ctx.lineTo(0, H + R)
} else {
ctx.lineTo(W, H - RT)
ctx.arcTo(W, H, W - RT, H, RT)
ctx.lineTo(RT, H)
ctx.arcTo(0, H, 0, H - RT, RT)
}
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
}
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.save()
if (isBottom) {
ctx.translate(W, H_raw)
ctx.rotate(Math.PI)
} else if (isLeft) {
ctx.translate(0, W)
ctx.rotate(-Math.PI / 2)
} else if (isRight) {
ctx.translate(H_raw, 0)
ctx.rotate(Math.PI / 2)
}
drawTopPath()
ctx.restore()
ctx.fillStyle = Qt.rgba(Theme.surface.r, Theme.surface.g, Theme.surface.b, alphaTint)
ctx.fill()
}
}
Canvas {
id: barBorder
anchors.fill: parent
visible: SettingsData.dankBarBorderEnabled
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
readonly property real correctWidth: Theme.px(root.width, dpr)
readonly property real correctHeight: Theme.px(root.height, dpr)
canvasSize: Qt.size(correctWidth, correctHeight)
property real wing: SettingsData.dankBarGothCornersEnabled ? Theme.px(barWindow._wingR, dpr) : 0
property real rt: SettingsData.dankBarSquareCorners ? 0 : Theme.px(Theme.cornerRadius, dpr)
property bool borderEnabled: SettingsData.dankBarBorderEnabled
antialiasing: rt > 0 || wing > 0
onWingChanged: root.requestRepaint()
onRtChanged: root.requestRepaint()
onBorderEnabledChanged: root.requestRepaint()
onCorrectWidthChanged: root.requestRepaint()
onCorrectHeightChanged: root.requestRepaint()
onVisibleChanged: if (visible) root.requestRepaint()
Component.onCompleted: root.requestRepaint()
Connections {
target: root
function onDprChanged() { root.requestRepaint() }
}
Connections {
target: Theme
function onIsLightModeChanged() { root.requestRepaint() }
function onSurfaceTextChanged() { root.requestRepaint() }
function onPrimaryChanged() { root.requestRepaint() }
function onSecondaryChanged() { root.requestRepaint() }
function onOutlineChanged() { root.requestRepaint() }
}
Connections {
target: SettingsData
function onDankBarBorderColorChanged() { root.requestRepaint() }
function onDankBarBorderOpacityChanged() { root.requestRepaint() }
function onDankBarBorderThicknessChanged() { root.requestRepaint() }
function onDankBarSpacingChanged() { root.requestRepaint() }
function onDankBarSquareCornersChanged() { root.requestRepaint() }
function onDankBarTransparencyChanged() { root.requestRepaint() }
function onDankBarGothCornerRadiusOverrideChanged() { root.requestRepaint() }
function onDankBarGothCornerRadiusValueChanged() { root.requestRepaint() }
}
onPaint: {
if (!borderEnabled) return
const ctx = getContext("2d")
const W = barWindow.isVertical ? correctHeight : correctWidth
const H_raw = barWindow.isVertical ? correctWidth : correctHeight
const R = wing
const RT = rt
const H = H_raw - (R > 0 ? R : 0)
const isTop = SettingsData.dankBarPosition === SettingsData.Position.Top
const isBottom = SettingsData.dankBarPosition === SettingsData.Position.Bottom
const isLeft = SettingsData.dankBarPosition === SettingsData.Position.Left
const isRight = SettingsData.dankBarPosition === SettingsData.Position.Right
const spacing = SettingsData.dankBarSpacing
const hasEdgeGap = spacing > 0 || RT > 0
ctx.reset()
ctx.clearRect(0, 0, W, H_raw)
ctx.save()
if (isBottom) {
ctx.translate(W, H_raw)
ctx.rotate(Math.PI)
} else if (isLeft) {
ctx.translate(0, W)
ctx.rotate(-Math.PI / 2)
} else if (isRight) {
ctx.translate(H_raw, 0)
ctx.rotate(Math.PI / 2)
}
const uiThickness = Math.max(1, SettingsData.dankBarBorderThickness ?? 1)
const devThickness = Math.max(1, Math.round(Theme.px(uiThickness, dpr)))
const key = SettingsData.dankBarBorderColor || "surfaceText"
const base = (key === "surfaceText") ? Theme.surfaceText
: (key === "primary") ? Theme.primary
: Theme.secondary
const color = Theme.withAlpha(base, SettingsData.dankBarBorderOpacity ?? 1.0)
ctx.globalCompositeOperation = "source-over"
ctx.fillStyle = color
function drawTopBorder() {
if (!hasEdgeGap) {
ctx.beginPath()
ctx.rect(0, H - devThickness, W, devThickness)
ctx.fill()
} else {
const thk = devThickness
const RTi = Math.max(0, RT - thk)
const Ri = Math.max(0, R - thk)
ctx.beginPath()
if (R > 0 && Ri > 0) {
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H)
ctx.lineTo(W, H + R)
ctx.arc(W - R, H + R, R, 0, -Math.PI / 2, true)
ctx.lineTo(R, H)
ctx.arc(R, H + R, R, -Math.PI / 2, -Math.PI, true)
ctx.lineTo(0, H + R)
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
ctx.moveTo(RT, thk)
ctx.arcTo(thk, thk, thk, RT, RTi)
ctx.lineTo(thk, H + R)
ctx.arc(R, H + R, Ri, -Math.PI, -Math.PI / 2, false)
ctx.lineTo(W - R, H + thk)
ctx.arc(W - R, H + R, Ri, -Math.PI / 2, 0, false)
ctx.lineTo(W - thk, H + R)
ctx.lineTo(W - thk, RT)
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
ctx.lineTo(RT, thk)
ctx.closePath()
} else {
ctx.moveTo(RT, 0)
ctx.lineTo(W - RT, 0)
ctx.arcTo(W, 0, W, RT, RT)
ctx.lineTo(W, H - RT)
ctx.arcTo(W, H, W - RT, H, RT)
ctx.lineTo(RT, H)
ctx.arcTo(0, H, 0, H - RT, RT)
ctx.lineTo(0, RT)
ctx.arcTo(0, 0, RT, 0, RT)
ctx.closePath()
ctx.moveTo(RT, thk)
ctx.arcTo(thk, thk, thk, RT, RTi)
ctx.lineTo(thk, H - RT)
ctx.arcTo(thk, H - thk, RT, H - thk, RTi)
ctx.lineTo(W - RT, H - thk)
ctx.arcTo(W - thk, H - thk, W - thk, H - RT, RTi)
ctx.lineTo(W - thk, RT)
ctx.arcTo(W - thk, thk, W - RT, thk, RTi)
ctx.lineTo(RT, thk)
ctx.closePath()
}
ctx.fill("evenodd")
}
}
drawTopBorder()
ctx.restore()
}
}
}

View File

@@ -0,0 +1,499 @@
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
property var widgetsModel: null
property var components: null
property bool noBackground: false
required property var axis
property string section: "center"
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property bool overrideAxisLayout: false
property bool forceVerticalLayout: false
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
readonly property real spacing: noBackground ? 2 : Theme.spacingXS
property var centerWidgets: []
property int totalWidgets: 0
property real totalSize: 0
function updateLayout() {
if ((isVertical ? height : width) <= 0 || !visible) {
return
}
centerWidgets = []
totalWidgets = 0
totalSize = 0
let configuredWidgets = 0
let configuredMiddleWidget = null
let configuredLeftWidget = null
let configuredRightWidget = null
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i)
if (item && getWidgetVisible(item.widgetId)) {
configuredWidgets++
}
}
const isOddConfigured = configuredWidgets % 2 === 1
const configuredMiddlePos = Math.floor(configuredWidgets / 2)
const configuredLeftPos = isOddConfigured ? -1 : ((configuredWidgets / 2) - 1)
const configuredRightPos = isOddConfigured ? -1 : (configuredWidgets / 2)
let currentConfigIndex = 0
for (var i = 0; i < centerRepeater.count; i++) {
const item = centerRepeater.itemAt(i)
if (item && getWidgetVisible(item.widgetId)) {
if (isOddConfigured && currentConfigIndex === configuredMiddlePos && item.active && item.item) {
configuredMiddleWidget = item.item
}
if (!isOddConfigured && currentConfigIndex === configuredLeftPos && item.active && item.item) {
configuredLeftWidget = item.item
}
if (!isOddConfigured && currentConfigIndex === configuredRightPos && item.active && item.item) {
configuredRightWidget = item.item
}
if (item.active && item.item) {
centerWidgets.push(item.item)
totalWidgets++
totalSize += isVertical ? item.item.height : item.item.width
}
currentConfigIndex++
}
}
if (totalWidgets === 0) {
return
}
if (totalWidgets > 1) {
totalSize += spacing * (totalWidgets - 1)
}
positionWidgets(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget)
}
function positionWidgets(configuredWidgets, configuredMiddleWidget, configuredLeftWidget, configuredRightWidget) {
const parentCenter = (isVertical ? height : width) / 2
const isOddConfigured = configuredWidgets % 2 === 1
centerWidgets.forEach(widget => {
if (isVertical) {
widget.anchors.verticalCenter = undefined
} else {
widget.anchors.horizontalCenter = undefined
}
})
if (isOddConfigured && configuredMiddleWidget) {
const middleWidget = configuredMiddleWidget
const middleIndex = centerWidgets.indexOf(middleWidget)
const middleSize = isVertical ? middleWidget.height : middleWidget.width
if (isVertical) {
middleWidget.y = parentCenter - (middleSize / 2)
} else {
middleWidget.x = parentCenter - (middleSize / 2)
}
let currentPos = isVertical ? middleWidget.y : middleWidget.x
for (var i = middleIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
for (var i = middleIndex + 1; i < totalWidgets; i++) {
currentPos += spacing
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
}
} else {
if (totalWidgets === 1) {
const widget = centerWidgets[0]
const size = isVertical ? widget.height : widget.width
if (isVertical) {
widget.y = parentCenter - (size / 2)
} else {
widget.x = parentCenter - (size / 2)
}
return
}
if (!configuredLeftWidget || !configuredRightWidget) {
if (totalWidgets % 2 === 1) {
const middleIndex = Math.floor(totalWidgets / 2)
const middleWidget = centerWidgets[middleIndex]
if (!middleWidget) {
return
}
const middleSize = isVertical ? middleWidget.height : middleWidget.width
if (isVertical) {
middleWidget.y = parentCenter - (middleSize / 2)
} else {
middleWidget.x = parentCenter - (middleSize / 2)
}
let currentPos = isVertical ? middleWidget.y : middleWidget.x
for (var i = middleIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? middleWidget.y : middleWidget.x) + middleSize
for (var i = middleIndex + 1; i < totalWidgets; i++) {
currentPos += spacing
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
}
} else {
const leftIndex = (totalWidgets / 2) - 1
const rightIndex = totalWidgets / 2
const fallbackLeft = centerWidgets[leftIndex]
const fallbackRight = centerWidgets[rightIndex]
if (!fallbackLeft || !fallbackRight) {
return
}
const halfSpacing = spacing / 2
const leftSize = isVertical ? fallbackLeft.height : fallbackLeft.width
if (isVertical) {
fallbackLeft.y = parentCenter - halfSpacing - leftSize
fallbackRight.y = parentCenter + halfSpacing
} else {
fallbackLeft.x = parentCenter - halfSpacing - leftSize
fallbackRight.x = parentCenter + halfSpacing
}
let currentPos = isVertical ? fallbackLeft.y : fallbackLeft.x
for (var i = leftIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? fallbackRight.y + fallbackRight.height : fallbackRight.x + fallbackRight.width)
for (var i = rightIndex + 1; i < totalWidgets; i++) {
currentPos += spacing
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
}
}
return
}
const leftWidget = configuredLeftWidget
const rightWidget = configuredRightWidget
const leftIndex = centerWidgets.indexOf(leftWidget)
const rightIndex = centerWidgets.indexOf(rightWidget)
const halfSpacing = spacing / 2
const leftSize = isVertical ? leftWidget.height : leftWidget.width
if (isVertical) {
leftWidget.y = parentCenter - halfSpacing - leftSize
rightWidget.y = parentCenter + halfSpacing
} else {
leftWidget.x = parentCenter - halfSpacing - leftSize
rightWidget.x = parentCenter + halfSpacing
}
let currentPos = isVertical ? leftWidget.y : leftWidget.x
for (var i = leftIndex - 1; i >= 0; i--) {
const size = isVertical ? centerWidgets[i].height : centerWidgets[i].width
currentPos -= (spacing + size)
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
}
currentPos = (isVertical ? rightWidget.y + rightWidget.height : rightWidget.x + rightWidget.width)
for (var i = rightIndex + 1; i < totalWidgets; i++) {
currentPos += spacing
if (isVertical) {
centerWidgets[i].y = currentPos
} else {
centerWidgets[i].x = currentPos
}
currentPos += isVertical ? centerWidgets[i].height : centerWidgets[i].width
}
}
}
function getWidgetVisible(widgetId) {
const widgetVisibility = {
"cpuUsage": DgopService.dgopAvailable,
"memUsage": DgopService.dgopAvailable,
"cpuTemp": DgopService.dgopAvailable,
"gpuTemp": DgopService.dgopAvailable,
"network_speed_monitor": DgopService.dgopAvailable
}
return widgetVisibility[widgetId] ?? true
}
function getWidgetComponent(widgetId) {
// Build dynamic component map including plugins
let baseMap = {
"launcherButton": "launcherButtonComponent",
"workspaceSwitcher": "workspaceSwitcherComponent",
"focusedWindow": "focusedWindowComponent",
"runningApps": "runningAppsComponent",
"clock": "clockComponent",
"music": "mediaComponent",
"weather": "weatherComponent",
"systemTray": "systemTrayComponent",
"privacyIndicator": "privacyIndicatorComponent",
"clipboard": "clipboardComponent",
"cpuUsage": "cpuUsageComponent",
"memUsage": "memUsageComponent",
"diskUsage": "diskUsageComponent",
"cpuTemp": "cpuTempComponent",
"gpuTemp": "gpuTempComponent",
"notificationButton": "notificationButtonComponent",
"battery": "batteryComponent",
"controlCenterButton": "controlCenterButtonComponent",
"idleInhibitor": "idleInhibitorComponent",
"spacer": "spacerComponent",
"separator": "separatorComponent",
"network_speed_monitor": "networkComponent",
"keyboard_layout_name": "keyboardLayoutNameComponent",
"vpn": "vpnComponent",
"notepadButton": "notepadButtonComponent",
"colorPicker": "colorPickerComponent",
"systemUpdate": "systemUpdateComponent"
}
// For built-in components, get from components property
const componentKey = baseMap[widgetId]
if (componentKey && root.components[componentKey]) {
return root.components[componentKey]
}
// For plugin components, get from PluginService
var parts = widgetId.split(":")
var pluginId = parts[0]
let pluginComponents = PluginService.getWidgetComponents()
return pluginComponents[pluginId] || null
}
height: parent.height
width: parent.width
anchors.centerIn: parent
Timer {
id: layoutTimer
interval: 0
repeat: false
onTriggered: root.updateLayout()
}
Component.onCompleted: {
layoutTimer.restart()
}
onWidthChanged: {
if (width > 0) {
layoutTimer.restart()
}
}
onHeightChanged: {
if (height > 0) {
layoutTimer.restart()
}
}
onVisibleChanged: {
if (visible && (isVertical ? height : width) > 0) {
layoutTimer.restart()
}
}
Repeater {
id: centerRepeater
model: root.widgetsModel
Loader {
property string widgetId: model.widgetId
property var widgetData: model
property int spacerSize: model.size || 20
anchors.verticalCenter: !root.isVertical ? parent.verticalCenter : undefined
anchors.horizontalCenter: root.isVertical ? parent.horizontalCenter : undefined
active: root.getWidgetVisible(model.widgetId) && (model.widgetId !== "music" || MprisController.activePlayer !== null)
sourceComponent: root.getWidgetComponent(model.widgetId)
opacity: (model.enabled !== false) ? 1 : 0
asynchronous: false
onLoaded: {
if (!item) {
return
}
item.widthChanged.connect(() => layoutTimer.restart())
item.heightChanged.connect(() => layoutTimer.restart())
if (root.axis && "axis" in item) {
item.axis = Qt.binding(() => root.axis)
}
if (root.axis && "isVertical" in item) {
try {
item.isVertical = Qt.binding(() => root.axis.isVertical)
} catch (e) {
}
}
// Inject properties for plugin widgets
if ("section" in item) {
item.section = root.section
}
if ("parentScreen" in item) {
item.parentScreen = Qt.binding(() => root.parentScreen)
}
if ("widgetThickness" in item) {
item.widgetThickness = Qt.binding(() => root.widgetThickness)
}
if ("barThickness" in item) {
item.barThickness = Qt.binding(() => root.barThickness)
}
if ("sectionSpacing" in item) {
item.sectionSpacing = Qt.binding(() => root.spacing)
}
if ("isFirst" in item) {
item.isFirst = Qt.binding(() => {
for (var i = 0; i < centerRepeater.count; i++) {
const checkItem = centerRepeater.itemAt(i)
if (checkItem && checkItem.active && checkItem.item) {
return checkItem.item === item
}
}
return false
})
}
if ("isLast" in item) {
item.isLast = Qt.binding(() => {
for (var i = centerRepeater.count - 1; i >= 0; i--) {
const checkItem = centerRepeater.itemAt(i)
if (checkItem && checkItem.active && checkItem.item) {
return checkItem.item === item
}
}
return false
})
}
if ("isLeftBarEdge" in item) {
item.isLeftBarEdge = false
}
if ("isRightBarEdge" in item) {
item.isRightBarEdge = false
}
if ("isTopBarEdge" in item) {
item.isTopBarEdge = false
}
if ("isBottomBarEdge" in item) {
item.isBottomBarEdge = false
}
if (item.pluginService !== undefined) {
var parts = model.widgetId.split(":")
var pluginId = parts[0]
var variantId = parts.length > 1 ? parts[1] : null
if (item.pluginId !== undefined) {
item.pluginId = pluginId
}
if (item.variantId !== undefined) {
item.variantId = variantId
}
if (item.variantData !== undefined && variantId) {
item.variantData = PluginService.getPluginVariantData(pluginId, variantId)
}
item.pluginService = PluginService
}
if (item.popoutService !== undefined) {
item.popoutService = PopoutService
}
layoutTimer.restart()
}
onActiveChanged: {
layoutTimer.restart()
}
}
}
Connections {
target: widgetsModel
function onCountChanged() {
layoutTimer.restart()
}
}
// Listen for plugin changes and refresh components
Connections {
target: PluginService
function onPluginLoaded(pluginId) {
// Force refresh of component lookups
for (var i = 0; i < centerRepeater.count; i++) {
var item = centerRepeater.itemAt(i)
if (item && item.widgetId.startsWith(pluginId)) {
item.sourceComponent = root.getWidgetComponent(item.widgetId)
}
}
}
function onPluginUnloaded(pluginId) {
// Force refresh of component lookups
for (var i = 0; i < centerRepeater.count; i++) {
var item = centerRepeater.itemAt(i)
if (item && item.widgetId.startsWith(pluginId)) {
item.sourceComponent = root.getWidgetComponent(item.widgetId)
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
import QtQuick
import qs.Common
Item {
id: root
property var widgetsModel: null
property var components: null
property bool noBackground: false
required property var axis
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property bool overrideAxisLayout: false
property bool forceVerticalLayout: false
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
implicitHeight: layoutLoader.item ? (layoutLoader.item.implicitHeight || layoutLoader.item.height) : 0
implicitWidth: layoutLoader.item ? (layoutLoader.item.implicitWidth || layoutLoader.item.width) : 0
Loader {
id: layoutLoader
anchors.fill: parent
sourceComponent: root.isVertical ? columnComp : rowComp
}
Component {
id: rowComp
Row {
readonly property real widgetSpacing: noBackground ? 2 : Theme.spacingXS
spacing: widgetSpacing
Repeater {
id: rowRepeater
model: root.widgetsModel
Item {
readonly property real rowSpacing: parent.widgetSpacing
width: widgetLoader.item ? widgetLoader.item.width : 0
height: widgetLoader.item ? widgetLoader.item.height : 0
WidgetHost {
id: widgetLoader
anchors.verticalCenter: parent.verticalCenter
widgetId: model.widgetId
widgetData: model
spacerSize: model.size || 20
components: root.components
isInColumn: false
axis: root.axis
section: "left"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
isFirst: model.index === 0
isLast: model.index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing
isLeftBarEdge: true
isRightBarEdge: false
}
}
}
}
}
Component {
id: columnComp
Column {
width: Math.max(parent.width, 200)
readonly property real widgetSpacing: noBackground ? 2 : Theme.spacingXS
spacing: widgetSpacing
Repeater {
id: columnRepeater
model: root.widgetsModel
Item {
readonly property real columnSpacing: parent.widgetSpacing
width: parent.width
height: widgetLoader.item ? widgetLoader.item.height : 0
WidgetHost {
id: widgetLoader
anchors.horizontalCenter: parent.horizontalCenter
widgetId: model.widgetId
widgetData: model
spacerSize: model.size || 20
components: root.components
isInColumn: true
axis: root.axis
section: "left"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
isFirst: model.index === 0
isLast: model.index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing
isTopBarEdge: true
isBottomBarEdge: false
}
}
}
}
}
}

View File

@@ -0,0 +1,648 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
DankPopout {
id: root
layerNamespace: "dms:battery"
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x;
triggerY = y;
triggerWidth = width;
triggerSection = section;
triggerScreen = screen;
}
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");
}
}
popupWidth: 400
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 400
triggerX: Screen.width - 380 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
triggerWidth: 70
positioning: ""
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
content: Component {
Rectangle {
id: batteryContent
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
antialiasing: true
smooth: true
focus: true
Component.onCompleted: {
if (root.shouldBeVisible) {
forceActiveFocus();
}
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
root.close();
event.accepted = true;
}
}
Connections {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function() {
batteryContent.forceActiveFocus();
});
}
}
target: root
}
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 0
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Theme.shadowMedium
border.width: 0
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Theme.outlineStrong
border.width: 0
radius: parent.radius
z: -1
}
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 {
width: parent.width
height: 48
spacing: Theme.spacingM
DankIcon {
name: {
if (!BatteryService.batteryAvailable)
return "power";
if (!BatteryService.isCharging && 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";
}
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";
} else {
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";
}
}
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 - 32 - Theme.spacingM * 2
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.batteryStatus
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
visible: BatteryService.batteryAvailable
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
}
}
Rectangle {
width: 32
height: 32
radius: 16
color: closeBatteryArea.containsMouse ? Theme.errorHover : "transparent"
anchors.top: parent.top
DankIcon {
anchors.centerIn: parent
name: "close"
size: Theme.iconSize - 4
color: closeBatteryArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeBatteryArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
root.close();
}
}
}
}
Row {
width: parent.width
spacing: Theme.spacingM
visible: BatteryService.batteryAvailable
StyledRect {
width: (parent.width - Theme.spacingM) / 2
height: 64
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("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: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: I18n.tr("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
}
}
}
}
// Individual battery details for multiple batteries
Column {
width: parent.width
spacing: Theme.spacingS
visible: !BatteryService.usePreferred && BatteryService.batteries.length > 1
StyledText {
text: I18n.tr("Individual Batteries")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
}
Repeater {
model: ScriptModel {
values: BatteryService.batteries
}
delegate: StyledRect {
required property var modelData
required property int index
width: parent.width
height: batteryColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.width: 0
Column {
id: batteryColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
// Top row: name and percentage
Row {
width: parent.width
spacing: Theme.spacingM
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: parent.width - percentText.width - chargingIcon.width - Theme.spacingM * 2
StyledText {
text: modelData.model || `Battery ${index + 1}`
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
width: parent.width
}
StyledText {
text: modelData.nativePath
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
elide: Text.ElideMiddle
width: parent.width
}
}
Item {
width: 1
height: parent.height
}
StyledText {
id: percentText
text: `${Math.round(100 * modelData.percentage)}%`
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Bold
anchors.verticalCenter: parent.verticalCenter
}
DankIcon {
id: chargingIcon
name: modelData.state === UPowerDeviceState.Charging ? "bolt" : ""
size: Theme.iconSizeSmall
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state === UPowerDeviceState.Charging
}
}
// Bottom row: Health, Capacity and Time
Flow {
width: parent.width
spacing: Theme.spacingS
StyledRect {
width: (parent.width - Theme.spacingS * 2) / 3
height: 48
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
text: I18n.tr("Health")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return "N/A"
return `${Math.round(modelData.healthPercentage)}%`
}
font.pixelSize: Theme.fontSizeSmall
color: {
if (!modelData.healthSupported || modelData.healthPercentage <= 0)
return Theme.surfaceText
return modelData.healthPercentage < 80 ? Theme.error : Theme.surfaceText
}
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
StyledRect {
width: (parent.width - Theme.spacingS * 2) / 3
height: 48
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
text: I18n.tr("Capacity")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: modelData.energyCapacity > 0 ? `${modelData.energyCapacity.toFixed(1)}` : "N/A"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
StyledRect {
width: (parent.width - Theme.spacingS * 2) / 3
height: 48
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.width: 0
Column {
anchors.centerIn: parent
spacing: 2
StyledText {
text: modelData.state === UPowerDeviceState.Charging
? I18n.tr("To Full")
: modelData.state === UPowerDeviceState.Discharging
? I18n.tr("Left") : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
const time = modelData.state === UPowerDeviceState.Charging
? modelData.timeToFull
: modelData.state === UPowerDeviceState.Discharging && BatteryService.changeRate > 0
? (3600 * modelData.energy) / BatteryService.changeRate : 0
if (!time || time <= 0 || time > 86400)
return "N/A"
const hours = Math.floor(time / 3600)
const minutes = Math.floor((time % 3600) / 60)
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
}
font.pixelSize: Theme.fontSizeSmall
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 => root.isActiveProfile(profile))
}
model: profileModel.map(profile => Theme.getPowerProfileLabel(profile))
currentIndex: currentProfileIndex
selectionMode: "single"
anchors.horizontalCenter: parent.horizontalCenter
onSelectionChanged: (index, selected) => {
if (!selected) return
root.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: 0
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: I18n.tr("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,439 @@
// No external details import; content inlined for consistency
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
DankPopout {
id: root
layerNamespace: "dms:vpn"
Ref {
service: DMSNetworkService
}
property bool wasVisible: false
onShouldBeVisibleChanged: {
if (shouldBeVisible && !wasVisible) {
DMSNetworkService.getState()
}
wasVisible = shouldBeVisible
}
property var triggerScreen: null
function setTriggerPosition(x, y, width, section, screen) {
triggerX = x;
triggerY = y;
triggerWidth = width;
triggerSection = section;
triggerScreen = screen;
}
popupWidth: 360
popupHeight: Math.min(Screen.height - 100, contentLoader.item ? contentLoader.item.implicitHeight : 260)
triggerX: Screen.width - 380 - Theme.spacingL
triggerY: Theme.barHeight - 4 + SettingsData.dankBarSpacing
triggerWidth: 70
positioning: ""
screen: triggerScreen
shouldBeVisible: false
visible: shouldBeVisible
content: Component {
Rectangle {
id: content
implicitHeight: contentColumn.height + Theme.spacingL * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
antialiasing: true
smooth: true
focus: true
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
root.close();
event.accepted = true;
}
}
// Outer subtle shadow rings to match BatteryPopout
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 0
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Theme.shadowMedium
border.width: 0
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Theme.outlineStrong
border.width: 0
radius: parent.radius
z: -1
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
Item {
width: parent.width
height: 32
StyledText {
text: I18n.tr("VPN Connections")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
// Close button (matches BatteryPopout)
Rectangle {
width: 32
height: 32
radius: 16
color: closeArea.containsMouse ? Theme.errorHover : "transparent"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: "close"
size: Theme.iconSize - 4
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: root.close()
}
}
}
// Inlined VPN details
Rectangle {
id: vpnDetail
width: parent.width
implicitHeight: detailsColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineStrong
border.width: 0
clip: true
Column {
id: detailsColumn
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
spacing: Theme.spacingS
width: parent.width
StyledText {
text: {
if (!DMSNetworkService.connected) {
return "Active: None";
}
const names = DMSNetworkService.activeNames || [];
if (names.length <= 1) {
return "Active: " + (names[0] || "VPN");
}
return "Active: " + names[0] + " +" + (names.length - 1);
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
wrapMode: Text.NoWrap
Layout.fillWidth: true
Layout.maximumWidth: parent.width - 140
}
// Removed Quick Connect for clarity
Item {
width: 1
height: 1
}
// Disconnect all (shown only when any active)
Rectangle {
height: 28
radius: 14
color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
visible: DMSNetworkService.connected
width: 130
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
border.width: 0
border.color: Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "link_off"
size: Theme.fontSizeSmall
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Disconnect")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: discAllArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.disconnectAllActive()
}
}
}
Rectangle {
height: 1
width: parent.width
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
DankFlickable {
width: parent.width
height: 160
contentHeight: listCol.height
clip: true
Column {
id: listCol
width: parent.width
spacing: Theme.spacingXS
Item {
width: parent.width
height: DMSNetworkService.profiles.length === 0 ? 120 : 0
visible: height > 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: "playlist_remove"
size: 36
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No VPN profiles found")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Add a VPN in NetworkManager")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Repeater {
model: DMSNetworkService.profiles
delegate: Rectangle {
required property var modelData
width: parent ? parent.width : 300
height: 50
radius: Theme.cornerRadius
color: rowArea.containsMouse ? Theme.primaryHoverLight : (DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primaryPressed : Theme.surfaceLight)
border.width: DMSNetworkService.isActiveUuid(modelData.uuid) ? 2 : 1
border.color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.outlineLight
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: DMSNetworkService.isActiveUuid(modelData.uuid) ? "vpn_lock" : "vpn_key_off"
size: Theme.iconSize - 4
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Column {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: DMSNetworkService.isActiveUuid(modelData.uuid) ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: {
if (modelData.type === "wireguard") {
return "WireGuard";
}
const svc = modelData.serviceType || "";
if (svc.indexOf("openvpn") !== -1) {
return "OpenVPN";
}
if (svc.indexOf("wireguard") !== -1) {
return "WireGuard (plugin)";
}
if (svc.indexOf("openconnect") !== -1) {
return "OpenConnect";
}
if (svc.indexOf("fortissl") !== -1 || svc.indexOf("forti") !== -1) {
return "Fortinet";
}
if (svc.indexOf("strongswan") !== -1) {
return "IPsec (strongSwan)";
}
if (svc.indexOf("libreswan") !== -1) {
return "IPsec (Libreswan)";
}
if (svc.indexOf("l2tp") !== -1) {
return "L2TP/IPsec";
}
if (svc.indexOf("pptp") !== -1) {
return "PPTP";
}
if (svc.indexOf("vpnc") !== -1) {
return "Cisco (vpnc)";
}
if (svc.indexOf("sstp") !== -1) {
return "SSTP";
}
if (svc) {
const parts = svc.split('.');
return parts[parts.length - 1];
}
return "VPN";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
}
}
Item {
Layout.fillWidth: true
height: 1
}
}
MouseArea {
id: rowArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
enabled: !DMSNetworkService.isBusy
onClicked: DMSNetworkService.toggle(modelData.uuid)
}
}
}
Item {
height: 1
width: 1
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,102 @@
import QtQuick
import qs.Common
Item {
id: root
property var widgetsModel: null
property var components: null
property bool noBackground: false
required property var axis
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property bool overrideAxisLayout: false
property bool forceVerticalLayout: false
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
Loader {
id: layoutLoader
width: parent.width
height: parent.height
sourceComponent: root.isVertical ? columnComp : rowComp
}
Component {
id: rowComp
Row {
readonly property real widgetSpacing: noBackground ? 2 : Theme.spacingXS
spacing: widgetSpacing
anchors.right: parent ? parent.right : undefined
Repeater {
id: rowRepeater
model: root.widgetsModel
Item {
readonly property real rowSpacing: parent.widgetSpacing
width: widgetLoader.item ? widgetLoader.item.width : 0
height: widgetLoader.item ? widgetLoader.item.height : 0
WidgetHost {
id: widgetLoader
anchors.verticalCenter: parent.verticalCenter
widgetId: model.widgetId
widgetData: model
spacerSize: model.size || 20
components: root.components
isInColumn: false
axis: root.axis
section: "right"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
isFirst: model.index === 0
isLast: model.index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing
isLeftBarEdge: false
isRightBarEdge: true
}
}
}
}
}
Component {
id: columnComp
Column {
width: parent ? parent.width : 0
readonly property real widgetSpacing: noBackground ? 2 : Theme.spacingXS
spacing: widgetSpacing
Repeater {
id: columnRepeater
model: root.widgetsModel
Item {
readonly property real columnSpacing: parent.widgetSpacing
width: parent.width
height: widgetLoader.item ? widgetLoader.item.height : 0
WidgetHost {
id: widgetLoader
anchors.horizontalCenter: parent.horizontalCenter
widgetId: model.widgetId
widgetData: model
spacerSize: model.size || 20
components: root.components
isInColumn: true
axis: root.axis
section: "right"
parentScreen: root.parentScreen
widgetThickness: root.widgetThickness
barThickness: root.barThickness
isFirst: model.index === 0
isLast: model.index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing
isTopBarEdge: false
isBottomBarEdge: true
}
}
}
}
}
}

View File

@@ -0,0 +1,232 @@
import QtQuick
import Quickshell.Services.Mpris
import qs.Services
Loader {
id: root
property string widgetId: ""
property var widgetData: null
property int spacerSize: 20
property var components: null
property bool isInColumn: false
property var axis: null
property string section: "center"
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
property bool isFirst: false
property bool isLast: false
property real sectionSpacing: 0
property bool isLeftBarEdge: false
property bool isRightBarEdge: false
property bool isTopBarEdge: false
property bool isBottomBarEdge: false
asynchronous: false
readonly property bool orientationMatches: (axis?.isVertical ?? false) === isInColumn
active: orientationMatches &&
getWidgetVisible(widgetId, DgopService.dgopAvailable) &&
(widgetId !== "music" || MprisController.activePlayer !== null)
sourceComponent: getWidgetComponent(widgetId, components)
opacity: getWidgetEnabled(widgetData?.enabled) ? 1 : 0
signal contentItemReady(var item)
Binding {
target: root.item
when: root.item && "parentScreen" in root.item
property: "parentScreen"
value: root.parentScreen
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "section" in root.item
property: "section"
value: root.section
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "widgetThickness" in root.item
property: "widgetThickness"
value: root.widgetThickness
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "barThickness" in root.item
property: "barThickness"
value: root.barThickness
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "axis" in root.item
property: "axis"
value: root.axis
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "widgetData" in root.item
property: "widgetData"
value: root.widgetData
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "isFirst" in root.item
property: "isFirst"
value: root.isFirst
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "isLast" in root.item
property: "isLast"
value: root.isLast
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "sectionSpacing" in root.item
property: "sectionSpacing"
value: root.sectionSpacing
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "isLeftBarEdge" in root.item
property: "isLeftBarEdge"
value: root.isLeftBarEdge
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "isRightBarEdge" in root.item
property: "isRightBarEdge"
value: root.isRightBarEdge
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "isTopBarEdge" in root.item
property: "isTopBarEdge"
value: root.isTopBarEdge
restoreMode: Binding.RestoreNone
}
Binding {
target: root.item
when: root.item && "isBottomBarEdge" in root.item
property: "isBottomBarEdge"
value: root.isBottomBarEdge
restoreMode: Binding.RestoreNone
}
onLoaded: {
if (item) {
contentItemReady(item)
if (axis && "isVertical" in item) {
try {
item.isVertical = axis.isVertical
} catch (e) {
}
}
if (item.pluginService !== undefined) {
var parts = widgetId.split(":")
var pluginId = parts[0]
var variantId = parts.length > 1 ? parts[1] : null
if (item.pluginId !== undefined) {
item.pluginId = pluginId
}
if (item.variantId !== undefined) {
item.variantId = variantId
}
if (item.variantData !== undefined && variantId) {
item.variantData = PluginService.getPluginVariantData(pluginId, variantId)
}
item.pluginService = PluginService
}
if (item.popoutService !== undefined) {
item.popoutService = PopoutService
}
}
}
function getWidgetComponent(widgetId, components) {
const componentMap = {
"launcherButton": components.launcherButtonComponent,
"workspaceSwitcher": components.workspaceSwitcherComponent,
"focusedWindow": components.focusedWindowComponent,
"runningApps": components.runningAppsComponent,
"clock": components.clockComponent,
"music": components.mediaComponent,
"weather": components.weatherComponent,
"systemTray": components.systemTrayComponent,
"privacyIndicator": components.privacyIndicatorComponent,
"clipboard": components.clipboardComponent,
"cpuUsage": components.cpuUsageComponent,
"memUsage": components.memUsageComponent,
"diskUsage": components.diskUsageComponent,
"cpuTemp": components.cpuTempComponent,
"gpuTemp": components.gpuTempComponent,
"notificationButton": components.notificationButtonComponent,
"battery": components.batteryComponent,
"controlCenterButton": components.controlCenterButtonComponent,
"idleInhibitor": components.idleInhibitorComponent,
"spacer": components.spacerComponent,
"separator": components.separatorComponent,
"network_speed_monitor": components.networkComponent,
"keyboard_layout_name": components.keyboardLayoutNameComponent,
"vpn": components.vpnComponent,
"notepadButton": components.notepadButtonComponent,
"colorPicker": components.colorPickerComponent,
"systemUpdate": components.systemUpdateComponent
}
if (componentMap[widgetId]) {
return componentMap[widgetId]
}
var parts = widgetId.split(":")
var pluginId = parts[0]
let pluginMap = PluginService.getWidgetComponents()
return pluginMap[pluginId] || null
}
function getWidgetVisible(widgetId, dgopAvailable) {
const widgetVisibility = {
"cpuUsage": dgopAvailable,
"memUsage": dgopAvailable,
"cpuTemp": dgopAvailable,
"gpuTemp": dgopAvailable,
"network_speed_monitor": dgopAvailable
}
return widgetVisibility[widgetId] ?? true
}
function getWidgetEnabled(enabled) {
return enabled !== false
}
}

View File

@@ -0,0 +1,77 @@
import QtQuick
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
Item {
id: root
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool hasActiveMedia: activePlayer !== null
readonly property bool isPlaying: hasActiveMedia && activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
width: 20
height: Theme.iconSize
Loader {
active: isPlaying
sourceComponent: Component {
Ref {
service: CavaService
}
}
}
Timer {
id: fallbackTimer
running: !CavaService.cavaAvailable && isPlaying
interval: 256
repeat: true
onTriggered: {
CavaService.values = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20, Math.random() * 45 + 15, Math.random() * 55 + 25];
}
}
Row {
anchors.centerIn: parent
spacing: 1.5
Repeater {
model: 6
Rectangle {
width: 2
height: {
if (root.isPlaying && CavaService.values.length > index) {
const rawLevel = CavaService.values[index] || 0;
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
const maxHeight = Theme.iconSize - 2;
const minHeight = 3;
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
}
return 3;
}
radius: 1.5
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standardDecel
}
}
}
}
}
}

View File

@@ -0,0 +1,114 @@
import QtQuick
import Quickshell.Services.UPower
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: battery
property bool batteryPopupVisible: false
property var popoutTarget: null
signal toggleBatteryPopup()
visible: true
content: Component {
Item {
implicitWidth: battery.isVerticalOrientation ? (battery.widgetThickness - battery.horizontalPadding * 2) : batteryContent.implicitWidth
implicitHeight: battery.isVerticalOrientation ? batteryColumn.implicitHeight : (battery.widgetThickness - battery.horizontalPadding * 2)
Column {
id: batteryColumn
visible: battery.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: BatteryService.getBatteryIcon()
size: Theme.barIconSize(battery.barThickness)
color: {
if (!BatteryService.batteryAvailable) {
return Theme.surfaceText
}
if (BatteryService.isLowBattery && !BatteryService.isCharging) {
return Theme.error
}
if (BatteryService.isCharging || BatteryService.isPluggedIn) {
return Theme.primary
}
return Theme.surfaceText
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: BatteryService.batteryLevel.toString()
font.pixelSize: Theme.barTextSize(battery.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
visible: BatteryService.batteryAvailable
}
}
Row {
id: batteryContent
visible: !battery.isVerticalOrientation
anchors.centerIn: parent
spacing: SettingsData.dankBarNoBackground ? 1 : 2
DankIcon {
name: BatteryService.getBatteryIcon()
size: Theme.barIconSize(battery.barThickness, -4)
color: {
if (!BatteryService.batteryAvailable) {
return Theme.surfaceText;
}
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.barTextSize(battery.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable
}
}
}
}
MouseArea {
x: -battery.leftMargin
y: -battery.topMargin
width: battery.width + battery.leftMargin + battery.rightMargin
height: battery.height + battery.topMargin + battery.bottomMargin
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = battery.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, battery.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
toggleBatteryPopup()
}
}
}

View File

@@ -0,0 +1,25 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Widgets
BasePill {
id: root
property bool isActive: false
property var clipboardHistoryModal: null
content: Component {
Item {
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
DankIcon {
anchors.centerIn: parent
name: "content_paste"
size: Theme.barIconSize(root.barThickness)
color: Theme.surfaceText
}
}
}
}

View File

@@ -0,0 +1,257 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.Plugins
import qs.Widgets
BasePill {
id: root
property bool compactMode: false
signal clockClicked
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : clockRow.implicitWidth
implicitHeight: root.isVerticalOrientation ? clockColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: clockColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: -2
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: {
if (SettingsData.use24HourClock) {
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(0)
} else {
const hours = systemClock?.date?.getHours()
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
return String(display).padStart(2, '0').charAt(0)
}
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
StyledText {
text: {
if (SettingsData.use24HourClock) {
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(1)
} else {
const hours = systemClock?.date?.getHours()
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
return String(display).padStart(2, '0').charAt(1)
}
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
}
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0)
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
StyledText {
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1)
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
}
Row {
visible: SettingsData.showSeconds
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0)
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1)
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
}
Item {
width: 12
height: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: 12
height: 1
color: Theme.outlineButton
anchors.centerIn: parent
}
}
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: {
const locale = Qt.locale()
const dateFormatShort = locale.dateFormat(Locale.ShortFormat)
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M')
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0')
return value.charAt(0)
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.primary
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
StyledText {
text: {
const locale = Qt.locale()
const dateFormatShort = locale.dateFormat(Locale.ShortFormat)
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M')
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0')
return value.charAt(1)
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.primary
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
}
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: {
const locale = Qt.locale()
const dateFormatShort = locale.dateFormat(Locale.ShortFormat)
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M')
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0')
return value.charAt(0)
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.primary
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
StyledText {
text: {
const locale = Qt.locale()
const dateFormatShort = locale.dateFormat(Locale.ShortFormat)
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M')
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0')
return value.charAt(1)
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.primary
width: 9
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignBottom
}
}
}
Row {
id: clockRow
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingS
StyledText {
id: timeText
text: {
return systemClock?.date?.toLocaleTimeString(Qt.locale(), SettingsData.getEffectiveTimeFormat())
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.baseline: dateText.baseline
}
StyledText {
id: middleDot
text: "•"
font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineButton
anchors.baseline: dateText.baseline
visible: !SettingsData.clockCompactMode
}
StyledText {
id: dateText
text: {
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat)
}
return systemClock?.date?.toLocaleDateString(Qt.locale(), "ddd d")
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.clockCompactMode
}
}
SystemClock {
id: systemClock
precision: SystemClock.Seconds
}
}
}
MouseArea {
x: -root.leftMargin
y: -root.topMargin
width: root.width + root.leftMargin + root.rightMargin
height: root.height + root.topMargin + root.bottomMargin
cursorShape: Qt.PointingHandCursor
onPressed: {
if (root.popoutTarget && root.popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = root.parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, root.visualWidth)
root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen)
}
root.clockClicked()
}
}
}

View File

@@ -0,0 +1,35 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Widgets
BasePill {
id: root
property bool isActive: false
signal colorPickerRequested()
content: Component {
Item {
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
DankIcon {
anchors.centerIn: parent
name: "palette"
size: Theme.barIconSize(root.barThickness, -4)
color: root.isActive ? Theme.primary : Theme.surfaceText
}
}
}
MouseArea {
z: 1
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: {
root.colorPickerRequested()
}
}
}

View File

@@ -0,0 +1,255 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool isActive: false
property var popoutTarget: null
property var widgetData: null
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : controlIndicators.implicitWidth
implicitHeight: root.isVerticalOrientation ? controlColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: controlColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
if (NetworkService.wifiToggling) {
return "sync"
}
const status = NetworkService.networkStatus
if (status === "ethernet") {
return "lan"
}
if (status === "vpn") {
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
}
return NetworkService.wifiSignalIcon
}
size: Theme.barIconSize(root.barThickness)
color: {
if (NetworkService.wifiToggling) {
return Theme.primary
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
}
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
DankIcon {
name: "bluetooth"
size: Theme.barIconSize(root.barThickness)
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
}
Rectangle {
width: audioIconV.implicitWidth + 4
height: audioIconV.implicitHeight + 4
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showAudioIcon
DankIcon {
id: audioIconV
name: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off"
} else if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down"
} else {
return "volume_up"
}
}
return "volume_up"
}
size: Theme.barIconSize(root.barThickness)
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
let newVolume
if (delta > 0) {
newVolume = Math.min(100, currentVolume + 5)
} else {
newVolume = Math.max(0, currentVolume - 5)
}
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newVolume / 100
}
wheelEvent.accepted = true
}
}
}
DankIcon {
name: "settings"
size: Theme.barIconSize(root.barThickness)
color: root.isActive ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
}
}
Row {
id: controlIndicators
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
id: networkIcon
name: {
if (NetworkService.wifiToggling) {
return "sync"
}
const status = NetworkService.networkStatus
if (status === "ethernet") {
return "lan"
}
if (status === "vpn") {
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
}
return NetworkService.wifiSignalIcon
}
size: Theme.barIconSize(root.barThickness)
color: {
if (NetworkService.wifiToggling) {
return Theme.primary
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
}
anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
DankIcon {
id: bluetoothIcon
name: "bluetooth"
size: Theme.barIconSize(root.barThickness)
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
}
Rectangle {
width: audioIcon.implicitWidth + 4
height: audioIcon.implicitHeight + 4
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showAudioIcon
DankIcon {
id: audioIcon
name: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off";
} else if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down";
} else {
return "volume_up";
}
}
return "volume_up";
}
size: Theme.barIconSize(root.barThickness)
color: Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: audioWheelArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y;
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
let newVolume;
if (delta > 0) {
newVolume = Math.min(100, currentVolume + 5);
} else {
newVolume = Math.max(0, currentVolume - 5);
}
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newVolume / 100;
}
wheelEvent.accepted = true;
}
}
}
DankIcon {
name: "mic"
size: Theme.barIconSize(root.barThickness)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: false
}
DankIcon {
name: "settings"
size: Theme.barIconSize(root.barThickness)
color: root.isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
}
}
}
}
MouseArea {
x: -root.leftMargin
y: -root.topMargin
width: root.width + root.leftMargin + root.rightMargin
height: root.height + root.topMargin + root.bottomMargin
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.clicked()
}
}
}

View File

@@ -0,0 +1,140 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool showPercentage: true
property bool showIcon: true
property var toggleProcessList
property var popoutTarget: null
property var widgetData: null
property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
Component.onCompleted: {
DgopService.addRef(["cpu"]);
}
Component.onDestruction: {
DgopService.removeRef(["cpu"]);
}
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : cpuContent.implicitWidth
implicitHeight: root.isVerticalOrientation ? cpuColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: cpuColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "memory"
size: Theme.barIconSize(root.barThickness)
color: {
if (DgopService.cpuUsage > 80) {
return Theme.tempDanger;
}
if (DgopService.cpuUsage > 60) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null || DgopService.cpuUsage === 0) {
return "--";
}
return DgopService.cpuUsage.toFixed(0);
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: cpuContent
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "memory"
size: Theme.barIconSize(root.barThickness)
color: {
if (DgopService.cpuUsage > 80) {
return Theme.tempDanger;
}
if (DgopService.cpuUsage > 60) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (DgopService.cpuUsage === undefined || DgopService.cpuUsage === null || DgopService.cpuUsage === 0) {
return "--%";
}
return DgopService.cpuUsage.toFixed(0) + "%";
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
StyledTextMetrics {
id: cpuBaseline
font.pixelSize: Theme.barTextSize(root.barThickness)
text: "100%"
}
width: root.minimumWidth ? Math.max(cpuBaseline.width, paintedWidth) : paintedWidth
Behavior on width {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
DgopService.setSortBy("cpu");
if (root.toggleProcessList) {
root.toggleProcessList();
}
}
}
}

View File

@@ -0,0 +1,140 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool showPercentage: true
property bool showIcon: true
property var toggleProcessList
property var popoutTarget: null
property var widgetData: null
property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
Component.onCompleted: {
DgopService.addRef(["cpu"]);
}
Component.onDestruction: {
DgopService.removeRef(["cpu"]);
}
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : cpuTempContent.implicitWidth
implicitHeight: root.isVerticalOrientation ? cpuTempColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: cpuTempColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "device_thermostat"
size: Theme.barIconSize(root.barThickness)
color: {
if (DgopService.cpuTemperature > 85) {
return Theme.tempDanger;
}
if (DgopService.cpuTemperature > 69) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) {
return "--";
}
return Math.round(DgopService.cpuTemperature).toString();
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: cpuTempContent
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "device_thermostat"
size: Theme.barIconSize(root.barThickness)
color: {
if (DgopService.cpuTemperature > 85) {
return Theme.tempDanger;
}
if (DgopService.cpuTemperature > 69) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (DgopService.cpuTemperature === undefined || DgopService.cpuTemperature === null || DgopService.cpuTemperature < 0) {
return "--°";
}
return Math.round(DgopService.cpuTemperature) + "°";
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
StyledTextMetrics {
id: tempBaseline
font.pixelSize: Theme.barTextSize(root.barThickness)
text: "100°"
}
width: root.minimumWidth ? Math.max(tempBaseline.width, paintedWidth) : paintedWidth
Behavior on width {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
DgopService.setSortBy("cpu");
if (root.toggleProcessList) {
root.toggleProcessList();
}
}
}
}

View File

@@ -0,0 +1,223 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property var widgetData: null
property string mountPath: (widgetData && widgetData.mountPath !== undefined) ? widgetData.mountPath : "/"
property bool isHovered: mouseArea.containsMouse
property var selectedMount: {
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
return null
}
const currentMountPath = root.mountPath || "/"
for (let i = 0; i < DgopService.diskMounts.length; i++) {
if (DgopService.diskMounts[i].mount === currentMountPath) {
return DgopService.diskMounts[i]
}
}
for (let i = 0; i < DgopService.diskMounts.length; i++) {
if (DgopService.diskMounts[i].mount === "/") {
return DgopService.diskMounts[i]
}
}
return DgopService.diskMounts[0] || null
}
property real diskUsagePercent: {
if (!selectedMount || !selectedMount.percent) {
return 0
}
const percentStr = selectedMount.percent.replace("%", "")
return parseFloat(percentStr) || 0
}
Component.onCompleted: {
DgopService.addRef(["diskmounts"])
}
Component.onDestruction: {
DgopService.removeRef(["diskmounts"])
}
Connections {
function onWidgetDataChanged() {
root.mountPath = Qt.binding(() => {
return (root.widgetData && root.widgetData.mountPath !== undefined) ? root.widgetData.mountPath : "/"
})
root.selectedMount = Qt.binding(() => {
if (!DgopService.diskMounts || DgopService.diskMounts.length === 0) {
return null
}
const currentMountPath = root.mountPath || "/"
for (let i = 0; i < DgopService.diskMounts.length; i++) {
if (DgopService.diskMounts[i].mount === currentMountPath) {
return DgopService.diskMounts[i]
}
}
for (let i = 0; i < DgopService.diskMounts.length; i++) {
if (DgopService.diskMounts[i].mount === "/") {
return DgopService.diskMounts[i]
}
}
return DgopService.diskMounts[0] || null
})
}
target: SettingsData
}
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : diskContent.implicitWidth
implicitHeight: root.isVerticalOrientation ? diskColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: diskColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "storage"
size: Theme.barIconSize(root.barThickness)
color: {
if (root.diskUsagePercent > 90) {
return Theme.tempDanger
}
if (root.diskUsagePercent > 75) {
return Theme.tempWarning
}
return Theme.surfaceText
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
return "--"
}
return root.diskUsagePercent.toFixed(0)
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: diskContent
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "storage"
size: Theme.barIconSize(root.barThickness)
color: {
if (root.diskUsagePercent > 90) {
return Theme.tempDanger
}
if (root.diskUsagePercent > 75) {
return Theme.tempWarning
}
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (!root.selectedMount) {
return "--"
}
return root.selectedMount.mount
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
}
StyledText {
text: {
if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
return "--%"
}
return root.diskUsagePercent.toFixed(0) + "%"
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
StyledTextMetrics {
id: diskBaseline
font.pixelSize: Theme.barTextSize(root.barThickness)
text: "100%"
}
width: Math.max(diskBaseline.width, paintedWidth)
Behavior on width {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
MouseArea {
id: mouseArea
z: 1
anchors.fill: parent
hoverEnabled: root.isVerticalOrientation
onEntered: {
if (root.isVerticalOrientation && root.selectedMount) {
tooltipLoader.active = true
if (tooltipLoader.item) {
const globalPos = mapToGlobal(width / 2, height / 2)
const currentScreen = root.parentScreen || Screen
const screenX = currentScreen ? currentScreen.x : 0
const screenY = currentScreen ? currentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (currentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(root.selectedMount.mount, screenX + tooltipX, relativeY, currentScreen, isLeft, !isLeft)
}
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}

View File

@@ -0,0 +1,254 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Hyprland
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool compactMode: SettingsData.focusedWindowCompactMode
property int availableWidth: 400
readonly property int maxNormalWidth: 456
readonly property int maxCompactWidth: 288
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
property var activeDesktopEntry: null
property bool isHovered: mouseArea.containsMouse
Component.onCompleted: {
updateDesktopEntry()
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
root.updateDesktopEntry()
}
}
Connections {
target: root
function onActiveWindowChanged() {
root.updateDesktopEntry()
}
}
function updateDesktopEntry() {
if (activeWindow && activeWindow.appId) {
const moddedId = Paths.moddedAppId(activeWindow.appId)
activeDesktopEntry = DesktopEntries.heuristicLookup(moddedId)
} else {
activeDesktopEntry = null
}
}
readonly property bool hasWindowsOnCurrentWorkspace: {
if (CompositorService.isNiri) {
let currentWorkspaceId = null
for (var i = 0; i < NiriService.allWorkspaces.length; i++) {
const ws = NiriService.allWorkspaces[i]
if (ws.is_focused) {
currentWorkspaceId = ws.id
break
}
}
if (!currentWorkspaceId) {
return false
}
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId)
return workspaceWindows.length > 0 && activeWindow && activeWindow.title
}
if (CompositorService.isHyprland) {
if (!Hyprland.focusedWorkspace || !activeWindow || !activeWindow.title) {
return false
}
try {
if (!Hyprland.toplevels) return false
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
const activeHyprToplevel = hyprlandToplevels.find(t => t?.wayland === activeWindow)
if (!activeHyprToplevel || !activeHyprToplevel.workspace) {
return false
}
return activeHyprToplevel.workspace.id === Hyprland.focusedWorkspace.id
} catch (e) {
console.error("FocusedApp: hasWindowsOnCurrentWorkspace error:", e)
return false
}
}
return activeWindow && activeWindow.title
}
visible: hasWindowsOnCurrentWorkspace
content: Component {
Item {
implicitWidth: {
if (!root.hasWindowsOnCurrentWorkspace) return 0
if (root.isVerticalOrientation) return root.widgetThickness - root.horizontalPadding * 2
const baseWidth = contentRow.implicitWidth
return compactMode ? Math.min(baseWidth, maxCompactWidth - root.horizontalPadding * 2) : Math.min(baseWidth, maxNormalWidth - root.horizontalPadding * 2)
}
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
clip: false
IconImage {
id: appIcon
anchors.centerIn: parent
width: 18
height: 18
visible: root.isVerticalOrientation && activeWindow && status === Image.Ready
source: {
if (!activeWindow || !activeWindow.appId) return ""
const moddedId = Paths.moddedAppId(activeWindow.appId)
if (moddedId.toLowerCase().includes("steam_app")) return ""
return Quickshell.iconPath(activeDesktopEntry?.icon, true)
}
smooth: true
mipmap: true
asynchronous: true
}
DankIcon {
anchors.centerIn: parent
size: 18
name: "sports_esports"
color: Theme.surfaceText
visible: {
if (!root.isVerticalOrientation || !activeWindow || !activeWindow.appId) return false
const moddedId = Paths.moddedAppId(activeWindow.appId)
return moddedId.toLowerCase().includes("steam_app")
}
}
Text {
anchors.centerIn: parent
visible: {
if (!root.isVerticalOrientation || !activeWindow || !activeWindow.appId) return false
if (appIcon.status === Image.Ready) return false
const moddedId = Paths.moddedAppId(activeWindow.appId)
return !moddedId.toLowerCase().includes("steam_app")
}
text: {
if (!activeWindow || !activeWindow.appId) return "?"
if (activeDesktopEntry && activeDesktopEntry.name) {
return activeDesktopEntry.name.charAt(0).toUpperCase()
}
return activeWindow.appId.charAt(0).toUpperCase()
}
font.pixelSize: 10
color: Theme.surfaceText
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !root.isVerticalOrientation
StyledText {
id: appText
text: {
if (!activeWindow || !activeWindow.appId) {
return "";
}
const desktopEntry = DesktopEntries.heuristicLookup(activeWindow.appId);
return desktopEntry && desktopEntry.name ? desktopEntry.name : activeWindow.appId;
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 80 : 180)
visible: !compactMode && text.length > 0
}
StyledText {
text: "•"
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: !compactMode && appText.text && titleText.text
}
StyledText {
id: titleText
text: {
const title = activeWindow && activeWindow.title ? activeWindow.title : "";
const appName = appText.text;
if (!title || !appName) {
return title;
}
if (title.endsWith(" - " + appName)) {
return title.substring(0, title.length - (" - " + appName).length);
}
if (title.endsWith(appName)) {
return title.substring(0, title.length - appName.length).replace(/ - $/, "");
}
return title;
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
maximumLineCount: 1
width: Math.min(implicitWidth, compactMode ? 280 : 250)
visible: text.length > 0
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: root.isVerticalOrientation
acceptedButtons: Qt.NoButton
onEntered: {
if (root.isVerticalOrientation && activeWindow && activeWindow.appId && root.parentScreen) {
tooltipLoader.active = true
if (tooltipLoader.item) {
const globalPos = mapToGlobal(width / 2, height / 2)
const currentScreen = root.parentScreen
const screenX = currentScreen ? currentScreen.x : 0
const screenY = currentScreen ? currentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (currentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const appName = activeDesktopEntry && activeDesktopEntry.name ? activeDesktopEntry.name : activeWindow.appId
const title = activeWindow.title || ""
const tooltipText = appName + (title ? " • " + title : "")
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, currentScreen, isLeft, !isLeft)
}
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
}

View File

@@ -0,0 +1,220 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool showPercentage: true
property bool showIcon: true
property var toggleProcessList
property var popoutTarget: null
property var widgetData: null
property int selectedGpuIndex: (widgetData && widgetData.selectedGpuIndex !== undefined) ? widgetData.selectedGpuIndex : 0
property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
property real displayTemp: {
if (!DgopService.availableGpus || DgopService.availableGpus.length === 0) {
return 0;
}
if (selectedGpuIndex >= 0 && selectedGpuIndex < DgopService.availableGpus.length) {
return DgopService.availableGpus[selectedGpuIndex].temperature || 0;
}
return 0;
}
function updateWidgetPciId(pciId) {
const sections = ["left", "center", "right"];
for (let s = 0; s < sections.length; s++) {
const sectionId = sections[s];
let widgets = [];
if (sectionId === "left") {
widgets = SettingsData.dankBarLeftWidgets.slice();
} else if (sectionId === "center") {
widgets = SettingsData.dankBarCenterWidgets.slice();
} else if (sectionId === "right") {
widgets = SettingsData.dankBarRightWidgets.slice();
}
for (let i = 0; i < widgets.length; i++) {
const widget = widgets[i];
if (typeof widget === "object" && widget.id === "gpuTemp" && (!widget.pciId || widget.pciId === "")) {
widgets[i] = {
"id": widget.id,
"enabled": widget.enabled !== undefined ? widget.enabled : true,
"selectedGpuIndex": 0,
"pciId": pciId
};
if (sectionId === "left") {
SettingsData.setDankBarLeftWidgets(widgets);
} else if (sectionId === "center") {
SettingsData.setDankBarCenterWidgets(widgets);
} else if (sectionId === "right") {
SettingsData.setDankBarRightWidgets(widgets);
}
return ;
}
}
}
}
Component.onCompleted: {
DgopService.addRef(["gpu"]);
if (widgetData && widgetData.pciId) {
DgopService.addGpuPciId(widgetData.pciId);
} else {
autoSaveTimer.running = true;
}
}
Component.onDestruction: {
DgopService.removeRef(["gpu"]);
if (widgetData && widgetData.pciId) {
DgopService.removeGpuPciId(widgetData.pciId);
}
}
Connections {
function onWidgetDataChanged() {
root.selectedGpuIndex = Qt.binding(() => {
return (root.widgetData && root.widgetData.selectedGpuIndex !== undefined) ? root.widgetData.selectedGpuIndex : 0;
});
}
target: SettingsData
}
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : gpuTempContent.implicitWidth
implicitHeight: root.isVerticalOrientation ? gpuTempColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: gpuTempColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "auto_awesome_mosaic"
size: Theme.barIconSize(root.barThickness)
color: {
if (root.displayTemp > 80) {
return Theme.tempDanger;
}
if (root.displayTemp > 65) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) {
return "--";
}
return Math.round(root.displayTemp).toString();
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: gpuTempContent
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "auto_awesome_mosaic"
size: Theme.barIconSize(root.barThickness)
color: {
if (root.displayTemp > 80) {
return Theme.tempDanger;
}
if (root.displayTemp > 65) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (root.displayTemp === undefined || root.displayTemp === null || root.displayTemp === 0) {
return "--°";
}
return Math.round(root.displayTemp) + "°";
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
StyledTextMetrics {
id: gpuTempBaseline
font.pixelSize: Theme.barTextSize(root.barThickness)
text: "100°"
}
width: root.minimumWidth ? Math.max(gpuTempBaseline.width, paintedWidth) : paintedWidth
Behavior on width {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
DgopService.setSortBy("cpu");
if (root.toggleProcessList) {
root.toggleProcessList();
}
}
}
Timer {
id: autoSaveTimer
interval: 100
running: false
onTriggered: {
if (DgopService.availableGpus && DgopService.availableGpus.length > 0) {
const firstGpu = DgopService.availableGpus[0];
if (firstGpu && firstGpu.pciId) {
updateWidgetPciId(firstGpu.pciId);
DgopService.addGpuPciId(firstGpu.pciId);
}
}
}
}
}

View File

@@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
content: Component {
Item {
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
DankIcon {
anchors.centerIn: parent
name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
size: Theme.barIconSize(root.barThickness, -4)
color: Theme.surfaceText
}
}
}
MouseArea {
z: 1
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
SessionService.toggleIdleInhibit()
}
}
}

View File

@@ -0,0 +1,148 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modules.Plugins
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool compactMode: SettingsData.keyboardLayoutNameCompactMode
property string currentLayout: CompositorService.isNiri ? NiriService.getCurrentKeyboardLayoutName() : ""
property string hyprlandKeyboard: ""
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth
implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: contentColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "keyboard"
size: Theme.barIconSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (!root.currentLayout) return ""
const parts = root.currentLayout.split(" ")
if (parts.length > 0) {
return parts[0].substring(0, 2).toUpperCase()
}
return root.currentLayout.substring(0, 2).toUpperCase()
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: contentRow
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingS
StyledText {
text: root.currentLayout
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
MouseArea {
z: 1
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CompositorService.isNiri) {
NiriService.cycleKeyboardLayout()
} else if (CompositorService.isHyprland) {
Quickshell.execDetached([
"hyprctl",
"switchxkblayout",
root.hyprlandKeyboard,
"next"
])
}
}
}
Connections {
target: CompositorService.isHyprland ? Hyprland : null
enabled: CompositorService.isHyprland
function onRawEvent(event) {
if (event.name === "activelayout") {
updateLayout()
}
}
}
Component.onCompleted: {
if (CompositorService.isHyprland) {
updateLayout()
}
}
function updateLayout() {
if (CompositorService.isHyprland) {
Proc.runCommand(null, ["hyprctl", "-j", "devices"], (output, exitCode) => {
if (exitCode !== 0) {
root.currentLayout = "Unknown"
return
}
try {
const data = JSON.parse(output)
const mainKeyboard = data.keyboards.find(kb => kb.main === true)
root.hyprlandKeyboard = mainKeyboard.name
if (mainKeyboard) {
const layout = mainKeyboard.layout
const variant = mainKeyboard.variant
const index = mainKeyboard.active_layout_index
if (root.compactMode && layout && variant && index !== undefined) {
const layouts = mainKeyboard.layout.split(",")
const variants = mainKeyboard.variant.split(",")
const index = mainKeyboard.active_layout_index
if (layouts[index] && variants[index] !== undefined) {
if (variants[index] === "") {
root.currentLayout = layouts[index]
} else {
root.currentLayout = layouts[index] + "-" + variants[index]
}
} else {
root.currentLayout = "Unknown"
}
} else if (mainKeyboard && mainKeyboard.active_keymap) {
root.currentLayout = mainKeyboard.active_keymap
} else {
root.currentLayout = "Unknown"
}
} else {
root.currentLayout = "Unknown"
}
} catch (e) {
root.currentLayout = "Unknown"
}
})
}
}
}

View File

@@ -0,0 +1,120 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool isActive: false
property var hyprlandOverviewLoader: null
content: Component {
Item {
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
DankIcon {
visible: SettingsData.launcherLogoMode === "apps"
anchors.centerIn: parent
name: "apps"
size: Theme.barIconSize(root.barThickness, -4)
color: Theme.surfaceText
}
SystemLogo {
visible: SettingsData.launcherLogoMode === "os"
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
colorOverride: Theme.effectiveLogoColor
brightnessOverride: SettingsData.launcherLogoBrightness
contrastOverride: SettingsData.launcherLogoContrast
}
IconImage {
visible: SettingsData.launcherLogoMode === "dank"
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
smooth: true
mipmap: true
asynchronous: true
source: "file://" + Theme.shellDir + "/assets/danklogo.svg"
layer.enabled: Theme.effectiveLogoColor !== ""
layer.smooth: true
layer.mipmap: true
layer.effect: MultiEffect {
saturation: 0
colorization: 1
colorizationColor: Theme.effectiveLogoColor
}
}
IconImage {
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isLabwc)
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
smooth: true
asynchronous: true
source: {
if (CompositorService.isNiri) {
return "file://" + Theme.shellDir + "/assets/niri.svg"
} else if (CompositorService.isHyprland) {
return "file://" + Theme.shellDir + "/assets/hyprland.svg"
} else if (CompositorService.isDwl) {
return "file://" + Theme.shellDir + "/assets/mango.png"
} else if (CompositorService.isSway) {
return "file://" + Theme.shellDir + "/assets/sway.svg"
} else if (CompositorService.isLabwc) {
return "file://" + Theme.shellDir + "/assets/labwc.png"
}
return ""
}
layer.enabled: Theme.effectiveLogoColor !== ""
layer.effect: MultiEffect {
saturation: 0
colorization: 1
colorizationColor: Theme.effectiveLogoColor
brightness: {
SettingsData.launcherLogoBrightness
}
contrast: {
SettingsData.launcherLogoContrast
}
}
}
IconImage {
visible: SettingsData.launcherLogoMode === "custom" && SettingsData.launcherLogoCustomPath !== ""
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset)
smooth: true
asynchronous: true
source: SettingsData.launcherLogoCustomPath ? "file://" + SettingsData.launcherLogoCustomPath.replace("file://", "") : ""
layer.enabled: Theme.effectiveLogoColor !== ""
layer.effect: MultiEffect {
saturation: 0
colorization: 1
colorizationColor: Theme.effectiveLogoColor
brightness: SettingsData.launcherLogoBrightness
contrast: SettingsData.launcherLogoContrast
}
}
}
}
onRightClicked: {
if (CompositorService.isNiri) {
NiriService.toggleOverview()
} else if (root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
}
}
}

View File

@@ -0,0 +1,328 @@
import QtQuick
import Quickshell.Services.Mpris
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
readonly property MprisPlayer activePlayer: MprisController.activePlayer
readonly property bool playerAvailable: activePlayer !== null
property bool compactMode: false
readonly property int textWidth: {
switch (SettingsData.mediaSize) {
case 0:
return 0;
case 2:
return 180;
default:
return 120;
}
}
readonly property int currentContentWidth: {
if (isVerticalOrientation) {
return widgetThickness - horizontalPadding * 2;
}
const controlsWidth = 20 + Theme.spacingXS + 24 + Theme.spacingXS + 20;
const audioVizWidth = 20;
const contentWidth = audioVizWidth + Theme.spacingXS + controlsWidth;
return contentWidth + (textWidth > 0 ? textWidth + Theme.spacingXS : 0);
}
readonly property int currentContentHeight: {
if (!isVerticalOrientation) {
return widgetThickness - horizontalPadding * 2;
}
const audioVizHeight = 20;
const playButtonHeight = 24;
return audioVizHeight + Theme.spacingXS + playButtonHeight;
}
content: Component {
Item {
implicitWidth: root.playerAvailable ? root.currentContentWidth : 0
implicitHeight: root.playerAvailable ? root.currentContentHeight : 0
opacity: root.playerAvailable ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on implicitWidth {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on implicitHeight {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Column {
id: verticalLayout
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingXS
AudioVisualization {
anchors.horizontalCenter: parent.horizontalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.popoutTarget && root.popoutTarget.setTriggerPosition) {
const globalPos = parent.mapToGlobal(0, 0)
const currentScreen = root.parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, parent.width)
root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen)
}
root.clicked()
}
}
}
Rectangle {
width: 24
height: 24
radius: 12
anchors.horizontalCenter: parent.horizontalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
}
MouseArea {
anchors.fill: parent
enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: (mouse) => {
if (!activePlayer) return
if (mouse.button === Qt.LeftButton) {
activePlayer.togglePlaying()
} else if (mouse.button === Qt.MiddleButton) {
activePlayer.previous()
} else if (mouse.button === Qt.RightButton) {
activePlayer.next()
}
}
}
}
}
Row {
id: mediaRow
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingXS
Row {
id: mediaInfo
spacing: Theme.spacingXS
AudioVisualization {
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
id: textContainer
property string displayText: {
if (!activePlayer || !activePlayer.trackTitle) {
return "";
}
let identity = activePlayer.identity || "";
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari");
let title = "";
let subtitle = "";
if (isWebMedia && activePlayer.trackTitle) {
title = activePlayer.trackTitle;
subtitle = activePlayer.trackArtist || identity;
} else {
title = activePlayer.trackTitle || "Unknown Track";
subtitle = activePlayer.trackArtist || "";
}
return subtitle.length > 0 ? title + " • " + subtitle : title;
}
anchors.verticalCenter: parent.verticalCenter
width: textWidth
height: root.widgetThickness
visible: SettingsData.mediaSize > 0
clip: true
color: "transparent"
StyledText {
id: mediaText
property bool needsScrolling: implicitWidth > textContainer.width
property real scrollOffset: 0
anchors.verticalCenter: parent.verticalCenter
text: textContainer.displayText
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
wrapMode: Text.NoWrap
x: needsScrolling ? -scrollOffset : 0
onTextChanged: {
scrollOffset = 0;
scrollAnimation.restart();
}
SequentialAnimation {
id: scrollAnimation
running: mediaText.needsScrolling && textContainer.visible
loops: Animation.Infinite
PauseAnimation {
duration: 2000
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
from: 0
to: mediaText.implicitWidth - textContainer.width + 5
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
}
PauseAnimation {
duration: 2000
}
NumberAnimation {
target: mediaText
property: "scrollOffset"
to: 0
duration: Math.max(1000, (mediaText.implicitWidth - textContainer.width + 5) * 60)
easing.type: Easing.Linear
}
}
}
MouseArea {
anchors.fill: parent
enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor
onPressed: {
if (root.popoutTarget && root.popoutTarget.setTriggerPosition) {
const globalPos = mapToGlobal(0, 0)
const currentScreen = root.parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, root.width)
root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen)
}
root.clicked()
}
}
}
}
Row {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
color: prevArea.containsMouse ? Theme.primaryHover : "transparent"
visible: root.playerAvailable
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 12
color: Theme.surfaceText
}
MouseArea {
id: prevArea
anchors.fill: parent
enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer) {
activePlayer.previous();
}
}
}
}
Rectangle {
width: 24
height: 24
radius: 12
anchors.verticalCenter: parent.verticalCenter
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Theme.primaryHover
visible: root.playerAvailable
opacity: activePlayer ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
size: 14
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
}
MouseArea {
anchors.fill: parent
enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer) {
activePlayer.togglePlaying();
}
}
}
}
Rectangle {
width: 20
height: 20
radius: 10
anchors.verticalCenter: parent.verticalCenter
color: nextArea.containsMouse ? Theme.primaryHover : "transparent"
visible: playerAvailable
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 12
color: Theme.surfaceText
}
MouseArea {
id: nextArea
anchors.fill: parent
enabled: root.playerAvailable
cursorShape: Qt.PointingHandCursor
onClicked: {
if (activePlayer) {
activePlayer.next();
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,161 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modules.Plugins
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
BasePill {
id: root
function formatNetworkSpeed(bytesPerSec) {
if (bytesPerSec < 1024) {
return bytesPerSec.toFixed(0) + " B/s"
} else if (bytesPerSec < 1024 * 1024) {
return (bytesPerSec / 1024).toFixed(1) + " KB/s"
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
}
}
Component.onCompleted: {
DgopService.addRef(["network"])
}
Component.onDestruction: {
DgopService.removeRef(["network"])
}
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : contentRow.implicitWidth
implicitHeight: root.isVerticalOrientation ? contentColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: contentColumn
anchors.centerIn: parent
spacing: 2
visible: root.isVerticalOrientation
DankIcon {
name: "network_check"
size: Theme.barIconSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
const rate = DgopService.networkRxRate
if (rate < 1024) return rate.toFixed(0)
if (rate < 1024 * 1024) return (rate / 1024).toFixed(0) + "K"
return (rate / (1024 * 1024)).toFixed(0) + "M"
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.info
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
const rate = DgopService.networkTxRate
if (rate < 1024) return rate.toFixed(0)
if (rate < 1024 * 1024) return (rate / 1024).toFixed(0) + "K"
return (rate / (1024 * 1024)).toFixed(0) + "M"
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !root.isVerticalOrientation
DankIcon {
name: "network_check"
size: Theme.barIconSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "↓"
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.info
}
StyledText {
text: DgopService.networkRxRate > 0 ? root.formatNetworkSpeed(DgopService.networkRxRate) : "0 B/s"
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
wrapMode: Text.NoWrap
StyledTextMetrics {
id: rxBaseline
font.pixelSize: Theme.barTextSize(root.barThickness)
text: "88.8 MB/s"
}
width: Math.max(rxBaseline.width, paintedWidth)
Behavior on width {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
StyledText {
text: "↑"
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.error
}
StyledText {
text: DgopService.networkTxRate > 0 ? root.formatNetworkSpeed(DgopService.networkTxRate) : "0 B/s"
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
wrapMode: Text.NoWrap
StyledTextMetrics {
id: txBaseline
font.pixelSize: Theme.barTextSize(root.barThickness)
text: "88.8 MB/s"
}
width: Math.max(txBaseline.width, paintedWidth)
Behavior on width {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
readonly property string focusedScreenName: (
CompositorService.isHyprland && typeof Hyprland !== "undefined" && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor ? (Hyprland.focusedWorkspace.monitor.name || "") :
CompositorService.isNiri && typeof NiriService !== "undefined" && NiriService.currentOutput ? NiriService.currentOutput : ""
)
function resolveNotepadInstance() {
if (typeof notepadSlideoutVariants === "undefined" || !notepadSlideoutVariants || !notepadSlideoutVariants.instances) {
return null
}
const targetScreen = focusedScreenName
if (targetScreen) {
for (var i = 0; i < notepadSlideoutVariants.instances.length; i++) {
var slideout = notepadSlideoutVariants.instances[i]
if (slideout.modelData && slideout.modelData.name === targetScreen) {
return slideout
}
}
}
return notepadSlideoutVariants.instances.length > 0 ? notepadSlideoutVariants.instances[0] : null
}
readonly property var notepadInstance: resolveNotepadInstance()
readonly property bool isActive: notepadInstance?.isVisible ?? false
content: Component {
Item {
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
DankIcon {
id: notepadIcon
anchors.centerIn: parent
name: "assignment"
size: Theme.barIconSize(root.barThickness, -4)
color: root.isActive ? Theme.primary : Theme.surfaceText
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onPressed: {
const inst = root.notepadInstance
if (inst) {
inst.toggle()
}
}
}
}

View File

@@ -0,0 +1,36 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Widgets
BasePill {
id: root
property bool hasUnread: false
property bool isActive: false
content: Component {
Item {
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
DankIcon {
id: notifIcon
anchors.centerIn: parent
name: SessionData.doNotDisturb ? "notifications_off" : "notifications"
size: Theme.barIconSize(root.barThickness, -4)
color: SessionData.doNotDisturb ? Theme.error : (root.isActive ? Theme.primary : Theme.surfaceText)
}
Rectangle {
width: 6
height: 6
radius: 3
color: Theme.error
anchors.right: notifIcon.right
anchors.top: notifIcon.top
visible: root.hasUnread
}
}
}
}

View File

@@ -0,0 +1,252 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "right"
property var popupTarget: null
property var parentScreen: null
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
readonly property bool hasActivePrivacy: PrivacyService.anyPrivacyActive
readonly property int activeCount: PrivacyService.microphoneActive + PrivacyService.cameraActive + PrivacyService.screensharingActive
readonly property real contentWidth: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
readonly property real contentHeight: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
readonly property real visualWidth: isVertical ? widgetThickness : (hasActivePrivacy ? (contentWidth + horizontalPadding * 2) : 0)
readonly property real visualHeight: isVertical ? (hasActivePrivacy ? (contentHeight + horizontalPadding * 2) : 0) : widgetThickness
width: isVertical ? barThickness : visualWidth
height: isVertical ? visualHeight : barThickness
visible: hasActivePrivacy
opacity: hasActivePrivacy ? 1 : 0
enabled: hasActivePrivacy
Rectangle {
id: visualContent
width: root.visualWidth
height: root.visualHeight
anchors.centerIn: parent
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground) {
return "transparent"
}
return Qt.rgba(privacyArea.containsMouse ? Theme.errorPressed.r : Theme.errorHover.r, privacyArea.containsMouse ? Theme.errorPressed.g : Theme.errorHover.g, privacyArea.containsMouse ? Theme.errorPressed.b : Theme.errorHover.b, (privacyArea.containsMouse ? Theme.errorPressed.a : Theme.errorHover.a) * Theme.widgetTransparency)
}
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: root.isVertical && root.hasActivePrivacy
Item {
width: 18
height: 18
visible: PrivacyService.microphoneActive
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: {
const sourceAudio = AudioService.source?.audio
const muted = !sourceAudio || sourceAudio.muted || sourceAudio.volume === 0.0
if (muted) return "mic_off"
return "mic"
}
size: Theme.iconSizeSmall
color: Theme.error
filled: true
anchors.centerIn: parent
}
}
Item {
width: 18
height: 18
visible: PrivacyService.cameraActive
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "camera_video"
size: Theme.iconSizeSmall
color: Theme.surfaceText
filled: true
anchors.centerIn: parent
}
Rectangle {
width: 6
height: 6
radius: 3
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -2
anchors.topMargin: -1
}
}
Item {
width: 18
height: 18
visible: PrivacyService.screensharingActive
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "screen_share"
size: Theme.iconSizeSmall
color: Theme.warning
filled: true
anchors.centerIn: parent
}
}
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: !root.isVertical && root.hasActivePrivacy
Item {
width: 18
height: 18
visible: PrivacyService.microphoneActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: {
const sourceAudio = AudioService.source?.audio
const muted = !sourceAudio || sourceAudio.muted || sourceAudio.volume === 0.0
if (muted) return "mic_off"
return "mic"
}
size: Theme.iconSizeSmall
color: Theme.error
filled: true
anchors.centerIn: parent
}
}
Item {
width: 18
height: 18
visible: PrivacyService.cameraActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "camera_video"
size: Theme.iconSizeSmall
color: Theme.surfaceText
filled: true
anchors.centerIn: parent
}
Rectangle {
width: 6
height: 6
radius: 3
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -2
anchors.topMargin: -1
}
}
Item {
width: 18
height: 18
visible: PrivacyService.screensharingActive
anchors.verticalCenter: parent.verticalCenter
DankIcon {
name: "screen_share"
size: Theme.iconSizeSmall
color: Theme.warning
filled: true
anchors.centerIn: parent
}
}
}
}
MouseArea {
id: privacyArea
z: -1
anchors.fill: parent
hoverEnabled: hasActivePrivacy
enabled: hasActivePrivacy
cursorShape: Qt.PointingHandCursor
onClicked: {
}
}
Rectangle {
id: tooltip
width: tooltipText.contentWidth + Theme.spacingM * 2
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.outlineMedium
border.width: 1
visible: false
opacity: privacyArea.containsMouse && hasActivePrivacy ? 1 : 0
z: 100
x: (parent.width - width) / 2
y: -height - Theme.spacingXS
StyledText {
id: tooltipText
anchors.centerIn: parent
text: PrivacyService.getPrivacySummary()
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
}
Rectangle {
width: 8
height: 8
color: parent.color
border.color: parent.border.color
border.width: parent.border.width
rotation: 45
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.bottom
anchors.topMargin: -4
}
Behavior on opacity {
enabled: hasActivePrivacy && root.visible
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on width {
enabled: hasActivePrivacy && visible && !isVertical
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
enabled: hasActivePrivacy && visible && isVertical
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}

View File

@@ -0,0 +1,163 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool showPercentage: true
property bool showIcon: true
property var toggleProcessList
property var popoutTarget: null
property var widgetData: null
property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
property bool showSwap: (widgetData && widgetData.showSwap !== undefined) ? widgetData.showSwap : false
readonly property real swapUsage: DgopService.totalSwapKB > 0 ? (DgopService.usedSwapKB / DgopService.totalSwapKB) * 100 : 0
Component.onCompleted: {
DgopService.addRef(["memory"]);
}
Component.onDestruction: {
DgopService.removeRef(["memory"]);
}
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : ramContent.implicitWidth
implicitHeight: root.isVerticalOrientation ? ramColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: ramColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: "developer_board"
size: Theme.barIconSize(root.barThickness)
color: {
if (DgopService.memoryUsage > 90) {
return Theme.tempDanger;
}
if (DgopService.memoryUsage > 75) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
if (DgopService.memoryUsage === undefined || DgopService.memoryUsage === null || DgopService.memoryUsage === 0) {
return "--";
}
return DgopService.memoryUsage.toFixed(0);
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
visible: root.showSwap && DgopService.totalSwapKB > 0
text: root.swapUsage.toFixed(0)
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: ramContent
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "developer_board"
size: Theme.barIconSize(root.barThickness)
color: {
if (DgopService.memoryUsage > 90) {
return Theme.tempDanger;
}
if (DgopService.memoryUsage > 75) {
return Theme.tempWarning;
}
return Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
if (DgopService.memoryUsage === undefined || DgopService.memoryUsage === null || DgopService.memoryUsage === 0) {
return "--%";
}
let ramText = DgopService.memoryUsage.toFixed(0) + "%";
if (root.showSwap && DgopService.totalSwapKB > 0) {
return ramText + " · " + root.swapUsage.toFixed(0) + "%";
}
return ramText;
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignLeft
elide: Text.ElideNone
wrapMode: Text.NoWrap
StyledTextMetrics {
id: ramBaseline
font.pixelSize: Theme.barTextSize(root.barThickness)
text: {
if (!root.showSwap) {
return "100%";
}
if (root.swapUsage < 10) {
return "100% · 0%";
}
return "100% · 100%";
}
}
width: root.minimumWidth ? Math.max(ramBaseline.width, paintedWidth) : paintedWidth
Behavior on width {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
DgopService.setSortBy("memory");
if (root.toggleProcessList) {
root.toggleProcessList();
}
}
}
}

View File

@@ -0,0 +1,798 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string section: "left"
property var parentScreen
property var hoveredItem: null
property var topBar: null
property real widgetThickness: 30
property real barThickness: 48
readonly property real horizontalPadding: SettingsData.dankBarNoBackground ? 2 : Theme.spacingS
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
property int _desktopEntriesUpdateTrigger: 0
property int _toplevelsUpdateTrigger: 0
readonly property var sortedToplevels: {
_toplevelsUpdateTrigger
const toplevels = CompositorService.sortedToplevels
if (!toplevels || toplevels.length === 0) return []
if (SettingsData.runningAppsCurrentWorkspace) {
return CompositorService.filterCurrentWorkspace(toplevels, parentScreen?.name) || []
}
return toplevels
}
Connections {
target: CompositorService
function onToplevelsChanged() {
_toplevelsUpdateTrigger++
}
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
_desktopEntriesUpdateTrigger++
}
}
readonly property var groupedWindows: {
if (!SettingsData.runningAppsGroupByApp) {
return []
}
try {
if (!sortedToplevels || sortedToplevels.length === 0) {
return []
}
const appGroups = new Map()
sortedToplevels.forEach((toplevel, index) => {
if (!toplevel)
return
const appId = toplevel?.appId || "unknown"
if (!appGroups.has(appId)) {
appGroups.set(appId, {
"appId": appId,
"windows": []
})
}
appGroups.get(appId).windows.push({
"toplevel": toplevel,
"windowId": index,
"windowTitle": toplevel?.title || "(Unnamed)"
})
})
return Array.from(appGroups.values())
} catch (e) {
console.error("RunningApps: groupedWindows error:", e)
return []
}
}
readonly property int windowCount: SettingsData.runningAppsGroupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
readonly property int calculatedSize: {
if (windowCount === 0) {
return 0
}
if (SettingsData.runningAppsCompactMode) {
return windowCount * 24 + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
} else {
return windowCount * (24 + Theme.spacingXS + 120) + (windowCount - 1) * Theme.spacingXS + horizontalPadding * 2
}
}
width: isVertical ? barThickness : calculatedSize
height: isVertical ? calculatedSize : barThickness
visible: windowCount > 0
Rectangle {
id: visualBackground
width: root.isVertical ? root.widgetThickness : root.calculatedSize
height: root.isVertical ? root.calculatedSize : root.widgetThickness
anchors.centerIn: parent
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
clip: false
color: {
if (windowCount === 0) {
return "transparent"
}
if (SettingsData.dankBarNoBackground) {
return "transparent"
}
const baseColor = Theme.widgetBaseBackgroundColor
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
property real scrollAccumulator: 0
property real touchpadThreshold: 500
onWheel: wheel => {
const deltaY = wheel.angleDelta.y
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0
const windows = root.sortedToplevels
if (windows.length < 2) {
return
}
if (isMouseWheel) {
// Direct mouse wheel action
let currentIndex = -1
for (var i = 0; i < windows.length; i++) {
if (windows[i].activated) {
currentIndex = i
break
}
}
let nextIndex
if (deltaY < 0) {
if (currentIndex === -1) {
nextIndex = 0
} else {
nextIndex = (currentIndex + 1) % windows.length
}
} else {
if (currentIndex === -1) {
nextIndex = windows.length - 1
} else {
nextIndex = (currentIndex - 1 + windows.length) % windows.length
}
}
const nextWindow = windows[nextIndex]
if (nextWindow) {
nextWindow.activate()
}
} else {
// Touchpad - accumulate small deltas
scrollAccumulator += deltaY
if (Math.abs(scrollAccumulator) >= touchpadThreshold) {
let currentIndex = -1
for (var i = 0; i < windows.length; i++) {
if (windows[i].activated) {
currentIndex = i
break
}
}
let nextIndex
if (scrollAccumulator < 0) {
if (currentIndex === -1) {
nextIndex = 0
} else {
nextIndex = (currentIndex + 1) % windows.length
}
} else {
if (currentIndex === -1) {
nextIndex = windows.length - 1
} else {
nextIndex = (currentIndex - 1 + windows.length) % windows.length
}
}
const nextWindow = windows[nextIndex]
if (nextWindow) {
nextWindow.activate()
}
scrollAccumulator = 0
}
}
wheel.accepted = true
}
}
Loader {
id: layoutLoader
anchors.centerIn: parent
sourceComponent: root.isVertical ? columnLayout : rowLayout
}
Component {
id: rowLayout
Row {
spacing: Theme.spacingXS
Repeater {
id: windowRepeater
model: ScriptModel {
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
}
delegate: Item {
id: delegateItem
property bool isGrouped: SettingsData.runningAppsGroupByApp
property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
property bool isFocused: toplevelData ? toplevelData.activated : false
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
property var toplevelObject: toplevelData
property int windowCount: isGrouped ? modelData.windows.length : 1
property string tooltipText: {
root._desktopEntriesUpdateTrigger
let appName = "Unknown"
if (appId) {
const desktopEntry = DesktopEntries.heuristicLookup(appId)
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
}
if (isGrouped && windowCount > 1) {
return appName + " (" + windowCount + " windows)"
}
return appName + (windowTitle ? " • " + windowTitle : "")
}
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
width: visualWidth
height: root.barThickness
Rectangle {
id: visualContent
width: delegateItem.visualWidth
height: 24
anchors.centerIn: parent
radius: Theme.cornerRadius
color: {
if (isFocused) {
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
} else {
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
}
}
// App icon
IconImage {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
source: {
root._desktopEntriesUpdateTrigger
const moddedId = Paths.moddedAppId(appId)
if (moddedId.toLowerCase().includes("steam_app")) {
return ""
}
return Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
}
DankIcon {
anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"
color: Theme.surfaceText
visible: {
const moddedId = Paths.moddedAppId(appId)
return moddedId.toLowerCase().includes("steam_app")
}
}
// Fallback text if no icon found
Text {
anchors.centerIn: parent
visible: {
const moddedId = Paths.moddedAppId(appId)
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
return !iconImg.visible && !isSteamApp
}
text: {
root._desktopEntriesUpdateTrigger
if (!appId) {
return "?"
}
const desktopEntry = DesktopEntries.heuristicLookup(appId)
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
}
return appId.charAt(0).toUpperCase()
}
font.pixelSize: 10
color: Theme.surfaceText
}
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
anchors.bottomMargin: -2
width: 14
height: 14
radius: 7
color: Theme.primary
visible: isGrouped && windowCount > 1
z: 10
StyledText {
anchors.centerIn: parent
text: windowCount > 9 ? "9+" : windowCount
font.pixelSize: 9
color: Theme.surface
}
}
// Window title text (only visible in expanded mode)
StyledText {
anchors.left: iconImg.right
anchors.leftMargin: Theme.spacingXS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.runningAppsCompactMode
text: windowTitle
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (isGrouped && windowCount > 1) {
let currentIndex = -1
for (var i = 0; i < groupData.windows.length; i++) {
if (groupData.windows[i].toplevel.activated) {
currentIndex = i
break
}
}
const nextIndex = (currentIndex + 1) % groupData.windows.length
groupData.windows[nextIndex].toplevel.activate()
} else if (toplevelObject) {
toplevelObject.activate()
}
} else if (mouse.button === Qt.RightButton) {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
windowContextMenuLoader.active = true
if (windowContextMenuLoader.item) {
windowContextMenuLoader.item.currentWindow = toplevelObject
if (root.isVertical) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const relativeX = globalPos.x - screenX
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
}
}
}
}
onEntered: {
root.hoveredItem = delegateItem
tooltipLoader.active = true
if (tooltipLoader.item) {
if (root.isVertical) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
const isBottom = root.axis?.edge === "bottom"
const tooltipY = isBottom
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
}
}
}
onExited: {
if (root.hoveredItem === delegateItem) {
root.hoveredItem = null
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
}
}
}
}
Component {
id: columnLayout
Column {
spacing: Theme.spacingXS
Repeater {
id: windowRepeater
model: ScriptModel {
values: SettingsData.runningAppsGroupByApp ? groupedWindows : sortedToplevels
objectProp: SettingsData.runningAppsGroupByApp ? "appId" : "address"
}
delegate: Item {
id: delegateItem
property bool isGrouped: SettingsData.runningAppsGroupByApp
property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
property bool isFocused: toplevelData ? toplevelData.activated : false
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
property var toplevelObject: toplevelData
property int windowCount: isGrouped ? modelData.windows.length : 1
property string tooltipText: {
root._desktopEntriesUpdateTrigger
let appName = "Unknown"
if (appId) {
const desktopEntry = DesktopEntries.heuristicLookup(appId)
appName = desktopEntry && desktopEntry.name ? desktopEntry.name : appId
}
if (isGrouped && windowCount > 1) {
return appName + " (" + windowCount + " windows)"
}
return appName + (windowTitle ? " • " + windowTitle : "")
}
readonly property real visualWidth: SettingsData.runningAppsCompactMode ? 24 : (24 + Theme.spacingXS + 120)
width: root.barThickness
height: 24
Rectangle {
id: visualContent
width: delegateItem.visualWidth
height: 24
anchors.centerIn: parent
radius: Theme.cornerRadius
color: {
if (isFocused) {
return mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
} else {
return mouseArea.containsMouse ? Qt.rgba(Theme.primaryHover.r, Theme.primaryHover.g, Theme.primaryHover.b, 0.1) : "transparent"
}
}
IconImage {
id: iconImg
anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: Theme.barIconSize(root.barThickness)
height: Theme.barIconSize(root.barThickness)
source: {
root._desktopEntriesUpdateTrigger
const moddedId = Paths.moddedAppId(appId)
if (moddedId.toLowerCase().includes("steam_app")) {
return ""
}
return Quickshell.iconPath(DesktopEntries.heuristicLookup(moddedId)?.icon, true)
}
smooth: true
mipmap: true
asynchronous: true
visible: status === Image.Ready
}
DankIcon {
anchors.left: parent.left
anchors.leftMargin: SettingsData.runningAppsCompactMode ? (parent.width - Theme.barIconSize(root.barThickness)) / 2 : Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
size: Theme.barIconSize(root.barThickness)
name: "sports_esports"
color: Theme.surfaceText
visible: {
const moddedId = Paths.moddedAppId(appId)
return moddedId.toLowerCase().includes("steam_app")
}
}
Text {
anchors.centerIn: parent
visible: {
const moddedId = Paths.moddedAppId(appId)
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
return !iconImg.visible && !isSteamApp
}
text: {
root._desktopEntriesUpdateTrigger
if (!appId) {
return "?"
}
const desktopEntry = DesktopEntries.heuristicLookup(appId)
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
}
return appId.charAt(0).toUpperCase()
}
font.pixelSize: 10
color: Theme.surfaceText
}
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: SettingsData.runningAppsCompactMode ? -2 : 2
anchors.bottomMargin: -2
width: 14
height: 14
radius: 7
color: Theme.primary
visible: isGrouped && windowCount > 1
z: 10
StyledText {
anchors.centerIn: parent
text: windowCount > 9 ? "9+" : windowCount
font.pixelSize: 9
color: Theme.surface
}
}
StyledText {
anchors.left: iconImg.right
anchors.leftMargin: Theme.spacingXS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
visible: !SettingsData.runningAppsCompactMode
text: windowTitle
font.pixelSize: Theme.barTextSize(barThickness)
color: Theme.surfaceText
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (isGrouped && windowCount > 1) {
let currentIndex = -1
for (var i = 0; i < groupData.windows.length; i++) {
if (groupData.windows[i].toplevel.activated) {
currentIndex = i
break
}
}
const nextIndex = (currentIndex + 1) % groupData.windows.length
groupData.windows[nextIndex].toplevel.activate()
} else if (toplevelObject) {
toplevelObject.activate()
}
} else if (mouse.button === Qt.RightButton) {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
windowContextMenuLoader.active = true
if (windowContextMenuLoader.item) {
windowContextMenuLoader.item.currentWindow = toplevelObject
if (root.isVertical) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const xPos = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
windowContextMenuLoader.item.showAt(xPos, relativeY, true, root.axis?.edge)
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, 0)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const relativeX = globalPos.x - screenX
const yPos = Theme.barHeight + SettingsData.dankBarSpacing - 7
windowContextMenuLoader.item.showAt(relativeX, yPos, false, "top")
}
}
}
}
onEntered: {
root.hoveredItem = delegateItem
tooltipLoader.active = true
if (tooltipLoader.item) {
if (root.isVertical) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(delegateItem.tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height)
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height
const isBottom = root.axis?.edge === "bottom"
const tooltipY = isBottom
? (screenHeight - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS - 35)
: (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS)
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
}
}
}
onExited: {
if (root.hoveredItem === delegateItem) {
root.hoveredItem = null
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}
}
}
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
Loader {
id: windowContextMenuLoader
active: false
sourceComponent: PanelWindow {
id: contextMenuWindow
property var currentWindow: null
property bool isVisible: false
property point anchorPos: Qt.point(0, 0)
property bool isVertical: false
property string edge: "top"
function showAt(x, y, vertical, barEdge) {
screen = root.parentScreen
anchorPos = Qt.point(x, y)
isVertical = vertical ?? false
edge = barEdge ?? "top"
isVisible = true
visible = true
}
function close() {
isVisible = false
visible = false
windowContextMenuLoader.active = false
}
implicitWidth: 100
implicitHeight: 40
visible: false
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
onClicked: contextMenuWindow.close()
}
Rectangle {
x: {
if (contextMenuWindow.isVertical) {
if (contextMenuWindow.edge === "left") {
return Math.min(contextMenuWindow.width - width - 10, contextMenuWindow.anchorPos.x)
} else {
return Math.max(10, contextMenuWindow.anchorPos.x - width)
}
} else {
const left = 10
const right = contextMenuWindow.width - width - 10
const want = contextMenuWindow.anchorPos.x - width / 2
return Math.max(left, Math.min(right, want))
}
}
y: {
if (contextMenuWindow.isVertical) {
const top = 10
const bottom = contextMenuWindow.height - height - 10
const want = contextMenuWindow.anchorPos.y - height / 2
return Math.max(top, Math.min(bottom, want))
} else {
return contextMenuWindow.anchorPos.y
}
}
width: 100
height: 32
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.width: 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
Rectangle {
anchors.fill: parent
radius: parent.radius
color: closeMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
}
StyledText {
anchors.centerIn: parent
text: I18n.tr("Close")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenuWindow.currentWindow) {
contextMenuWindow.currentWindow.close()
}
contextMenuWindow.close()
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
property bool isActive: false
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
readonly property bool isChecking: SystemUpdateService.isChecking
Ref {
service: SystemUpdateService
}
content: Component {
Item {
implicitWidth: root.isVerticalOrientation ? (root.widgetThickness - root.horizontalPadding * 2) : updaterIcon.implicitWidth
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
DankIcon {
id: statusIcon
anchors.centerIn: parent
visible: root.isVerticalOrientation
name: {
if (root.isChecking) return "refresh"
if (SystemUpdateService.hasError) return "error"
if (root.hasUpdates) return "system_update_alt"
return "check_circle"
}
size: Theme.barIconSize(root.barThickness, -4)
color: {
if (SystemUpdateService.hasError) return Theme.error
if (root.hasUpdates) return Theme.primary
return root.isActive ? Theme.primary : Theme.surfaceText
}
RotationAnimation {
id: rotationAnimation
target: statusIcon
property: "rotation"
from: 0
to: 360
duration: 1000
running: root.isChecking
loops: Animation.Infinite
onRunningChanged: {
if (!running) {
statusIcon.rotation = 0
}
}
}
}
Rectangle {
width: 8
height: 8
radius: 4
color: Theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: SettingsData.dankBarNoBackground ? 0 : 6
anchors.topMargin: SettingsData.dankBarNoBackground ? 0 : 6
visible: root.isVerticalOrientation && root.hasUpdates && !root.isChecking
}
Row {
id: updaterIcon
anchors.centerIn: parent
spacing: Theme.spacingXS
visible: !root.isVerticalOrientation
DankIcon {
id: statusIconHorizontal
anchors.verticalCenter: parent.verticalCenter
name: {
if (root.isChecking) return "refresh"
if (SystemUpdateService.hasError) return "error"
if (root.hasUpdates) return "system_update_alt"
return "check_circle"
}
size: Theme.barIconSize(root.barThickness, -4)
color: {
if (SystemUpdateService.hasError) return Theme.error
if (root.hasUpdates) return Theme.primary
return root.isActive ? Theme.primary : Theme.surfaceText
}
RotationAnimation {
id: rotationAnimationHorizontal
target: statusIconHorizontal
property: "rotation"
from: 0
to: 360
duration: 1000
running: root.isChecking
loops: Animation.Infinite
onRunningChanged: {
if (!running) {
statusIconHorizontal.rotation = 0
}
}
}
}
StyledText {
id: countText
anchors.verticalCenter: parent.verticalCenter
text: SystemUpdateService.updateCount.toString()
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
visible: root.hasUpdates && !root.isChecking
}
}
}
}
MouseArea {
z: 1
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.clicked()
}
}
}

View File

@@ -0,0 +1,112 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
Ref {
service: DMSNetworkService
}
property var popoutTarget: null
property bool isHovered: clickArea.containsMouse
signal toggleVpnPopup()
content: Component {
Item {
implicitWidth: root.widgetThickness - root.horizontalPadding * 2
implicitHeight: root.widgetThickness - root.horizontalPadding * 2
DankIcon {
id: icon
name: DMSNetworkService.connected ? "vpn_lock" : "vpn_key_off"
size: Theme.barIconSize(root.barThickness, -4)
color: DMSNetworkService.connected ? Theme.primary : Theme.surfaceText
opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
anchors.centerIn: parent
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.InOutQuad
}
}
}
}
}
Loader {
id: tooltipLoader
active: false
sourceComponent: DankTooltip {}
}
MouseArea {
id: clickArea
anchors.fill: parent
hoverEnabled: true
cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
enabled: !DMSNetworkService.isBusy
onPressed: {
if (popoutTarget && popoutTarget.setTriggerPosition) {
const globalPos = root.visualContent.mapToGlobal(0, 0)
const currentScreen = parentScreen || Screen
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth)
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen)
}
root.toggleVpnPopup();
}
onEntered: {
if (root.parentScreen && !(popoutTarget && popoutTarget.shouldBeVisible)) {
tooltipLoader.active = true
if (tooltipLoader.item) {
let tooltipText = ""
if (!DMSNetworkService.connected) {
tooltipText = "VPN Disconnected"
} else {
const names = DMSNetworkService.activeNames || []
if (names.length <= 1) {
const name = names[0] || ""
const maxLength = 25
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
tooltipText = "VPN Connected • " + displayName
} else {
const name = names[0]
const maxLength = 20
const displayName = name.length > maxLength ? name.substring(0, maxLength) + "..." : name
tooltipText = "VPN Connected • " + displayName + " +" + (names.length - 1)
}
}
if (root.isVerticalOrientation) {
const globalPos = mapToGlobal(width / 2, height / 2)
const screenX = root.parentScreen ? root.parentScreen.x : 0
const screenY = root.parentScreen ? root.parentScreen.y : 0
const relativeY = globalPos.y - screenY
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - SettingsData.dankBarSpacing - Theme.spacingXS)
const isLeft = root.axis?.edge === "left"
tooltipLoader.item.show(tooltipText, screenX + tooltipX, relativeY, root.parentScreen, isLeft, !isLeft)
} else {
const globalPos = mapToGlobal(width / 2, height)
const tooltipY = Theme.barHeight + SettingsData.dankBarSpacing + Theme.spacingXS
tooltipLoader.item.show(tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false)
}
}
}
}
onExited: {
if (tooltipLoader.item) {
tooltipLoader.item.hide()
}
tooltipLoader.active = false
}
}
}

View File

@@ -0,0 +1,81 @@
import QtQuick
import qs.Common
import qs.Modules.Plugins
import qs.Services
import qs.Widgets
BasePill {
id: root
visible: SettingsData.weatherEnabled
Ref {
service: WeatherService
}
content: Component {
Item {
implicitWidth: {
if (!SettingsData.weatherEnabled) return 0
if (root.isVerticalOrientation) return root.widgetThickness - root.horizontalPadding * 2
return Math.min(100 - root.horizontalPadding * 2, weatherRow.implicitWidth)
}
implicitHeight: root.isVerticalOrientation ? weatherColumn.implicitHeight : (root.widgetThickness - root.horizontalPadding * 2)
Column {
id: weatherColumn
visible: root.isVerticalOrientation
anchors.centerIn: parent
spacing: 1
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.barIconSize(root.barThickness, -6)
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: {
const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
if (temp === undefined || temp === null || temp === 0) {
return "--";
}
return temp;
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
}
Row {
id: weatherRow
visible: !root.isVerticalOrientation
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.barIconSize(root.barThickness, -6)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
if (temp === undefined || temp === null || temp === 0) {
return "--°" + (SettingsData.useFahrenheit ? "F" : "C");
}
return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C");
}
font.pixelSize: Theme.barTextSize(root.barThickness)
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}

View File

@@ -0,0 +1,950 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
import Quickshell.I3
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool isVertical: axis?.isVertical ?? false
property var axis: null
property string screenName: ""
property real widgetHeight: 30
property real barThickness: 48
property var hyprlandOverviewLoader: null
property var parentScreen: null
property int _desktopEntriesUpdateTrigger: 0
readonly property var sortedToplevels: {
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
}
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && ExtWorkspaceService.extWorkspaceAvailable)
Connections {
target: DesktopEntries
function onApplicationsChanged() {
_desktopEntriesUpdateTrigger++
}
}
property var currentWorkspace: {
if (useExtWorkspace) return getExtWorkspaceActiveWorkspace()
switch (CompositorService.compositor) {
case "niri":
return getNiriActiveWorkspace()
case "hyprland":
return getHyprlandActiveWorkspace()
case "dwl":
const activeTags = getDwlActiveTags()
return activeTags.length > 0 ? activeTags[0] : -1
case "sway":
return getSwayActiveWorkspace()
default:
return 1
}
}
property var dwlActiveTags: {
if (CompositorService.isDwl) {
return getDwlActiveTags()
}
return []
}
property var workspaceList: {
if (useExtWorkspace) {
const baseList = getExtWorkspaceWorkspaces()
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
}
let baseList
switch (CompositorService.compositor) {
case "niri":
baseList = getNiriWorkspaces()
break
case "hyprland":
baseList = getHyprlandWorkspaces()
break
case "dwl":
baseList = getDwlTags()
break
case "sway":
baseList = getSwayWorkspaces()
break
default:
return [1]
}
return SettingsData.showWorkspacePadding ? padWorkspaces(baseList) : baseList
}
function getSwayWorkspaces() {
const workspaces = I3.workspaces?.values || []
if (workspaces.length === 0) return [{"num": 1}]
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return workspaces.slice().sort((a, b) => a.num - b.num)
}
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [{"num": 1}]
}
function getSwayActiveWorkspace() {
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
return focusedWs ? focusedWs.num : 1
}
const focusedWs = I3.workspaces?.values?.find(ws => ws.monitor?.name === root.screenName && ws.focused === true)
return focusedWs ? focusedWs.num : 1
}
function getHyprlandWorkspaces() {
const workspaces = Hyprland.workspaces?.values || []
if (workspaces.length === 0) return [{id: 1}]
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return workspaces.slice().sort((a, b) => a.id - b.id)
}
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName)
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.id - b.id) : [{id: 1}]
}
function getHyprlandActiveWorkspace() {
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return Hyprland.focusedWorkspace?.id || 1
}
const monitor = Hyprland.monitors?.values?.find(m => m.name === root.screenName)
return monitor?.activeWorkspace?.id || 1
}
function getWorkspaceIcons(ws) {
_desktopEntriesUpdateTrigger
if (!SettingsData.showWorkspaceApps || !ws) {
return []
}
let targetWorkspaceId
if (CompositorService.isNiri) {
const wsNumber = typeof ws === "number" ? ws : -1
if (wsNumber <= 0) {
return []
}
const workspace = NiriService.allWorkspaces.find(w => w.idx + 1 === wsNumber && w.output === root.screenName)
if (!workspace) {
return []
}
targetWorkspaceId = workspace.id
} else if (CompositorService.isHyprland) {
targetWorkspaceId = ws.id !== undefined ? ws.id : ws
} else if (CompositorService.isDwl) {
if (typeof ws !== "object" || ws.tag === undefined) {
return []
}
targetWorkspaceId = ws.tag
} else if (CompositorService.isSway) {
targetWorkspaceId = ws.num !== undefined ? ws.num : ws
} else {
return []
}
const wins = CompositorService.isNiri ? (NiriService.windows || []) : CompositorService.sortedToplevels
const byApp = {}
let isActiveWs = false
if (CompositorService.isNiri) {
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active)
} else if (CompositorService.isSway) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true)
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false
} else if (CompositorService.isDwl) {
const output = DwlService.getOutputState(root.screenName)
if (output && output.tags) {
const tag = output.tags.find(t => t.tag === targetWorkspaceId)
isActiveWs = tag ? (tag.state === 1) : false
}
} else {
isActiveWs = targetWorkspaceId === root.currentWorkspace
}
wins.forEach((w, i) => {
if (!w) {
return
}
let winWs = null
if (CompositorService.isNiri) {
winWs = w.workspace_id
} else if (CompositorService.isSway) {
winWs = w.workspace?.num
} else {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || [])
const hyprToplevel = hyprlandToplevels.find(ht => ht.wayland === w)
winWs = hyprToplevel?.workspace?.id
}
if (winWs === undefined || winWs === null || winWs !== targetWorkspaceId) {
return
}
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown")
const key = isActiveWs ? `${keyBase}_${i}` : keyBase
if (!byApp[key]) {
const moddedId = Paths.moddedAppId(keyBase)
const isSteamApp = moddedId.toLowerCase().includes("steam_app")
const icon = isSteamApp ? "" : DesktopService.resolveIconPath(moddedId)
byApp[key] = {
"type": "icon",
"icon": icon,
"isSteamApp": isSteamApp,
"active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
"count": 1,
"windowId": w.address || w.id,
"fallbackText": w.appId || w.class || w.title || ""
}
} else {
byApp[key].count++
if ((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)) {
byApp[key].active = true
}
}
})
return Object.values(byApp)
}
function padWorkspaces(list) {
const padded = list.slice()
let placeholder
if (useExtWorkspace) {
placeholder = {"id": "", "name": "", "active": false, "hidden": true}
} else if (CompositorService.isHyprland) {
placeholder = {"id": -1, "name": ""}
} else if (CompositorService.isDwl) {
placeholder = {"tag": -1}
} else if (CompositorService.isSway) {
placeholder = {"num": -1}
} else {
placeholder = -1
}
while (padded.length < 3) {
padded.push(placeholder)
}
return padded
}
function getNiriWorkspaces() {
if (NiriService.allWorkspaces.length === 0) {
return [1, 2]
}
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return NiriService.getCurrentOutputWorkspaceNumbers()
}
const displayWorkspaces = NiriService.allWorkspaces.filter(ws => ws.output === root.screenName).map(ws => ws.idx + 1)
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
}
function getNiriActiveWorkspace() {
if (NiriService.allWorkspaces.length === 0) {
return 1
}
if (!root.screenName || !SettingsData.workspacesPerMonitor) {
return NiriService.getCurrentWorkspaceNumber()
}
const activeWs = NiriService.allWorkspaces.find(ws => ws.output === root.screenName && ws.is_active)
return activeWs ? activeWs.idx + 1 : 1
}
function getDwlTags() {
if (!DwlService.dwlAvailable) {
return []
}
const output = DwlService.getOutputState(root.screenName)
if (!output || !output.tags || output.tags.length === 0) {
return []
}
if (SettingsData.dwlShowAllTags) {
return output.tags.map(tag => ({"tag": tag.tag, "state": tag.state, "clients": tag.clients, "focused": tag.focused}))
}
const visibleTagIndices = DwlService.getVisibleTags(root.screenName)
return visibleTagIndices.map(tagIndex => {
const tagData = output.tags.find(t => t.tag === tagIndex)
return {
"tag": tagIndex,
"state": tagData?.state ?? 0,
"clients": tagData?.clients ?? 0,
"focused": tagData?.focused ?? false
}
})
}
function getDwlActiveTags() {
if (!DwlService.dwlAvailable) {
return []
}
const activeTags = DwlService.getActiveTags(root.screenName)
return activeTags
}
function getExtWorkspaceWorkspaces() {
const groups = ExtWorkspaceService.groups
if (!ExtWorkspaceService.extWorkspaceAvailable || groups.length === 0) {
return [{"id": "1", "name": "1", "active": false}]
}
const group = groups.find(g => g.outputs && g.outputs.includes(root.screenName))
if (!group || !group.workspaces) {
return [{"id": "1", "name": "1", "active": false}]
}
const visible = group.workspaces.filter(ws => !ws.hidden).sort((a, b) => {
const coordsA = a.coordinates || [0, 0]
const coordsB = b.coordinates || [0, 0]
if (coordsA[0] !== coordsB[0]) return coordsA[0] - coordsB[0]
return coordsA[1] - coordsB[1]
}).map(ws => ({
id: ws.id,
name: ws.name,
coordinates: ws.coordinates,
state: ws.state,
active: ws.active,
urgent: ws.urgent,
hidden: ws.hidden,
groupID: group.id
}))
return visible.length > 0 ? visible : [{"id": "1", "name": "1", "active": false}]
}
function getExtWorkspaceActiveWorkspace() {
if (!ExtWorkspaceService.extWorkspaceAvailable) {
return 1
}
const activeWs = ExtWorkspaceService.getActiveWorkspaceForOutput(root.screenName)
return activeWs ? (activeWs.id || activeWs.name || "1") : "1"
}
readonly property real padding: Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
readonly property real visualWidth: isVertical ? widgetHeight : (workspaceRow.implicitWidth + padding * 2)
readonly property real visualHeight: isVertical ? (workspaceRow.implicitHeight + padding * 2) : widgetHeight
function getRealWorkspaces() {
return root.workspaceList.filter(ws => {
if (useExtWorkspace) return ws && ws.id !== "" && !ws.hidden
if (CompositorService.isHyprland) return ws && ws.id !== -1
if (CompositorService.isDwl) return ws && ws.tag !== -1
if (CompositorService.isSway) return ws && ws.num !== -1
return ws !== -1
})
}
function switchWorkspace(direction) {
if (useExtWorkspace) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => (ws.id || ws.name) === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
const nextWorkspace = realWorkspaces[nextIndex]
ExtWorkspaceService.activateWorkspace(nextWorkspace.id || nextWorkspace.name, nextWorkspace.groupID || "")
} else if (CompositorService.isNiri) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
NiriService.switchToWorkspace(realWorkspaces[nextIndex] - 1)
} else if (CompositorService.isHyprland) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws.id === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
Hyprland.dispatch(`workspace ${realWorkspaces[nextIndex].id}`)
} else if (CompositorService.isDwl) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws.tag === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag)
} else if (CompositorService.isSway) {
const realWorkspaces = getRealWorkspaces()
if (realWorkspaces.length < 2) {
return
}
const currentIndex = realWorkspaces.findIndex(ws => ws.num === root.currentWorkspace)
const validIndex = currentIndex === -1 ? 0 : currentIndex
const nextIndex = direction > 0 ? Math.min(validIndex + 1, realWorkspaces.length - 1) : Math.max(validIndex - 1, 0)
if (nextIndex === validIndex) {
return
}
try { I3.dispatch(`workspace number ${realWorkspaces[nextIndex].num}`) } catch(_){}
}
}
width: isVertical ? barThickness : visualWidth
height: isVertical ? visualHeight : barThickness
visible: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || useExtWorkspace
Rectangle {
id: visualBackground
width: root.visualWidth
height: root.visualHeight
anchors.centerIn: parent
radius: SettingsData.dankBarNoBackground ? 0 : Theme.cornerRadius
color: {
if (SettingsData.dankBarNoBackground)
return "transparent"
const baseColor = Theme.widgetBaseBackgroundColor
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * Theme.widgetTransparency)
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
if (CompositorService.isNiri) {
NiriService.toggleOverview()
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
}
}
}
}
Flow {
id: workspaceRow
anchors.centerIn: parent
spacing: Theme.spacingS
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
Repeater {
model: ScriptModel {
values: root.workspaceList
}
Item {
id: delegateRoot
property bool isActive: {
if (root.useExtWorkspace) return (modelData?.id || modelData?.name) === root.currentWorkspace
if (CompositorService.isHyprland) return !!(modelData && modelData.id === root.currentWorkspace)
if (CompositorService.isDwl) return !!(modelData && root.dwlActiveTags.includes(modelData.tag))
if (CompositorService.isSway) return !!(modelData && modelData.num === root.currentWorkspace)
return modelData === root.currentWorkspace
}
property bool isPlaceholder: {
if (root.useExtWorkspace) return !!(modelData && modelData.hidden)
if (CompositorService.isHyprland) return !!(modelData && modelData.id === -1)
if (CompositorService.isDwl) return !!(modelData && modelData.tag === -1)
if (CompositorService.isSway) return !!(modelData && modelData.num === -1)
return modelData === -1
}
property bool isHovered: mouseArea.containsMouse
property var loadedWorkspaceData: null
property bool loadedIsUrgent: false
property bool isUrgent: {
if (root.useExtWorkspace) return modelData?.urgent ?? false
if (CompositorService.isHyprland) return modelData?.urgent ?? false
if (CompositorService.isNiri) return loadedIsUrgent
if (CompositorService.isDwl) return modelData?.state === 2
if (CompositorService.isSway) return loadedIsUrgent
return false
}
property var loadedIconData: null
property bool loadedHasIcon: false
property var loadedIcons: []
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? widgetHeight * 0.7 : widgetHeight * 0.5)
readonly property real iconsExtraWidth: {
if (!root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
}
return 0
}
readonly property real iconsExtraHeight: {
if (root.isVertical && SettingsData.showWorkspaceApps && loadedIcons.length > 0) {
const numIcons = Math.min(loadedIcons.length, SettingsData.maxWorkspaceIcons)
return numIcons * 18 + (numIcons > 0 ? (numIcons - 1) * Theme.spacingXS : 0) + (isActive ? Theme.spacingXS : 0)
}
return 0
}
readonly property real visualWidth: baseWidth + iconsExtraWidth
readonly property real visualHeight: baseHeight + iconsExtraHeight
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: !isPlaceholder
cursorShape: isPlaceholder ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !isPlaceholder
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (isPlaceholder) return
const isRightClick = mouse.button === Qt.RightButton
if (root.useExtWorkspace && (modelData?.id || modelData?.name)) {
ExtWorkspaceService.activateWorkspace(modelData.id || modelData.name, modelData.groupID || "")
} else if (CompositorService.isNiri) {
if (isRightClick) {
NiriService.toggleOverview()
} else {
NiriService.switchToWorkspace(modelData - 1)
}
} else if (CompositorService.isHyprland && modelData?.id) {
if (isRightClick && root.hyprlandOverviewLoader?.item) {
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
} else {
Hyprland.dispatch(`workspace ${modelData.id}`)
}
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
console.log("DWL click - tag:", modelData.tag, "rightClick:", isRightClick)
if (isRightClick) {
console.log("Calling toggleTag")
DwlService.toggleTag(root.screenName, modelData.tag)
} else {
console.log("Calling switchToTag")
DwlService.switchToTag(root.screenName, modelData.tag)
}
} else if (CompositorService.isSway && modelData?.num) {
try { I3.dispatch(`workspace number ${modelData.num}`) } catch(_){}
}
}
}
Timer {
id: dataUpdateTimer
interval: 50
onTriggered: {
if (isPlaceholder) {
delegateRoot.loadedWorkspaceData = null
delegateRoot.loadedIconData = null
delegateRoot.loadedHasIcon = false
delegateRoot.loadedIcons = []
delegateRoot.loadedIsUrgent = false
return
}
var wsData = null;
if (root.useExtWorkspace) {
wsData = modelData;
} else if (CompositorService.isNiri) {
wsData = NiriService.allWorkspaces.find(ws => ws.idx + 1 === modelData && ws.output === root.screenName) || null;
} else if (CompositorService.isHyprland) {
wsData = modelData;
} else if (CompositorService.isDwl) {
wsData = modelData;
} else if (CompositorService.isSway) {
wsData = modelData;
}
delegateRoot.loadedWorkspaceData = wsData;
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
var icData = null;
if (wsData?.name) {
icData = SettingsData.getWorkspaceNameIcon(wsData.name);
}
delegateRoot.loadedIconData = icData;
delegateRoot.loadedHasIcon = icData !== null;
if (SettingsData.showWorkspaceApps) {
if (CompositorService.isDwl || CompositorService.isSway) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else {
delegateRoot.loadedIcons = root.getWorkspaceIcons(CompositorService.isHyprland ? modelData : (modelData === -1 ? null : modelData));
}
} else {
delegateRoot.loadedIcons = [];
}
}
}
function updateAllData() {
dataUpdateTimer.restart()
}
width: root.isVertical ? root.barThickness : visualWidth
height: root.isVertical ? visualHeight : root.barThickness
Rectangle {
id: visualContent
width: delegateRoot.visualWidth
height: delegateRoot.visualHeight
anchors.centerIn: parent
radius: Theme.cornerRadius
color: isActive ? Theme.primary : isUrgent ? Theme.error : isPlaceholder ? Theme.surfaceTextLight : isHovered ? Theme.outlineButton : Theme.surfaceTextAlpha
border.width: isUrgent && !isActive ? 2 : 0
border.color: isUrgent && !isActive ? Theme.error : Theme.withAlpha(Theme.error, 0)
Behavior on width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on height {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Loader {
id: appIconsLoader
anchors.fill: parent
active: SettingsData.showWorkspaceApps
sourceComponent: Item {
Loader {
id: contentRow
anchors.centerIn: parent
sourceComponent: root.isVertical ? columnLayout : rowLayout
}
Component {
id: rowLayout
Row {
spacing: 4
visible: loadedIcons.length > 0
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
}
delegate: Item {
width: 18
height: 18
IconImage {
id: appIcon
property var windowId: modelData.windowId
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
visible: !modelData.isSteamApp
}
DankIcon {
anchors.centerIn: parent
size: 18
name: "sports_esports"
color: Theme.surfaceText
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp
}
MouseArea {
id: appMouseArea
anchors.fill: parent
enabled: isActive
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!appIcon.windowId) return
if (CompositorService.isHyprland) {
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
} else if (CompositorService.isNiri) {
NiriService.focusWindow(appIcon.windowId)
}
}
}
Rectangle {
visible: modelData.count > 1 && !isActive
width: 12
height: 12
radius: 6
color: "black"
border.color: "white"
border.width: 1
anchors.right: parent.right
anchors.bottom: parent.bottom
z: 2
Text {
anchors.centerIn: parent
text: modelData.count
font.pixelSize: 8
color: "white"
}
}
}
}
}
}
Component {
id: columnLayout
Column {
spacing: 4
visible: loadedIcons.length > 0
Repeater {
model: ScriptModel {
values: loadedIcons.slice(0, SettingsData.maxWorkspaceIcons)
}
delegate: Item {
width: 18
height: 18
IconImage {
id: appIcon
property var windowId: modelData.windowId
anchors.fill: parent
source: modelData.icon
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
visible: !modelData.isSteamApp
}
DankIcon {
anchors.centerIn: parent
size: 18
name: "sports_esports"
color: Theme.surfaceText
opacity: modelData.active ? 1.0 : appMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp
}
MouseArea {
id: appMouseArea
anchors.fill: parent
enabled: isActive
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!appIcon.windowId) return
if (CompositorService.isHyprland) {
Hyprland.dispatch(`focuswindow address:${appIcon.windowId}`)
} else if (CompositorService.isNiri) {
NiriService.focusWindow(appIcon.windowId)
}
}
}
Rectangle {
visible: modelData.count > 1 && !isActive
width: 12
height: 12
radius: 6
color: "black"
border.color: "white"
border.width: 1
anchors.right: parent.right
anchors.bottom: parent.bottom
z: 2
Text {
anchors.centerIn: parent
text: modelData.count
font.pixelSize: 8
color: "white"
}
}
}
}
}
}
}
}
// Loader for Custom Name Icon
Loader {
id: customIconLoader
anchors.fill: parent
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "icon" && !SettingsData.showWorkspaceApps
sourceComponent: Item {
DankIcon {
anchors.centerIn: parent
name: loadedIconData ? loadedIconData.value : "" // NULL CHECK
size: Theme.fontSizeSmall
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
weight: isActive && !isPlaceholder ? 500 : 400
}
}
}
// Loader for Custom Name Text
Loader {
id: customTextLoader
anchors.fill: parent
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "text" && !SettingsData.showWorkspaceApps
sourceComponent: Item {
StyledText {
anchors.centerIn: parent
text: loadedIconData ? loadedIconData.value : "" // NULL CHECK
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
}
}
// Loader for Workspace Index
Loader {
id: indexLoader
anchors.fill: parent
active: !isPlaceholder && SettingsData.showWorkspaceIndex && !loadedHasIcon && !SettingsData.showWorkspaceApps
sourceComponent: Item {
StyledText {
anchors.centerIn: parent
text: {
let isPlaceholder
if (root.useExtWorkspace) {
isPlaceholder = modelData?.hidden === true
} else if (CompositorService.isHyprland) {
isPlaceholder = modelData?.id === -1
} else if (CompositorService.isDwl) {
isPlaceholder = modelData?.tag === -1
} else if (CompositorService.isSway) {
isPlaceholder = modelData?.num === -1
} else {
isPlaceholder = modelData === -1
}
if (isPlaceholder) return index + 1
if (root.useExtWorkspace) return modelData?.name || modelData?.id || ""
if (CompositorService.isHyprland) return modelData?.id || ""
if (CompositorService.isDwl) return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""
if (CompositorService.isSway) return modelData?.num || ""
return modelData - 1
}
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
}
}
}
Component.onCompleted: updateAllData()
Connections {
target: CompositorService
function onSortedToplevelsChanged() { delegateRoot.updateAllData() }
}
Connections {
target: NiriService
enabled: CompositorService.isNiri
function onAllWorkspacesChanged() { delegateRoot.updateAllData() }
function onWindowUrgentChanged() { delegateRoot.updateAllData() }
function onWindowsChanged() { delegateRoot.updateAllData() }
}
Connections {
target: SettingsData
function onShowWorkspaceAppsChanged() { delegateRoot.updateAllData() }
function onWorkspaceNameIconsChanged() { delegateRoot.updateAllData() }
}
Connections {
target: DwlService
enabled: CompositorService.isDwl
function onStateChanged() { delegateRoot.updateAllData() }
}
Connections {
target: Hyprland.workspaces
enabled: CompositorService.isHyprland
function onValuesChanged() { delegateRoot.updateAllData() }
}
Connections {
target: I3.workspaces
enabled: CompositorService.isSway
function onValuesChanged() { delegateRoot.updateAllData() }
}
Connections {
target: ExtWorkspaceService
enabled: root.useExtWorkspace
function onStateChanged() { delegateRoot.updateAllData() }
}
}
}
}
Component.onCompleted: {
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
DMSService.addSubscription("extworkspace")
}
}
}

View File

@@ -0,0 +1,329 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import qs.Common
import qs.Widgets
import qs.Modules.DankDash
DankPopout {
id: root
layerNamespace: "dms:dash"
property bool dashVisible: false
property var triggerScreen: null
property int currentTabIndex: 0
keyboardFocusMode: WlrKeyboardFocus.Exclusive
function setTriggerPosition(x, y, width, section, screen) {
triggerSection = section
triggerScreen = screen
triggerY = y
if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)) {
const screenWidth = screen ? screen.width : Screen.width
triggerX = (screenWidth - popupWidth) / 2
triggerWidth = popupWidth
} else if (section === "center" && (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)) {
const screenHeight = screen ? screen.height : Screen.height
triggerX = (screenHeight - popupHeight) / 2
triggerWidth = popupHeight
} else {
triggerX = x
triggerWidth = width
}
}
popupWidth: 700
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerX: Screen.width - 620 - Theme.spacingL
triggerY: Math.max(26 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap - 2
triggerWidth: 80
shouldBeVisible: dashVisible
visible: shouldBeVisible
property bool __focusArmed: false
property bool __contentReady: false
function __tryFocusOnce() {
if (!__focusArmed)
return
const win = root.window
if (!win || !win.visible)
return
if (!contentLoader.item)
return
if (win.requestActivate)
win.requestActivate()
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
if (contentLoader.item.activeFocus)
__focusArmed = false
}
onDashVisibleChanged: {
if (dashVisible) {
__focusArmed = true
__contentReady = !!contentLoader.item
open()
__tryFocusOnce()
} else {
__focusArmed = false
__contentReady = false
close()
}
}
Connections {
target: contentLoader
function onLoaded() {
__contentReady = true
if (__focusArmed)
__tryFocusOnce()
}
}
Connections {
target: root.window ? root.window : null
enabled: !!root.window
function onVisibleChanged() {
if (__focusArmed)
__tryFocusOnce()
}
}
onBackgroundClicked: {
dashVisible = false
}
content: Component {
Rectangle {
id: mainContainer
implicitHeight: contentColumn.height + Theme.spacingM * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
focus: true
Component.onCompleted: {
if (root.shouldBeVisible) {
mainContainer.forceActiveFocus()
}
}
Connections {
target: root
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function () {
mainContainer.forceActiveFocus()
})
}
}
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.dashVisible = false
event.accepted = true
return
}
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
let nextIndex = root.currentTabIndex + 1
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
nextIndex++
}
if (nextIndex >= tabBar.model.length) {
nextIndex = 0
}
root.currentTabIndex = nextIndex
event.accepted = true
return
}
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
let prevIndex = root.currentTabIndex - 1
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
}
if (prevIndex < 0) {
prevIndex = tabBar.model.length - 1
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
}
}
if (prevIndex >= 0) {
root.currentTabIndex = prevIndex
}
event.accepted = true
return
}
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
if (wallpaperTab.handleKeyEvent(event)) {
event.accepted = true
return
}
}
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
radius: parent.radius
SequentialAnimation on opacity {
running: root.shouldBeVisible
loops: Animation.Infinite
NumberAnimation {
to: 0.08
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
NumberAnimation {
to: 0.02
duration: Theme.extraLongDuration
easing.type: Theme.standardEasing
}
}
}
Column {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankTabBar {
id: tabBar
width: parent.width
height: 48
currentIndex: root.currentTabIndex
spacing: Theme.spacingS
equalWidthTabs: true
enableArrowNavigation: false
focus: false
activeFocusOnTab: false
nextFocusTarget: {
const item = pages.currentItem
if (!item)
return null
if (item.focusTarget)
return item.focusTarget
return item
}
model: {
let tabs = [{
"icon": "dashboard",
"text": I18n.tr("Overview")
}, {
"icon": "music_note",
"text": I18n.tr("Media")
}, {
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}]
if (SettingsData.weatherEnabled) {
tabs.push({
"icon": "wb_sunny",
"text": I18n.tr("Weather")
})
}
tabs.push({
"icon": "settings",
"text": I18n.tr("Settings"),
"isAction": true
})
return tabs
}
onTabClicked: function (index) {
root.currentTabIndex = index
}
onActionTriggered: function (index) {
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
if (index === settingsIndex) {
dashVisible = false
settingsModal.show()
}
}
}
Item {
width: parent.width
height: Theme.spacingXS
}
StackLayout {
id: pages
width: parent.width
implicitHeight: {
if (currentIndex === 0)
return overviewTab.implicitHeight
if (currentIndex === 1)
return mediaTab.implicitHeight
if (currentIndex === 2)
return wallpaperTab.implicitHeight
if (SettingsData.weatherEnabled && currentIndex === 3)
return weatherTab.implicitHeight
return overviewTab.implicitHeight
}
currentIndex: root.currentTabIndex
OverviewTab {
id: overviewTab
onCloseDash: {
root.dashVisible = false
}
onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) {
tabBar.currentIndex = 3
tabBar.tabClicked(3)
}
}
onSwitchToMediaTab: {
tabBar.currentIndex = 1
tabBar.tabClicked(1)
}
}
MediaPlayerTab {
id: mediaTab
}
WallpaperTab {
id: wallpaperTab
active: root.currentTabIndex === 2
tabBarItem: tabBar
keyForwardTarget: mainContainer
targetScreen: root.triggerScreen
}
WeatherTab {
id: weatherTab
visible: SettingsData.weatherEnabled && root.currentTabIndex === 3
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,447 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property bool showEventDetails: false
property date selectedDate: systemClock.date
property var selectedDateEvents: []
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
signal closeDash()
function weekStartJs() {
return Qt.locale().firstDayOfWeek % 7
}
function startOfWeek(dateObj) {
const d = new Date(dateObj)
const jsDow = d.getDay()
const diff = (jsDow - weekStartJs() + 7) % 7
d.setDate(d.getDate() - diff)
return d
}
function endOfWeek(dateObj) {
const d = new Date(dateObj)
const jsDow = d.getDay()
const add = (weekStartJs() + 6 - jsDow + 7) % 7
d.setDate(d.getDate() + add)
return d
}
function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) {
const events = CalendarService.getEventsForDate(selectedDate)
selectedDateEvents = events
} else {
selectedDateEvents = []
}
}
function loadEventsForMonth() {
if (!CalendarService || !CalendarService.khalAvailable) {
return
}
const firstOfMonth = new Date(calendarGrid.displayDate.getFullYear(),
calendarGrid.displayDate.getMonth(), 1)
const lastOfMonth = new Date(calendarGrid.displayDate.getFullYear(),
calendarGrid.displayDate.getMonth() + 1, 0)
const startDate = startOfWeek(firstOfMonth)
startDate.setDate(startDate.getDate() - 7)
const endDate = endOfWeek(lastOfMonth)
endDate.setDate(endDate.getDate() + 7)
CalendarService.loadEvents(startDate, endDate)
}
onSelectedDateChanged: updateSelectedDateEvents()
Component.onCompleted: {
loadEventsForMonth()
updateSelectedDateEvents()
}
Connections {
function onEventsByDateChanged() {
updateSelectedDateEvents()
}
function onKhalAvailableChanged() {
if (CalendarService && CalendarService.khalAvailable) {
loadEventsForMonth()
}
updateSelectedDateEvents()
}
target: CalendarService
enabled: CalendarService !== null
}
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Item {
width: parent.width
height: 40
visible: showEventDetails
Rectangle {
width: 32
height: 32
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
radius: Theme.cornerRadius
color: backButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: 14
color: Theme.primary
}
MouseArea {
id: backButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.showEventDetails = false
}
}
StyledText {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 32 + Theme.spacingS * 2
anchors.rightMargin: Theme.spacingS
height: 40
anchors.verticalCenter: parent.verticalCenter
text: {
const dateStr = Qt.formatDate(selectedDate, "MMM d")
if (selectedDateEvents && selectedDateEvents.length > 0) {
const eventCount = selectedDateEvents.length === 1 ? I18n.tr("1 event") : selectedDateEvents.length + " " + I18n.tr("events")
return dateStr + " • " + eventCount
}
return dateStr
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
Row {
width: parent.width
height: 28
visible: !showEventDetails
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_left"
size: 14
color: Theme.primary
}
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(calendarGrid.displayDate)
newDate.setMonth(newDate.getMonth() - 1)
calendarGrid.displayDate = newDate
loadEventsForMonth()
}
}
}
StyledText {
width: parent.width - 56
height: 28
text: calendarGrid.displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 28
height: 28
radius: Theme.cornerRadius
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "chevron_right"
size: 14
color: Theme.primary
}
MouseArea {
id: nextMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(calendarGrid.displayDate)
newDate.setMonth(newDate.getMonth() + 1)
calendarGrid.displayDate = newDate
loadEventsForMonth()
}
}
}
}
Row {
width: parent.width
height: 18
visible: !showEventDetails
Repeater {
model: {
const days = []
const loc = Qt.locale()
const qtFirst = loc.firstDayOfWeek
for (let i = 0; i < 7; ++i) {
const qtDay = ((qtFirst - 1 + i) % 7) + 1
days.push(loc.dayName(qtDay, Locale.ShortFormat))
}
return days
}
Rectangle {
width: parent.width / 7
height: 18
color: "transparent"
StyledText {
anchors.centerIn: parent
text: modelData
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
Grid {
id: calendarGrid
visible: !showEventDetails
property date displayDate: systemClock.date
property date selectedDate: systemClock.date
readonly property date firstDay: {
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
return startOfWeek(firstOfMonth)
}
width: parent.width
height: parent.height - 28 - 18 - Theme.spacingS * 2
columns: 7
rows: 6
Repeater {
model: 42
Rectangle {
readonly property date dayDate: {
const date = new Date(parent.firstDay)
date.setDate(date.getDate() + index)
return date
}
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 4, parent.height - 4, 32)
height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: width / 2
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday ? Font.Medium : Font.Normal
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 4
width: 12
height: 2
radius: 1
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
root.selectedDate = dayDate
root.showEventDetails = true
}
}
}
}
}
}
DankListView {
width: parent.width - Theme.spacingS * 2
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
model: selectedDateEvents
visible: showEventDetails
clip: true
spacing: Theme.spacingXS
delegate: Rectangle {
width: parent ? parent.width : 0
height: eventContent.implicitHeight + Theme.spacingS
radius: Theme.cornerRadius
color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06)
}
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
}
border.color: {
if (modelData.url && eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
} else if (eventMouseArea.containsMouse) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
}
return "transparent"
}
border.width: 1
Rectangle {
width: 3
height: parent.height - 6
anchors.left: parent.left
anchors.leftMargin: 3
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: Theme.primary
opacity: 0.8
}
Column {
id: eventContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS + 6
anchors.rightMargin: Theme.spacingXS
spacing: 2
StyledText {
width: parent.width
text: modelData.title
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
maximumLineCount: 1
}
StyledText {
width: parent.width
text: {
if (!modelData || modelData.allDay) {
return I18n.tr("All day")
} else if (modelData.start && modelData.end) {
const timeFormat = SettingsData.use24HourClock ? "HH:mm" : "h:mm AP"
const startTime = Qt.formatTime(modelData.start, timeFormat)
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime()) {
return startTime + " " + Qt.formatTime(modelData.end, timeFormat)
}
return startTime
}
return ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
font.weight: Font.Normal
visible: text !== ""
}
}
MouseArea {
id: eventMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.url !== ""
onClicked: {
if (modelData.url && modelData.url !== "") {
if (Qt.openUrlExternally(modelData.url) === false) {
console.warn("Failed to open URL: " + modelData.url)
} else {
root.closeDash()
}
}
}
}
}
}
}
SystemClock {
id: systemClock
precision: SystemClock.Hours
}
}

View File

@@ -0,0 +1,22 @@
import QtQuick
import QtQuick.Controls
import qs.Common
Rectangle {
id: card
property int pad: Theme.spacingM
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
default property alias content: contentItem.data
Item {
id: contentItem
anchors.fill: parent
anchors.margins: card.pad
}
}

View File

@@ -0,0 +1,118 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import qs.Common
import qs.Widgets
Card {
id: root
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Column {
spacing: -8
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: {
if (SettingsData.use24HourClock) {
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(0)
} else {
const hours = systemClock?.date?.getHours()
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
return String(display).padStart(2, '0').charAt(0)
}
}
font.pixelSize: 48
color: Theme.primary
font.weight: Font.Medium
width: 28
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: {
if (SettingsData.use24HourClock) {
return String(systemClock?.date?.getHours()).padStart(2, '0').charAt(1)
} else {
const hours = systemClock?.date?.getHours()
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours
return String(display).padStart(2, '0').charAt(1)
}
}
font.pixelSize: 48
color: Theme.primary
font.weight: Font.Medium
width: 28
horizontalAlignment: Text.AlignHCenter
}
}
Row {
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0)
font.pixelSize: 48
color: Theme.primary
font.weight: Font.Medium
width: 28
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1)
font.pixelSize: 48
color: Theme.primary
font.weight: Font.Medium
width: 28
horizontalAlignment: Text.AlignHCenter
}
}
Row {
visible: SettingsData.showSeconds
spacing: 0
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0)
font.pixelSize: 48
color: Theme.primary
font.weight: Font.Medium
width: 28
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1)
font.pixelSize: 48
color: Theme.primary
font.weight: Font.Medium
width: 28
horizontalAlignment: Text.AlignHCenter
}
}
}
StyledText {
text: systemClock?.date?.toLocaleDateString(Qt.locale(), "MMM dd")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
SystemClock {
id: systemClock
precision: SystemClock.Seconds
}
}

View File

@@ -0,0 +1,218 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import Quickshell.Services.Mpris
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
clip: false
signal clicked()
property MprisPlayer activePlayer: MprisController.activePlayer
property real currentPosition: activePlayer?.positionSupported ? activePlayer.position : 0
property real displayPosition: currentPosition
readonly property real ratio: {
if (!activePlayer || activePlayer.length <= 0) return 0
const pos = displayPosition % Math.max(1, activePlayer.length)
const calculatedRatio = pos / activePlayer.length
return Math.max(0, Math.min(1, calculatedRatio))
}
onActivePlayerChanged: {
if (activePlayer?.positionSupported) {
currentPosition = Qt.binding(() => activePlayer?.position || 0)
} else {
currentPosition = 0
}
}
Timer {
interval: 300
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
repeat: true
onTriggered: activePlayer?.positionSupported && activePlayer.positionChanged()
}
property bool isSeeking: false
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !activePlayer
DankIcon {
name: "music_note"
size: Theme.iconSize
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No Media")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.centerIn: parent
width: parent.width - Theme.spacingXS * 2
spacing: Theme.spacingL
visible: activePlayer
Item {
width: 140
height: 110
anchors.horizontalCenter: parent.horizontalCenter
clip: false
DankAlbumArt {
width: 110
height: 80
anchors.centerIn: parent
activePlayer: root.activePlayer
albumSize: 76
animationScale: 1.05
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
topPadding: Theme.spacingL
StyledText {
text: activePlayer?.trackTitle || "Unknown"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
}
StyledText {
text: activePlayer?.trackArtist || "Unknown Artist"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
}
}
DankSeekbar {
width: parent.width + 4
height: 20
x: -2
activePlayer: root.activePlayer
isSeeking: root.isSeeking
onIsSeekingChanged: root.isSeeking = isSeeking
}
Item {
width: parent.width
height: 32
Row {
spacing: Theme.spacingS
anchors.centerIn: parent
Rectangle {
width: 28
height: 28
radius: 14
anchors.verticalCenter: playPauseButton.verticalCenter
color: prevArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_previous"
size: 14
color: Theme.surfaceText
}
MouseArea {
id: prevArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!activePlayer) return
if (activePlayer.position > 8 && activePlayer.canSeek) {
activePlayer.position = 0
} else {
activePlayer.previous()
}
}
}
}
Rectangle {
id: playPauseButton
width: 32
height: 32
radius: 16
color: Theme.primary
DankIcon {
anchors.centerIn: parent
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
size: 16
color: Theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.togglePlaying()
}
}
Rectangle {
width: 28
height: 28
radius: 14
anchors.verticalCenter: playPauseButton.verticalCenter
color: nextArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "skip_next"
size: 14
color: Theme.surfaceText
}
MouseArea {
id: nextArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: activePlayer?.next()
}
}
}
}
}
MouseArea {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 123
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
visible: activePlayer
}
}

View File

@@ -0,0 +1,178 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
Component.onCompleted: {
DgopService.addRef(["cpu", "memory", "system"])
}
Component.onDestruction: {
DgopService.removeRef(["cpu", "memory", "system"])
}
Row {
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingM
// CPU Bar
Column {
width: (parent.width - 2 * Theme.spacingM) / 3
height: parent.height
spacing: Theme.spacingS
Rectangle {
width: 8
height: parent.height - Theme.iconSizeSmall - Theme.spacingS
radius: 4
anchors.horizontalCenter: parent.horizontalCenter
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: parent.width
height: parent.height * Math.min((DgopService.cpuUsage || 6) / 100, 1)
radius: parent.radius
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (DgopService.cpuUsage > 80) return Theme.error
if (DgopService.cpuUsage > 60) return Theme.warning
return Theme.primary
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Item {
width: parent.width
height: Theme.iconSizeSmall
DankIcon {
name: "memory"
size: Theme.iconSizeSmall
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
color: {
if (DgopService.cpuUsage > 80) return Theme.error
if (DgopService.cpuUsage > 60) return Theme.warning
return Theme.primary
}
}
}
}
// Temperature Bar
Column {
width: (parent.width - 2 * Theme.spacingM) / 3
height: parent.height
spacing: Theme.spacingS
Rectangle {
width: 8
height: parent.height - Theme.iconSizeSmall - Theme.spacingS
radius: 4
anchors.horizontalCenter: parent.horizontalCenter
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: parent.width
height: parent.height * Math.min(Math.max((DgopService.cpuTemperature || 40) / 100, 0), 1)
radius: parent.radius
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (DgopService.cpuTemperature > 85) return Theme.error
if (DgopService.cpuTemperature > 69) return Theme.warning
return Theme.primary
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Item {
width: parent.width
height: Theme.iconSizeSmall
DankIcon {
name: "device_thermostat"
size: Theme.iconSizeSmall
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
color: {
if (DgopService.cpuTemperature > 85) return Theme.error
if (DgopService.cpuTemperature > 69) return Theme.warning
return Theme.primary
}
}
}
}
// RAM Bar
Column {
width: (parent.width - 2 * Theme.spacingM) / 3
height: parent.height
spacing: Theme.spacingS
Rectangle {
width: 8
height: parent.height - Theme.iconSizeSmall - Theme.spacingS
radius: 4
anchors.horizontalCenter: parent.horizontalCenter
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: parent.width
height: parent.height * Math.min((DgopService.memoryUsage || 42) / 100, 1)
radius: parent.radius
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (DgopService.memoryUsage > 90) return Theme.error
if (DgopService.memoryUsage > 75) return Theme.warning
return Theme.primary
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Item {
width: parent.width
height: Theme.iconSizeSmall
DankIcon {
name: "developer_board"
size: Theme.iconSizeSmall
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
color: {
if (DgopService.memoryUsage > 90) return Theme.error
if (DgopService.memoryUsage > 75) return Theme.warning
return Theme.primary
}
}
}
}
}
}

View File

@@ -0,0 +1,110 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankCircularImage {
id: avatarContainer
width: 77
height: 77
anchors.verticalCenter: parent.verticalCenter
imageSource: {
if (PortalService.profileImage === "")
return ""
if (PortalService.profileImage.startsWith("/"))
return "file://" + PortalService.profileImage
return PortalService.profileImage
}
fallbackIcon: "person"
}
Column {
spacing: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: UserInfoService.username || "brandon"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
width: parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3
}
Row {
spacing: Theme.spacingS
SystemLogo {
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
colorOverride: Theme.primary
}
StyledText {
text: {
if (CompositorService.isNiri) return "on niri"
if (CompositorService.isHyprland) return "on Hyprland"
// technically they might not be on mangowc, but its what we support in the docs
if (CompositorService.isDwl) return "on MangoWC"
if (CompositorService.isSway) return "on Sway"
return ""
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS
}
}
Row {
spacing: Theme.spacingS
DankIcon {
name: "schedule"
size: 16
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: uptimeText
property real availableWidth: parent.parent.parent.parent.width - avatarContainer.width - Theme.spacingM * 3 - 16 - Theme.spacingS
property real longTextWidth: {
const fontSize = Math.round(Theme.fontSizeSmall || 12)
const testMetrics = Qt.createQmlObject('import QtQuick; TextMetrics { font.pixelSize: ' + fontSize + ' }', uptimeText)
testMetrics.text = UserInfoService.uptime || "up 1 hour, 23 minutes"
const result = testMetrics.width
testMetrics.destroy()
return result
}
// Just using truncated is always true initially idk
property bool shouldUseShort: longTextWidth > availableWidth
text: shouldUseShort ? UserInfoService.shortUptime : UserInfoService.uptime || "up 1h 23m"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: availableWidth
wrapMode: Text.NoWrap
}
}
}
}
}

View File

@@ -0,0 +1,91 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import qs.Common
import qs.Services
import qs.Widgets
Card {
id: root
signal clicked()
Component.onCompleted: WeatherService.addRef()
Component.onDestruction: WeatherService.removeRef()
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
DankIcon {
name: "cloud_off"
size: 24
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.loading ? "Loading..." : "No Weather"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
Button {
text: I18n.tr("Refresh")
flat: true
visible: !WeatherService.weather.loading
anchors.horizontalCenter: parent.horizontalCenter
onClicked: WeatherService.forceRefresh()
}
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingL
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
DankIcon {
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: 48
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
StyledText {
text: {
const temp = SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp;
if (temp === undefined || temp === null || temp === 0) {
return "--°" + (SettingsData.useFahrenheit ? "F" : "C");
}
return temp + "°" + (SettingsData.useFahrenheit ? "F" : "C");
}
font.pixelSize: Theme.fontSizeXLarge + 4
color: Theme.surfaceText
font.weight: Font.Light
}
StyledText {
text: WeatherService.getWeatherCondition(WeatherService.weather.wCode)
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
elide: Text.ElideRight
width: parent.parent.parent.width - 48 - Theme.spacingL * 2
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
}

View File

@@ -0,0 +1,76 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankDash.Overview
Item {
id: root
implicitWidth: 700
implicitHeight: 410
signal switchToWeatherTab()
signal switchToMediaTab()
signal closeDash()
Item {
anchors.fill: parent
// Clock - top left (narrower and shorter)
ClockCard {
x: 0
y: 0
width: parent.width * 0.2 - Theme.spacingM * 2
height: 180
}
// Weather - top middle-left (narrower)
WeatherOverviewCard {
x: SettingsData.weatherEnabled ? parent.width * 0.2 - Theme.spacingM : 0
y: 0
width: SettingsData.weatherEnabled ? parent.width * 0.3 : 0
height: 100
visible: SettingsData.weatherEnabled
onClicked: root.switchToWeatherTab()
}
// UserInfo - top middle-right (extend when weather disabled)
UserInfoCard {
x: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.2 - Theme.spacingM
y: 0
width: SettingsData.weatherEnabled ? parent.width * 0.5 : parent.width * 0.8
height: 100
}
// SystemMonitor - middle left (narrow and shorter)
SystemMonitorCard {
x: 0
y: 180 + Theme.spacingM
width: parent.width * 0.2 - Theme.spacingM * 2
height: 220
}
// Calendar - bottom middle (wider and taller)
CalendarOverviewCard {
x: parent.width * 0.2 - Theme.spacingM
y: 100 + Theme.spacingM
width: parent.width * 0.6
height: 300
onCloseDash: root.closeDash()
}
// Media - bottom right (narrow and taller)
MediaOverviewCard {
x: parent.width * 0.8
y: 100 + Theme.spacingM
width: parent.width * 0.2
height: 300
onClicked: root.switchToMediaTab()
}
}
}

View File

@@ -0,0 +1,575 @@
import Qt.labs.folderlistmodel
import QtCore
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
Item {
id: root
implicitWidth: 700
implicitHeight: 410
property string wallpaperDir: ""
property int currentPage: 0
property int itemsPerPage: 16
property int totalPages: Math.max(1, Math.ceil(wallpaperFolderModel.count / itemsPerPage))
property bool active: false
property Item focusTarget: wallpaperGrid
property Item tabBarItem: null
property int gridIndex: 0
property Item keyForwardTarget: null
property int lastPage: 0
property bool enableAnimation: false
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
property string selectedFileName: ""
property var targetScreen: null
property string targetScreenName: targetScreen ? targetScreen.name : ""
signal requestTabChange(int newIndex)
function getCurrentWallpaper() {
if (SessionData.perMonitorWallpaper && targetScreenName) {
return SessionData.getMonitorWallpaper(targetScreenName)
}
return SessionData.wallpaperPath
}
function setCurrentWallpaper(path) {
if (SessionData.perMonitorWallpaper && targetScreenName) {
SessionData.setMonitorWallpaper(targetScreenName, path)
} else {
SessionData.setWallpaper(path)
}
}
onCurrentPageChanged: {
if (currentPage !== lastPage) {
enableAnimation = false
lastPage = currentPage
}
updateSelectedFileName()
}
onGridIndexChanged: {
updateSelectedFileName()
}
onVisibleChanged: {
if (visible && active) {
setInitialSelection()
}
}
Component.onCompleted: {
loadWallpaperDirectory()
}
onActiveChanged: {
if (active && visible) {
setInitialSelection()
}
}
function handleKeyEvent(event) {
const columns = 4
const currentCol = gridIndex % columns
const visibleCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage)
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (gridIndex >= 0 && gridIndex < visibleCount) {
const absoluteIndex = currentPage * itemsPerPage + gridIndex
if (absoluteIndex < wallpaperFolderModel.count) {
const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath")
if (filePath) {
setCurrentWallpaper(filePath.toString().replace(/^file:\/\//, ''))
}
}
}
return true
}
if (event.key === Qt.Key_Right) {
if (gridIndex + 1 < visibleCount) {
gridIndex++
} else if (currentPage < totalPages - 1) {
gridIndex = 0
currentPage++
}
return true
}
if (event.key === Qt.Key_Left) {
if (gridIndex > 0) {
gridIndex--
} else if (currentPage > 0) {
currentPage--
const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage)
gridIndex = prevPageCount - 1
}
return true
}
if (event.key === Qt.Key_Down) {
if (gridIndex + columns < visibleCount) {
gridIndex += columns
} else if (currentPage < totalPages - 1) {
gridIndex = currentCol
currentPage++
}
return true
}
if (event.key === Qt.Key_Up) {
if (gridIndex >= columns) {
gridIndex -= columns
} else if (currentPage > 0) {
currentPage--
const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage)
const prevPageRows = Math.ceil(prevPageCount / columns)
gridIndex = (prevPageRows - 1) * columns + currentCol
gridIndex = Math.min(gridIndex, prevPageCount - 1)
}
return true
}
if (event.key === Qt.Key_PageUp && currentPage > 0) {
gridIndex = 0
currentPage--
return true
}
if (event.key === Qt.Key_PageDown && currentPage < totalPages - 1) {
gridIndex = 0
currentPage++
return true
}
if (event.key === Qt.Key_Home && event.modifiers & Qt.ControlModifier) {
gridIndex = 0
currentPage = 0
return true
}
if (event.key === Qt.Key_End && event.modifiers & Qt.ControlModifier) {
currentPage = totalPages - 1
const lastPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage)
gridIndex = Math.max(0, lastPageCount - 1)
return true
}
return false
}
function setInitialSelection() {
const currentWallpaper = getCurrentWallpaper()
if (!currentWallpaper || wallpaperFolderModel.count === 0) {
gridIndex = 0
updateSelectedFileName()
Qt.callLater(() => {
enableAnimation = true
})
return
}
for (var i = 0; i < wallpaperFolderModel.count; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath")
if (filePath && filePath.toString().replace(/^file:\/\//, '') === currentWallpaper) {
const targetPage = Math.floor(i / itemsPerPage)
const targetIndex = i % itemsPerPage
currentPage = targetPage
gridIndex = targetIndex
updateSelectedFileName()
Qt.callLater(() => {
enableAnimation = true
})
return
}
}
gridIndex = 0
updateSelectedFileName()
Qt.callLater(() => {
enableAnimation = true
})
}
function loadWallpaperDirectory() {
const currentWallpaper = getCurrentWallpaper()
if (!currentWallpaper || currentWallpaper.startsWith("#")) {
if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") {
wallpaperDir = CacheData.wallpaperLastPath
} else {
wallpaperDir = ""
}
return
}
wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'))
}
function updateSelectedFileName() {
if (wallpaperFolderModel.count === 0) {
selectedFileName = ""
return
}
const absoluteIndex = currentPage * itemsPerPage + gridIndex
if (absoluteIndex < wallpaperFolderModel.count) {
const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath")
if (filePath) {
const pathStr = filePath.toString().replace(/^file:\/\//, '')
selectedFileName = pathStr.substring(pathStr.lastIndexOf('/') + 1)
return
}
}
selectedFileName = ""
}
Connections {
target: SessionData
function onWallpaperPathChanged() {
loadWallpaperDirectory()
if (visible && active) {
setInitialSelection()
}
}
function onMonitorWallpapersChanged() {
loadWallpaperDirectory()
if (visible && active) {
setInitialSelection()
}
}
}
onTargetScreenNameChanged: {
loadWallpaperDirectory()
if (visible && active) {
setInitialSelection()
}
}
Connections {
target: wallpaperFolderModel
function onCountChanged() {
if (wallpaperFolderModel.status === FolderListModel.Ready) {
if (visible && active) {
setInitialSelection()
}
updateSelectedFileName()
}
}
function onStatusChanged() {
if (wallpaperFolderModel.status === FolderListModel.Ready && wallpaperFolderModel.count > 0) {
if (visible && active) {
setInitialSelection()
}
updateSelectedFileName()
}
}
}
FolderListModel {
id: wallpaperFolderModel
showDirsFirst: false
showDotAndDotDot: false
showHidden: false
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
showFiles: true
showDirs: false
sortField: FolderListModel.Name
folder: wallpaperDir ? "file://" + wallpaperDir : ""
}
Loader {
id: wallpaperBrowserLoader
active: false
asynchronous: true
sourceComponent: FileBrowserModal {
Component.onCompleted: {
open()
}
browserTitle: "Select Wallpaper Directory"
browserIcon: "folder_open"
browserType: "wallpaper"
showHiddenFiles: false
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
allowStacking: true
onFileSelected: path => {
const cleanPath = path.replace(/^file:\/\//, '')
setCurrentWallpaper(cleanPath)
const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'))
if (dirPath) {
wallpaperDir = dirPath
CacheData.wallpaperLastPath = dirPath
CacheData.saveCache()
}
close()
}
onDialogClosed: {
Qt.callLater(() => wallpaperBrowserLoader.active = false)
}
}
}
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: parent.height - 50
GridView {
id: wallpaperGrid
anchors.centerIn: parent
width: parent.width - Theme.spacingS
height: parent.height - Theme.spacingS
cellWidth: width / 4
cellHeight: height / 4
clip: true
enabled: root.active
interactive: root.active
boundsBehavior: Flickable.StopAtBounds
keyNavigationEnabled: false
activeFocusOnTab: false
highlightFollowsCurrentItem: true
highlightMoveDuration: enableAnimation ? Theme.shortDuration : 0
focus: false
highlight: Item {
z: 1000
Rectangle {
anchors.fill: parent
anchors.margins: Theme.spacingXS
color: "transparent"
border.width: 3
border.color: Theme.primary
radius: Theme.cornerRadius
}
}
model: {
const startIndex = currentPage * itemsPerPage
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count)
const items = []
for (var i = startIndex; i < endIndex; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath")
if (filePath) {
items.push(filePath.toString().replace(/^file:\/\//, ''))
}
}
return items
}
onModelChanged: {
const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0
if (gridIndex !== clampedIndex) {
gridIndex = clampedIndex
}
}
onCountChanged: {
if (count > 0) {
const clampedIndex = Math.min(gridIndex, count - 1)
currentIndex = clampedIndex
positionViewAtIndex(clampedIndex, GridView.Contain)
}
enableAnimation = true
}
Connections {
target: root
function onGridIndexChanged() {
if (wallpaperGrid.count > 0) {
wallpaperGrid.currentIndex = gridIndex
if (!enableAnimation) {
wallpaperGrid.positionViewAtIndex(gridIndex, GridView.Contain)
}
}
}
}
delegate: Item {
width: wallpaperGrid.cellWidth
height: wallpaperGrid.cellHeight
property string wallpaperPath: modelData || ""
property bool isSelected: getCurrentWallpaper() === modelData
Rectangle {
id: wallpaperCard
anchors.fill: parent
anchors.margins: Theme.spacingXS
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
radius: Theme.cornerRadius
clip: true
Rectangle {
anchors.fill: parent
color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15) : "transparent"
radius: parent.radius
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Image {
id: thumbnailImage
anchors.fill: parent
source: modelData ? `file://${modelData}` : ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
smooth: true
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskThresholdMin: 0.5
maskSpreadAtMin: 1.0
maskSource: ShaderEffectSource {
sourceItem: Rectangle {
width: thumbnailImage.width
height: thumbnailImage.height
radius: Theme.cornerRadius
}
}
}
}
BusyIndicator {
anchors.centerIn: parent
running: thumbnailImage.status === Image.Loading
visible: running
}
StateLayer {
anchors.fill: parent
cornerRadius: parent.radius
stateColor: Theme.primary
}
MouseArea {
id: wallpaperMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
gridIndex = index
if (modelData) {
setCurrentWallpaper(modelData)
}
}
}
}
}
}
StyledText {
anchors.centerIn: parent
visible: wallpaperFolderModel.count === 0
text: "No wallpapers found\n\nClick the folder icon below to browse"
font.pixelSize: 14
color: Theme.outline
horizontalAlignment: Text.AlignHCenter
}
}
Column {
width: parent.width
height: 50
Row {
width: parent.width
height: 32
spacing: Theme.spacingS
Item {
width: (parent.width - controlsRow.width - browseButton.width - Theme.spacingS) / 2
height: parent.height
}
Row {
id: controlsRow
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "skip_previous"
iconSize: 20
buttonSize: 32
enabled: currentPage > 0
opacity: enabled ? 1.0 : 0.3
onClicked: {
if (currentPage > 0) {
currentPage--
}
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: wallpaperFolderModel.count > 0 ? `${wallpaperFolderModel.count} wallpapers ${currentPage + 1} / ${totalPages}` : "No wallpapers"
font.pixelSize: 14
color: Theme.surfaceText
opacity: 0.7
}
DankActionButton {
anchors.verticalCenter: parent.verticalCenter
iconName: "skip_next"
iconSize: 20
buttonSize: 32
enabled: currentPage < totalPages - 1
opacity: enabled ? 1.0 : 0.3
onClicked: {
if (currentPage < totalPages - 1) {
currentPage++
}
}
}
}
DankActionButton {
id: browseButton
anchors.verticalCenter: parent.verticalCenter
iconName: "folder_open"
iconSize: 20
buttonSize: 32
opacity: 0.7
onClicked: wallpaperBrowserLoader.active = true
}
}
StyledText {
width: parent.width
height: 18
text: selectedFileName
font.pixelSize: 12
color: Theme.surfaceText
opacity: 0.5
visible: selectedFileName !== ""
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
}
}
}
}

View File

@@ -0,0 +1,642 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
implicitWidth: 700
implicitHeight: 410
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
DankIcon {
name: "cloud_off"
size: Theme.iconSize * 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("No Weather Data Available")
font.pixelSize: Theme.fontSizeLarge
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingM
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
Item {
width: parent.width
height: 70
DankIcon {
id: refreshButton
name: "refresh"
size: Theme.iconSize - 4
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
anchors.right: parent.right
anchors.top: parent.top
property bool isRefreshing: false
enabled: !isRefreshing
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
onClicked: {
refreshButton.isRefreshing = true
WeatherService.forceRefresh()
refreshTimer.restart()
}
enabled: parent.enabled
}
Timer {
id: refreshTimer
interval: 2000
onTriggered: refreshButton.isRefreshing = false
}
NumberAnimation on rotation {
running: refreshButton.isRefreshing
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
}
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: weatherIcon.width + tempColumn.width + sunriseColumn.width + Theme.spacingM * 2
height: 70
DankIcon {
id: weatherIcon
name: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
size: Theme.iconSize * 1.5
color: Theme.primary
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 4
shadowBlur: 0.8
shadowColor: Qt.rgba(0, 0, 0, 0.2)
shadowOpacity: 0.2
}
}
Column {
id: tempColumn
spacing: Theme.spacingXS
anchors.left: weatherIcon.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
Item {
width: tempText.width + unitText.width + Theme.spacingXS
height: tempText.height
StyledText {
id: tempText
text: (SettingsData.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°"
font.pixelSize: Theme.fontSizeLarge + 4
color: Theme.surfaceText
font.weight: Font.Light
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: unitText
text: SettingsData.useFahrenheit ? "F" : "C"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.left: tempText.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (WeatherService.weather.available) {
SettingsData.set("temperatureUnit", !SettingsData.useFahrenheit)
}
}
enabled: WeatherService.weather.available
}
}
}
StyledText {
text: WeatherService.weather.city || ""
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: text.length > 0
}
}
Column {
id: sunriseColumn
spacing: Theme.spacingXS
anchors.left: tempColumn.right
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
visible: WeatherService.weather.sunrise && WeatherService.weather.sunset
Item {
width: sunriseIcon.width + sunriseText.width + Theme.spacingXS
height: sunriseIcon.height
DankIcon {
id: sunriseIcon
name: "wb_twilight"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: sunriseText
text: WeatherService.weather.sunrise || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: sunriseIcon.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
width: sunsetIcon.width + sunsetText.width + Theme.spacingXS
height: sunsetIcon.height
DankIcon {
id: sunsetIcon
name: "bedtime"
size: Theme.iconSize - 6
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
id: sunsetText
text: WeatherService.weather.sunset || ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.left: sunsetIcon.right
anchors.leftMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
GridLayout {
width: parent.width
height: 95
columns: 6
columnSpacing: Theme.spacingS
rowSpacing: 0
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "device_thermostat"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Feels Like")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: (SettingsData.useFahrenheit ? (WeatherService.weather.feelsLikeF || WeatherService.weather.tempF) : (WeatherService.weather.feelsLike || WeatherService.weather.temp)) + "°"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "humidity_low"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Humidity")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "air"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Wind")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.wind || "--"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "speed"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Pressure")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.pressure ? WeatherService.weather.pressure + " hPa" : "--"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "rainy"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Rain Chance")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: WeatherService.weather.precipitationProbability ? WeatherService.weather.precipitationProbability + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
anchors.centerIn: parent
name: "wb_sunny"
size: Theme.iconSize - 4
color: Theme.primary
}
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 2
StyledText {
text: I18n.tr("Visibility")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("Good")
font.pixelSize: Theme.fontSizeSmall + 1
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
}
Column {
width: parent.width
height: parent.height - 70 - 95 - Theme.spacingM * 3 - 2
spacing: Theme.spacingS
StyledText {
text: I18n.tr("7-Day Forecast")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
}
Row {
width: parent.width
height: parent.height - Theme.fontSizeMedium - Theme.spacingS - Theme.spacingL
spacing: Theme.spacingXS
Repeater {
model: 7
Rectangle {
width: (parent.width - Theme.spacingXS * 6) / 7
height: parent.height
radius: Theme.cornerRadius
property var dayDate: {
const date = new Date()
date.setDate(date.getDate() + index)
return date
}
property bool isToday: index === 0
property var forecastData: {
if (WeatherService.weather.forecast && WeatherService.weather.forecast.length > index) {
return WeatherService.weather.forecast[index]
}
return null
}
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
border.width: isToday ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
StyledText {
text: Qt.locale().dayName(dayDate.getDay(), Locale.ShortFormat)
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : Theme.surfaceText
font.weight: isToday ? Font.Medium : Font.Normal
anchors.horizontalCenter: parent.horizontalCenter
}
DankIcon {
name: forecastData ? WeatherService.getWeatherIcon(forecastData.wCode || 0) : "cloud"
size: Theme.iconSize
color: isToday ? Theme.primary : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
anchors.horizontalCenter: parent.horizontalCenter
}
Column {
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
StyledText {
text: forecastData ? (SettingsData.useFahrenheit ? (forecastData.tempMaxF || forecastData.tempMax) : (forecastData.tempMax || 0)) + "°/" + (SettingsData.useFahrenheit ? (forecastData.tempMinF || forecastData.tempMin) : (forecastData.tempMin || 0)) + "°" : "--/--"
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Column {
spacing: 1
anchors.horizontalCenter: parent.horizontalCenter
visible: forecastData && forecastData.sunrise && forecastData.sunset
Row {
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "wb_twilight"
size: 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: forecastData ? forecastData.sunrise : ""
font.pixelSize: Theme.fontSizeSmall - 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: 2
anchors.horizontalCenter: parent.horizontalCenter
DankIcon {
name: "bedtime"
size: 8
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: forecastData ? forecastData.sunset : ""
font.pixelSize: Theme.fontSizeSmall - 2
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,397 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
pragma ComponentBehavior: Bound
Variants {
id: dockVariants
model: SettingsData.getFilteredScreens("dock")
property var contextMenu
delegate: PanelWindow {
id: dock
WlrLayershell.namespace: "dms:dock"
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
anchors {
top: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top) : true
bottom: !isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom) : true
left: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Left)
right: !isVertical ? true : (SettingsData.dockPosition === SettingsData.Position.Right)
}
property var modelData: item
property bool autoHide: SettingsData.dockAutoHide
property real backgroundTransparency: SettingsData.dockTransparency
property bool groupByApp: SettingsData.dockGroupByApp
readonly property real widgetHeight: SettingsData.dockIconSize
readonly property real effectiveBarHeight: widgetHeight + SettingsData.dockSpacing * 2 + 10
readonly property real barSpacing: {
const barIsHorizontal = (SettingsData.dankBarPosition === SettingsData.Position.Top || SettingsData.dankBarPosition === SettingsData.Position.Bottom)
const barIsVertical = (SettingsData.dankBarPosition === SettingsData.Position.Left || SettingsData.dankBarPosition === SettingsData.Position.Right)
const samePosition = (SettingsData.dockPosition === SettingsData.dankBarPosition)
const dockIsHorizontal = !isVertical
const dockIsVertical = isVertical
if (!SettingsData.dankBarVisible) return 0
if (dockIsHorizontal && barIsHorizontal && samePosition) {
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
}
if (dockIsVertical && barIsVertical && samePosition) {
return SettingsData.dankBarSpacing + effectiveBarHeight + SettingsData.dankBarBottomGap
}
return 0
}
readonly property real dockMargin: SettingsData.dockSpacing
readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin
readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1
function px(v) { return Math.round(v * _dpr) / _dpr }
property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData)
property bool revealSticky: false
Timer {
id: revealHold
interval: 250
repeat: false
onTriggered: dock.revealSticky = false
}
property bool reveal: {
if (CompositorService.isNiri && NiriService.inOverview && SettingsData.dockOpenOnOverview) {
return true
}
return !autoHide || dockMouseArea.containsMouse || dockApps.requestDockShow || contextMenuOpen || revealSticky
}
onContextMenuOpenChanged: {
if (!contextMenuOpen && autoHide && !dockMouseArea.containsMouse) {
revealSticky = true
revealHold.restart()
}
}
Connections {
target: SettingsData
function onDockTransparencyChanged() {
dock.backgroundTransparency = SettingsData.dockTransparency
}
}
screen: modelData
visible: {
if (CompositorService.isNiri && NiriService.inOverview) {
return SettingsData.dockOpenOnOverview
}
return SettingsData.showDock
}
color: "transparent"
exclusiveZone: {
if (!SettingsData.showDock || autoHide) return -1
if (barSpacing > 0) return -1
return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin)
}
property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35)
implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0
Item {
id: maskItem
parent: dock.contentItem
visible: false
x: {
const baseX = dockCore.x + dockMouseArea.x
if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) {
return baseX - animationHeadroom
}
return baseX
}
y: {
const baseY = dockCore.y + dockMouseArea.y
if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) {
return baseY - animationHeadroom
}
return baseY
}
width: dockMouseArea.width + (isVertical ? animationHeadroom : 0)
height: dockMouseArea.height + (!isVertical ? animationHeadroom : 0)
}
mask: Region {
item: maskItem
}
property var hoveredButton: {
if (!dockApps.children[0]) {
return null
}
const layoutItem = dockApps.children[0]
const flowLayout = layoutItem.children[0]
let repeater = null
for (var i = 0; i < flowLayout.children.length; i++) {
const child = flowLayout.children[i]
if (child && typeof child.count !== "undefined" && typeof child.itemAt === "function") {
repeater = child
break
}
}
if (!repeater || !repeater.itemAt) {
return null
}
for (var i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i)
if (item && item.dockButton && item.dockButton.showTooltip) {
return item.dockButton
}
}
return null
}
DankTooltip {
id: dockTooltip
targetScreen: dock.screen
}
Timer {
id: tooltipRevealDelay
interval: 250
repeat: false
onTriggered: dock.showTooltipForHoveredButton()
}
function showTooltipForHoveredButton() {
dockTooltip.hide()
if (dock.hoveredButton && dock.reveal && !slideXAnimation.running && !slideYAnimation.running) {
const buttonGlobalPos = dock.hoveredButton.mapToGlobal(0, 0)
const tooltipText = dock.hoveredButton.tooltipText || ""
if (tooltipText) {
const screenX = dock.screen ? (dock.screen.x || 0) : 0
const screenY = dock.screen ? (dock.screen.y || 0) : 0
const screenHeight = dock.screen ? dock.screen.height : 0
if (!dock.isVertical) {
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2
const screenRelativeY = isBottom
? (screenHeight - dock.effectiveBarHeight - SettingsData.dockSpacing - SettingsData.dockBottomGap - SettingsData.dockMargin - 35)
: (buttonGlobalPos.y - screenY + dock.hoveredButton.height + Theme.spacingS)
dockTooltip.show(tooltipText,
globalX,
screenRelativeY,
dock.screen,
false, false)
} else {
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + Theme.spacingXS
const tooltipX = isLeft ? tooltipOffset : (dock.screen.width - tooltipOffset)
const screenRelativeY = buttonGlobalPos.y - screenY + dock.hoveredButton.height / 2
dockTooltip.show(tooltipText,
screenX + tooltipX,
screenRelativeY,
dock.screen,
isLeft,
!isLeft)
}
}
}
}
Connections {
target: dock
function onRevealChanged() {
if (!dock.reveal) {
tooltipRevealDelay.stop()
dockTooltip.hide()
} else {
tooltipRevealDelay.restart()
}
}
function onHoveredButtonChanged() {
dock.showTooltipForHoveredButton()
}
}
Item {
id: dockCore
anchors.fill: parent
x: isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? animationHeadroom : 0
y: !isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? animationHeadroom : 0
Connections {
target: dockMouseArea
function onContainsMouseChanged() {
if (dockMouseArea.containsMouse) {
dock.revealSticky = true
revealHold.stop()
} else {
if (dock.autoHide && !dock.contextMenuOpen) {
revealHold.restart()
}
}
}
}
MouseArea {
id: dockMouseArea
property real currentScreen: modelData ? modelData : dock.screen
property real screenWidth: currentScreen ? currentScreen.geometry.width : 1920
property real screenHeight: currentScreen ? currentScreen.geometry.height : 1080
property real maxDockWidth: screenWidth * 0.98
property real maxDockHeight: screenHeight * 0.98
height: {
if (dock.isVertical) {
return dock.reveal ? Math.min(dockBackground.implicitHeight + 4, maxDockHeight) : Math.min(Math.max(dockBackground.implicitHeight + 64, 200), screenHeight * 0.5)
} else {
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
}
}
width: {
if (dock.isVertical) {
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1
} else {
return dock.reveal ? Math.min(dockBackground.implicitWidth + 4, maxDockWidth) : Math.min(Math.max(dockBackground.implicitWidth + 64, 200), screenWidth * 0.5)
}
}
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? undefined : parent.top) : undefined
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.left) : undefined
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
hoverEnabled: true
acceptedButtons: Qt.NoButton
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Item {
id: dockContainer
anchors.fill: parent
clip: false
transform: Translate {
id: dockSlide
x: {
if (!dock.isVertical) return 0
if (dock.reveal) return 0
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
if (SettingsData.dockPosition === SettingsData.Position.Right) {
return hideDistance
} else {
return -hideDistance
}
}
y: {
if (dock.isVertical) return 0
if (dock.reveal) return 0
const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10
if (SettingsData.dockPosition === SettingsData.Position.Bottom) {
return hideDistance
} else {
return -hideDistance
}
}
Behavior on x {
NumberAnimation {
id: slideXAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Behavior on y {
NumberAnimation {
id: slideYAnimation
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
}
Item {
id: dockBackground
objectName: "dockBackground"
anchors {
top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined) : undefined
bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined) : undefined
horizontalCenter: !dock.isVertical ? parent.horizontalCenter : undefined
left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined) : undefined
right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined
verticalCenter: dock.isVertical ? parent.verticalCenter : undefined
}
anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 : 0
anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 : 0
anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 : 0
anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 : 0
implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2)
implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2)
width: implicitWidth
height: implicitHeight
layer.enabled: true
clip: false
DankRectangle {
anchors.fill: parent
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, backgroundTransparency)
overlayColor: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
}
}
DockApps {
id: dockApps
anchors.top: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Top ? dockBackground.top : undefined) : undefined
anchors.bottom: !dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Bottom ? dockBackground.bottom : undefined) : undefined
anchors.horizontalCenter: !dock.isVertical ? dockBackground.horizontalCenter : undefined
anchors.left: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Left ? dockBackground.left : undefined) : undefined
anchors.right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? dockBackground.right : undefined) : undefined
anchors.verticalCenter: dock.isVertical ? dockBackground.verticalCenter : undefined
anchors.topMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.bottomMargin: !dock.isVertical ? SettingsData.dockSpacing : 0
anchors.leftMargin: dock.isVertical ? SettingsData.dockSpacing : 0
anchors.rightMargin: dock.isVertical ? SettingsData.dockSpacing : 0
contextMenu: dockVariants.contextMenu
groupByApp: dock.groupByApp
isVertical: dock.isVertical
dockScreen: dock.screen
iconSize: dock.widgetHeight
}
}
}
}
}
}

View File

@@ -0,0 +1,534 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
clip: false
property var appData
property var contextMenu: null
property var dockApps: null
property int index: -1
property var parentDockScreen: null
property bool longPressing: false
property bool dragging: false
property point dragStartPos: Qt.point(0, 0)
property point dragOffset: Qt.point(0, 0)
property int targetIndex: -1
property int originalIndex: -1
property bool showWindowTitle: false
property string windowTitle: ""
property bool isHovered: mouseArea.containsMouse && !dragging
property bool showTooltip: mouseArea.containsMouse && !dragging
property var cachedDesktopEntry: null
property real actualIconSize: 40
function updateDesktopEntry() {
if (!appData || appData.appId === "__SEPARATOR__") {
cachedDesktopEntry = null
return
}
const moddedId = Paths.moddedAppId(appData.appId)
cachedDesktopEntry = DesktopEntries.heuristicLookup(moddedId)
}
Component.onCompleted: updateDesktopEntry()
onAppDataChanged: updateDesktopEntry()
Connections {
target: DesktopEntries
function onApplicationsChanged() {
updateDesktopEntry()
}
}
property bool isWindowFocused: {
if (!appData) {
return false
}
if (appData.type === "window") {
const toplevel = getToplevelObject()
if (!toplevel) {
return false
}
return toplevel.activated
} else if (appData.type === "grouped") {
// For grouped apps, check if any window is focused
const allToplevels = ToplevelManager.toplevels.values
for (let i = 0; i < allToplevels.length; i++) {
const toplevel = allToplevels[i]
if (toplevel.appId === appData.appId && toplevel.activated) {
return true
}
}
}
return false
}
property string tooltipText: {
if (!appData) {
return ""
}
if ((appData.type === "window" && showWindowTitle) || (appData.type === "grouped" && appData.windowTitle)) {
const appName = cachedDesktopEntry && cachedDesktopEntry.name ? cachedDesktopEntry.name : appData.appId
const title = appData.type === "window" ? windowTitle : appData.windowTitle
return appName + (title ? " • " + title : "")
}
if (!appData.appId) {
return ""
}
return cachedDesktopEntry && cachedDesktopEntry.name ? cachedDesktopEntry.name : appData.appId
}
function getToplevelObject() {
return appData?.toplevel || null
}
function getGroupedToplevels() {
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || []
}
onIsHoveredChanged: {
if (mouseArea.pressed) return
if (isHovered) {
exitAnimation.stop()
if (!bounceAnimation.running) {
bounceAnimation.restart()
}
} else {
bounceAnimation.stop()
exitAnimation.restart()
}
}
readonly property bool animateX: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
readonly property real animationDistance: actualIconSize
readonly property real animationDirection: {
if (SettingsData.dockPosition === SettingsData.Position.Bottom) return -1
if (SettingsData.dockPosition === SettingsData.Position.Top) return 1
if (SettingsData.dockPosition === SettingsData.Position.Right) return -1
if (SettingsData.dockPosition === SettingsData.Position.Left) return 1
return -1
}
SequentialAnimation {
id: bounceAnimation
running: false
NumberAnimation {
target: iconTransform
property: animateX ? "x" : "y"
to: animationDirection * animationDistance * 0.25
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedAccel
}
NumberAnimation {
target: iconTransform
property: animateX ? "x" : "y"
to: animationDirection * animationDistance * 0.2
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
NumberAnimation {
id: exitAnimation
running: false
target: iconTransform
property: animateX ? "x" : "y"
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
Timer {
id: longPressTimer
interval: 500
repeat: false
onTriggered: {
if (appData && appData.isPinned) {
longPressing = true
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: true
preventStealing: true
cursorShape: longPressing ? Qt.DragMoveCursor : Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: mouse => {
if (mouse.button === Qt.LeftButton && appData && appData.isPinned) {
dragStartPos = Qt.point(mouse.x, mouse.y)
longPressTimer.start()
}
}
onReleased: mouse => {
longPressTimer.stop()
if (longPressing) {
if (dragging && targetIndex >= 0 && targetIndex !== originalIndex && dockApps) {
dockApps.movePinnedApp(originalIndex, targetIndex)
}
longPressing = false
dragging = false
dragOffset = Qt.point(0, 0)
targetIndex = -1
originalIndex = -1
}
}
onPositionChanged: mouse => {
if (longPressing && !dragging) {
const distance = Math.sqrt(Math.pow(mouse.x - dragStartPos.x, 2) + Math.pow(mouse.y - dragStartPos.y, 2))
if (distance > 5) {
dragging = true
targetIndex = index
originalIndex = index
}
}
if (dragging) {
dragOffset = Qt.point(mouse.x - dragStartPos.x, mouse.y - dragStartPos.y)
if (dockApps) {
const threshold = actualIconSize
let newTargetIndex = targetIndex
if (dragOffset.x > threshold && targetIndex < dockApps.pinnedAppCount - 1) {
newTargetIndex = targetIndex + 1
} else if (dragOffset.x < -threshold && targetIndex > 0) {
newTargetIndex = targetIndex - 1
}
if (newTargetIndex !== targetIndex) {
targetIndex = newTargetIndex
dragStartPos = Qt.point(mouse.x, mouse.y)
}
}
}
}
onClicked: mouse => {
if (!appData || longPressing) {
return
}
if (mouse.button === Qt.LeftButton) {
if (appData.type === "pinned") {
if (appData && appData.appId) {
const desktopEntry = cachedDesktopEntry
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
})
}
SessionService.launchDesktopEntry(desktopEntry)
}
} else if (appData.type === "window") {
const toplevel = getToplevelObject()
if (toplevel) {
toplevel.activate()
}
} else if (appData.type === "grouped") {
if (appData.windowCount === 0) {
if (appData && appData.appId) {
const desktopEntry = cachedDesktopEntry
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
})
}
SessionService.launchDesktopEntry(desktopEntry)
}
} else if (appData.windowCount === 1) {
// For single window, activate directly
const toplevel = getToplevelObject()
if (toplevel) {
console.log("Activating grouped app window:", appData.windowTitle)
toplevel.activate()
} else {
console.warn("No toplevel found for grouped app")
}
} else {
if (contextMenu) {
contextMenu.showForButton(root, appData, root.height + 25, true, cachedDesktopEntry, parentDockScreen)
}
}
}
} else if (mouse.button === Qt.MiddleButton) {
if (appData?.type === "window") {
appData?.toplevel?.close()
} else if (appData?.type === "grouped") {
if (contextMenu) {
contextMenu.showForButton(root, appData, root.height, false, cachedDesktopEntry, parentDockScreen)
}
} else if (appData && appData.appId) {
const desktopEntry = cachedDesktopEntry
if (desktopEntry) {
AppUsageHistoryData.addAppUsage({
"id": appData.appId,
"name": desktopEntry.name || appData.appId,
"icon": desktopEntry.icon || "",
"exec": desktopEntry.exec || "",
"comment": desktopEntry.comment || ""
})
}
SessionService.launchDesktopEntry(desktopEntry)
}
} else if (mouse.button === Qt.RightButton) {
if (contextMenu && appData) {
contextMenu.showForButton(root, appData, root.height, false, cachedDesktopEntry, parentDockScreen)
} else {
console.warn("No context menu or appData available")
}
}
}
}
Item {
id: visualContent
anchors.fill: parent
transform: Translate {
id: iconTransform
x: 0
y: 0
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
border.color: Theme.primary
visible: dragging
z: -1
}
IconImage {
id: iconImg
anchors.centerIn: parent
implicitSize: actualIconSize
source: {
if (appData.appId === "__SEPARATOR__") {
return ""
}
const moddedId = Paths.moddedAppId(appData.appId)
if (moddedId.toLowerCase().includes("steam_app")) {
return ""
}
return cachedDesktopEntry && cachedDesktopEntry.icon ? Quickshell.iconPath(cachedDesktopEntry.icon, true) : ""
}
mipmap: true
smooth: true
asynchronous: true
visible: status === Image.Ready
}
DankIcon {
anchors.centerIn: parent
size: actualIconSize
name: "sports_esports"
color: Theme.surfaceText
visible: {
if (!appData || !appData.appId || appData.appId === "__SEPARATOR__") {
return false
}
const moddedId = Paths.moddedAppId(appData.appId)
return moddedId.toLowerCase().includes("steam_app")
}
}
Rectangle {
width: actualIconSize
height: actualIconSize
anchors.centerIn: parent
visible: iconImg.status !== Image.Ready
color: Theme.surfaceLight
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.primarySelected
Text {
anchors.centerIn: parent
text: {
if (!appData || !appData.appId) {
return "?"
}
const desktopEntry = cachedDesktopEntry
if (desktopEntry && desktopEntry.name) {
return desktopEntry.name.charAt(0).toUpperCase()
}
return appData.appId.charAt(0).toUpperCase()
}
font.pixelSize: Math.max(8, parent.width * 0.35)
color: Theme.primary
font.weight: Font.Bold
}
}
Loader {
anchors.horizontalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? undefined : parent.horizontalCenter
anchors.verticalCenter: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? parent.verticalCenter : undefined
anchors.bottom: SettingsData.dockPosition === SettingsData.Position.Bottom ? parent.bottom : undefined
anchors.top: SettingsData.dockPosition === SettingsData.Position.Top ? parent.top : undefined
anchors.left: SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
anchors.right: SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
anchors.bottomMargin: SettingsData.dockPosition === SettingsData.Position.Bottom ? -(SettingsData.dockSpacing / 2) : 0
anchors.topMargin: SettingsData.dockPosition === SettingsData.Position.Top ? -(SettingsData.dockSpacing / 2) : 0
anchors.leftMargin: SettingsData.dockPosition === SettingsData.Position.Left ? -(SettingsData.dockSpacing / 2) : 0
anchors.rightMargin: SettingsData.dockPosition === SettingsData.Position.Right ? -(SettingsData.dockSpacing / 2) : 0
sourceComponent: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right ? columnIndicator : rowIndicator
visible: {
if (!appData) return false
if (appData.type === "window") return true
if (appData.type === "grouped") return appData.windowCount > 0
return appData.isRunning
}
}
}
Component {
id: rowIndicator
Row {
spacing: 2
Repeater {
model: {
if (!appData) return 0
if (appData.type === "grouped") {
return Math.min(appData.windowCount, 4)
} else if (appData.type === "window" || appData.isRunning) {
return 1
}
return 0
}
Rectangle {
width: {
if (SettingsData.dockIndicatorStyle === "circle") {
return Math.max(4, actualIconSize * 0.1)
}
return appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2)
}
height: {
if (SettingsData.dockIndicatorStyle === "circle") {
return Math.max(4, actualIconSize * 0.1)
}
return Math.max(2, actualIconSize * 0.05)
}
radius: SettingsData.dockIndicatorStyle === "circle" ? width / 2 : Theme.cornerRadius
color: {
if (!appData) {
return "transparent"
}
if (appData.type !== "grouped" || appData.windowCount === 1) {
if (isWindowFocused) {
return Theme.primary
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
if (appData.type === "grouped" && appData.windowCount > 1) {
const groupToplevels = getGroupedToplevels()
if (index < groupToplevels.length && groupToplevels[index].activated) {
return Theme.primary
}
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
}
}
}
}
Component {
id: columnIndicator
Column {
spacing: 2
Repeater {
model: {
if (!appData) return 0
if (appData.type === "grouped") {
return Math.min(appData.windowCount, 4)
} else if (appData.type === "window" || appData.isRunning) {
return 1
}
return 0
}
Rectangle {
width: {
if (SettingsData.dockIndicatorStyle === "circle") {
return Math.max(4, actualIconSize * 0.1)
}
return Math.max(2, actualIconSize * 0.05)
}
height: {
if (SettingsData.dockIndicatorStyle === "circle") {
return Math.max(4, actualIconSize * 0.1)
}
return appData && appData.type === "grouped" && appData.windowCount > 1 ? Math.max(3, actualIconSize * 0.1) : Math.max(6, actualIconSize * 0.2)
}
radius: SettingsData.dockIndicatorStyle === "circle" ? width / 2 : Theme.cornerRadius
color: {
if (!appData) {
return "transparent"
}
if (appData.type !== "grouped" || appData.windowCount === 1) {
if (isWindowFocused) {
return Theme.primary
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
if (appData.type === "grouped" && appData.windowCount > 1) {
const groupToplevels = getGroupedToplevels()
if (index < groupToplevels.length && groupToplevels[index].activated) {
return Theme.primary
}
}
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
}
}
}
}
}
}

View File

@@ -0,0 +1,251 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var contextMenu: null
property bool requestDockShow: false
property int pinnedAppCount: 0
property bool groupByApp: false
property bool isVertical: false
property var dockScreen: null
property real iconSize: 40
clip: false
implicitWidth: isVertical ? appLayout.height : appLayout.width
implicitHeight: isVertical ? appLayout.width : appLayout.height
function movePinnedApp(fromIndex, toIndex) {
if (fromIndex === toIndex) {
return
}
const currentPinned = [...(SessionData.pinnedApps || [])]
if (fromIndex < 0 || fromIndex >= currentPinned.length || toIndex < 0 || toIndex >= currentPinned.length) {
return
}
const movedApp = currentPinned.splice(fromIndex, 1)[0]
currentPinned.splice(toIndex, 0, movedApp)
SessionData.setPinnedApps(currentPinned)
}
Item {
id: appLayout
width: layoutFlow.width
height: layoutFlow.height
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
anchors.right: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined
anchors.top: root.isVertical ? undefined : parent.top
Flow {
id: layoutFlow
flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight
spacing: Math.min(8, Math.max(4, root.iconSize * 0.08))
Repeater {
id: repeater
property var dockItems: []
model: ScriptModel {
values: repeater.dockItems
objectProp: "uniqueKey"
}
Component.onCompleted: updateModel()
function updateModel() {
const items = []
const pinnedApps = [...(SessionData.pinnedApps || [])]
const sortedToplevels = CompositorService.sortedToplevels
if (root.groupByApp) {
const appGroups = new Map()
pinnedApps.forEach(appId => {
appGroups.set(appId, {
appId: appId,
isPinned: true,
windows: []
})
})
sortedToplevels.forEach((toplevel, index) => {
const appId = toplevel.appId || "unknown"
if (!appGroups.has(appId)) {
appGroups.set(appId, {
appId: appId,
isPinned: false,
windows: []
})
}
appGroups.get(appId).windows.push({
toplevel: toplevel,
index: index
})
})
const pinnedGroups = []
const unpinnedGroups = []
Array.from(appGroups.entries()).forEach(([appId, group]) => {
const firstWindow = group.windows.length > 0 ? group.windows[0] : null
const item = {
uniqueKey: "grouped_" + appId,
type: "grouped",
appId: appId,
toplevel: firstWindow ? firstWindow.toplevel : null,
isPinned: group.isPinned,
isRunning: group.windows.length > 0,
windowCount: group.windows.length,
allWindows: group.windows
}
if (group.isPinned) {
pinnedGroups.push(item)
} else {
unpinnedGroups.push(item)
}
})
pinnedGroups.forEach(item => items.push(item))
if (pinnedGroups.length > 0 && unpinnedGroups.length > 0) {
items.push({
uniqueKey: "separator_grouped",
type: "separator",
appId: "__SEPARATOR__",
toplevel: null,
isPinned: false,
isRunning: false
})
}
unpinnedGroups.forEach(item => items.push(item))
root.pinnedAppCount = pinnedGroups.length
} else {
pinnedApps.forEach(appId => {
items.push({
uniqueKey: "pinned_" + appId,
type: "pinned",
appId: appId,
toplevel: null,
isPinned: true,
isRunning: false
})
})
root.pinnedAppCount = pinnedApps.length
if (pinnedApps.length > 0 && sortedToplevels.length > 0) {
items.push({
uniqueKey: "separator_ungrouped",
type: "separator",
appId: "__SEPARATOR__",
toplevel: null,
isPinned: false,
isRunning: false
})
}
sortedToplevels.forEach((toplevel, index) => {
let uniqueKey = "window_" + index
if (CompositorService.isHyprland && Hyprland.toplevels) {
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
for (let i = 0; i < hyprlandToplevels.length; i++) {
if (hyprlandToplevels[i].wayland === toplevel) {
uniqueKey = "window_" + hyprlandToplevels[i].address
break
}
}
}
items.push({
uniqueKey: uniqueKey,
type: "window",
appId: toplevel.appId,
toplevel: toplevel,
isPinned: false,
isRunning: true
})
})
}
dockItems = items
}
delegate: Item {
id: delegateItem
property alias dockButton: button
property var itemData: modelData
clip: false
width: itemData.type === "separator" ? (root.isVertical ? root.iconSize : 8) : (root.isVertical ? root.iconSize : root.iconSize * 1.2)
height: itemData.type === "separator" ? (root.isVertical ? 8 : root.iconSize) : (root.isVertical ? root.iconSize * 1.2 : root.iconSize)
Rectangle {
visible: itemData.type === "separator"
width: root.isVertical ? root.iconSize * 0.5 : 2
height: root.isVertical ? 2 : root.iconSize * 0.5
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
radius: 1
anchors.centerIn: parent
}
DockAppButton {
id: button
visible: itemData.type !== "separator"
anchors.centerIn: parent
width: delegateItem.width
height: delegateItem.height
actualIconSize: root.iconSize
appData: itemData
contextMenu: root.contextMenu
dockApps: root
index: model.index
parentDockScreen: root.dockScreen
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
windowTitle: {
const title = itemData?.toplevel?.title || "(Unnamed)"
return title.length > 50 ? title.substring(0, 47) + "..." : title
}
}
}
}
}
}
Connections {
target: CompositorService
function onToplevelsChanged() {
repeater.updateModel()
}
}
Connections {
target: SessionData
function onPinnedAppsChanged() {
repeater.updateModel()
}
}
onGroupByAppChanged: {
repeater.updateModel()
}
}

View File

@@ -0,0 +1,498 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Widgets
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
id: root
WlrLayershell.namespace: "dms:dock-context-menu"
property var appData: null
property var anchorItem: null
property real dockVisibleHeight: 40
property int margin: 10
property bool hidePin: false
property var desktopEntry: null
function showForButton(button, data, dockHeight, hidePinOption, entry, dockScreen) {
if (dockScreen) {
root.screen = dockScreen
}
anchorItem = button
appData = data
dockVisibleHeight = dockHeight || 40
hidePin = hidePinOption || false
desktopEntry = entry || null
visible = true
}
function close() {
visible = false
}
screen: null
visible: false
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
property point anchorPos: Qt.point(screen.width / 2, screen.height - 100)
onAnchorItemChanged: updatePosition()
onVisibleChanged: {
if (visible) {
updatePosition()
}
}
function updatePosition() {
if (!anchorItem) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
}
const dockWindow = anchorItem.Window.window
if (!dockWindow) {
anchorPos = Qt.point(screen.width / 2, screen.height - 100)
return
}
const buttonPosInDock = anchorItem.mapToItem(dockWindow.contentItem, 0, 0)
let actualDockHeight = root.dockVisibleHeight
function findDockBackground(item) {
if (item.objectName === "dockBackground") {
return item
}
for (var i = 0; i < item.children.length; i++) {
const found = findDockBackground(item.children[i])
if (found) {
return found
}
}
return null
}
const dockBackground = findDockBackground(dockWindow.contentItem)
let actualDockWidth = dockWindow.width
if (dockBackground) {
actualDockHeight = dockBackground.height
actualDockWidth = dockBackground.width
}
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
const dockMargin = SettingsData.dockMargin + 16
let buttonScreenX, buttonScreenY
if (isVertical) {
const dockContentHeight = dockWindow.height
const screenHeight = root.screen.height
const dockTopMargin = Math.round((screenHeight - dockContentHeight) / 2)
buttonScreenY = dockTopMargin + buttonPosInDock.y + anchorItem.height / 2
if (SettingsData.dockPosition === SettingsData.Position.Right) {
buttonScreenX = root.screen.width - actualDockWidth - dockMargin - 20
} else {
buttonScreenX = actualDockWidth + dockMargin + 20
}
} else {
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
if (isDockAtBottom) {
buttonScreenY = root.screen.height - actualDockHeight - dockMargin - 20
} else {
buttonScreenY = actualDockHeight + dockMargin + 20
}
const dockContentWidth = dockWindow.width
const screenWidth = root.screen.width
const dockLeftMargin = Math.round((screenWidth - dockContentWidth) / 2)
buttonScreenX = dockLeftMargin + buttonPosInDock.x + anchorItem.width / 2
}
anchorPos = Qt.point(buttonScreenX, buttonScreenY)
}
Rectangle {
id: menuContainer
x: {
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
if (isVertical) {
const isDockAtRight = SettingsData.dockPosition === SettingsData.Position.Right
if (isDockAtRight) {
return Math.max(10, root.anchorPos.x - width + 30)
} else {
return Math.min(root.width - width - 10, root.anchorPos.x - 30)
}
} else {
const left = 10
const right = root.width - width - 10
const want = root.anchorPos.x - width / 2
return Math.max(left, Math.min(right, want))
}
}
y: {
const isVertical = SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
if (isVertical) {
const top = 10
const bottom = root.height - height - 10
const want = root.anchorPos.y - height / 2
return Math.max(top, Math.min(bottom, want))
} else {
const isDockAtBottom = SettingsData.dockPosition === SettingsData.Position.Bottom
if (isDockAtBottom) {
return Math.max(10, root.anchorPos.y - height + 30)
} else {
return Math.min(root.height - height - 10, root.anchorPos.y - 30)
}
}
}
width: Math.min(400, Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2))
height: Math.max(60, menuColumn.implicitHeight + Theme.spacingS * 2)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
opacity: root.visible ? 1 : 0
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
Column {
id: menuColumn
width: parent.width - Theme.spacingS * 2
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
spacing: 1
// Window list for grouped apps
Repeater {
model: {
if (!root.appData || root.appData.type !== "grouped") return []
const toplevels = []
const allToplevels = ToplevelManager.toplevels.values
for (let i = 0; i < allToplevels.length; i++) {
const toplevel = allToplevels[i]
if (toplevel.appId === root.appData.appId) {
toplevels.push(toplevel)
}
}
return toplevels
}
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: windowArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: closeButton.left
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
text: (modelData && modelData.title) ? modelData.title: I18n.tr("(Unnamed)")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
Rectangle {
id: closeButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingXS
anchors.verticalCenter: parent.verticalCenter
width: 20
height: 20
radius: 10
color: closeMouseArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.2) : "transparent"
DankIcon {
anchors.centerIn: parent
name: "close"
size: 12
color: closeMouseArea.containsMouse ? Theme.error : Theme.surfaceText
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.close) {
modelData.close()
}
root.close()
}
}
}
MouseArea {
id: windowArea
anchors.fill: parent
anchors.rightMargin: 24
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.activate) {
modelData.activate()
}
root.close()
}
}
}
}
Rectangle {
visible: {
if (!root.appData) return false
if (root.appData.type !== "grouped") return false
return root.appData.windowCount > 0
}
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Repeater {
model: root.desktopEntry && root.desktopEntry.actions ? root.desktopEntry.actions : []
Rectangle {
width: parent.width
height: 28
radius: Theme.cornerRadius
color: actionArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Item {
anchors.verticalCenter: parent.verticalCenter
width: 16
height: 16
visible: modelData.icon && modelData.icon !== ""
IconImage {
anchors.fill: parent
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: modelData.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
}
MouseArea {
id: actionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
SessionService.launchDesktopAction(root.desktopEntry, modelData)
}
root.close()
}
}
}
}
Rectangle {
visible: root.desktopEntry && root.desktopEntry.actions && root.desktopEntry.actions.length > 0
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: !root.hidePin
width: parent.width
height: 28
radius: Theme.cornerRadius
color: pinArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.appData && root.appData.isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: pinArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (!root.appData) {
return
}
if (root.appData.isPinned) {
SessionData.removePinnedApp(root.appData.appId)
} else {
SessionData.addPinnedApp(root.appData.appId)
}
root.close()
}
}
}
Rectangle {
visible: (root.appData && root.appData.type === "window") || (root.desktopEntry && SessionService.hasPrimeRun)
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: root.desktopEntry && SessionService.hasPrimeRun
width: parent.width
height: 28
radius: Theme.cornerRadius
color: primeRunArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Launch on dGPU")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: primeRunArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.desktopEntry) {
SessionService.launchDesktopEntry(root.desktopEntry, true)
}
root.close()
}
}
}
Rectangle {
visible: root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0))
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
Rectangle {
visible: root.appData && (root.appData.type === "window" || (root.appData.type === "grouped" && root.appData.windowCount > 0))
width: parent.width
height: 28
radius: Theme.cornerRadius
color: closeArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: {
if (root.appData && root.appData.type === "grouped") {
return "Close All Windows"
}
return "Close Window"
}
font.pixelSize: Theme.fontSizeSmall
color: closeArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.appData?.type === "window") {
root.appData?.toplevel?.close()
} else if (root.appData?.type === "grouped") {
root.appData?.allWindows?.forEach(window => window.toplevel?.close())
}
root.close()
}
}
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: root.close()
}
}

View File

@@ -0,0 +1,105 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
readonly property string memoryFile: greetCfgDir + "/memory.json"
property string lastSessionId: ""
property string lastSuccessfulUser: ""
property bool isLightMode: false
property bool nightModeEnabled: false
Component.onCompleted: {
Quickshell.execDetached(["mkdir", "-p", greetCfgDir])
loadMemory()
loadSessionConfig()
}
function loadMemory() {
parseMemory(memoryFileView.text())
}
function loadSessionConfig() {
parseSessionConfig(sessionConfigFileView.text())
}
function parseSessionConfig(content) {
try {
if (content && content.trim()) {
const config = JSON.parse(content)
isLightMode = config.isLightMode !== undefined ? config.isLightMode : false
nightModeEnabled = config.nightModeEnabled !== undefined ? config.nightModeEnabled : false
}
} catch (e) {
console.warn("Failed to parse greeter session config:", e)
}
}
function parseMemory(content) {
try {
if (content && content.trim()) {
const memory = JSON.parse(content)
lastSessionId = memory.lastSessionId !== undefined ? memory.lastSessionId : ""
lastSuccessfulUser = memory.lastSuccessfulUser !== undefined ? memory.lastSuccessfulUser : ""
}
} catch (e) {
console.warn("Failed to parse greetd memory:", e)
}
}
function saveMemory() {
memoryFileView.setText(JSON.stringify({
"lastSessionId": lastSessionId,
"lastSuccessfulUser": lastSuccessfulUser
}, null, 2))
}
function setLastSessionId(id) {
lastSessionId = id || ""
saveMemory()
}
function setLastSuccessfulUser(username) {
lastSuccessfulUser = username || ""
saveMemory()
}
FileView {
id: memoryFileView
path: root.memoryFile
blockLoading: false
blockWrites: false
atomicWrites: true
watchChanges: false
printErrors: false
onLoaded: {
parseMemory(memoryFileView.text())
}
}
FileView {
id: sessionConfigFileView
path: root.sessionConfigPath
blockLoading: false
blockWrites: true
atomicWrites: false
watchChanges: false
printErrors: true
onLoaded: {
parseSessionConfig(sessionConfigFileView.text())
}
onLoadFailed: error => {
console.warn("Could not load greeter session config from", root.sessionConfigPath, "error:", error)
}
}
}

View File

@@ -0,0 +1,116 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
readonly property string configPath: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
return greetCfgDir + "/settings.json"
}
property string currentThemeName: "blue"
property bool settingsLoaded: false
property string customThemeFile: ""
property string matugenScheme: "scheme-tonal-spot"
property bool use24HourClock: true
property bool showSeconds: false
property bool useFahrenheit: false
property bool nightModeEnabled: false
property string weatherLocation: "New York, NY"
property string weatherCoordinates: "40.7128,-74.0060"
property bool useAutoLocation: false
property bool weatherEnabled: true
property string iconTheme: "System Default"
property bool useOSLogo: false
property string osLogoColorOverride: ""
property real osLogoBrightness: 0.5
property real osLogoContrast: 1
property string fontFamily: "Inter Variable"
property string monoFontFamily: "Fira Code"
property int fontWeight: Font.Normal
property real fontScale: 1.0
property real cornerRadius: 12
property string widgetBackgroundColor: "sch"
property string lockDateFormat: ""
property bool lockScreenShowPowerActions: true
property var screenPreferences: ({})
property int animationSpeed: 2
property string wallpaperFillMode: "Fill"
readonly property string defaultFontFamily: "Inter Variable"
readonly property string defaultMonoFontFamily: "Fira Code"
function parseSettings(content) {
try {
if (content && content.trim()) {
const settings = JSON.parse(content)
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "blue"
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : ""
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot"
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY"
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060"
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default"
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : ""
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : defaultFontFamily
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : defaultMonoFontFamily
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch"
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : ""
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({})
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill"
settingsLoaded = true
if (typeof Theme !== "undefined") {
Theme.applyGreeterTheme(currentThemeName)
}
}
} catch (e) {
console.warn("Failed to parse greetd settings:", e)
}
}
function getEffectiveLockDateFormat() {
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat
}
function getFilteredScreens(componentId) {
const prefs = screenPreferences && screenPreferences[componentId] || ["all"]
if (prefs.includes("all")) {
return Quickshell.screens
}
return Quickshell.screens.filter(screen => prefs.includes(screen.name))
}
FileView {
id: settingsFile
path: root.configPath
blockLoading: false
blockWrites: true
atomicWrites: false
watchChanges: false
printErrors: true
onLoaded: {
parseSettings(settingsFile.text())
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
import QtQuick
import Quickshell
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
property string passwordBuffer: ""
property string username: ""
property string usernameInput: ""
property bool showPasswordInput: false
property string selectedSession: ""
property string pamState: ""
property bool unlocking: false
property var sessionList: []
property var sessionExecs: []
property var sessionPaths: []
property int currentSessionIndex: 0
function reset() {
showPasswordInput = false
username = ""
usernameInput = ""
passwordBuffer = ""
pamState = ""
}
}

View File

@@ -0,0 +1,34 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Greetd
import qs.Common
Variants {
model: Quickshell.screens
PanelWindow {
id: root
property var modelData
screen: modelData
anchors {
left: true
right: true
top: true
bottom: true
}
exclusionMode: ExclusionMode.Normal
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
color: "transparent"
GreeterContent {
anchors.fill: parent
screenName: root.screen?.name ?? ""
}
}
}

Some files were not shown because too many files have changed in this diff Show More