summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals
diff options
context:
space:
mode:
authorAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
committerAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
commit70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch)
treeab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals
parent5d94c0a7d44a2255b81815a52a7056a94a39842d (diff)
downloadRaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz
RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/AppPickerModal.qml471
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/BluetoothPairingModal.qml409
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/BrowserPickerModal.qml51
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogContent.qml250
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogFeatureCard.qml78
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogModal.qml156
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogUpgradeNote.qml27
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml21
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml260
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml155
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml87
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml174
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml197
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml166
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml51
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml178
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/ConfirmModal.qml291
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/DankModal.qml496
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/InputModal.qml312
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankColorPickerModal.qml771
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ActionPanel.qml256
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Controller.qml1894
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ControllerUtils.js128
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml465
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/GridItem.qml105
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ItemTransformers.js237
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContent.qml1075
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml510
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/NavigationHelpers.js254
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultItem.qml196
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultsList.qml498
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Scorer.js265
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Section.qml114
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/SectionHeader.qml340
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/TileItem.qml199
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DisplayConfirmationModal.qml212
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserContent.qml920
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml253
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml258
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserModal.qml97
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserNavigation.qml130
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserOverwriteDialog.qml127
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSaveRow.qml74
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSidebar.qml70
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSortMenu.qml183
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml66
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileInfo.qml237
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/KeyboardHints.qml50
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterCompletePage.qml492
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterDoctorPage.qml427
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterDoctorResultItem.qml109
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterFeatureCard.qml74
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterModal.qml333
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterQuickLink.qml60
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterSettingsCard.qml78
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterStatusCard.qml75
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterWelcomePage.qml165
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/KeybindsModal.qml347
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/MuxModal.qml633
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NetworkInfoModal.qml155
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NetworkWiredInfoModal.qml155
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NotificationModal.qml225
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/PolkitAuthModal.qml412
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/PowerMenuModal.qml809
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/ProcessListModal.qml609
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/ProfileSection.qml144
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsContent.qml522
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsModal.qml382
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsSidebar.qml1023
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WifiPasswordModal.qml764
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WifiQRCodeModal.qml170
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WindowRuleModal.qml1184
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WorkspaceRenameModal.qml230
73 files changed, 23391 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/AppPickerModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/AppPickerModal.qml
new file mode 100644
index 0000000..2188111
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/AppPickerModal.qml
@@ -0,0 +1,471 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Modals.Common
+import qs.Widgets
+import qs.Services
+
+DankModal {
+ id: root
+
+ property string title: I18n.tr("Select Application")
+ property string targetData: ""
+ property string targetDataLabel: ""
+ property string searchQuery: ""
+ property int selectedIndex: 0
+ property int gridColumns: SettingsData.appLauncherGridColumns
+ property bool keyboardNavigationActive: false
+ property string viewMode: "grid"
+ property var categoryFilter: []
+ property var usageHistoryKey: ""
+ property bool showTargetData: true
+
+ signal applicationSelected(var app, string targetData)
+
+ shouldBeVisible: false
+ allowStacking: true
+ modalWidth: 520
+ modalHeight: 500
+
+ onBackgroundClicked: close()
+
+ onDialogClosed: {
+ searchQuery = ""
+ selectedIndex = 0
+ keyboardNavigationActive = false
+ }
+
+ onOpened: {
+ searchQuery = ""
+ updateApplicationList()
+ selectedIndex = 0
+ Qt.callLater(() => {
+ if (contentLoader.item && contentLoader.item.searchField) {
+ contentLoader.item.searchField.text = ""
+ contentLoader.item.searchField.forceActiveFocus()
+ }
+ })
+ }
+
+ function updateApplicationList() {
+ applicationsModel.clear()
+ const apps = AppSearchService.applications
+ const usageHistory = usageHistoryKey && SettingsData[usageHistoryKey] ? SettingsData[usageHistoryKey] : {}
+ let filteredApps = []
+
+ for (const app of apps) {
+ if (!app || !app.categories) continue
+
+ let matchesCategory = categoryFilter.length === 0
+
+ if (categoryFilter.length > 0) {
+ try {
+ for (const cat of app.categories) {
+ if (categoryFilter.includes(cat)) {
+ matchesCategory = true
+ break
+ }
+ }
+ } catch (e) {
+ console.warn("AppPicker: Error iterating categories for", app.name, ":", e)
+ continue
+ }
+ }
+
+ if (matchesCategory) {
+ const name = app.name || ""
+ const lowerName = name.toLowerCase()
+ const lowerQuery = searchQuery.toLowerCase()
+
+ if (searchQuery === "" || lowerName.includes(lowerQuery)) {
+ filteredApps.push({
+ name: name,
+ icon: app.icon || "application-x-executable",
+ exec: app.exec || app.execString || "",
+ startupClass: app.startupWMClass || "",
+ appData: app
+ })
+ }
+ }
+ }
+
+ filteredApps.sort((a, b) => {
+ const aId = a.appData.id || a.appData.execString || a.appData.exec || ""
+ const bId = b.appData.id || b.appData.execString || b.appData.exec || ""
+ const aUsage = usageHistory[aId] ? usageHistory[aId].count : 0
+ const bUsage = usageHistory[bId] ? usageHistory[bId].count : 0
+ if (aUsage !== bUsage) {
+ return bUsage - aUsage
+ }
+ return (a.name || "").localeCompare(b.name || "")
+ })
+
+ filteredApps.forEach(app => {
+ applicationsModel.append({
+ name: app.name,
+ icon: app.icon,
+ exec: app.exec,
+ startupClass: app.startupClass,
+ appId: app.appData.id || app.appData.execString || app.appData.exec || ""
+ })
+ })
+
+ console.log("AppPicker: Found " + filteredApps.length + " applications")
+ }
+
+ onSearchQueryChanged: updateApplicationList()
+
+ ListModel {
+ id: applicationsModel
+ }
+
+ content: Component {
+ FocusScope {
+ id: appContent
+
+ property alias searchField: searchField
+
+ anchors.fill: parent
+ focus: true
+
+ Keys.onEscapePressed: event => {
+ root.close()
+ event.accepted = true
+ }
+
+ Keys.onPressed: event => {
+ if (applicationsModel.count === 0) return
+
+ // Toggle view mode with Tab key
+ if (event.key === Qt.Key_Tab) {
+ root.viewMode = root.viewMode === "grid" ? "list" : "grid"
+ event.accepted = true
+ return
+ }
+
+ if (root.viewMode === "grid") {
+ if (event.key === Qt.Key_Left) {
+ root.keyboardNavigationActive = true
+ root.selectedIndex = Math.max(0, root.selectedIndex - 1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Right) {
+ root.keyboardNavigationActive = true
+ root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Up) {
+ root.keyboardNavigationActive = true
+ root.selectedIndex = Math.max(0, root.selectedIndex - root.gridColumns)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Down) {
+ root.keyboardNavigationActive = true
+ root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + root.gridColumns)
+ event.accepted = true
+ }
+ } else {
+ if (event.key === Qt.Key_Up) {
+ root.keyboardNavigationActive = true
+ root.selectedIndex = Math.max(0, root.selectedIndex - 1)
+ event.accepted = true
+ } else if (event.key === Qt.Key_Down) {
+ root.keyboardNavigationActive = true
+ root.selectedIndex = Math.min(applicationsModel.count - 1, root.selectedIndex + 1)
+ event.accepted = true
+ }
+ }
+
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ if (root.selectedIndex >= 0 && root.selectedIndex < applicationsModel.count) {
+ const app = applicationsModel.get(root.selectedIndex)
+ launchApplication(app)
+ }
+ event.accepted = true
+ }
+ }
+
+ 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: root.title
+ font.pixelSize: Theme.fontSizeLarge + 4
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+
+ Row {
+ spacing: 4
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankActionButton {
+ buttonSize: 36
+ circular: false
+ iconName: "view_list"
+ iconSize: 20
+ iconColor: root.viewMode === "list" ? Theme.primary : Theme.surfaceText
+ backgroundColor: root.viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+ onClicked: {
+ root.viewMode = "list"
+ }
+ }
+
+ DankActionButton {
+ buttonSize: 36
+ circular: false
+ iconName: "grid_view"
+ iconSize: 20
+ iconColor: root.viewMode === "grid" ? Theme.primary : Theme.surfaceText
+ backgroundColor: root.viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
+ onClicked: {
+ root.viewMode = "grid"
+ }
+ }
+ }
+ }
+
+ 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: root.shouldBeVisible
+ ignoreLeftRightKeys: root.viewMode !== "list"
+ ignoreTabKeys: true
+ keyForwardTargets: [appContent]
+
+ onTextEdited: {
+ root.searchQuery = text
+ }
+
+ Keys.onPressed: function (event) {
+ if (event.key === Qt.Key_Escape) {
+ root.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 (root.keyboardNavigationActive && applicationsModel.count > 0) {
+ const app = applicationsModel.get(root.selectedIndex)
+ launchApplication(app)
+ } else if (applicationsModel.count > 0) {
+ const app = applicationsModel.get(0)
+ launchApplication(app)
+ }
+ 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 (!root.shouldBeVisible) {
+ searchField.focus = false
+ }
+ }
+
+ target: root
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: {
+ let usedHeight = 40 + Theme.spacingS
+ usedHeight += 52 + Theme.spacingS
+ if (root.showTargetData) {
+ usedHeight += 36 + Theme.spacingS
+ }
+ return parent.height - usedHeight
+ }
+ radius: Theme.cornerRadius
+ color: "transparent"
+
+ DankListView {
+ id: appList
+
+ property int itemHeight: 60
+ property int itemSpacing: Theme.spacingS
+
+ function ensureVisible(index) {
+ if (index < 0 || index >= count) return
+
+ const itemY = index * (itemHeight + itemSpacing)
+ const 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: root.viewMode === "list"
+ model: applicationsModel
+ currentIndex: root.selectedIndex
+ clip: true
+ spacing: itemSpacing
+
+ onCurrentIndexChanged: {
+ root.selectedIndex = currentIndex
+ if (root.keyboardNavigationActive) {
+ ensureVisible(currentIndex)
+ }
+ }
+
+ delegate: AppLauncherListDelegate {
+ listView: appList
+ itemHeight: 60
+ iconSize: 40
+ showDescription: false
+
+ isCurrentItem: index === root.selectedIndex
+ keyboardNavigationActive: root.keyboardNavigationActive
+ hoverUpdatesSelection: true
+
+ onItemClicked: (idx, modelData) => {
+ launchApplication(modelData)
+ }
+
+ onKeyboardNavigationReset: {
+ root.keyboardNavigationActive = false
+ }
+ }
+ }
+
+ DankGridView {
+ id: appGrid
+
+ function ensureVisible(index) {
+ if (index < 0 || index >= count) return
+
+ const itemY = Math.floor(index / root.gridColumns) * cellHeight
+ const 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: root.viewMode === "grid"
+ model: applicationsModel
+ cellWidth: width / root.gridColumns
+ cellHeight: 120
+ clip: true
+ currentIndex: root.selectedIndex
+
+ onCurrentIndexChanged: {
+ root.selectedIndex = currentIndex
+ if (root.keyboardNavigationActive) {
+ ensureVisible(currentIndex)
+ }
+ }
+
+ delegate: AppLauncherGridDelegate {
+ gridView: appGrid
+ cellWidth: appGrid.cellWidth
+ cellHeight: appGrid.cellHeight
+
+ currentIndex: root.selectedIndex
+ keyboardNavigationActive: root.keyboardNavigationActive
+ hoverUpdatesSelection: true
+
+ onItemClicked: (idx, modelData) => {
+ launchApplication(modelData)
+ }
+
+ onKeyboardNavigationReset: {
+ root.keyboardNavigationActive = false
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 36
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, 0.5)
+ border.color: Theme.outlineMedium
+ border.width: 1
+ visible: root.showTargetData && root.targetData.length > 0
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ text: root.targetDataLabel.length > 0 ? root.targetDataLabel + ": " + root.targetData : root.targetData
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ elide: Text.ElideMiddle
+ wrapMode: Text.NoWrap
+ maximumLineCount: 1
+ }
+ }
+ }
+
+ function launchApplication(app) {
+ if (!app) return
+
+ root.applicationSelected(app, root.targetData)
+
+ if (usageHistoryKey && app.appId) {
+ const usageHistory = SettingsData[usageHistoryKey] || {}
+ const currentCount = usageHistory[app.appId] ? usageHistory[app.appId].count : 0
+ usageHistory[app.appId] = {
+ count: currentCount + 1,
+ lastUsed: Date.now(),
+ name: app.name
+ }
+ SettingsData.set(usageHistoryKey, usageHistory)
+ }
+
+ root.close()
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/BluetoothPairingModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/BluetoothPairingModal.qml
new file mode 100644
index 0000000..cd3c9b7
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/BluetoothPairingModal.qml
@@ -0,0 +1,409 @@
+import QtQuick
+import Quickshell.Hyprland
+import qs.Common
+import qs.Modals.Common
+import qs.Services
+import qs.Widgets
+
+DankModal {
+ id: root
+
+ layerNamespace: "dms:bluetooth-pairing"
+
+ HyprlandFocusGrab {
+ windows: [root.contentWindow]
+ active: root.useHyprlandFocusGrab && root.shouldHaveFocus
+ }
+
+ property string deviceName: ""
+ property string deviceAddress: ""
+ property string requestType: ""
+ property string token: ""
+ property int passkey: 0
+ property string pinInput: ""
+ property string passkeyInput: ""
+
+ function show(pairingData) {
+ console.log("BluetoothPairingModal.show() called:", JSON.stringify(pairingData));
+ token = pairingData.token || "";
+ deviceName = pairingData.deviceName || "";
+ deviceAddress = pairingData.deviceAddr || "";
+ requestType = pairingData.requestType || "";
+ passkey = pairingData.passkey || 0;
+ pinInput = "";
+ passkeyInput = "";
+
+ console.log("BluetoothPairingModal: Calling open()");
+ open();
+ Qt.callLater(() => {
+ if (contentLoader.item) {
+ if (requestType === "pin" && contentLoader.item.pinInputField) {
+ contentLoader.item.pinInputField.forceActiveFocus();
+ } else if (requestType === "passkey" && contentLoader.item.passkeyInputField) {
+ contentLoader.item.passkeyInputField.forceActiveFocus();
+ }
+ }
+ });
+ }
+
+ shouldBeVisible: false
+ allowStacking: true
+ keepPopoutsOpen: true
+ modalWidth: 420
+ modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240
+
+ onShouldBeVisibleChanged: () => {
+ if (!shouldBeVisible) {
+ pinInput = "";
+ passkeyInput = "";
+ }
+ }
+
+ onOpened: {
+ Qt.callLater(() => {
+ if (contentLoader.item) {
+ if (requestType === "pin" && contentLoader.item.pinInputField) {
+ contentLoader.item.pinInputField.forceActiveFocus();
+ } else if (requestType === "passkey" && contentLoader.item.passkeyInputField) {
+ contentLoader.item.passkeyInputField.forceActiveFocus();
+ }
+ }
+ });
+ }
+
+ onBackgroundClicked: () => {
+ if (token) {
+ DMSService.bluetoothCancelPairing(token);
+ }
+ close();
+ token = "";
+ pinInput = "";
+ passkeyInput = "";
+ }
+
+ content: Component {
+ FocusScope {
+ id: pairingContent
+
+ property alias pinInputField: pinInputField
+ property alias passkeyInputField: passkeyInputField
+
+ anchors.fill: parent
+ focus: true
+ implicitHeight: mainColumn.implicitHeight
+
+ Keys.onEscapePressed: event => {
+ if (token) {
+ DMSService.bluetoothCancelPairing(token);
+ }
+ close();
+ token = "";
+ pinInput = "";
+ passkeyInput = "";
+ event.accepted = true;
+ }
+
+ Column {
+ id: mainColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ spacing: requestType === "pin" || requestType === "passkey" ? Theme.spacingM : Theme.spacingS
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Pair Bluetooth Device")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: {
+ switch (requestType) {
+ case "confirm":
+ return I18n.tr("Confirm passkey for ") + deviceName;
+ case "display-passkey":
+ return I18n.tr("Enter this passkey on ") + deviceName;
+ case "authorize":
+ return I18n.tr("Authorize pairing with ") + deviceName;
+ case "pin":
+ return I18n.tr("Enter PIN for ") + deviceName;
+ case "passkey":
+ return I18n.tr("Enter passkey for ") + deviceName;
+ default:
+ if (requestType.startsWith("authorize-service"))
+ return I18n.tr("Authorize service for ") + deviceName;
+ return deviceName;
+ }
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceTextMedium
+ width: parent.width - 40
+ elide: Text.ElideRight
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 50
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: pinInputField.activeFocus ? Theme.primary : Theme.outlineStrong
+ border.width: pinInputField.activeFocus ? 2 : 1
+ visible: requestType === "pin"
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: () => {
+ pinInputField.forceActiveFocus();
+ }
+ }
+
+ DankTextField {
+ id: pinInputField
+
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ text: pinInput
+ placeholderText: I18n.tr("Enter PIN")
+ backgroundColor: "transparent"
+ enabled: root.shouldBeVisible
+ onTextEdited: () => {
+ pinInput = text;
+ }
+ onAccepted: () => {
+ submitPairing();
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 50
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: passkeyInputField.activeFocus ? Theme.primary : Theme.outlineStrong
+ border.width: passkeyInputField.activeFocus ? 2 : 1
+ visible: requestType === "passkey"
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: () => {
+ passkeyInputField.forceActiveFocus();
+ }
+ }
+
+ DankTextField {
+ id: passkeyInputField
+
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ text: passkeyInput
+ placeholderText: I18n.tr("Enter 6-digit passkey")
+ backgroundColor: "transparent"
+ enabled: root.shouldBeVisible
+ onTextEdited: () => {
+ passkeyInput = text;
+ }
+ onAccepted: () => {
+ submitPairing();
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 56
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
+ visible: requestType === "confirm" || requestType === "display-passkey"
+
+ Column {
+ anchors.centerIn: parent
+ spacing: 2
+
+ StyledText {
+ text: I18n.tr("Passkey:")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: String(passkey).padStart(6, "0")
+ font.pixelSize: Theme.fontSizeXLarge
+ color: Theme.surfaceText
+ font.weight: Font.Bold
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: 36
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ Rectangle {
+ width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
+ border.color: Theme.surfaceVariantAlpha
+ border.width: 1
+
+ StyledText {
+ id: cancelText
+
+ anchors.centerIn: parent
+ text: I18n.tr("Cancel")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: cancelArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: () => {
+ if (token) {
+ DMSService.bluetoothCancelPairing(token);
+ }
+ close();
+ token = "";
+ pinInput = "";
+ passkeyInput = "";
+ }
+ }
+ }
+
+ Rectangle {
+ width: Math.max(80, pairText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: pairArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
+ enabled: {
+ if (requestType === "pin")
+ return pinInput.length > 0;
+ if (requestType === "passkey")
+ return passkeyInput.length === 6;
+ return true;
+ }
+ opacity: enabled ? 1 : 0.5
+
+ StyledText {
+ id: pairText
+
+ anchors.centerIn: parent
+ text: {
+ switch (requestType) {
+ case "confirm":
+ case "display-passkey":
+ return I18n.tr("Confirm");
+ case "authorize":
+ return I18n.tr("Authorize");
+ default:
+ if (requestType.startsWith("authorize-service"))
+ return I18n.tr("Authorize");
+ return I18n.tr("Pair");
+ }
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.background
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: pairArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: parent.enabled
+ onClicked: () => {
+ submitPairing();
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+
+ DankActionButton {
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.topMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: () => {
+ if (token) {
+ DMSService.bluetoothCancelPairing(token);
+ }
+ close();
+ token = "";
+ pinInput = "";
+ passkeyInput = "";
+ }
+ }
+ }
+ }
+
+ function submitPairing() {
+ const secrets = {};
+
+ switch (requestType) {
+ case "pin":
+ secrets["pin"] = pinInput;
+ break;
+ case "passkey":
+ secrets["passkey"] = passkeyInput;
+ break;
+ case "confirm":
+ case "display-passkey":
+ case "authorize":
+ secrets["decision"] = "yes";
+ break;
+ default:
+ if (requestType.startsWith("authorize-service")) {
+ secrets["decision"] = "yes";
+ }
+ break;
+ }
+
+ DMSService.bluetoothSubmitPairing(token, secrets, true, response => {
+ if (response.error) {
+ ToastService.showError(I18n.tr("Pairing failed"), response.error);
+ }
+ });
+
+ close();
+ token = "";
+ pinInput = "";
+ passkeyInput = "";
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/BrowserPickerModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/BrowserPickerModal.qml
new file mode 100644
index 0000000..e4afcb8
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/BrowserPickerModal.qml
@@ -0,0 +1,51 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Modals
+
+AppPickerModal {
+ id: root
+
+ property string url: ""
+
+ title: I18n.tr("Open with...")
+ targetData: url
+ targetDataLabel: ""
+ categoryFilter: ["WebBrowser", "X-WebBrowser"]
+ viewMode: SettingsData.browserPickerViewMode || "grid"
+ usageHistoryKey: "browserUsageHistory"
+ showTargetData: true
+
+ function shellEscape(str) {
+ return "'" + str.replace(/'/g, "'\\''") + "'"
+ }
+
+ onApplicationSelected: (app, url) => {
+ if (!app) return
+
+ let cmd = app.exec || ""
+ const escapedUrl = shellEscape(url)
+
+ let hasField = false
+ if (cmd.includes("%u")) { cmd = cmd.replace("%u", escapedUrl); hasField = true }
+ else if (cmd.includes("%U")) { cmd = cmd.replace("%U", escapedUrl); hasField = true }
+ else if (cmd.includes("%f")) { cmd = cmd.replace("%f", escapedUrl); hasField = true }
+ else if (cmd.includes("%F")) { cmd = cmd.replace("%F", escapedUrl); hasField = true }
+
+ cmd = cmd.replace(/%[ikc]/g, "")
+
+ if (!hasField) {
+ cmd += " " + escapedUrl
+ }
+
+ console.log("BrowserPicker: Launching", cmd)
+
+ Quickshell.execDetached({
+ command: ["sh", "-c", cmd]
+ })
+ }
+
+ onViewModeChanged: {
+ SettingsData.set("browserPickerViewMode", viewMode)
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogContent.qml
new file mode 100644
index 0000000..8fd3129
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogContent.qml
@@ -0,0 +1,250 @@
+import QtQuick
+import QtQuick.Effects
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Column {
+ id: root
+
+ readonly property real logoSize: Math.round(Theme.iconSize * 2.8)
+ readonly property real badgeHeight: Math.round(Theme.fontSizeSmall * 1.7)
+
+ topPadding: Theme.spacingL
+ spacing: Theme.spacingL
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Row {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingM
+
+ Image {
+ width: root.logoSize
+ height: width * (569.94629 / 506.50931)
+ anchors.verticalCenter: parent.verticalCenter
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ mipmap: true
+ asynchronous: true
+ source: "file://" + Theme.shellDir + "/assets/danklogonormal.svg"
+ layer.enabled: true
+ layer.smooth: true
+ layer.mipmap: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.primary
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ Row {
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: "DMS " + ChangelogService.currentVersion
+ font.pixelSize: Theme.fontSizeXLarge + 2
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Rectangle {
+ width: codenameText.implicitWidth + Theme.spacingM * 2
+ height: root.badgeHeight
+ radius: root.badgeHeight / 2
+ color: Theme.primaryContainer
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ id: codenameText
+ anchors.centerIn: parent
+ text: "Saffron Bloom"
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.primary
+ }
+ }
+ }
+
+ StyledText {
+ text: "New launcher, enhanced plugin system, KDE Connect, & more"
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ opacity: 0.3
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: "What's New"
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ Grid {
+ width: parent.width
+ columns: 2
+ rowSpacing: Theme.spacingS
+ columnSpacing: Theme.spacingS
+
+ ChangelogFeatureCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "space_dashboard"
+ title: "Dank Launcher V2"
+ description: "New capabilities & plugins"
+ onClicked: PopoutService.openDankLauncherV2()
+ }
+
+ ChangelogFeatureCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "smartphone"
+ title: "Phone Connect"
+ description: "KDE Connect & Valent"
+ onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dms-plugins/tree/master/DankKDEConnect")
+ }
+
+ ChangelogFeatureCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "monitor_heart"
+ title: "System Monitor"
+ description: "Redesigned process list"
+ onClicked: PopoutService.showProcessListModal()
+ }
+
+ ChangelogFeatureCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "window"
+ title: "Window Rules"
+ description: "niri window rule manager"
+ visible: CompositorService.isNiri
+ onClicked: PopoutService.openSettingsWithTab("window_rules")
+ }
+
+ ChangelogFeatureCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "notifications_active"
+ title: "Enhanced Notifications"
+ description: "Configurable rules & styling"
+ visible: !CompositorService.isNiri
+ onClicked: PopoutService.openSettingsWithTab("notifications")
+ }
+
+ ChangelogFeatureCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "dock_to_bottom"
+ title: "Dock Enhancements"
+ description: "Bar dock widget & more"
+ onClicked: PopoutService.openSettingsWithTab("dock")
+ }
+
+ ChangelogFeatureCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "volume_up"
+ title: "Audio Aliases"
+ description: "Custom device names"
+ onClicked: PopoutService.openSettingsWithTab("audio")
+ }
+
+ ChangelogFeatureCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "extension"
+ title: "Enhanced Plugin System"
+ description: "Enables new types of plugins"
+ onClicked: PopoutService.openSettingsWithTab("plugins")
+ }
+
+ ChangelogFeatureCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "light_mode"
+ title: "Auto Light/Dark"
+ description: "Automatic mode switching"
+ onClicked: PopoutService.openSettingsWithTab("theme")
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ opacity: 0.3
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Row {
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "warning"
+ size: Theme.iconSizeSmall
+ color: Theme.warning
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: "Upgrade Notes"
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: upgradeNotesColumn.height + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.warning, 0.08)
+ border.width: 1
+ border.color: Theme.withAlpha(Theme.warning, 0.2)
+
+ Column {
+ id: upgradeNotesColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ ChangelogUpgradeNote {
+ width: parent.width
+ text: "Spotlight replaced by Dank Launcher V2 — check settings for new options"
+ }
+
+ ChangelogUpgradeNote {
+ width: parent.width
+ text: "Plugin API updated — third-party plugins may need updates"
+ }
+ }
+ }
+
+ // StyledText {
+ // text: "See full release notes for migration steps"
+ // font.pixelSize: Theme.fontSizeSmall
+ // color: Theme.surfaceVariantText
+ // width: parent.width
+ // }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogFeatureCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogFeatureCard.qml
new file mode 100644
index 0000000..eab4f70
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogFeatureCard.qml
@@ -0,0 +1,78 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property string iconName: ""
+ property string title: ""
+ property string description: ""
+
+ signal clicked
+
+ readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.3)
+
+ height: Math.round(Theme.fontSizeMedium * 4.2)
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainerHigh
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: Theme.primary
+ opacity: mouseArea.containsMouse ? 0.12 : 0
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ Rectangle {
+ width: root.iconContainerSize
+ height: root.iconContainerSize
+ radius: Math.round(root.iconContainerSize * 0.28)
+ color: Theme.primaryContainer
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.iconName
+ size: Theme.iconSize - 6
+ color: Theme.primary
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+ width: parent.width - root.iconContainerSize - Theme.spacingS
+
+ StyledText {
+ text: root.title
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ width: parent.width
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clicked()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogModal.qml
new file mode 100644
index 0000000..524298b
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogModal.qml
@@ -0,0 +1,156 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+FloatingWindow {
+ id: root
+
+ property bool disablePopupTransparency: true
+ readonly property int modalWidth: 680
+ readonly property int modalHeight: screen ? Math.min(720, screen.height - 80) : 720
+
+ signal changelogDismissed
+
+ function show() {
+ visible = true;
+ }
+
+ objectName: "changelogModal"
+ title: i18n("What's New")
+ minimumSize: Qt.size(modalWidth, modalHeight)
+ maximumSize: Qt.size(modalWidth, modalHeight)
+ color: Theme.surfaceContainer
+ visible: false
+
+ FocusScope {
+ id: contentFocusScope
+ anchors.fill: parent
+ focus: true
+
+ Keys.onEscapePressed: event => {
+ root.dismiss();
+ event.accepted = true;
+ }
+
+ Keys.onPressed: event => {
+ switch (event.key) {
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ root.dismiss();
+ event.accepted = true;
+ break;
+ }
+ }
+
+ MouseArea {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ height: headerRow.height + Theme.spacingM
+ onPressed: windowControls.tryStartMove()
+ onDoubleClicked: windowControls.tryToggleMaximize()
+ }
+
+ Item {
+ id: headerRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingM
+ height: Math.round(Theme.fontSizeMedium * 2.85)
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ visible: windowControls.supported && windowControls.canMaximize
+ iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: windowControls.tryToggleMaximize()
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: root.dismiss()
+
+ DankTooltip {
+ text: i18n("Close")
+ }
+ }
+ }
+ }
+
+ DankFlickable {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: headerRow.bottom
+ anchors.bottom: footerRow.top
+ anchors.topMargin: Theme.spacingS
+ clip: true
+ contentHeight: mainColumn.height + Theme.spacingL * 2
+ contentWidth: width
+
+ ChangelogContent {
+ id: mainColumn
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: Math.min(600, parent.width - Theme.spacingXL * 2)
+ }
+ }
+
+ Rectangle {
+ id: footerRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: Math.round(Theme.fontSizeMedium * 4.5)
+ color: Theme.surfaceContainerHigh
+
+ Rectangle {
+ anchors.top: parent.top
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ opacity: 0.5
+ }
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+
+ DankButton {
+ text: i18n("Read Full Release Notes")
+ iconName: "open_in_new"
+ backgroundColor: Theme.surfaceContainerHighest
+ textColor: Theme.surfaceText
+ onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-4-release")
+ }
+
+ DankButton {
+ text: i18n("Got It")
+ iconName: "check"
+ backgroundColor: Theme.primary
+ textColor: Theme.primaryText
+ onClicked: root.dismiss()
+ }
+ }
+ }
+ }
+
+ FloatingWindowControls {
+ id: windowControls
+ targetWindow: root
+ }
+
+ function dismiss() {
+ ChangelogService.dismissChangelog();
+ changelogDismissed();
+ visible = false;
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogUpgradeNote.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogUpgradeNote.qml
new file mode 100644
index 0000000..545eb9a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Changelog/ChangelogUpgradeNote.qml
@@ -0,0 +1,27 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Row {
+ id: root
+
+ property alias text: noteText.text
+
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "arrow_right"
+ size: Theme.iconSizeSmall - 2
+ color: Theme.surfaceVariantText
+ anchors.top: parent.top
+ anchors.topMargin: 2
+ }
+
+ StyledText {
+ id: noteText
+ width: root.width - Theme.iconSizeSmall - Theme.spacingS
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ wrapMode: Text.WordWrap
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml
new file mode 100644
index 0000000..f796ffe
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml
@@ -0,0 +1,21 @@
+pragma Singleton
+
+import QtQuick
+import Quickshell
+
+Singleton {
+ id: root
+ readonly property int previewLength: 100
+ readonly property int longTextThreshold: 200
+ readonly property int modalWidth: 650
+ readonly property int modalHeight: 550
+ readonly property int popoutWidth: 550
+ readonly property int popoutHeight: 500
+ readonly property int itemHeight: 72
+ readonly property int thumbnailSize: 100
+ readonly property int retryInterval: 50
+ readonly property int viewportBuffer: 100
+ readonly property int extendedBuffer: 200
+ readonly property int keyboardHintsHeight: 80
+ readonly property int headerHeight: 32
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml
new file mode 100644
index 0000000..e00bc22
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml
@@ -0,0 +1,260 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: clipboardContent
+
+ required property var modal
+ required property var clearConfirmDialog
+
+ property alias searchField: searchField
+ property alias clipboardListView: clipboardListView
+
+ anchors.fill: parent
+
+ Column {
+ id: headerColumn
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingM
+ focus: false
+
+ ClipboardHeader {
+ id: header
+ width: parent.width
+ totalCount: modal.totalCount
+ showKeyboardHints: modal.showKeyboardHints
+ activeTab: modal.activeTab
+ pinnedCount: modal.pinnedCount
+ onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
+ onTabChanged: tabName => modal.activeTab = tabName
+ onClearAllClicked: {
+ const hasPinned = modal.pinnedCount > 0;
+ const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(modal.pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
+ clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
+ modal.clearAll();
+ modal.hide();
+ }, function () {});
+ }
+ onCloseClicked: modal.hide()
+ }
+
+ DankTextField {
+ id: searchField
+ width: parent.width
+ placeholderText: ""
+ leftIconName: "search"
+ showClearButton: true
+ focus: true
+ ignoreTabKeys: true
+ keyForwardTargets: [modal.modalFocusScope]
+ onTextChanged: {
+ modal.searchText = text;
+ modal.updateFilteredModel();
+ }
+ Keys.onEscapePressed: function (event) {
+ modal.hide();
+ event.accepted = true;
+ }
+ Component.onCompleted: {
+ Qt.callLater(function () {
+ forceActiveFocus();
+ });
+ }
+
+ Connections {
+ target: modal
+ function onOpened() {
+ Qt.callLater(function () {
+ searchField.forceActiveFocus();
+ });
+ }
+ }
+ }
+ }
+
+ Item {
+ id: listContainer
+ anchors.top: headerColumn.bottom
+ anchors.topMargin: Theme.spacingM
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.bottomMargin: (modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0) + Theme.spacingXS
+ clip: true
+
+ DankListView {
+ id: clipboardListView
+ anchors.fill: parent
+ model: ScriptModel {
+ values: clipboardContent.modal.unpinnedEntries
+ objectProp: "id"
+ }
+ visible: modal.activeTab === "recents"
+
+ currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
+ spacing: Theme.spacingXS
+ interactive: true
+ flickDeceleration: 1500
+ maximumFlickVelocity: 2000
+ boundsBehavior: Flickable.DragAndOvershootBounds
+ boundsMovement: Flickable.FollowBoundsBehavior
+ pressDelay: 0
+ flickableDirection: Flickable.VerticalFlick
+
+ function ensureVisible(index) {
+ if (index < 0 || index >= count) {
+ return;
+ }
+ positionViewAtIndex(index, ListView.Contain);
+ }
+
+ onCurrentIndexChanged: {
+ if (clipboardContent.modal?.keyboardNavigationActive && currentIndex >= 0) {
+ ensureVisible(currentIndex);
+ }
+ }
+
+ StyledText {
+ text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…")
+ anchors.centerIn: parent
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ visible: clipboardContent.modal.unpinnedEntries.length === 0
+ }
+
+ delegate: ClipboardEntry {
+ required property int index
+ required property var modelData
+
+ width: clipboardListView.width
+ height: ClipboardConstants.itemHeight
+ entry: modelData
+ entryIndex: index + 1
+ itemIndex: index
+ isSelected: clipboardContent.modal?.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
+ modal: clipboardContent.modal
+ listView: clipboardListView
+ onCopyRequested: clipboardContent.modal.copyEntry(modelData)
+ onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
+ onPinRequested: clipboardContent.modal.pinEntry(modelData)
+ onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
+ }
+ }
+
+ DankListView {
+ id: savedListView
+ anchors.fill: parent
+ model: ScriptModel {
+ values: clipboardContent.modal.pinnedEntries
+ objectProp: "id"
+ }
+ visible: modal.activeTab === "saved"
+
+ currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
+ spacing: Theme.spacingXS
+ interactive: true
+ flickDeceleration: 1500
+ maximumFlickVelocity: 2000
+ boundsBehavior: Flickable.DragAndOvershootBounds
+ boundsMovement: Flickable.FollowBoundsBehavior
+ pressDelay: 0
+ flickableDirection: Flickable.VerticalFlick
+
+ function ensureVisible(index) {
+ if (index < 0 || index >= count) {
+ return;
+ }
+ positionViewAtIndex(index, ListView.Contain);
+ }
+
+ onCurrentIndexChanged: {
+ if (clipboardContent.modal?.keyboardNavigationActive && currentIndex >= 0) {
+ ensureVisible(currentIndex);
+ }
+ }
+
+ StyledText {
+ text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…")
+ anchors.centerIn: parent
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ visible: clipboardContent.modal.pinnedEntries.length === 0
+ }
+
+ delegate: ClipboardEntry {
+ required property int index
+ required property var modelData
+
+ width: savedListView.width
+ height: ClipboardConstants.itemHeight
+ entry: modelData
+ entryIndex: index + 1
+ itemIndex: index
+ isSelected: clipboardContent.modal?.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
+ modal: clipboardContent.modal
+ listView: savedListView
+ onCopyRequested: clipboardContent.modal.copyEntry(modelData)
+ onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
+ onPinRequested: clipboardContent.modal.pinEntry(modelData)
+ onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
+ }
+ }
+
+ Rectangle {
+ id: bottomFade
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: 24
+ z: 100
+ visible: {
+ const listView = modal.activeTab === "recents" ? clipboardListView : savedListView;
+ if (listView.contentHeight <= listView.height)
+ return false;
+ const atBottom = listView.contentY >= listView.contentHeight - listView.height - 5;
+ return !atBottom;
+ }
+ gradient: Gradient {
+ GradientStop {
+ position: 0.0
+ color: "transparent"
+ }
+ GradientStop {
+ position: 1.0
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: keyboardHintsLoader
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.bottomMargin: active ? Theme.spacingM : 0
+ active: modal.showKeyboardHints
+ height: active ? ClipboardConstants.keyboardHintsHeight : 0
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ sourceComponent: ClipboardKeyboardHints {
+ wtypeAvailable: modal.wtypeAvailable
+ enterToPaste: SettingsData.clipboardEnterToPaste
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml
new file mode 100644
index 0000000..53a2ece
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml
@@ -0,0 +1,155 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ required property var entry
+ required property int entryIndex
+ required property int itemIndex
+ required property bool isSelected
+ required property var modal
+ required property var listView
+
+ signal copyRequested
+ signal deleteRequested
+ signal pinRequested
+ signal unpinRequested
+
+ readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
+ readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
+ readonly property bool hasPinnedDuplicate: !entry.pinned && ClipboardService.hashedPinnedEntry(entry.hash)
+
+ radius: Theme.cornerRadius
+ color: {
+ if (isSelected) {
+ return Theme.primaryPressed;
+ }
+ return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
+ }
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: Theme.surfaceText
+ cornerRadius: root.radius
+ }
+
+ Rectangle {
+ id: indexBadge
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ width: 24
+ height: 24
+ radius: 12
+ color: Theme.primarySelected
+
+ StyledText {
+ anchors.centerIn: parent
+ text: entryIndex.toString()
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Bold
+ color: Theme.primary
+ }
+ }
+
+ Row {
+ id: actionButtons
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ iconName: "push_pin"
+ iconSize: Theme.iconSize - 6
+ iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText
+ backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent"
+ onClicked: entry.pinned ? unpinRequested() : pinRequested()
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 6
+ iconColor: Theme.surfaceText
+ onClicked: deleteRequested()
+ }
+ }
+
+ Item {
+ anchors.left: indexBadge.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: actionButtons.left
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ // height: contentColumn.implicitHeight
+ height: ClipboardConstants.itemHeight
+ clip: true
+
+ ClipboardThumbnail {
+ id: thumbnail
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
+ height: entryType === "image" ? ClipboardConstants.itemHeight - 4 : Theme.iconSize // 100 - 4 = 96, 96:72 = 4:3
+ entry: root.entry
+ entryType: root.entryType
+ modal: root.modal
+ listView: root.listView
+ itemIndex: root.itemIndex
+ }
+
+ Column {
+ id: contentColumn
+ anchors.left: thumbnail.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: {
+ switch (entryType) {
+ case "image":
+ return I18n.tr("Image") + " • " + entryPreview;
+ case "long_text":
+ return I18n.tr("Long Text");
+ default:
+ return I18n.tr("Text");
+ }
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ font.weight: Font.Medium
+ width: parent.width
+ elide: Text.ElideRight
+ }
+
+ StyledText {
+ text: entryPreview
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ maximumLineCount: entryType === "long_text" ? 3 : 1
+ elide: Text.ElideRight
+ textFormat: Text.PlainText
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ anchors.rightMargin: 80
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ const pos = mouseArea.mapToItem(root, mouse.x, mouse.y);
+ rippleLayer.trigger(pos.x, pos.y);
+ }
+ onClicked: copyRequested()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml
new file mode 100644
index 0000000..fedba15
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml
@@ -0,0 +1,87 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+import qs.Modals.Clipboard
+
+Item {
+ id: header
+
+ property int totalCount: 0
+ property bool showKeyboardHints: false
+ property string activeTab: "recents"
+ property int pinnedCount: 0
+
+ signal keyboardHintsToggled
+ signal clearAllClicked
+ signal closeClicked
+ signal tabChanged(string tabName)
+
+ height: ClipboardConstants.headerHeight
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "content_paste"
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Clipboard History") + ` (${totalCount})`
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ DankActionButton {
+ iconName: "push_pin"
+ iconSize: Theme.iconSize - 4
+ iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
+ visible: header.pinnedCount > 0
+ tooltipText: I18n.tr("Saved")
+ onClicked: tabChanged("saved")
+ }
+
+ DankActionButton {
+ iconName: "history"
+ iconSize: Theme.iconSize - 4
+ iconColor: header.activeTab === "recents" ? Theme.primary : Theme.surfaceText
+ tooltipText: I18n.tr("History")
+ onClicked: tabChanged("recents")
+ }
+
+ DankActionButton {
+ iconName: "info"
+ iconSize: Theme.iconSize - 4
+ iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
+ tooltipText: I18n.tr("Keyboard Shortcuts")
+ onClicked: keyboardHintsToggled()
+ }
+
+ DankActionButton {
+ iconName: "delete_sweep"
+ iconSize: Theme.iconSize
+ iconColor: Theme.surfaceText
+ tooltipText: I18n.tr("Clear All")
+ onClicked: clearAllClicked()
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: closeClicked()
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml
new file mode 100644
index 0000000..2f76cc7
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml
@@ -0,0 +1,174 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell.Hyprland
+import qs.Common
+import qs.Modals.Clipboard
+import qs.Modals.Common
+import qs.Services
+
+DankModal {
+ id: clipboardHistoryModal
+
+ layerNamespace: "dms:clipboard"
+
+ HyprlandFocusGrab {
+ windows: [clipboardHistoryModal.contentWindow]
+ active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
+ }
+
+ property string activeTab: "recents"
+ onActiveTabChanged: {
+ ClipboardService.selectedIndex = 0;
+ ClipboardService.keyboardNavigationActive = false;
+ }
+ property bool showKeyboardHints: false
+ property Component clipboardContent
+ property int activeImageLoads: 0
+ readonly property int maxConcurrentLoads: 3
+
+ readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
+ readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
+ readonly property int totalCount: ClipboardService.totalCount
+ readonly property var clipboardEntries: ClipboardService.clipboardEntries
+ readonly property var pinnedEntries: ClipboardService.pinnedEntries
+ readonly property int pinnedCount: ClipboardService.pinnedCount
+ readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
+ readonly property int selectedIndex: ClipboardService.selectedIndex
+ readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
+ property string searchText: ClipboardService.searchText
+ onSearchTextChanged: ClipboardService.searchText = searchText
+
+ Ref {
+ service: ClipboardService
+ }
+
+ function updateFilteredModel() {
+ ClipboardService.updateFilteredModel();
+ }
+
+ function pasteSelected() {
+ ClipboardService.pasteSelected(instantClose);
+ }
+
+ function toggle() {
+ if (shouldBeVisible) {
+ hide();
+ } else {
+ show();
+ }
+ }
+
+ function show() {
+ open();
+ activeImageLoads = 0;
+ shouldHaveFocus = true;
+ ClipboardService.reset();
+ if (clipboardAvailable)
+ ClipboardService.refresh();
+ keyboardController.reset();
+
+ Qt.callLater(function () {
+ if (contentLoader.item?.searchField) {
+ contentLoader.item.searchField.text = "";
+ contentLoader.item.searchField.forceActiveFocus();
+ }
+ });
+ }
+
+ function hide() {
+ close();
+ }
+
+ onDialogClosed: {
+ activeImageLoads = 0;
+ ClipboardService.reset();
+ keyboardController.reset();
+ }
+
+ function refreshClipboard() {
+ ClipboardService.refresh();
+ }
+
+ function copyEntry(entry) {
+ ClipboardService.copyEntry(entry, hide);
+ }
+
+ function deleteEntry(entry) {
+ ClipboardService.deleteEntry(entry);
+ }
+
+ function deletePinnedEntry(entry) {
+ ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
+ }
+
+ function pinEntry(entry) {
+ ClipboardService.pinEntry(entry);
+ }
+
+ function unpinEntry(entry) {
+ ClipboardService.unpinEntry(entry);
+ }
+
+ function clearAll() {
+ ClipboardService.clearAll();
+ }
+
+ function getEntryPreview(entry) {
+ return ClipboardService.getEntryPreview(entry);
+ }
+
+ function getEntryType(entry) {
+ return ClipboardService.getEntryType(entry);
+ }
+
+ visible: false
+ modalWidth: ClipboardConstants.modalWidth
+ modalHeight: ClipboardConstants.modalHeight
+ backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ cornerRadius: Theme.cornerRadius
+ borderColor: Theme.outlineMedium
+ borderWidth: 1
+ enableShadow: true
+ onBackgroundClicked: hide()
+ modalFocusScope.Keys.onPressed: function (event) {
+ keyboardController.handleKey(event);
+ }
+ content: clipboardContent
+
+ ClipboardKeyboardController {
+ id: keyboardController
+ modal: clipboardHistoryModal
+ }
+
+ ConfirmModal {
+ id: clearConfirmDialog
+ confirmButtonText: I18n.tr("Clear All")
+ confirmButtonColor: Theme.primary
+ onVisibleChanged: {
+ if (visible) {
+ clipboardHistoryModal.shouldHaveFocus = false;
+ return;
+ }
+ Qt.callLater(function () {
+ if (!clipboardHistoryModal.shouldBeVisible) {
+ return;
+ }
+ clipboardHistoryModal.shouldHaveFocus = true;
+ clipboardHistoryModal.modalFocusScope.forceActiveFocus();
+ if (clipboardHistoryModal.contentLoader.item?.searchField) {
+ clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
+ }
+ });
+ }
+ }
+
+ property var confirmDialog: clearConfirmDialog
+
+ clipboardContent: Component {
+ ClipboardContent {
+ modal: clipboardHistoryModal
+ clearConfirmDialog: clipboardHistoryModal.confirmDialog
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml
new file mode 100644
index 0000000..c4f0296
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml
@@ -0,0 +1,197 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Modals.Clipboard
+import qs.Modals.Common
+import qs.Services
+import qs.Widgets
+
+DankPopout {
+ id: root
+
+ layerNamespace: "dms:clipboard-popout"
+
+ property var parentWidget: null
+ property var triggerScreen: null
+ property string activeTab: "recents"
+ property bool showKeyboardHints: false
+ property int activeImageLoads: 0
+ readonly property int maxConcurrentLoads: 3
+
+ readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
+ readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
+ readonly property int totalCount: ClipboardService.totalCount
+ readonly property var clipboardEntries: ClipboardService.clipboardEntries
+ readonly property var pinnedEntries: ClipboardService.pinnedEntries
+ readonly property int pinnedCount: ClipboardService.pinnedCount
+ readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
+ readonly property int selectedIndex: ClipboardService.selectedIndex
+ readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
+ property string searchText: ClipboardService.searchText
+ onSearchTextChanged: ClipboardService.searchText = searchText
+
+ readonly property var modalFocusScope: contentLoader.item ?? null
+
+ Ref {
+ service: ClipboardService
+ }
+
+ function updateFilteredModel() {
+ ClipboardService.updateFilteredModel();
+ }
+
+ function pasteSelected() {
+ ClipboardService.pasteSelected(instantClose);
+ }
+
+ function instantClose() {
+ close();
+ }
+
+ function show() {
+ open();
+ activeImageLoads = 0;
+ ClipboardService.reset();
+ if (clipboardAvailable)
+ ClipboardService.refresh();
+ keyboardController.reset();
+
+ Qt.callLater(function () {
+ if (contentLoader.item?.searchField) {
+ contentLoader.item.searchField.text = "";
+ contentLoader.item.searchField.forceActiveFocus();
+ }
+ });
+ }
+
+ function hide() {
+ close();
+ activeImageLoads = 0;
+ ClipboardService.reset();
+ keyboardController.reset();
+ }
+
+ function refreshClipboard() {
+ ClipboardService.refresh();
+ }
+
+ function copyEntry(entry) {
+ ClipboardService.copyEntry(entry, hide);
+ }
+
+ function deleteEntry(entry) {
+ ClipboardService.deleteEntry(entry);
+ }
+
+ function deletePinnedEntry(entry) {
+ ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
+ }
+
+ function pinEntry(entry) {
+ ClipboardService.pinEntry(entry);
+ }
+
+ function unpinEntry(entry) {
+ ClipboardService.unpinEntry(entry);
+ }
+
+ function clearAll() {
+ ClipboardService.clearAll();
+ }
+
+ function getEntryPreview(entry) {
+ return ClipboardService.getEntryPreview(entry);
+ }
+
+ function getEntryType(entry) {
+ return ClipboardService.getEntryType(entry);
+ }
+
+ popupWidth: ClipboardConstants.popoutWidth
+ popupHeight: ClipboardConstants.popoutHeight
+ triggerWidth: 55
+ positioning: ""
+ screen: triggerScreen
+ shouldBeVisible: false
+ contentHandlesKeys: true
+
+ onBackgroundClicked: hide()
+
+ onShouldBeVisibleChanged: {
+ if (!shouldBeVisible)
+ return;
+ if (clipboardAvailable)
+ ClipboardService.refresh();
+ keyboardController.reset();
+ Qt.callLater(function () {
+ if (contentLoader.item?.searchField) {
+ contentLoader.item.searchField.text = "";
+ contentLoader.item.searchField.forceActiveFocus();
+ }
+ });
+ }
+
+ onPopoutClosed: {
+ activeImageLoads = 0;
+ ClipboardService.reset();
+ keyboardController.reset();
+ }
+
+ ClipboardKeyboardController {
+ id: keyboardController
+ modal: root
+ }
+
+ ConfirmModal {
+ id: clearConfirmDialog
+ confirmButtonText: I18n.tr("Clear All")
+ confirmButtonColor: Theme.primary
+ }
+
+ property var confirmDialog: clearConfirmDialog
+
+ content: Component {
+ FocusScope {
+ id: contentFocusScope
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ focus: true
+
+ property alias searchField: clipboardContentItem.searchField
+
+ Keys.onPressed: function (event) {
+ keyboardController.handleKey(event);
+ }
+
+ Component.onCompleted: {
+ if (root.shouldBeVisible)
+ forceActiveFocus();
+ }
+
+ Connections {
+ target: root
+ function onShouldBeVisibleChanged() {
+ if (root.shouldBeVisible) {
+ Qt.callLater(() => contentFocusScope.forceActiveFocus());
+ }
+ }
+ function onOpened() {
+ Qt.callLater(() => {
+ if (clipboardContentItem.searchField) {
+ clipboardContentItem.searchField.forceActiveFocus();
+ }
+ });
+ }
+ }
+
+ ClipboardContent {
+ id: clipboardContentItem
+ modal: root
+ clearConfirmDialog: root.confirmDialog
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml
new file mode 100644
index 0000000..f2a457e
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml
@@ -0,0 +1,166 @@
+import QtQuick
+import qs.Common
+import qs.Services
+
+QtObject {
+ id: keyboardController
+
+ required property var modal
+
+ function reset() {
+ ClipboardService.selectedIndex = 0;
+ ClipboardService.keyboardNavigationActive = false;
+ modal.showKeyboardHints = false;
+ }
+
+ function selectNext() {
+ const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
+ if (!entries || entries.length === 0) {
+ return;
+ }
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = Math.min(ClipboardService.selectedIndex + 1, entries.length - 1);
+ }
+
+ function selectPrevious() {
+ const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
+ if (!entries || entries.length === 0) {
+ return;
+ }
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = Math.max(ClipboardService.selectedIndex - 1, 0);
+ }
+
+ function copySelected() {
+ const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
+ if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) {
+ return;
+ }
+ const selectedEntry = entries[ClipboardService.selectedIndex];
+ modal.copyEntry(selectedEntry);
+ }
+
+ function deleteSelected() {
+ const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
+ if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) {
+ return;
+ }
+ const selectedEntry = entries[ClipboardService.selectedIndex];
+ if (modal.activeTab === "saved") {
+ modal.deletePinnedEntry(selectedEntry);
+ } else {
+ modal.deleteEntry(selectedEntry);
+ }
+ }
+
+ function handleKey(event) {
+ switch (event.key) {
+ case Qt.Key_Escape:
+ if (ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = false;
+ } else {
+ modal.hide();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_Down:
+ case Qt.Key_Tab:
+ if (!ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = 0;
+ } else {
+ selectNext();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_Up:
+ case Qt.Key_Backtab:
+ if (!ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = 0;
+ } else if (ClipboardService.selectedIndex === 0) {
+ ClipboardService.keyboardNavigationActive = false;
+ } else {
+ selectPrevious();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_F10:
+ modal.showKeyboardHints = !modal.showKeyboardHints;
+ event.accepted = true;
+ return;
+ }
+
+ if (event.modifiers & Qt.ControlModifier) {
+ switch (event.key) {
+ case Qt.Key_N:
+ case Qt.Key_J:
+ if (!ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = 0;
+ } else {
+ selectNext();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_P:
+ case Qt.Key_K:
+ if (!ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = 0;
+ } else if (ClipboardService.selectedIndex === 0) {
+ ClipboardService.keyboardNavigationActive = false;
+ } else {
+ selectPrevious();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_C:
+ if (ClipboardService.keyboardNavigationActive) {
+ copySelected();
+ event.accepted = true;
+ }
+ return;
+ }
+ }
+
+ if (event.modifiers & Qt.ShiftModifier) {
+ switch (event.key) {
+ case Qt.Key_Delete:
+ modal.clearAll();
+ modal.hide();
+ event.accepted = true;
+ return;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (ClipboardService.keyboardNavigationActive) {
+ if (SettingsData.clipboardEnterToPaste) {
+ copySelected();
+ } else {
+ modal.pasteSelected();
+ }
+ event.accepted = true;
+ }
+ return;
+ }
+ }
+
+ if (ClipboardService.keyboardNavigationActive) {
+ switch (event.key) {
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (SettingsData.clipboardEnterToPaste) {
+ modal.pasteSelected();
+ } else {
+ copySelected();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_Delete:
+ deleteSelected();
+ event.accepted = true;
+ return;
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml
new file mode 100644
index 0000000..0285952
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml
@@ -0,0 +1,51 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: keyboardHints
+
+ property bool wtypeAvailable: false
+ property bool enterToPaste: false
+ readonly property string hintsText: {
+ if (!wtypeAvailable)
+ return I18n.tr("Shift+Del: Clear All • Esc: Close");
+ return enterToPaste ? I18n.tr("Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close");
+ }
+
+ height: ClipboardConstants.keyboardHintsHeight
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
+ border.color: Theme.primary
+ border.width: 2
+ opacity: visible ? 1 : 0
+ z: 100
+
+ Column {
+ anchors.centerIn: parent
+ spacing: 2
+
+ StyledText {
+ text: keyboardHints.enterToPaste
+ ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled")
+ : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: keyboardHints.hintsText
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml
new file mode 100644
index 0000000..26a9210
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml
@@ -0,0 +1,178 @@
+import QtQuick
+import QtQuick.Effects
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: thumbnail
+
+ required property var entry
+ required property string entryType
+ required property var modal
+ required property var listView
+ required property int itemIndex
+
+ Image {
+ id: thumbnailImage
+
+ property bool isVisible: false
+ property string cachedImageData: ""
+ property bool loadQueued: false
+
+ anchors.fill: parent
+ source: cachedImageData ? `data:image/png;base64,${cachedImageData}` : ""
+ fillMode: Image.PreserveAspectCrop
+ smooth: true
+ cache: false
+ visible: false
+ asynchronous: true
+ sourceSize.width: 128
+ sourceSize.height: 128
+
+ function tryLoadImage() {
+ if (thumbnailImage.loadQueued || entryType !== "image" || thumbnailImage.cachedImageData) {
+ return;
+ }
+ thumbnailImage.loadQueued = true;
+ if (modal.activeImageLoads < modal.maxConcurrentLoads) {
+ modal.activeImageLoads++;
+ thumbnailImage.loadImage();
+ } else {
+ retryTimer.restart();
+ }
+ }
+
+ function loadImage() {
+ DMSService.sendRequest("clipboard.getEntry", {
+ "id": entry.id
+ }, function (response) {
+ thumbnailImage.loadQueued = false;
+ if (modal.activeImageLoads > 0) {
+ modal.activeImageLoads--;
+ }
+ if (response.error) {
+ console.warn("ClipboardThumbnail: Failed to load image:", entry.id);
+ return;
+ }
+ const data = response.result?.data;
+ if (data) {
+ thumbnailImage.cachedImageData = data;
+ }
+ });
+ }
+
+ Timer {
+ id: retryTimer
+ interval: ClipboardConstants.retryInterval
+ onTriggered: {
+ if (!thumbnailImage.loadQueued) {
+ return;
+ }
+ if (modal.activeImageLoads < modal.maxConcurrentLoads) {
+ modal.activeImageLoads++;
+ thumbnailImage.loadImage();
+ } else {
+ retryTimer.restart();
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ if (entryType !== "image" || listView.height <= 0) {
+ return;
+ }
+
+ const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing);
+ const viewTop = listView.contentY;
+ const viewBottom = viewTop + listView.height;
+ isVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom);
+
+ if (isVisible) {
+ tryLoadImage();
+ }
+ }
+
+ Timer {
+ id: visibilityTimer
+ interval: 100
+ onTriggered: thumbnailImage.checkVisibility()
+ }
+
+ function checkVisibility() {
+ if (entryType !== "image" || listView.height <= 0 || isVisible) {
+ return;
+ }
+ const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing);
+ const viewTop = listView.contentY - ClipboardConstants.viewportBuffer;
+ const viewBottom = viewTop + listView.height + ClipboardConstants.extendedBuffer;
+ const nowVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom);
+ if (nowVisible) {
+ isVisible = true;
+ tryLoadImage();
+ }
+ }
+
+ Connections {
+ target: listView
+
+ function onContentYChanged() {
+ if (thumbnailImage.isVisible || entryType !== "image") {
+ return;
+ }
+ visibilityTimer.restart();
+ }
+
+ function onHeightChanged() {
+ if (thumbnailImage.isVisible || entryType !== "image") {
+ return;
+ }
+ visibilityTimer.restart();
+ }
+ }
+ }
+
+ MultiEffect {
+ anchors.fill: parent
+ anchors.margins: 2
+ source: thumbnailImage
+ maskEnabled: true
+ maskSource: clipboardRoundedRectangularMask
+ visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 1
+ }
+
+ Item {
+ id: clipboardRoundedRectangularMask
+ width: ClipboardConstants.thumbnailSize
+ height: ClipboardConstants.itemHeight - 4
+ layer.enabled: true
+ layer.smooth: true
+ visible: false
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px)
+ color: "black"
+ antialiasing: true
+ }
+ }
+
+ DankIcon {
+ visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
+ name: {
+ switch (entryType) {
+ case "image":
+ return "image";
+ case "long_text":
+ return "subject";
+ default:
+ return "content_copy";
+ }
+ }
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.centerIn: parent
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/ConfirmModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/ConfirmModal.qml
new file mode 100644
index 0000000..b9c9b90
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/ConfirmModal.qml
@@ -0,0 +1,291 @@
+import QtQuick
+import qs.Common
+import qs.Modals.Common
+import qs.Widgets
+
+DankModal {
+ id: root
+
+ layerNamespace: "dms:confirm-modal"
+ keepPopoutsOpen: true
+
+ property string confirmTitle: ""
+ property string confirmMessage: ""
+ property string confirmButtonText: "Confirm"
+ property string cancelButtonText: "Cancel"
+ property color confirmButtonColor: Theme.primary
+ property var onConfirm: function () {}
+ property var onCancel: function () {}
+ property int selectedButton: -1
+ property bool keyboardNavigation: false
+
+ function show(title, message, onConfirmCallback, onCancelCallback) {
+ confirmTitle = title || "";
+ confirmMessage = message || "";
+ confirmButtonText = "Confirm";
+ cancelButtonText = "Cancel";
+ confirmButtonColor = Theme.primary;
+ onConfirm = onConfirmCallback || (() => {});
+ onCancel = onCancelCallback || (() => {});
+ selectedButton = -1;
+ keyboardNavigation = false;
+ open();
+ }
+
+ function showWithOptions(options) {
+ confirmTitle = options.title || "";
+ confirmMessage = options.message || "";
+ confirmButtonText = options.confirmText || "Confirm";
+ cancelButtonText = options.cancelText || "Cancel";
+ confirmButtonColor = options.confirmColor || Theme.primary;
+ onConfirm = options.onConfirm || (() => {});
+ onCancel = options.onCancel || (() => {});
+ selectedButton = -1;
+ keyboardNavigation = false;
+ open();
+ }
+
+ function selectButton() {
+ close();
+ if (selectedButton === 0) {
+ if (onCancel) {
+ onCancel();
+ }
+ } else {
+ if (onConfirm) {
+ onConfirm();
+ }
+ }
+ }
+
+ shouldBeVisible: false
+ allowStacking: true
+ modalWidth: 350
+ modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160
+ enableShadow: true
+ shouldHaveFocus: true
+ onBackgroundClicked: {
+ close();
+ if (onCancel) {
+ onCancel();
+ }
+ }
+ onOpened: {
+ Qt.callLater(function () {
+ modalFocusScope.forceActiveFocus();
+ modalFocusScope.focus = true;
+ shouldHaveFocus = true;
+ });
+ }
+ modalFocusScope.Keys.onPressed: function (event) {
+ switch (event.key) {
+ case Qt.Key_Escape:
+ close();
+ if (onCancel) {
+ onCancel();
+ }
+ event.accepted = true;
+ break;
+ case Qt.Key_Left:
+ case Qt.Key_Up:
+ keyboardNavigation = true;
+ selectedButton = 0;
+ event.accepted = true;
+ break;
+ case Qt.Key_Right:
+ case Qt.Key_Down:
+ keyboardNavigation = true;
+ selectedButton = 1;
+ event.accepted = true;
+ break;
+ case Qt.Key_N:
+ if (event.modifiers & Qt.ControlModifier) {
+ keyboardNavigation = true;
+ selectedButton = (selectedButton + 1) % 2;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_P:
+ if (event.modifiers & Qt.ControlModifier) {
+ keyboardNavigation = true;
+ selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_J:
+ if (event.modifiers & Qt.ControlModifier) {
+ keyboardNavigation = true;
+ selectedButton = 1;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_K:
+ if (event.modifiers & Qt.ControlModifier) {
+ keyboardNavigation = true;
+ selectedButton = 0;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_H:
+ if (event.modifiers & Qt.ControlModifier) {
+ keyboardNavigation = true;
+ selectedButton = 0;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_L:
+ if (event.modifiers & Qt.ControlModifier) {
+ keyboardNavigation = true;
+ selectedButton = 1;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_Tab:
+ keyboardNavigation = true;
+ selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2;
+ event.accepted = true;
+ break;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (selectedButton !== -1) {
+ selectButton();
+ } else {
+ selectedButton = 1;
+ selectButton();
+ }
+ event.accepted = true;
+ break;
+ }
+ }
+
+ content: Component {
+ Item {
+ anchors.fill: parent
+ implicitHeight: mainColumn.implicitHeight
+
+ Column {
+ id: mainColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.leftMargin: Theme.spacingL
+ anchors.rightMargin: Theme.spacingL
+ anchors.topMargin: Theme.spacingL
+ spacing: 0
+
+ StyledText {
+ text: confirmTitle
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingL
+ }
+
+ StyledText {
+ text: confirmMessage
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingL * 1.5
+ }
+
+ Row {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: 120
+ height: 40
+ radius: Theme.cornerRadius
+ color: {
+ if (keyboardNavigation && selectedButton === 0) {
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
+ } else if (cancelButton.containsMouse) {
+ return Theme.surfacePressed;
+ } else {
+ return Theme.surfaceVariantAlpha;
+ }
+ }
+ border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
+ border.width: (keyboardNavigation && selectedButton === 0) ? 1 : 0
+
+ StyledText {
+ text: cancelButtonText
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: cancelButton
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ selectedButton = 0;
+ selectButton();
+ }
+ }
+ }
+
+ Rectangle {
+ width: 120
+ height: 40
+ radius: Theme.cornerRadius
+ color: {
+ const baseColor = confirmButtonColor;
+ if (keyboardNavigation && selectedButton === 1) {
+ return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
+ } else if (confirmButton.containsMouse) {
+ return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
+ } else {
+ return baseColor;
+ }
+ }
+ border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
+ border.width: (keyboardNavigation && selectedButton === 1) ? 1 : 0
+
+ StyledText {
+ text: confirmButtonText
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.primaryText
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: confirmButton
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ selectedButton = 1;
+ selectButton();
+ }
+ }
+ }
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingL
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/DankModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/DankModal.qml
new file mode 100644
index 0000000..cafa133
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/DankModal.qml
@@ -0,0 +1,496 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property string layerNamespace: "dms:modal"
+ property alias content: contentLoader.sourceComponent
+ property alias contentLoader: contentLoader
+ property Item directContent: null
+ property real modalWidth: 400
+ property real modalHeight: 300
+ property var targetScreen
+ readonly property var effectiveScreen: contentWindow.screen ?? targetScreen
+ readonly property real screenWidth: effectiveScreen?.width ?? 1920
+ readonly property real screenHeight: effectiveScreen?.height ?? 1080
+ readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
+ property bool showBackground: true
+ property real backgroundOpacity: 0.5
+ property string positioning: "center"
+ property point customPosition: Qt.point(0, 0)
+ property bool closeOnEscapeKey: true
+ property bool closeOnBackgroundClick: true
+ property string animationType: "scale"
+ property int animationDuration: Theme.modalAnimationDuration
+ property real animationScaleCollapsed: 0.96
+ property real animationOffset: Theme.spacingL
+ property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
+ property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
+ property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ property color borderColor: Theme.outlineMedium
+ property real borderWidth: 0
+ property real cornerRadius: Theme.cornerRadius
+ property bool enableShadow: true
+ property alias modalFocusScope: focusScope
+ property bool shouldBeVisible: false
+ property bool shouldHaveFocus: shouldBeVisible
+ property bool allowFocusOverride: false
+ property bool allowStacking: false
+ property bool keepContentLoaded: false
+ property bool keepPopoutsOpen: false
+ property var customKeyboardFocus: null
+ property bool useOverlayLayer: false
+ readonly property alias contentWindow: contentWindow
+ readonly property alias clickCatcher: clickCatcher
+ readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
+ readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
+ readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
+
+ signal opened
+ signal dialogClosed
+ signal backgroundClicked
+
+ property bool animationsEnabled: true
+
+ function open() {
+ closeTimer.stop();
+ const focusedScreen = CompositorService.getFocusedScreen();
+ const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
+ if (focusedScreen) {
+ if (screenChanged)
+ contentWindow.visible = false;
+ contentWindow.screen = focusedScreen;
+ if (!useSingleWindow) {
+ if (screenChanged)
+ clickCatcher.visible = false;
+ clickCatcher.screen = focusedScreen;
+ }
+ }
+ if (screenChanged) {
+ Qt.callLater(() => root._finishOpen());
+ } else {
+ _finishOpen();
+ }
+ }
+
+ function _finishOpen() {
+ ModalManager.openModal(root);
+ shouldBeVisible = true;
+ if (!useSingleWindow)
+ clickCatcher.visible = true;
+ contentWindow.visible = true;
+ shouldHaveFocus = false;
+ Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
+ }
+
+ function close() {
+ shouldBeVisible = false;
+ shouldHaveFocus = false;
+ ModalManager.closeModal(root);
+ closeTimer.restart();
+ }
+
+ function instantClose() {
+ animationsEnabled = false;
+ shouldBeVisible = false;
+ shouldHaveFocus = false;
+ ModalManager.closeModal(root);
+ closeTimer.stop();
+ contentWindow.visible = false;
+ if (!useSingleWindow)
+ clickCatcher.visible = false;
+ dialogClosed();
+ Qt.callLater(() => animationsEnabled = true);
+ }
+
+ function toggle() {
+ shouldBeVisible ? close() : open();
+ }
+
+ Connections {
+ target: ModalManager
+ function onCloseAllModalsExcept(excludedModal) {
+ if (excludedModal !== root && !allowStacking && shouldBeVisible)
+ close();
+ }
+ }
+
+ Connections {
+ target: Quickshell
+ function onScreensChanged() {
+ if (!contentWindow.screen)
+ return;
+ const currentScreenName = contentWindow.screen.name;
+ let screenStillExists = false;
+ for (let i = 0; i < Quickshell.screens.length; i++) {
+ if (Quickshell.screens[i].name === currentScreenName) {
+ screenStillExists = true;
+ break;
+ }
+ }
+ if (screenStillExists)
+ return;
+ const newScreen = CompositorService.getFocusedScreen();
+ if (newScreen) {
+ contentWindow.screen = newScreen;
+ if (!useSingleWindow)
+ clickCatcher.screen = newScreen;
+ }
+ }
+ }
+
+ Timer {
+ id: closeTimer
+ interval: animationDuration + 50
+ onTriggered: {
+ if (shouldBeVisible)
+ return;
+ contentWindow.visible = false;
+ if (!useSingleWindow)
+ clickCatcher.visible = false;
+ dialogClosed();
+ }
+ }
+
+ readonly property var shadowLevel: Theme.elevationLevel3
+ readonly property real shadowFallbackOffset: 6
+ readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
+ readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
+ readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
+ readonly property real alignedWidth: Theme.px(modalWidth, dpr)
+ readonly property real alignedHeight: Theme.px(modalHeight, dpr)
+
+ readonly property real alignedX: Theme.snap((() => {
+ switch (positioning) {
+ case "center":
+ return (screenWidth - alignedWidth) / 2;
+ case "top-right":
+ return Math.max(Theme.spacingL, screenWidth - alignedWidth - Theme.spacingL);
+ case "custom":
+ return customPosition.x;
+ default:
+ return 0;
+ }
+ })(), dpr)
+
+ readonly property real alignedY: Theme.snap((() => {
+ switch (positioning) {
+ case "center":
+ return (screenHeight - alignedHeight) / 2;
+ case "top-right":
+ return Theme.barHeight + Theme.spacingXS;
+ case "custom":
+ return customPosition.y;
+ default:
+ return 0;
+ }
+ })(), dpr)
+
+ PanelWindow {
+ id: clickCatcher
+ visible: false
+ color: "transparent"
+
+ WlrLayershell.namespace: root.layerNamespace + ":clickcatcher"
+ WlrLayershell.layer: WlrLayershell.Top
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ mask: Region {
+ item: Rectangle {
+ x: root.alignedX
+ y: root.alignedY
+ width: root.alignedWidth
+ height: root.alignedHeight
+ }
+ intersection: Intersection.Xor
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.closeOnBackgroundClick && root.shouldBeVisible
+ onClicked: root.backgroundClicked()
+ }
+ }
+
+ PanelWindow {
+ id: contentWindow
+ visible: false
+ color: "transparent"
+
+ WindowBlur {
+ targetWindow: contentWindow
+ readonly property real s: Math.min(1, modalContainer.scaleValue)
+ blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
+ blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
+ blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
+ blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
+ blurRadius: root.cornerRadius
+ }
+
+ WlrLayershell.namespace: root.layerNamespace
+ WlrLayershell.layer: {
+ if (root.useOverlayLayer)
+ return WlrLayershell.Overlay;
+ switch (Quickshell.env("DMS_MODAL_LAYER")) {
+ case "bottom":
+ console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
+ return WlrLayershell.Top;
+ case "background":
+ console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
+ return WlrLayershell.Top;
+ case "overlay":
+ return WlrLayershell.Overlay;
+ default:
+ return WlrLayershell.Top;
+ }
+ }
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: {
+ if (customKeyboardFocus !== null)
+ return customKeyboardFocus;
+ if (!shouldHaveFocus)
+ return WlrKeyboardFocus.None;
+ if (root.useHyprlandFocusGrab)
+ return WlrKeyboardFocus.OnDemand;
+ return WlrKeyboardFocus.Exclusive;
+ }
+
+ anchors {
+ left: true
+ top: true
+ right: root.useSingleWindow
+ bottom: root.useSingleWindow
+ }
+
+ WlrLayershell.margins {
+ left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
+ top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
+ right: 0
+ bottom: 0
+ }
+
+ implicitWidth: root.useSingleWindow ? 0 : root.alignedWidth + (shadowBuffer * 2)
+ implicitHeight: root.useSingleWindow ? 0 : root.alignedHeight + (shadowBuffer * 2)
+
+ onVisibleChanged: {
+ if (visible) {
+ opened();
+ } else {
+ if (Qt.inputMethod) {
+ Qt.inputMethod.hide();
+ Qt.inputMethod.reset();
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
+ z: -2
+ onClicked: root.backgroundClicked()
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ z: -1
+ color: "black"
+ opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
+ visible: root.useBackground
+
+ Behavior on opacity {
+ enabled: root.animationsEnabled
+ DankAnim {
+ duration: root.animationDuration
+ easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
+ }
+ }
+ }
+
+ Item {
+ id: modalContainer
+ x: root.useSingleWindow ? root.alignedX : shadowBuffer
+ y: root.useSingleWindow ? root.alignedY : shadowBuffer
+
+ width: root.alignedWidth
+ height: root.alignedHeight
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.useSingleWindow && root.shouldBeVisible
+ hoverEnabled: false
+ acceptedButtons: Qt.AllButtons
+ onPressed: mouse.accepted = true
+ onClicked: mouse.accepted = true
+ z: -1
+ }
+
+ readonly property bool slide: root.animationType === "slide"
+ readonly property real offsetX: slide ? 15 : 0
+ readonly property real offsetY: slide ? -30 : root.animationOffset
+
+ property real animX: 0
+ property real animY: 0
+ property real scaleValue: root.animationScaleCollapsed
+
+ onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
+ onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
+
+ Connections {
+ target: root
+ function onShouldBeVisibleChanged() {
+ modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
+ modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
+ modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
+ }
+ }
+
+ Behavior on animX {
+ enabled: root.animationsEnabled
+ DankAnim {
+ duration: root.animationDuration
+ easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
+ }
+ }
+
+ Behavior on animY {
+ enabled: root.animationsEnabled
+ DankAnim {
+ duration: root.animationDuration
+ easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
+ }
+ }
+
+ Behavior on scaleValue {
+ enabled: root.animationsEnabled
+ DankAnim {
+ duration: root.animationDuration
+ easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
+ }
+ }
+
+ Item {
+ id: contentContainer
+ anchors.centerIn: parent
+ width: parent.width
+ height: parent.height
+ clip: false
+
+ Item {
+ id: animatedContent
+ anchors.fill: parent
+ clip: false
+ opacity: root.shouldBeVisible ? 1 : 0
+ scale: modalContainer.scaleValue
+ x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
+ y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
+
+ Behavior on opacity {
+ enabled: root.animationsEnabled
+ NumberAnimation {
+ duration: animationDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
+ }
+ }
+
+ ElevationShadow {
+ id: modalShadowLayer
+ anchors.fill: parent
+ level: root.shadowLevel
+ fallbackOffset: root.shadowFallbackOffset
+ targetRadius: root.cornerRadius
+ targetColor: root.backgroundColor
+ borderColor: root.borderColor
+ borderWidth: root.borderWidth
+ shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: root.cornerRadius
+ color: "transparent"
+ border.color: BlurService.borderColor
+ border.width: BlurService.borderWidth
+ z: 100
+ }
+
+ FocusScope {
+ anchors.fill: parent
+ focus: root.shouldBeVisible
+ clip: false
+
+ Item {
+ id: directContentWrapper
+ anchors.fill: parent
+ visible: root.directContent !== null
+ focus: true
+ clip: false
+
+ Component.onCompleted: {
+ if (root.directContent) {
+ root.directContent.parent = directContentWrapper;
+ root.directContent.anchors.fill = directContentWrapper;
+ Qt.callLater(() => root.directContent.forceActiveFocus());
+ }
+ }
+
+ Connections {
+ target: root
+ function onDirectContentChanged() {
+ if (root.directContent) {
+ root.directContent.parent = directContentWrapper;
+ root.directContent.anchors.fill = directContentWrapper;
+ Qt.callLater(() => root.directContent.forceActiveFocus());
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: contentLoader
+ anchors.fill: parent
+ active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
+ asynchronous: false
+ focus: true
+ clip: false
+ visible: root.directContent === null
+
+ onLoaded: {
+ if (item) {
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ FocusScope {
+ id: focusScope
+ objectName: "modalFocusScope"
+ anchors.fill: parent
+ visible: root.shouldBeVisible || contentWindow.visible
+ focus: root.shouldBeVisible
+ Keys.onEscapePressed: event => {
+ if (root.closeOnEscapeKey && shouldHaveFocus) {
+ root.close();
+ event.accepted = true;
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/InputModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/InputModal.qml
new file mode 100644
index 0000000..c65f3e0
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Common/InputModal.qml
@@ -0,0 +1,312 @@
+import QtQuick
+import qs.Common
+import qs.Modals.Common
+import qs.Widgets
+
+DankModal {
+ id: root
+
+ layerNamespace: "dms:input-modal"
+ keepPopoutsOpen: true
+
+ property string inputTitle: ""
+ property string inputMessage: ""
+ property string inputPlaceholder: ""
+ property string inputText: ""
+ property string confirmButtonText: "Confirm"
+ property string cancelButtonText: "Cancel"
+ property color confirmButtonColor: Theme.primary
+ property var onConfirm: function (text) {}
+ property var onCancel: function () {}
+ property int selectedButton: -1
+ property bool keyboardNavigation: false
+
+ function show(title, message, onConfirmCallback, onCancelCallback) {
+ inputTitle = title || "";
+ inputMessage = message || "";
+ inputPlaceholder = "";
+ inputText = "";
+ confirmButtonText = "Confirm";
+ cancelButtonText = "Cancel";
+ confirmButtonColor = Theme.primary;
+ onConfirm = onConfirmCallback || ((text) => {});
+ onCancel = onCancelCallback || (() => {});
+ selectedButton = -1;
+ keyboardNavigation = false;
+ open();
+ }
+
+ function showWithOptions(options) {
+ inputTitle = options.title || "";
+ inputMessage = options.message || "";
+ inputPlaceholder = options.placeholder || "";
+ inputText = options.initialText || "";
+ confirmButtonText = options.confirmText || "Confirm";
+ cancelButtonText = options.cancelText || "Cancel";
+ confirmButtonColor = options.confirmColor || Theme.primary;
+ onConfirm = options.onConfirm || ((text) => {});
+ onCancel = options.onCancel || (() => {});
+ selectedButton = -1;
+ keyboardNavigation = false;
+ open();
+ }
+
+ function confirmAndClose() {
+ const text = inputText;
+ close();
+ if (onConfirm) {
+ onConfirm(text);
+ }
+ }
+
+ function cancelAndClose() {
+ close();
+ if (onCancel) {
+ onCancel();
+ }
+ }
+
+ function selectButton() {
+ if (selectedButton === 0) {
+ cancelAndClose();
+ } else {
+ confirmAndClose();
+ }
+ }
+
+ shouldBeVisible: false
+ allowStacking: true
+ modalWidth: 350
+ modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
+ enableShadow: true
+ shouldHaveFocus: true
+ onBackgroundClicked: cancelAndClose()
+ onOpened: {
+ Qt.callLater(function () {
+ if (contentLoader.item && contentLoader.item.textInputRef) {
+ contentLoader.item.textInputRef.forceActiveFocus();
+ }
+ });
+ }
+
+ content: Component {
+ FocusScope {
+ anchors.fill: parent
+ implicitHeight: mainColumn.implicitHeight
+ focus: true
+
+ property alias textInputRef: textInput
+
+ Keys.onPressed: function (event) {
+ const textFieldFocused = textInput.activeFocus;
+
+ switch (event.key) {
+ case Qt.Key_Escape:
+ root.cancelAndClose();
+ event.accepted = true;
+ break;
+ case Qt.Key_Tab:
+ if (textFieldFocused) {
+ root.keyboardNavigation = true;
+ root.selectedButton = 0;
+ textInput.focus = false;
+ } else {
+ root.keyboardNavigation = true;
+ if (root.selectedButton === -1) {
+ root.selectedButton = 0;
+ } else if (root.selectedButton === 0) {
+ root.selectedButton = 1;
+ } else {
+ root.selectedButton = -1;
+ textInput.forceActiveFocus();
+ }
+ }
+ event.accepted = true;
+ break;
+ case Qt.Key_Left:
+ if (!textFieldFocused) {
+ root.keyboardNavigation = true;
+ root.selectedButton = 0;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_Right:
+ if (!textFieldFocused) {
+ root.keyboardNavigation = true;
+ root.selectedButton = 1;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (root.selectedButton !== -1) {
+ root.selectButton();
+ } else {
+ root.confirmAndClose();
+ }
+ event.accepted = true;
+ break;
+ }
+ }
+
+ Column {
+ id: mainColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.leftMargin: Theme.spacingL
+ anchors.rightMargin: Theme.spacingL
+ anchors.topMargin: Theme.spacingL
+ spacing: 0
+
+ StyledText {
+ text: root.inputTitle
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingL
+ }
+
+ StyledText {
+ text: root.inputMessage
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ width: parent.width
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ visible: root.inputMessage !== ""
+ }
+
+ Item {
+ width: 1
+ height: root.inputMessage !== "" ? Theme.spacingL : 0
+ visible: root.inputMessage !== ""
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 40
+ radius: Theme.cornerRadius
+ color: Theme.surfaceVariantAlpha
+ border.color: textInput.activeFocus ? Theme.primary : "transparent"
+ border.width: textInput.activeFocus ? 1 : 0
+
+ TextInput {
+ id: textInput
+
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ verticalAlignment: TextInput.AlignVCenter
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ selectionColor: Theme.primary
+ selectedTextColor: Theme.primaryText
+ clip: true
+ text: root.inputText
+ onTextChanged: root.inputText = text
+
+ StyledText {
+ anchors.fill: parent
+ verticalAlignment: Text.AlignVCenter
+ font.pixelSize: Theme.fontSizeMedium
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
+ text: root.inputPlaceholder
+ visible: textInput.text === "" && !textInput.activeFocus
+ }
+ }
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingL * 1.5
+ }
+
+ Row {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: 120
+ height: 40
+ radius: Theme.cornerRadius
+ color: {
+ if (root.keyboardNavigation && root.selectedButton === 0) {
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
+ } else if (cancelButton.containsMouse) {
+ return Theme.surfacePressed;
+ } else {
+ return Theme.surfaceVariantAlpha;
+ }
+ }
+ border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent"
+ border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0
+
+ StyledText {
+ text: root.cancelButtonText
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: cancelButton
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.cancelAndClose()
+ }
+ }
+
+ Rectangle {
+ width: 120
+ height: 40
+ radius: Theme.cornerRadius
+ color: {
+ const baseColor = root.confirmButtonColor;
+ if (root.keyboardNavigation && root.selectedButton === 1) {
+ return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
+ } else if (confirmButton.containsMouse) {
+ return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
+ } else {
+ return baseColor;
+ }
+ }
+ border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent"
+ border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0
+
+ StyledText {
+ text: root.confirmButtonText
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.primaryText
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: confirmButton
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.confirmAndClose()
+ }
+ }
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingL
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankColorPickerModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankColorPickerModal.qml
new file mode 100644
index 0000000..8a33878
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankColorPickerModal.qml
@@ -0,0 +1,771 @@
+import QtQuick
+import Quickshell
+import Quickshell.Hyprland
+import Quickshell.Io
+import qs.Common
+import qs.Modals.Common
+import qs.Services
+import qs.Widgets
+
+DankModal {
+ id: root
+
+ layerNamespace: "dms:color-picker"
+
+ HyprlandFocusGrab {
+ windows: [root.contentWindow]
+ active: root.useHyprlandFocusGrab && root.shouldHaveFocus
+ }
+
+ property string pickerTitle: I18n.tr("Choose Color")
+ property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
+ property var onColorSelectedCallback: null
+
+ signal colorSelected(color selectedColor)
+
+ property color currentColor: Theme.primary
+ property real hue: 0
+ property real saturation: 1
+ property real value: 1
+ property real alpha: 1
+ property real gradientX: 0
+ property real gradientY: 0
+
+ readonly property var standardColors: ["#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4", "#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722", "#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7", "#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19", "#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f", "#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315", "#ffffff", "#9e9e9e", "#212121"]
+
+ function show() {
+ currentColor = selectedColor;
+ updateFromColor(currentColor);
+ open();
+ }
+
+ function hide() {
+ onColorSelectedCallback = null;
+ close();
+ }
+
+ function hideInstant() {
+ instantClose();
+ }
+
+ function toggle() {
+ if (shouldBeVisible) {
+ hide();
+ } else {
+ show();
+ }
+ }
+
+ function toggleInstant() {
+ if (shouldBeVisible) {
+ hideInstant();
+ } else {
+ show();
+ }
+ }
+
+ onColorSelected: color => {
+ if (onColorSelectedCallback) {
+ onColorSelectedCallback(color);
+ }
+ }
+
+ function copyColorToClipboard(colorValue) {
+ Quickshell.execDetached(["dms", "cl", "copy", colorValue]);
+ ToastService.showInfo(`Color ${colorValue} copied`);
+ SessionData.addRecentColor(currentColor);
+ }
+
+ function updateFromColor(color) {
+ hue = color.hsvHue;
+ saturation = color.hsvSaturation;
+ value = color.hsvValue;
+ alpha = color.a;
+ gradientX = saturation;
+ gradientY = 1 - value;
+ }
+
+ function updateColor() {
+ currentColor = Qt.hsva(hue, saturation, value, alpha);
+ }
+
+ function updateColorFromGradient(x, y) {
+ saturation = Math.max(0, Math.min(1, x));
+ value = Math.max(0, Math.min(1, 1 - y));
+ updateColor();
+ selectedColor = currentColor;
+ }
+
+ function applyPickedColor(colorStr) {
+ if (colorStr.length < 7 || !colorStr.startsWith('#'))
+ return;
+ const pickedColor = Qt.color(colorStr);
+ root.selectedColor = pickedColor;
+ root.currentColor = pickedColor;
+ root.updateFromColor(pickedColor);
+ copyColorToClipboard(colorStr);
+ root.show();
+ }
+
+ function pickColorFromScreen() {
+ hideInstant();
+ Proc.runCommand("dms-color-pick", ["dms", "color", "pick", "--json"], (output, exitCode) => {
+ if (exitCode !== 0) {
+ console.warn("dms color pick exited with code:", exitCode);
+ root.show();
+ return;
+ }
+ try {
+ const result = JSON.parse(output);
+ if (result.hex) {
+ applyPickedColor(result.hex);
+ } else {
+ console.warn("Failed to parse dms color pick output: missing hex");
+ root.show();
+ }
+ } catch (e) {
+ console.warn("Failed to parse dms color pick JSON:", e);
+ root.show();
+ }
+ }, 0, Proc.noTimeout);
+ }
+
+ modalWidth: 680
+ modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
+ backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ cornerRadius: Theme.cornerRadius
+ borderColor: Theme.outlineMedium
+ borderWidth: 1
+ keepContentLoaded: true
+ allowStacking: true
+
+ onBackgroundClicked: hide()
+
+ IpcHandler {
+ function open(): string {
+ root.show();
+ return "COLOR_PICKER_MODAL_OPEN_SUCCESS";
+ }
+
+ function openColor(color: string): string {
+ root.selectedColor = Qt.color(color);
+ root.currentColor = Qt.color(color);
+ root.updateFromColor(Qt.color(color));
+ return open();
+ }
+
+ function close(): string {
+ root.hide();
+ return "COLOR_PICKER_MODAL_CLOSE_SUCCESS";
+ }
+
+ function closeInstant(): string {
+ root.hideInstant();
+ return "COLOR_PICKER_MODAL_CLOSE_INSTANT_SUCCESS";
+ }
+
+ function toggle(): string {
+ root.toggle();
+ return "COLOR_PICKER_MODAL_TOGGLE_SUCCESS";
+ }
+
+ function toggleInstant(): string {
+ root.toggleInstant();
+ return "COLOR_PICKER_MODAL_TOGGLE_INSTANT_SUCCESS";
+ }
+
+ target: "color-picker"
+ }
+
+ content: Component {
+ FocusScope {
+ id: colorContent
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property alias hexInput: hexInput
+
+ anchors.fill: parent
+ implicitHeight: mainColumn.implicitHeight
+ focus: true
+
+ Keys.onEscapePressed: event => {
+ root.hide();
+ event.accepted = true;
+ }
+
+ Column {
+ id: mainColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Column {
+ width: parent.width - 90
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: root.pickerTitle
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.left: parent.left
+ }
+
+ StyledText {
+ text: I18n.tr("Select a color from the palette or use custom sliders")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceTextMedium
+ anchors.left: parent.left
+ }
+ }
+
+ DankActionButton {
+ iconName: "colorize"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: () => {
+ root.pickColorFromScreen();
+ }
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: () => {
+ root.hide();
+ }
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Rectangle {
+ id: gradientPicker
+ width: parent.width - 70
+ height: 280
+ radius: Theme.cornerRadius
+ border.color: Theme.outlineStrong
+ border.width: 1
+ clip: true
+
+ Rectangle {
+ anchors.fill: parent
+ color: Qt.hsva(root.hue, 1, 1, 1)
+
+ Rectangle {
+ anchors.fill: parent
+ gradient: Gradient {
+ orientation: Gradient.Horizontal
+ GradientStop {
+ position: 0.0
+ color: "#ffffff"
+ }
+ GradientStop {
+ position: 1.0
+ color: "transparent"
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ gradient: Gradient {
+ orientation: Gradient.Vertical
+ GradientStop {
+ position: 0.0
+ color: "transparent"
+ }
+ GradientStop {
+ position: 1.0
+ color: "#000000"
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: pickerCircle
+ width: 16
+ height: 16
+ radius: 8
+ border.color: "white"
+ border.width: 2
+ color: "transparent"
+ x: root.gradientX * parent.width - width / 2
+ y: root.gradientY * parent.height - height / 2
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: parent.width - 4
+ height: parent.height - 4
+ radius: width / 2
+ border.color: "black"
+ border.width: 1
+ color: "transparent"
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.CrossCursor
+ onPressed: mouse => {
+ const x = Math.max(0, Math.min(1, mouse.x / width));
+ const y = Math.max(0, Math.min(1, mouse.y / height));
+ root.gradientX = x;
+ root.gradientY = y;
+ root.updateColorFromGradient(x, y);
+ }
+ onPositionChanged: mouse => {
+ if (pressed) {
+ const x = Math.max(0, Math.min(1, mouse.x / width));
+ const y = Math.max(0, Math.min(1, mouse.y / height));
+ root.gradientX = x;
+ root.gradientY = y;
+ root.updateColorFromGradient(x, y);
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: hueSlider
+ width: 50
+ height: 280
+ radius: Theme.cornerRadius
+ border.color: Theme.outlineStrong
+ border.width: 1
+
+ gradient: Gradient {
+ orientation: Gradient.Vertical
+ GradientStop {
+ position: 0.00
+ color: "#ff0000"
+ }
+ GradientStop {
+ position: 0.17
+ color: "#ffff00"
+ }
+ GradientStop {
+ position: 0.33
+ color: "#00ff00"
+ }
+ GradientStop {
+ position: 0.50
+ color: "#00ffff"
+ }
+ GradientStop {
+ position: 0.67
+ color: "#0000ff"
+ }
+ GradientStop {
+ position: 0.83
+ color: "#ff00ff"
+ }
+ GradientStop {
+ position: 1.00
+ color: "#ff0000"
+ }
+ }
+
+ Rectangle {
+ id: hueIndicator
+ width: parent.width
+ height: 4
+ color: "white"
+ border.color: "black"
+ border.width: 1
+ y: root.hue * parent.height - height / 2
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.SizeVerCursor
+ onPressed: mouse => {
+ const h = Math.max(0, Math.min(1, mouse.y / height));
+ root.hue = h;
+ root.updateColor();
+ root.selectedColor = root.currentColor;
+ }
+ onPositionChanged: mouse => {
+ if (pressed) {
+ const h = Math.max(0, Math.min(1, mouse.y / height));
+ root.hue = h;
+ root.updateColor();
+ root.selectedColor = root.currentColor;
+ }
+ }
+ }
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: I18n.tr("Material Colors")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.left: parent.left
+ }
+
+ GridView {
+ width: parent.width
+ height: 140
+ cellWidth: 38
+ cellHeight: 38
+ clip: true
+ interactive: false
+ model: root.standardColors
+
+ delegate: Rectangle {
+ width: 36
+ height: 36
+ color: modelData
+ radius: 4
+ border.color: Theme.outlineStrong
+ border.width: 1
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: () => {
+ const pickedColor = Qt.color(modelData);
+ root.selectedColor = pickedColor;
+ root.currentColor = pickedColor;
+ root.updateFromColor(pickedColor);
+ }
+ }
+ }
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Column {
+ width: 210
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Recent Colors")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.left: parent.left
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: 5
+
+ Rectangle {
+ width: 36
+ height: 36
+ radius: 4
+ border.color: Theme.outlineStrong
+ border.width: 1
+
+ color: {
+ if (index < SessionData.recentColors.length) {
+ return SessionData.recentColors[index];
+ }
+ return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
+ }
+
+ opacity: index < SessionData.recentColors.length ? 1.0 : 0.3
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: index < SessionData.recentColors.length ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: index < SessionData.recentColors.length
+ onClicked: () => {
+ if (index < SessionData.recentColors.length) {
+ const pickedColor = SessionData.recentColors[index];
+ root.selectedColor = pickedColor;
+ root.currentColor = pickedColor;
+ root.updateFromColor(pickedColor);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Column {
+ width: parent.width - 330
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Opacity")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.left: parent.left
+ }
+
+ DankSlider {
+ width: parent.width
+ value: Math.round(root.alpha * 100)
+ minimum: 0
+ maximum: 100
+ showValue: false
+ onSliderValueChanged: newValue => {
+ root.alpha = newValue / 100;
+ root.updateColor();
+ root.selectedColor = root.currentColor;
+ }
+ }
+ }
+
+ Rectangle {
+ width: 100
+ height: 50
+ radius: Theme.cornerRadius
+ color: root.currentColor
+ border.color: Theme.outlineStrong
+ border.width: 2
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Column {
+ width: (parent.width - Theme.spacingM * 2) / 3
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Hex")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ font.weight: Font.Medium
+ anchors.left: parent.left
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ DankTextField {
+ id: hexInput
+ width: parent.width - 36
+ height: 36
+ text: root.currentColor.toString()
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: {
+ if (text.length === 0)
+ return Theme.surfaceText;
+ const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
+ return hexPattern.test(text) ? Theme.surfaceText : Theme.error;
+ }
+ placeholderText: "#000000"
+ backgroundColor: Theme.surfaceHover
+ borderWidth: 1
+ focusedBorderWidth: 2
+ topPadding: Theme.spacingS
+ bottomPadding: Theme.spacingS
+ onAccepted: () => {
+ const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
+ if (!hexPattern.test(text))
+ return;
+ const color = Qt.color(text);
+ if (color) {
+ root.selectedColor = color;
+ root.currentColor = color;
+ root.updateFromColor(color);
+ }
+ }
+ }
+
+ DankActionButton {
+ iconName: "content_copy"
+ iconSize: Theme.iconSize - 6
+ iconColor: Theme.surfaceText
+ buttonSize: 36
+ anchors.verticalCenter: parent.verticalCenter
+ onClicked: () => {
+ root.copyColorToClipboard(hexInput.text);
+ }
+ }
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM * 2) / 3
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("RGB")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ font.weight: Font.Medium
+ anchors.left: parent.left
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ Rectangle {
+ width: parent.width - 36
+ height: 36
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: Theme.outline
+ border.width: 1
+
+ StyledText {
+ anchors.centerIn: parent
+ text: {
+ const r = Math.round(root.currentColor.r * 255);
+ const g = Math.round(root.currentColor.g * 255);
+ const b = Math.round(root.currentColor.b * 255);
+ if (root.alpha < 1) {
+ const a = Math.round(root.alpha * 255);
+ return `${r}, ${g}, ${b}, ${a}`;
+ }
+ return `${r}, ${g}, ${b}`;
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ }
+ }
+
+ DankActionButton {
+ iconName: "content_copy"
+ iconSize: Theme.iconSize - 6
+ iconColor: Theme.surfaceText
+ buttonSize: 36
+ anchors.verticalCenter: parent.verticalCenter
+ onClicked: () => {
+ const r = Math.round(root.currentColor.r * 255);
+ const g = Math.round(root.currentColor.g * 255);
+ const b = Math.round(root.currentColor.b * 255);
+ let rgbString;
+ if (root.alpha < 1) {
+ const a = Math.round(root.alpha * 255);
+ rgbString = `rgba(${r}, ${g}, ${b}, ${a})`;
+ } else {
+ rgbString = `rgb(${r}, ${g}, ${b})`;
+ }
+ Quickshell.execDetached(["dms", "cl", "copy", rgbString]);
+ ToastService.showInfo(`${rgbString} copied`);
+ }
+ }
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM * 2) / 3
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("HSV")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ font.weight: Font.Medium
+ anchors.left: parent.left
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ Rectangle {
+ width: parent.width - 36
+ height: 36
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: Theme.outline
+ border.width: 1
+
+ StyledText {
+ anchors.centerIn: parent
+ text: {
+ const h = Math.round(root.hue * 360);
+ const s = Math.round(root.saturation * 100);
+ const v = Math.round(root.value * 100);
+ if (root.alpha < 1) {
+ const a = Math.round(root.alpha * 100);
+ return `${h}°, ${s}%, ${v}%, ${a}%`;
+ }
+ return `${h}°, ${s}%, ${v}%`;
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ }
+ }
+
+ DankActionButton {
+ iconName: "content_copy"
+ iconSize: Theme.iconSize - 6
+ iconColor: Theme.surfaceText
+ buttonSize: 36
+ anchors.verticalCenter: parent.verticalCenter
+ onClicked: () => {
+ const h = Math.round(root.hue * 360);
+ const s = Math.round(root.saturation * 100);
+ const v = Math.round(root.value * 100);
+ let hsvString;
+ if (root.alpha < 1) {
+ const a = Math.round(root.alpha * 100);
+ hsvString = `${h}, ${s}, ${v}, ${a}`;
+ } else {
+ hsvString = `${h}, ${s}, ${v}`;
+ }
+ Quickshell.execDetached(["dms", "cl", "copy", hsvString]);
+ ToastService.showInfo(`HSV ${hsvString} copied`);
+ }
+ }
+ }
+ }
+ }
+
+ DankButton {
+ visible: root.onColorSelectedCallback !== null && root.onColorSelectedCallback !== undefined
+ width: 70
+ buttonHeight: 36
+ text: I18n.tr("Save")
+ backgroundColor: Theme.primary
+ textColor: Theme.background
+ anchors.right: parent.right
+ onClicked: {
+ SessionData.addRecentColor(root.currentColor);
+ root.colorSelected(root.currentColor);
+ root.hide();
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ActionPanel.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ActionPanel.qml
new file mode 100644
index 0000000..31ada32
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ActionPanel.qml
@@ -0,0 +1,256 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var selectedItem: null
+ property var controller: null
+ property bool expanded: false
+ property int selectedActionIndex: 0
+
+ function getPluginContextMenuActions() {
+ if (selectedItem?.type !== "plugin" || !selectedItem?.pluginId)
+ return [];
+ var instance = PluginService.pluginInstances[selectedItem.pluginId];
+ if (!instance)
+ return [];
+ if (typeof instance.getContextMenuActions !== "function")
+ return [];
+ var actions = instance.getContextMenuActions(selectedItem.data);
+ if (!Array.isArray(actions))
+ return [];
+ return actions;
+ }
+
+ readonly property var actions: {
+ var result = [];
+ if (selectedItem?.primaryAction) {
+ result.push(selectedItem.primaryAction);
+ }
+
+ switch (selectedItem?.type) {
+ case "plugin":
+ var pluginActions = getPluginContextMenuActions();
+ for (var i = 0; i < pluginActions.length; i++) {
+ var act = pluginActions[i];
+ result.push({
+ name: act.text || act.name || "",
+ icon: act.icon || "play_arrow",
+ action: "plugin_action",
+ pluginAction: act.action
+ });
+ }
+ break;
+ case "plugin_browse":
+ if (selectedItem?.actions) {
+ for (var i = 0; i < selectedItem.actions.length; i++) {
+ result.push(selectedItem.actions[i]);
+ }
+ }
+ break;
+ case "app":
+ if (selectedItem?.isCore)
+ break;
+ if (SessionService.nvidiaCommand) {
+ result.push({
+ name: I18n.tr("Launch on dGPU"),
+ icon: "memory",
+ action: "launch_dgpu"
+ });
+ }
+ if (selectedItem?.actions) {
+ for (var i = 0; i < selectedItem.actions.length; i++) {
+ result.push(selectedItem.actions[i]);
+ }
+ }
+ break;
+ }
+ return result;
+ }
+
+ readonly property bool hasActions: {
+ switch (selectedItem?.type) {
+ case "app":
+ return !selectedItem?.isCore;
+ case "plugin":
+ return getPluginContextMenuActions().length > 0;
+ case "plugin_browse":
+ return selectedItem?.actions?.length > 0;
+ default:
+ return actions.length > 1;
+ }
+ }
+
+ width: parent?.width ?? 200
+ height: expanded && hasActions ? 52 : 0
+ color: Theme.surfaceContainerHigh
+ radius: Theme.cornerRadius
+
+ clip: true
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Rectangle {
+ anchors.top: parent.top
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ }
+
+ Item {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+
+ Flickable {
+ id: actionsFlickable
+ anchors.left: parent.left
+ anchors.right: tabHint.left
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ height: parent.height
+ contentWidth: actionsRow.width
+ contentHeight: height
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+ flickableDirection: Flickable.HorizontalFlick
+
+ Row {
+ id: actionsRow
+ height: parent.height
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: root.actions
+
+ Rectangle {
+ id: actionButton
+
+ required property var modelData
+ required property int index
+
+ width: actionContent.implicitWidth + Theme.spacingM * 2
+ height: actionsRow.height
+ radius: Theme.cornerRadius
+ color: index === root.selectedActionIndex ? Theme.primaryHover : actionArea.containsMouse ? Theme.surfaceHover : "transparent"
+
+ Row {
+ id: actionContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: actionButton.modelData?.icon ?? "play_arrow"
+ size: 16
+ color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: actionButton.modelData?.name ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ MouseArea {
+ id: actionArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.controller && root.selectedItem) {
+ root.controller.executeAction(root.selectedItem, actionButton.modelData);
+ }
+ }
+ onEntered: root.selectedActionIndex = actionButton.index
+ }
+ }
+ }
+ }
+ }
+
+ StyledText {
+ id: tabHint
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ visible: root.hasActions
+ text: "Tab"
+ font.pixelSize: Theme.fontSizeSmall - 2
+ color: Theme.outlineButton
+ }
+ }
+
+ function toggle() {
+ expanded = !expanded;
+ selectedActionIndex = 0;
+ }
+
+ function show() {
+ expanded = true;
+ selectedActionIndex = actions.length > 1 ? 1 : 0;
+ }
+
+ function hide() {
+ expanded = false;
+ selectedActionIndex = 0;
+ }
+
+ function cycleAction(reverse = false) {
+ if (actions.length > 0) {
+ if (! reverse)
+ selectedActionIndex = (selectedActionIndex + 1) % actions.length;
+ else
+ selectedActionIndex = (selectedActionIndex - 1) % actions.length;
+ ensureSelectedVisible();
+ }
+ }
+
+ function ensureSelectedVisible() {
+ if (selectedActionIndex < 0 || !actionsRow.children || selectedActionIndex >= actionsRow.children.length)
+ return;
+ var buttonX = 0;
+ for (var i = 0; i < selectedActionIndex; i++) {
+ var child = actionsRow.children[i];
+ if (child)
+ buttonX += child.width + actionsRow.spacing;
+ }
+
+ var button = actionsRow.children[selectedActionIndex];
+ if (!button)
+ return;
+ var buttonRight = buttonX + button.width;
+ var viewLeft = actionsFlickable.contentX;
+ var viewRight = viewLeft + actionsFlickable.width;
+
+ if (buttonX < viewLeft) {
+ actionsFlickable.contentX = Math.max(0, buttonX - Theme.spacingS);
+ } else if (buttonRight > viewRight) {
+ actionsFlickable.contentX = Math.min(actionsFlickable.contentWidth - actionsFlickable.width, buttonRight - actionsFlickable.width + Theme.spacingS);
+ }
+ }
+
+ function executeSelectedAction() {
+ if (!controller || !selectedItem || selectedActionIndex >= actions.length)
+ return;
+ var action = actions[selectedActionIndex];
+ if (action.action === "plugin_action" && typeof action.pluginAction === "function") {
+ action.pluginAction();
+ controller.performSearch();
+ controller.itemExecuted();
+ } else {
+ controller.executeAction(selectedItem, action);
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Controller.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Controller.qml
new file mode 100644
index 0000000..67cb606
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Controller.qml
@@ -0,0 +1,1894 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Common
+import qs.Services
+import "Scorer.js" as Scorer
+import "ControllerUtils.js" as Utils
+import "NavigationHelpers.js" as Nav
+import "ItemTransformers.js" as Transform
+
+Item {
+ id: root
+
+ property string searchQuery: ""
+ property string searchMode: "all"
+ property string previousSearchMode: "all"
+ property bool autoSwitchedToFiles: false
+ property bool isFileSearching: false
+ property var sections: []
+ property var flatModel: []
+ property int selectedFlatIndex: 0
+ property var selectedItem: null
+ property bool isSearching: false
+ property string activePluginId: ""
+ property var collapsedSections: ({})
+ property bool keyboardNavigationActive: false
+ property bool active: false
+ property var _modeSectionsCache: ({})
+ property bool _queryDrivenSearch: false
+ property bool _diskCacheConsumed: false
+ property var sectionViewModes: ({})
+ property var pluginViewPreferences: ({})
+ property int gridColumns: SettingsData.appLauncherGridColumns
+ property int viewModeVersion: 0
+ property string viewModeContext: "spotlight"
+
+ signal itemExecuted
+ signal searchCompleted
+ signal modeChanged(string mode)
+ signal queryChanged(string query)
+ signal viewModeChanged(string sectionId, string mode)
+ signal searchQueryRequested(string query)
+
+ onActiveChanged: {
+ if (!active) {
+ SessionData.addLauncherHistory(searchQuery);
+
+ sections = [];
+ flatModel = [];
+ selectedItem = null;
+ _clearModeCache();
+ }
+ }
+
+ onSearchModeChanged: {
+ if (searchMode === "apps") {
+ _loadAppCategories();
+ } else {
+ appCategory = "";
+ appCategories = [];
+ }
+ }
+
+ Connections {
+ target: SettingsData
+ function onSortAppsAlphabeticallyChanged() {
+ AppSearchService.invalidateLauncherCache();
+ _clearModeCache();
+ }
+ }
+
+ Connections {
+ target: AppSearchService
+ function onCacheVersionChanged() {
+ if (!active)
+ return;
+ _clearModeCache();
+ if (searchMode === "apps") {
+ _loadAppCategories();
+ performSearch();
+ } else if (!searchQuery && searchMode === "all") {
+ performSearch();
+ }
+ }
+ }
+
+ Connections {
+ target: PluginService
+ function onRequestLauncherUpdate(pluginId) {
+ if (!active)
+ return;
+ if (activePluginId === pluginId) {
+ if (activePluginCategories.length <= 1)
+ loadPluginCategories(pluginId);
+ performSearch();
+ return;
+ }
+ if (searchQuery)
+ performSearch();
+ }
+ }
+
+ Process {
+ id: wtypeProcess
+ command: ["wtype", "-M", "ctrl", "-P", "v", "-p", "v", "-m", "ctrl"]
+ running: false
+ }
+
+ Process {
+ id: copyProcess
+ running: false
+ onExited: pasteTimer.start()
+ }
+
+ Timer {
+ id: pasteTimer
+ interval: 200
+ repeat: false
+ onTriggered: wtypeProcess.running = true
+ }
+
+ function pasteSelected() {
+ if (!selectedItem)
+ return;
+ if (!SessionService.wtypeAvailable) {
+ ToastService.showError("wtype not available - install wtype for paste support");
+ return;
+ }
+
+ const pluginId = selectedItem.pluginId;
+ if (!pluginId)
+ return;
+ const pasteArgs = AppSearchService.getPluginPasteArgs(pluginId, selectedItem.data);
+ if (!pasteArgs)
+ return;
+ copyProcess.command = pasteArgs;
+ copyProcess.running = true;
+ itemExecuted();
+ }
+
+ readonly property var sectionDefinitions: [
+ {
+ id: "favorites",
+ title: I18n.tr("Pinned"),
+ icon: "push_pin",
+ priority: 1,
+ defaultViewMode: "list"
+ },
+ {
+ id: "apps",
+ title: I18n.tr("Applications"),
+ icon: "apps",
+ priority: 2,
+ defaultViewMode: "list"
+ },
+ {
+ id: "browse_plugins",
+ title: I18n.tr("Browse"),
+ icon: "category",
+ priority: 2.5,
+ defaultViewMode: "grid"
+ },
+ {
+ id: "files",
+ title: I18n.tr("Files"),
+ icon: "folder",
+ priority: 4,
+ defaultViewMode: "list"
+ },
+ {
+ id: "fallback",
+ title: I18n.tr("Commands"),
+ icon: "terminal",
+ priority: 5,
+ defaultViewMode: "list"
+ }
+ ]
+
+ property int historyIndex: -1
+ property string typingBackup: ""
+
+ function navigateHistory(direction) {
+ let history = SessionData.launcherQueryHistory;
+ if (history.length === 0)
+ return;
+
+ if (historyIndex === -1)
+ typingBackup = searchQuery;
+
+ let nextIndex = historyIndex + (direction === "up" ? 1 : -1);
+ if (nextIndex >= history.length)
+ nextIndex = history.length - 1;
+ if (nextIndex < -1)
+ nextIndex = -1;
+
+ if (nextIndex === historyIndex)
+ return;
+ historyIndex = nextIndex;
+
+ let targetText = (historyIndex === -1) ? typingBackup : history[historyIndex];
+
+ setSearchQuery(targetText);
+ searchQueryRequested(targetText);
+ }
+
+ property string fileSearchType: "all"
+ property string fileSearchExt: ""
+ property string fileSearchFolder: ""
+ property string fileSearchSort: "score"
+
+ property string pluginFilter: ""
+ property string activePluginName: ""
+ property var activePluginCategories: []
+ property string activePluginCategory: ""
+ property string appCategory: ""
+ property var appCategories: []
+
+ function getSectionViewMode(sectionId) {
+ if (sectionId === "browse_plugins")
+ return "list";
+ if (pluginViewPreferences[sectionId]?.enforced)
+ return pluginViewPreferences[sectionId].mode;
+ if (sectionViewModes[sectionId])
+ return sectionViewModes[sectionId];
+
+ var savedModes = viewModeContext === "appDrawer" ? (SettingsData.appDrawerSectionViewModes || {}) : (SettingsData.spotlightSectionViewModes || {});
+ if (savedModes[sectionId])
+ return savedModes[sectionId];
+
+ for (var i = 0; i < sectionDefinitions.length; i++) {
+ if (sectionDefinitions[i].id === sectionId)
+ return sectionDefinitions[i].defaultViewMode || "list";
+ }
+
+ if (pluginViewPreferences[sectionId]?.mode)
+ return pluginViewPreferences[sectionId].mode;
+
+ return "list";
+ }
+
+ function setSectionViewMode(sectionId, mode) {
+ if (sectionId === "browse_plugins")
+ return;
+ if (pluginViewPreferences[sectionId]?.enforced)
+ return;
+ sectionViewModes = Object.assign({}, sectionViewModes, {
+ [sectionId]: mode
+ });
+ viewModeVersion++;
+ if (viewModeContext === "appDrawer") {
+ var savedModes = Object.assign({}, SettingsData.appDrawerSectionViewModes || {}, {
+ [sectionId]: mode
+ });
+ SettingsData.appDrawerSectionViewModes = savedModes;
+ } else {
+ var savedModes = Object.assign({}, SettingsData.spotlightSectionViewModes || {}, {
+ [sectionId]: mode
+ });
+ SettingsData.spotlightSectionViewModes = savedModes;
+ }
+ viewModeChanged(sectionId, mode);
+ }
+
+ function canChangeSectionViewMode(sectionId) {
+ if (sectionId === "browse_plugins")
+ return false;
+ return !pluginViewPreferences[sectionId]?.enforced;
+ }
+
+ function canCollapseSection(sectionId) {
+ return searchMode === "all";
+ }
+
+ function setPluginViewPreference(pluginId, mode, enforced) {
+ var prefs = Object.assign({}, pluginViewPreferences);
+ prefs[pluginId] = {
+ mode: mode,
+ enforced: enforced || false
+ };
+ pluginViewPreferences = prefs;
+ }
+
+ function applyActivePluginViewPreference(pluginId, isBuiltIn) {
+ var sectionId = "plugin_" + pluginId;
+ var pref = null;
+ if (isBuiltIn) {
+ var builtIn = AppSearchService.builtInPlugins[pluginId];
+ if (builtIn && builtIn.viewMode) {
+ pref = {
+ mode: builtIn.viewMode,
+ enforced: builtIn.viewModeEnforced === true
+ };
+ }
+ } else {
+ pref = PluginService.getPluginViewPreference(pluginId);
+ }
+
+ if (pref && pref.mode) {
+ setPluginViewPreference(sectionId, pref.mode, pref.enforced);
+ } else {
+ var prefs = Object.assign({}, pluginViewPreferences);
+ delete prefs[sectionId];
+ pluginViewPreferences = prefs;
+ }
+ }
+
+ function clearActivePluginViewPreference() {
+ var prefs = {};
+ for (var key in pluginViewPreferences) {
+ if (!key.startsWith("plugin_")) {
+ prefs[key] = pluginViewPreferences[key];
+ }
+ }
+ pluginViewPreferences = prefs;
+ }
+
+ property int _searchVersion: 0
+ property bool _pluginPhasePending: false
+ property bool _pluginPhaseForceFirst: false
+ property var _phase1Items: []
+
+ Timer {
+ id: searchDebounce
+ interval: 60
+ onTriggered: root.performSearch()
+ }
+
+ Timer {
+ id: pluginPhaseTimer
+ interval: 1
+ onTriggered: root._performPluginPhase()
+ }
+
+ Timer {
+ id: fileSearchDebounce
+ interval: 200
+ onTriggered: root.performFileSearch()
+ }
+
+ function getOrTransformApp(app) {
+ return AppSearchService.getOrTransformApp(app, transformApp);
+ }
+
+ function setSearchQuery(query) {
+ _searchVersion++;
+ _queryDrivenSearch = true;
+ _pluginPhasePending = false;
+ _phase1Items = [];
+ pluginPhaseTimer.stop();
+ searchQuery = query;
+ searchDebounce.restart();
+
+ var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll);
+ if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
+ fileSearchDebounce.restart();
+ }
+ }
+
+ function setMode(mode, isAutoSwitch) {
+ if (searchMode === mode)
+ return;
+ if (isAutoSwitch) {
+ previousSearchMode = searchMode;
+ autoSwitchedToFiles = true;
+ } else {
+ autoSwitchedToFiles = false;
+ }
+ searchMode = mode;
+ modeChanged(mode);
+ performSearch();
+ var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
+ if (mode === "files" || filesInAll) {
+ fileSearchDebounce.restart();
+ }
+ }
+
+ function restorePreviousMode() {
+ if (!autoSwitchedToFiles)
+ return;
+ autoSwitchedToFiles = false;
+ searchMode = previousSearchMode;
+ modeChanged(previousSearchMode);
+ performSearch();
+ }
+
+ function cycleMode(reverse = false) {
+ var modes = ["all", "apps", "files", "plugins"];
+ var currentIndex = modes.indexOf(searchMode);
+ if (!reverse)
+ var nextIndex = (currentIndex + 1) % modes.length;
+ else
+ var nextIndex = (currentIndex - 1 + modes.length) % modes.length;
+ setMode(modes[nextIndex]);
+ }
+
+ function reset() {
+ searchQuery = "";
+ searchMode = "all";
+ previousSearchMode = "all";
+ autoSwitchedToFiles = false;
+ isFileSearching = false;
+ fileSearchType = "all";
+ fileSearchExt = "";
+ fileSearchFolder = "";
+ fileSearchSort = "score";
+ sections = [];
+ flatModel = [];
+ selectedFlatIndex = 0;
+ selectedItem = null;
+ isSearching = false;
+ activePluginId = "";
+ activePluginName = "";
+ activePluginCategories = [];
+ activePluginCategory = "";
+ appCategory = "";
+ appCategories = [];
+ pluginFilter = "";
+ collapsedSections = {};
+ _clearModeCache();
+ _queryDrivenSearch = false;
+ _pluginPhasePending = false;
+ _pluginPhaseForceFirst = false;
+ _phase1Items = [];
+ pluginPhaseTimer.stop();
+ }
+
+ function loadPluginCategories(pluginId) {
+ if (!pluginId) {
+ if (activePluginCategories.length > 0) {
+ activePluginCategories = [];
+ activePluginCategory = "";
+ }
+ return;
+ }
+
+ const categories = AppSearchService.getPluginLauncherCategories(pluginId);
+ if (categories.length === activePluginCategories.length) {
+ let same = true;
+ for (let i = 0; i < categories.length; i++) {
+ if (categories[i].id !== activePluginCategories[i]?.id) {
+ same = false;
+ break;
+ }
+ }
+ if (same)
+ return;
+ }
+ activePluginCategories = categories;
+ activePluginCategory = "";
+ AppSearchService.setPluginLauncherCategory(pluginId, "");
+ }
+
+ function setActivePluginCategory(categoryId) {
+ if (activePluginCategory === categoryId)
+ return;
+ activePluginCategory = categoryId;
+ AppSearchService.setPluginLauncherCategory(activePluginId, categoryId);
+ performSearch();
+ }
+
+ function setAppCategory(category) {
+ if (appCategory === category)
+ return;
+ appCategory = category;
+ _queryDrivenSearch = true;
+ _clearModeCache();
+ performSearch();
+ }
+
+ function _loadAppCategories() {
+ appCategories = AppSearchService.getAllCategories();
+ }
+
+ function setFileSearchType(type) {
+ if (fileSearchType === type)
+ return;
+ fileSearchType = type;
+ performFileSearch();
+ }
+
+ function setFileSearchExt(ext) {
+ if (fileSearchExt === ext)
+ return;
+ fileSearchExt = ext;
+ performFileSearch();
+ }
+
+ function setFileSearchFolder(folder) {
+ if (fileSearchFolder === folder)
+ return;
+ fileSearchFolder = folder;
+ performFileSearch();
+ }
+
+ function setFileSearchSort(sort) {
+ if (fileSearchSort === sort)
+ return;
+ fileSearchSort = sort;
+ performFileSearch();
+ }
+
+ function clearPluginFilter() {
+ if (pluginFilter) {
+ pluginFilter = "";
+ performSearch();
+ return true;
+ }
+ return false;
+ }
+
+ function preserveSelectionAfterUpdate(forceFirst) {
+ if (forceFirst)
+ return function () {
+ return getFirstItemIndex();
+ };
+ var previousSelectedId = selectedItem?.id || "";
+ return function (newFlatModel) {
+ if (!previousSelectedId)
+ return getFirstItemIndex();
+ for (var i = 0; i < newFlatModel.length; i++) {
+ if (!newFlatModel[i].isHeader && newFlatModel[i].item?.id === previousSelectedId)
+ return i;
+ }
+ return getFirstItemIndex();
+ };
+ }
+
+ function performSearch() {
+ queryChanged(searchQuery);
+
+ var currentVersion = _searchVersion;
+ isSearching = true;
+ var shouldResetSelection = _queryDrivenSearch;
+ _queryDrivenSearch = false;
+ var restoreSelection = preserveSelectionAfterUpdate(shouldResetSelection);
+
+ var cachedSections = AppSearchService.getCachedDefaultSections();
+ if (!cachedSections && !_diskCacheConsumed && !searchQuery && searchMode === "all" && !pluginFilter) {
+ _diskCacheConsumed = true;
+ var diskSections = _loadDiskCache();
+ if (diskSections) {
+ activePluginId = "";
+ activePluginName = "";
+ activePluginCategories = [];
+ activePluginCategory = "";
+ clearActivePluginViewPreference();
+ for (var i = 0; i < diskSections.length; i++) {
+ if (collapsedSections[diskSections[i].id] !== undefined)
+ diskSections[i].collapsed = collapsedSections[diskSections[i].id];
+ }
+ _applyHighlights(diskSections, "");
+ flatModel = Scorer.flattenSections(diskSections);
+ sections = diskSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+ }
+
+ if (cachedSections && !searchQuery && searchMode === "all" && !pluginFilter) {
+ activePluginId = "";
+ activePluginName = "";
+ activePluginCategories = [];
+ activePluginCategory = "";
+ clearActivePluginViewPreference();
+ var modeCache = _getCachedModeData("all");
+ if (modeCache) {
+ _applyHighlights(modeCache.sections, "");
+ sections = modeCache.sections;
+ flatModel = modeCache.flatModel;
+ } else {
+ var newSections = cachedSections.map(function (s) {
+ var copy = Object.assign({}, s, {
+ items: s.items ? s.items.slice() : []
+ });
+ if (collapsedSections[s.id] !== undefined)
+ copy.collapsed = collapsedSections[s.id];
+ return copy;
+ });
+ _applyHighlights(newSections, "");
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ _setCachedModeData("all", sections, flatModel);
+ }
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ var allItems = [];
+
+ var triggerMatch = detectTrigger(searchQuery);
+ if (triggerMatch.pluginId) {
+ var pluginChanged = activePluginId !== triggerMatch.pluginId;
+ activePluginId = triggerMatch.pluginId;
+ activePluginName = getPluginName(triggerMatch.pluginId, triggerMatch.isBuiltIn);
+ applyActivePluginViewPreference(triggerMatch.pluginId, triggerMatch.isBuiltIn);
+
+ if (pluginChanged && !triggerMatch.isBuiltIn)
+ loadPluginCategories(triggerMatch.pluginId);
+
+ var pluginItems = getPluginItems(triggerMatch.pluginId, triggerMatch.query);
+ for (var k = 0; k < pluginItems.length; k++)
+ allItems.push(pluginItems[k]);
+
+ if (triggerMatch.isBuiltIn) {
+ var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
+ for (var j = 0; j < builtInItems.length; j++) {
+ allItems.push(transformBuiltInLauncherItem(builtInItems[j], triggerMatch.pluginId));
+ }
+ }
+
+ var dynamicDefs = buildDynamicSectionDefs(allItems);
+ var scoredItems = Scorer.scoreItems(allItems, triggerMatch.query, getFrecencyForItem);
+ var sortAlpha = !triggerMatch.query && SettingsData.sortAppsAlphabetically;
+ var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500);
+
+ for (var sid in collapsedSections) {
+ for (var i = 0; i < newSections.length; i++) {
+ if (newSections[i].id === sid) {
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+ }
+ }
+
+ _applyHighlights(newSections, triggerMatch.query);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ activePluginId = "";
+ activePluginName = "";
+ activePluginCategories = [];
+ activePluginCategory = "";
+ clearActivePluginViewPreference();
+
+ if (searchMode === "files") {
+ var fileQuery = searchQuery.startsWith("/") ? searchQuery.substring(1).trim() : searchQuery.trim();
+ isFileSearching = fileQuery.length >= 2 && DSearchService.dsearchAvailable;
+ sections = [];
+ flatModel = [];
+ selectedFlatIndex = 0;
+ selectedItem = null;
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ if (searchMode === "apps") {
+ var isCategoryFiltered = appCategory && appCategory !== I18n.tr("All");
+ var cachedSections = AppSearchService.getCachedDefaultSections();
+ if (cachedSections && !searchQuery && !isCategoryFiltered) {
+ var modeCache = _getCachedModeData("apps");
+ if (modeCache) {
+ _applyHighlights(modeCache.sections, "");
+ sections = modeCache.sections;
+ flatModel = modeCache.flatModel;
+ } else {
+ var appSectionIds = ["favorites", "apps"];
+ var newSections = cachedSections.filter(function (s) {
+ return appSectionIds.indexOf(s.id) !== -1;
+ }).map(function (s) {
+ var copy = Object.assign({}, s, {
+ items: s.items ? s.items.slice() : []
+ });
+ if (collapsedSections[s.id] !== undefined)
+ copy.collapsed = collapsedSections[s.id];
+ return copy;
+ });
+ _applyHighlights(newSections, "");
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ _setCachedModeData("apps", sections, flatModel);
+ }
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ if (isCategoryFiltered) {
+ var rawApps = AppSearchService.getAppsInCategory(appCategory);
+ for (var i = 0; i < rawApps.length; i++) {
+ allItems.push(getOrTransformApp(rawApps[i]));
+ }
+ // Also include core apps (DMS Settings etc.) that match this category
+ var allCoreApps = AppSearchService.getCoreApps("");
+ for (var i = 0; i < allCoreApps.length; i++) {
+ var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
+ if (coreAppCats.indexOf(appCategory) !== -1)
+ allItems.push(transformCoreApp(allCoreApps[i]));
+ }
+ } else {
+ var apps = searchApps(searchQuery);
+ for (var i = 0; i < apps.length; i++) {
+ allItems.push(apps[i]);
+ }
+ }
+
+ var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
+ var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
+ var newSections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500);
+
+ for (var sid in collapsedSections) {
+ for (var i = 0; i < newSections.length; i++) {
+ if (newSections[i].id === sid) {
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+ }
+ }
+
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ if (searchMode === "plugins") {
+ if (!searchQuery && !pluginFilter) {
+ var browseItems = getPluginBrowseItems();
+ for (var k = 0; k < browseItems.length; k++)
+ allItems.push(browseItems[k]);
+ } else if (pluginFilter) {
+ var isBuiltInFilter = !!AppSearchService.builtInPlugins[pluginFilter];
+ applyActivePluginViewPreference(pluginFilter, isBuiltInFilter);
+
+ var filterItems = getPluginItems(pluginFilter, searchQuery);
+ for (var k = 0; k < filterItems.length; k++)
+ allItems.push(filterItems[k]);
+
+ var builtInItems = AppSearchService.getBuiltInLauncherItems(pluginFilter, searchQuery);
+ for (var j = 0; j < builtInItems.length; j++) {
+ allItems.push(transformBuiltInLauncherItem(builtInItems[j], pluginFilter));
+ }
+ } else {
+ var emptyTriggerPlugins = getEmptyTriggerPlugins();
+ for (var i = 0; i < emptyTriggerPlugins.length; i++) {
+ var pluginId = emptyTriggerPlugins[i];
+ var pItems = getPluginItems(pluginId, searchQuery);
+ for (var k = 0; k < pItems.length; k++)
+ allItems.push(pItems[k]);
+ }
+
+ var builtInLauncherPlugins = getBuiltInEmptyTriggerLaunchers();
+ for (var i = 0; i < builtInLauncherPlugins.length; i++) {
+ var pluginId = builtInLauncherPlugins[i];
+ var blItems = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery);
+ for (var j = 0; j < blItems.length; j++) {
+ allItems.push(transformBuiltInLauncherItem(blItems[j], pluginId));
+ }
+ }
+ }
+
+ var dynamicDefs = buildDynamicSectionDefs(allItems);
+ var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
+ var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
+ var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, 500);
+
+ for (var sid in collapsedSections) {
+ for (var i = 0; i < newSections.length; i++) {
+ if (newSections[i].id === sid) {
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+ }
+ }
+
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+
+ isSearching = false;
+ searchCompleted();
+ return;
+ }
+
+ var apps = searchApps(searchQuery);
+ for (var i = 0; i < apps.length; i++) {
+ allItems.push(apps[i]);
+ }
+
+ if (searchMode === "all") {
+ if (searchQuery && searchQuery.length >= 2) {
+ _pluginPhasePending = true;
+ _phase1Items = allItems.slice();
+ _pluginPhaseForceFirst = shouldResetSelection;
+ pluginPhaseTimer.restart();
+ isSearching = true;
+ searchCompleted();
+ return;
+ } else if (!searchQuery) {
+ var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered();
+ for (var i = 0; i < emptyTriggerOrdered.length; i++) {
+ var plugin = emptyTriggerOrdered[i];
+ if (plugin.isBuiltIn) {
+ var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
+ for (var j = 0; j < blItems.length; j++)
+ allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id));
+ } else {
+ var pItems = getPluginItems(plugin.id, searchQuery);
+ for (var j = 0; j < pItems.length; j++)
+ allItems.push(pItems[j]);
+ }
+ }
+
+ var browseItems = getPluginBrowseItems();
+ for (var i = 0; i < browseItems.length; i++)
+ allItems.push(browseItems[i]);
+ }
+ }
+
+ var dynamicDefs = buildDynamicSectionDefs(allItems);
+
+ if (currentVersion !== _searchVersion) {
+ isSearching = false;
+ return;
+ }
+
+ var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
+ var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
+ var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, sortAlpha, searchQuery ? 50 : 500);
+
+ if (currentVersion !== _searchVersion) {
+ isSearching = false;
+ return;
+ }
+
+ for (var i = 0; i < newSections.length; i++) {
+ var sid = newSections[i].id;
+ if (collapsedSections[sid] !== undefined) {
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+ }
+
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+
+ if (!AppSearchService.isCacheValid() && !searchQuery && searchMode === "all" && !pluginFilter) {
+ AppSearchService.setCachedDefaultSections(sections, flatModel);
+ _saveDiskCache(sections);
+ }
+
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+
+ isSearching = _pluginPhasePending;
+ searchCompleted();
+ }
+
+ function _performPluginPhase() {
+ _pluginPhasePending = false;
+ if (!searchQuery || searchQuery.length < 2 || searchMode !== "all")
+ return;
+
+ var currentVersion = _searchVersion;
+ var restoreSelection = preserveSelectionAfterUpdate(_pluginPhaseForceFirst);
+ var allItems = _phase1Items;
+ _phase1Items = [];
+
+ var allPluginsOrdered = getAllVisiblePluginsOrdered();
+ var maxPerPlugin = 10;
+ for (var i = 0; i < allPluginsOrdered.length; i++) {
+ if (currentVersion !== _searchVersion)
+ return;
+ var plugin = allPluginsOrdered[i];
+ if (plugin.isBuiltIn) {
+ var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
+ var blLimit = Math.min(blItems.length, maxPerPlugin);
+ for (var j = 0; j < blLimit; j++) {
+ var item = transformBuiltInLauncherItem(blItems[j], plugin.id);
+ item._preScored = 900 - j;
+ allItems.push(item);
+ }
+ } else {
+ var pItems = getPluginItems(plugin.id, searchQuery, maxPerPlugin);
+ for (var j = 0; j < pItems.length; j++) {
+ pItems[j]._preScored = 900 - j;
+ allItems.push(pItems[j]);
+ }
+ }
+ }
+
+ if (currentVersion !== _searchVersion)
+ return;
+
+ var dynamicDefs = buildDynamicSectionDefs(allItems);
+ var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
+ var newSections = Scorer.groupBySection(scoredItems, dynamicDefs, false, 50);
+
+ if (currentVersion !== _searchVersion)
+ return;
+
+ for (var i = 0; i < newSections.length; i++) {
+ var sid = newSections[i].id;
+ if (collapsedSections[sid] !== undefined)
+ newSections[i].collapsed = collapsedSections[sid];
+ }
+
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = restoreSelection(flatModel);
+ updateSelectedItem();
+ isSearching = false;
+ searchCompleted();
+ }
+
+ function performFileSearch() {
+ if (!DSearchService.dsearchAvailable)
+ return;
+ var fileQuery = "";
+ var effectiveType = fileSearchType || "all";
+ var includeFiles = SettingsData.dankLauncherV2IncludeFilesInAll;
+ var includeFolders = SettingsData.dankLauncherV2IncludeFoldersInAll;
+
+ if (searchQuery.startsWith("/")) {
+ fileQuery = searchQuery.substring(1).trim();
+ } else if (searchMode === "files") {
+ fileQuery = searchQuery.trim();
+ } else if (searchMode === "all" && (includeFiles || includeFolders)) {
+ fileQuery = searchQuery.trim();
+ if (includeFiles && !includeFolders)
+ effectiveType = "file";
+ else if (!includeFiles && includeFolders)
+ effectiveType = "dir";
+ else
+ effectiveType = "all";
+ } else {
+ return;
+ }
+
+ if (fileQuery.length < 2) {
+ isFileSearching = false;
+ return;
+ }
+
+ isFileSearching = true;
+
+ var splitBothTypes = searchMode === "all" && includeFiles && includeFolders && DSearchService.supportsTypeFilter;
+ var queryTypes = splitBothTypes ? ["file", "dir"] : [effectiveType];
+ var pending = queryTypes.length;
+ var aggregatedItems = [];
+
+ for (var t = 0; t < queryTypes.length; t++) {
+ var queryType = queryTypes[t];
+ var params = {
+ limit: 20,
+ fuzzy: true,
+ sort: fileSearchSort || "score",
+ desc: true
+ };
+
+ if (DSearchService.supportsTypeFilter) {
+ params.type = (queryType && queryType !== "all") ? queryType : "all";
+ }
+ if (fileSearchExt) {
+ params.ext = fileSearchExt;
+ }
+ if (fileSearchFolder) {
+ params.folder = fileSearchFolder;
+ }
+
+ DSearchService.search(fileQuery, params, function (response) {
+ pending--;
+ if (!response.error) {
+ var hits = response.result?.hits || [];
+ for (var i = 0; i < hits.length; i++) {
+ var hit = hits[i];
+ var docTypes = hit.locations?.doc_type;
+ var isDir = docTypes ? !!docTypes["dir"] : false;
+ aggregatedItems.push(transformFileResult({
+ path: hit.id || "",
+ score: hit.score || 0,
+ is_dir: isDir
+ }));
+ }
+ }
+ if (pending > 0)
+ return;
+
+ isFileSearching = false;
+ _applyFileSearchResults(aggregatedItems, effectiveType);
+ });
+ }
+ }
+
+ function _applyFileSearchResults(fileItems, effectiveType) {
+ var fileSections = [];
+ var showType = effectiveType;
+ var order = SettingsData.launcherPluginOrder || [];
+ var filesOrderIdx = order.indexOf("__files");
+ var foldersOrderIdx = order.indexOf("__folders");
+ var filesPriority = filesOrderIdx !== -1 ? 2.6 + filesOrderIdx * 0.01 : 4;
+ var foldersPriority = foldersOrderIdx !== -1 ? 2.6 + foldersOrderIdx * 0.01 : 4.1;
+
+ if (showType === "all" && DSearchService.supportsTypeFilter) {
+ var onlyFiles = [];
+ var onlyDirs = [];
+ for (var j = 0; j < fileItems.length; j++) {
+ if (fileItems[j].data?.is_dir)
+ onlyDirs.push(fileItems[j]);
+ else
+ onlyFiles.push(fileItems[j]);
+ }
+ if (onlyFiles.length > 0) {
+ fileSections.push({
+ id: "files",
+ title: I18n.tr("Files"),
+ icon: "insert_drive_file",
+ priority: filesPriority,
+ items: onlyFiles,
+ collapsed: collapsedSections["files"] || false,
+ flatStartIndex: 0
+ });
+ }
+ if (onlyDirs.length > 0) {
+ fileSections.push({
+ id: "folders",
+ title: I18n.tr("Folders"),
+ icon: "folder",
+ priority: foldersPriority,
+ items: onlyDirs,
+ collapsed: collapsedSections["folders"] || false,
+ flatStartIndex: 0
+ });
+ }
+ } else {
+ var filesIcon = showType === "dir" ? "folder" : showType === "file" ? "insert_drive_file" : "folder";
+ var filesTitle = showType === "dir" ? I18n.tr("Folders") : I18n.tr("Files");
+ var singlePriority = showType === "dir" ? foldersPriority : filesPriority;
+ if (fileItems.length > 0) {
+ fileSections.push({
+ id: "files",
+ title: filesTitle,
+ icon: filesIcon,
+ priority: singlePriority,
+ items: fileItems,
+ collapsed: collapsedSections["files"] || false,
+ flatStartIndex: 0
+ });
+ }
+ }
+
+ var newSections;
+ if (searchMode === "files") {
+ newSections = fileSections;
+ } else {
+ var existingNonFile = sections.filter(function (s) {
+ return s.id !== "files" && s.id !== "folders";
+ });
+ newSections = existingNonFile.concat(fileSections);
+ }
+ newSections.sort(function (a, b) {
+ return a.priority - b.priority;
+ });
+ _applyHighlights(newSections, searchQuery);
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+ selectedFlatIndex = getFirstItemIndex();
+ updateSelectedItem();
+ }
+
+ function searchApps(query) {
+ var apps = AppSearchService.searchApplications(query);
+ var items = [];
+
+ for (var i = 0; i < apps.length; i++) {
+ items.push(getOrTransformApp(apps[i]));
+ }
+
+ var coreApps = AppSearchService.getCoreApps(query);
+ for (var i = 0; i < coreApps.length; i++) {
+ items.push(transformCoreApp(coreApps[i]));
+ }
+
+ return items;
+ }
+
+ function transformApp(app) {
+ var appId = app.id || app.execString || app.exec || "";
+ var override = SessionData.getAppOverride(appId);
+ return Transform.transformApp(app, override, [], I18n.tr("Launch"));
+ }
+
+ function transformCoreApp(app) {
+ return Transform.transformCoreApp(app, I18n.tr("Open"));
+ }
+
+ function transformBuiltInLauncherItem(item, pluginId) {
+ return Transform.transformBuiltInLauncherItem(item, pluginId, I18n.tr("Open"));
+ }
+
+ function transformFileResult(file) {
+ return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"), I18n.tr("Open in terminal"));
+ }
+
+ function detectTrigger(query) {
+ if (!query || query.length === 0)
+ return {
+ pluginId: null,
+ query: query
+ };
+
+ var pluginTriggers = PluginService.getAllPluginTriggers();
+ for (var trigger in pluginTriggers) {
+ if (trigger && query.startsWith(trigger)) {
+ return {
+ pluginId: pluginTriggers[trigger],
+ query: query.substring(trigger.length).trim()
+ };
+ }
+ }
+
+ var builtInTriggers = AppSearchService.getBuiltInLauncherTriggers();
+ for (var trigger in builtInTriggers) {
+ if (trigger && query.startsWith(trigger)) {
+ return {
+ pluginId: builtInTriggers[trigger],
+ query: query.substring(trigger.length).trim(),
+ isBuiltIn: true
+ };
+ }
+ }
+
+ return {
+ pluginId: null,
+ query: query
+ };
+ }
+
+ function getEmptyTriggerPlugins() {
+ var plugins = PluginService.getPluginsWithEmptyTrigger();
+ var visible = plugins.filter(function (pluginId) {
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ });
+ return sortPluginIdsByOrder(visible);
+ }
+
+ function getAllLauncherPluginIds() {
+ var launchers = PluginService.getLauncherPlugins();
+ return Object.keys(launchers);
+ }
+
+ function getVisibleLauncherPluginIds() {
+ var launchers = PluginService.getLauncherPlugins();
+ var visible = Object.keys(launchers).filter(function (pluginId) {
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ });
+ return sortPluginIdsByOrder(visible);
+ }
+
+ function getAllBuiltInLauncherIds() {
+ var launchers = AppSearchService.getBuiltInLauncherPlugins();
+ return Object.keys(launchers);
+ }
+
+ function getVisibleBuiltInLauncherIds() {
+ var launchers = AppSearchService.getBuiltInLauncherPlugins();
+ var visible = Object.keys(launchers).filter(function (pluginId) {
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ });
+ return sortPluginIdsByOrder(visible);
+ }
+
+ function sortPluginIdsByOrder(pluginIds) {
+ return Utils.sortPluginIdsByOrder(pluginIds, SettingsData.launcherPluginOrder || []);
+ }
+
+ function getAllVisiblePluginsOrdered() {
+ var thirdPartyLaunchers = PluginService.getLauncherPlugins() || {};
+ var builtInLaunchers = AppSearchService.getBuiltInLauncherPlugins() || {};
+ var all = [];
+ for (var id in thirdPartyLaunchers) {
+ if (SettingsData.getPluginAllowWithoutTrigger(id))
+ all.push({
+ id: id,
+ isBuiltIn: false
+ });
+ }
+ for (var id in builtInLaunchers) {
+ if (SettingsData.getPluginAllowWithoutTrigger(id))
+ all.push({
+ id: id,
+ isBuiltIn: true
+ });
+ }
+ return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []);
+ }
+
+ function getEmptyTriggerPluginsOrdered() {
+ var thirdParty = PluginService.getPluginsWithEmptyTrigger() || [];
+ var builtIn = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger() || [];
+ var all = [];
+ for (var i = 0; i < thirdParty.length; i++) {
+ var id = thirdParty[i];
+ if (SettingsData.getPluginAllowWithoutTrigger(id))
+ all.push({
+ id: id,
+ isBuiltIn: false
+ });
+ }
+ for (var i = 0; i < builtIn.length; i++) {
+ var id = builtIn[i];
+ if (SettingsData.getPluginAllowWithoutTrigger(id))
+ all.push({
+ id: id,
+ isBuiltIn: true
+ });
+ }
+ return Utils.sortPluginsOrdered(all, SettingsData.launcherPluginOrder || []);
+ }
+
+ function getPluginBrowseItems() {
+ var items = [];
+ var browseLabel = I18n.tr("Browse");
+ var triggerLabel = I18n.tr("Trigger: %1");
+ var noTriggerLabel = I18n.tr("No trigger");
+
+ var launchers = PluginService.getLauncherPlugins();
+ for (var pluginId in launchers) {
+ var trigger = PluginService.getPluginTrigger(pluginId);
+ var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ items.push(Transform.createPluginBrowseItem(pluginId, launchers[pluginId], trigger, false, isAllowed, browseLabel, triggerLabel, noTriggerLabel));
+ }
+
+ var builtInLaunchers = AppSearchService.getBuiltInLauncherPlugins();
+ for (var pluginId in builtInLaunchers) {
+ var trigger = AppSearchService.getBuiltInPluginTrigger(pluginId);
+ var isAllowed = SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ items.push(Transform.createPluginBrowseItem(pluginId, builtInLaunchers[pluginId], trigger, true, isAllowed, browseLabel, triggerLabel, noTriggerLabel));
+ }
+
+ return items;
+ }
+
+ function getBuiltInEmptyTriggerLaunchers() {
+ var plugins = AppSearchService.getBuiltInLauncherPluginsWithEmptyTrigger();
+ var visible = plugins.filter(function (pluginId) {
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ });
+ return sortPluginIdsByOrder(visible);
+ }
+
+ function getPluginItems(pluginId, query, limit) {
+ var items = AppSearchService.getPluginItemsForPlugin(pluginId, query);
+ var count = limit > 0 && limit < items.length ? limit : items.length;
+ var transformed = [];
+
+ for (var i = 0; i < count; i++) {
+ transformed.push(transformPluginItem(items[i], pluginId));
+ }
+
+ return transformed;
+ }
+
+ function getPluginName(pluginId, isBuiltIn) {
+ if (isBuiltIn) {
+ var plugin = AppSearchService.builtInPlugins[pluginId];
+ return plugin ? plugin.name : pluginId;
+ }
+ var launchers = PluginService.getLauncherPlugins();
+ if (launchers[pluginId]) {
+ return launchers[pluginId].name || pluginId;
+ }
+ return pluginId;
+ }
+
+ function getPluginMetadata(pluginId) {
+ var builtIn = AppSearchService.builtInPlugins[pluginId];
+ if (builtIn) {
+ return {
+ name: builtIn.name || pluginId,
+ icon: builtIn.cornerIcon || "extension"
+ };
+ }
+ var launchers = PluginService.getLauncherPlugins();
+ if (launchers[pluginId]) {
+ var rawIcon = launchers[pluginId].icon || "extension";
+ return {
+ name: launchers[pluginId].name || pluginId,
+ icon: Utils.stripIconPrefix(rawIcon)
+ };
+ }
+ return {
+ name: pluginId,
+ icon: "extension"
+ };
+ }
+
+ function buildDynamicSectionDefs(items) {
+ var baseDefs = sectionDefinitions.slice();
+ var pluginSections = {};
+ var order = SettingsData.launcherPluginOrder || [];
+ var orderMap = {};
+ for (var k = 0; k < order.length; k++)
+ orderMap[order[k]] = k;
+ var unorderedPriority = 2.6 + order.length * 0.01;
+
+ for (var i = 0; i < items.length; i++) {
+ var section = items[i].section;
+ if (!section || !section.startsWith("plugin_"))
+ continue;
+ if (pluginSections[section])
+ continue;
+ var pluginId = section.substring(7);
+ var meta = getPluginMetadata(pluginId);
+ var viewPref = getPluginViewPref(pluginId);
+ var orderIdx = orderMap[pluginId];
+ var priority;
+ if (orderIdx !== undefined) {
+ priority = 2.6 + orderIdx * 0.01;
+ } else {
+ priority = unorderedPriority;
+ unorderedPriority += 0.01;
+ }
+
+ pluginSections[section] = {
+ id: section,
+ title: meta.name,
+ icon: meta.icon,
+ priority: priority,
+ defaultViewMode: viewPref.mode || "list"
+ };
+
+ if (viewPref.mode)
+ setPluginViewPreference(section, viewPref.mode, viewPref.enforced);
+ }
+
+ for (var sectionId in pluginSections) {
+ baseDefs.push(pluginSections[sectionId]);
+ }
+
+ baseDefs.sort(function (a, b) {
+ return a.priority - b.priority;
+ });
+ return baseDefs;
+ }
+
+ function getPluginViewPref(pluginId) {
+ var builtIn = AppSearchService.builtInPlugins[pluginId];
+ if (builtIn && builtIn.viewMode) {
+ return {
+ mode: builtIn.viewMode,
+ enforced: builtIn.viewModeEnforced === true
+ };
+ }
+
+ var pref = PluginService.getPluginViewPreference(pluginId);
+ if (pref && pref.mode) {
+ return pref;
+ }
+
+ return {
+ mode: "list",
+ enforced: false
+ };
+ }
+
+ function transformPluginItem(item, pluginId) {
+ return Transform.transformPluginItem(item, pluginId, I18n.tr("Select"));
+ }
+
+ function getFrecencyForItem(item) {
+ if (item.type !== "app")
+ return null;
+
+ var appId = item.id;
+ var usageRanking = AppUsageHistoryData.appUsageRanking || {};
+
+ var idVariants = [appId, appId.replace(".desktop", "")];
+ var usageData = null;
+
+ for (var i = 0; i < idVariants.length; i++) {
+ if (usageRanking[idVariants[i]]) {
+ usageData = usageRanking[idVariants[i]];
+ break;
+ }
+ }
+
+ return {
+ usageCount: usageData?.usageCount || 0
+ };
+ }
+
+ function getFirstItemIndex() {
+ return Nav.getFirstItemIndex(flatModel);
+ }
+
+ function _getCachedModeData(mode) {
+ return _modeSectionsCache[mode] || null;
+ }
+
+ function _setCachedModeData(mode, sectionsData, flatModelData) {
+ var cache = Object.assign({}, _modeSectionsCache);
+ cache[mode] = {
+ sections: sectionsData,
+ flatModel: flatModelData
+ };
+ _modeSectionsCache = cache;
+ }
+
+ function _clearModeCache() {
+ _modeSectionsCache = {};
+ }
+
+ function _saveDiskCache(sectionsData) {
+ var serializable = [];
+ for (var i = 0; i < sectionsData.length; i++) {
+ var s = sectionsData[i];
+ var items = [];
+ var srcItems = s.items || [];
+ for (var j = 0; j < srcItems.length; j++) {
+ var it = srcItems[j];
+ items.push({
+ id: it.id,
+ type: it.type,
+ name: it.name || "",
+ subtitle: it.subtitle || "",
+ icon: it.icon || "",
+ iconType: it.iconType || "image",
+ iconFull: it.iconFull || "",
+ section: it.section || "",
+ isCore: it.isCore || false,
+ isBuiltInLauncher: it.isBuiltInLauncher || false,
+ pluginId: it.pluginId || ""
+ });
+ }
+ serializable.push({
+ id: s.id,
+ title: s.title || "",
+ icon: s.icon || "",
+ priority: s.priority || 0,
+ items: items
+ });
+ }
+ CacheData.saveLauncherCache(serializable);
+ }
+
+ function _actionsFromDesktopEntry(appId) {
+ if (!appId)
+ return [];
+ var entry = DesktopEntries.heuristicLookup(appId);
+ if (!entry || !entry.actions || entry.actions.length === 0)
+ return [];
+ var result = [];
+ for (var i = 0; i < entry.actions.length; i++) {
+ result.push({
+ name: entry.actions[i].name,
+ icon: "play_arrow",
+ actionData: entry.actions[i]
+ });
+ }
+ return result;
+ }
+
+ function _loadDiskCache() {
+ var cached = CacheData.loadLauncherCache();
+ if (!cached || !Array.isArray(cached) || cached.length === 0)
+ return null;
+
+ var sectionsData = [];
+ for (var i = 0; i < cached.length; i++) {
+ var s = cached[i];
+ var items = [];
+ var srcItems = s.items || [];
+ for (var j = 0; j < srcItems.length; j++) {
+ var it = srcItems[j];
+ items.push({
+ id: it.id || "",
+ type: it.type || "app",
+ name: it.name || "",
+ subtitle: it.subtitle || "",
+ icon: it.icon || "",
+ iconType: it.iconType || "image",
+ iconFull: it.iconFull || "",
+ section: it.section || "",
+ isCore: it.isCore || false,
+ isBuiltInLauncher: it.isBuiltInLauncher || false,
+ pluginId: it.pluginId || "",
+ data: {
+ id: it.id
+ },
+ actions: _actionsFromDesktopEntry(it.id),
+ primaryAction: it.type === "app" && !it.isCore ? {
+ name: I18n.tr("Launch"),
+ icon: "open_in_new",
+ action: "launch"
+ } : null,
+ _diskCached: true,
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ });
+ }
+ sectionsData.push({
+ id: s.id || "",
+ title: s.title || "",
+ icon: s.icon || "",
+ priority: s.priority || 0,
+ items: items,
+ collapsed: false,
+ flatStartIndex: 0
+ });
+ }
+ return sectionsData;
+ }
+
+ function updateSelectedItem() {
+ if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
+ var entry = flatModel[selectedFlatIndex];
+ selectedItem = entry.isHeader ? null : entry.item;
+ } else {
+ selectedItem = null;
+ }
+ }
+
+ function _applyHighlights(sectionsData, query) {
+ if (!query || query.length === 0) {
+ for (var i = 0; i < sectionsData.length; i++) {
+ var items = sectionsData[i].items;
+ for (var j = 0; j < items.length; j++) {
+ var item = items[j];
+ item._hName = item.name || "";
+ item._hSub = item.subtitle || "";
+ item._hRich = false;
+ }
+ }
+ return;
+ }
+
+ var highlightColor = Theme.primary;
+ var nameColor = Theme.surfaceText;
+ var subColor = Theme.surfaceVariantText;
+ var lowerQuery = query.toLowerCase();
+
+ for (var i = 0; i < sectionsData.length; i++) {
+ var items = sectionsData[i].items;
+ for (var j = 0; j < items.length; j++) {
+ var item = items[j];
+ item._hName = _highlightField(item.name || "", lowerQuery, query.length, nameColor, highlightColor);
+ item._hSub = _highlightField(item.subtitle || "", lowerQuery, query.length, subColor, highlightColor);
+ item._hRich = true;
+ }
+ }
+ }
+
+ function _highlightField(text, lowerQuery, queryLen, baseColor, highlightColor) {
+ if (!text)
+ return "";
+ var idx = text.toLowerCase().indexOf(lowerQuery);
+ if (idx === -1)
+ return text;
+ var before = text.substring(0, idx);
+ var match = text.substring(idx, idx + queryLen);
+ var after = text.substring(idx + queryLen);
+ return '<span style="color:' + baseColor + '">' + before + '</span><span style="color:' + highlightColor + '; font-weight:600">' + match + '</span><span style="color:' + baseColor + '">' + after + '</span>';
+ }
+
+ function getCurrentSectionViewMode() {
+ if (selectedFlatIndex < 0 || selectedFlatIndex >= flatModel.length)
+ return "list";
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader)
+ return "list";
+ return getSectionViewMode(entry.sectionId);
+ }
+
+ function getGridColumns(sectionId) {
+ return Nav.getGridColumns(getSectionViewMode(sectionId), gridColumns);
+ }
+
+ function _cancelPendingSelectionReset() {
+ _queryDrivenSearch = false;
+ _pluginPhaseForceFirst = false;
+ }
+
+ function selectNext() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectPrevious() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectRight() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewMode);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectLeft() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewMode);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectNextSection() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculateNextSectionIndex(flatModel, selectedFlatIndex);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectPreviousSection() {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculatePrevSectionIndex(flatModel, selectedFlatIndex);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectPageDown(visibleItems) {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectPageUp(visibleItems) {
+ keyboardNavigationActive = true;
+ _cancelPendingSelectionReset();
+ var newIndex = Nav.calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems);
+ if (newIndex !== selectedFlatIndex) {
+ selectedFlatIndex = newIndex;
+ updateSelectedItem();
+ }
+ }
+
+ function selectIndex(index) {
+ keyboardNavigationActive = false;
+ if (index >= 0 && index < flatModel.length && !flatModel[index].isHeader) {
+ selectedFlatIndex = index;
+ updateSelectedItem();
+ }
+ }
+
+ function toggleSection(sectionId) {
+ _clearModeCache();
+ var newCollapsed = Object.assign({}, collapsedSections);
+ var currentState = newCollapsed[sectionId];
+
+ if (currentState === undefined) {
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i].id === sectionId) {
+ currentState = sections[i].collapsed || false;
+ break;
+ }
+ }
+ }
+
+ newCollapsed[sectionId] = !currentState;
+ collapsedSections = newCollapsed;
+
+ var newSections = sections.slice();
+ for (var i = 0; i < newSections.length; i++) {
+ if (newSections[i].id === sectionId) {
+ newSections[i] = Object.assign({}, newSections[i], {
+ collapsed: newCollapsed[sectionId]
+ });
+ }
+ }
+ flatModel = Scorer.flattenSections(newSections);
+ sections = newSections;
+
+ if (selectedFlatIndex >= flatModel.length) {
+ selectedFlatIndex = getFirstItemIndex();
+ }
+ updateSelectedItem();
+ }
+
+ function executeSelected() {
+ if (searchDebounce.running) {
+ searchDebounce.stop();
+ performSearch();
+ }
+ if (!selectedItem)
+ return;
+ executeItem(selectedItem);
+ }
+
+ function executeItem(item) {
+ if (!item)
+ return;
+
+ SessionData.addLauncherHistory(searchQuery);
+
+ if (item.type === "plugin_browse") {
+ var browsePluginId = item.data?.pluginId;
+ if (!browsePluginId)
+ return;
+ var browseTrigger = item.data.isBuiltIn ? AppSearchService.getBuiltInPluginTrigger(browsePluginId) : PluginService.getPluginTrigger(browsePluginId);
+
+ if (browseTrigger && browseTrigger.length > 0) {
+ searchQueryRequested(browseTrigger);
+ } else {
+ setMode("plugins");
+ pluginFilter = browsePluginId;
+ performSearch();
+ }
+ return;
+ }
+
+ switch (item.type) {
+ case "app":
+ if (item.isCore) {
+ AppSearchService.executeCoreApp(item.data);
+ } else if (item.data?.isAction) {
+ launchAppAction(item.data);
+ } else {
+ launchApp(item.data);
+ }
+ break;
+ case "plugin":
+ if (item.isBuiltInLauncher) {
+ AppSearchService.executeBuiltInLauncherItem(item.data);
+ } else {
+ AppSearchService.executePluginItem(item.data, item.pluginId);
+ }
+ break;
+ case "file":
+ openFile(item.data?.path);
+ break;
+ default:
+ return;
+ }
+
+ itemExecuted();
+ }
+
+ function executeAction(item, action) {
+ if (!item || !action)
+ return;
+ switch (action.action) {
+ case "launch":
+ executeItem(item);
+ break;
+ case "open":
+ openFile(item.data.path);
+ break;
+ case "open_folder":
+ openFolder(item.data.path);
+ break;
+ case "copy_path":
+ copyToClipboard(item.data.path);
+ break;
+ case "open_terminal":
+ openTerminal(item.data.path);
+ break;
+ case "copy":
+ copyToClipboard(item.name);
+ break;
+ case "execute":
+ executeItem(item);
+ break;
+ case "launch_dgpu":
+ if (item.type === "app" && item.data) {
+ launchAppWithNvidia(item.data);
+ }
+ break;
+ case "toggle_all_visibility":
+ if (item.type === "plugin_browse" && item.data?.pluginId) {
+ var pluginId = item.data.pluginId;
+ var currentState = SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ SettingsData.setPluginAllowWithoutTrigger(pluginId, !currentState);
+ performSearch();
+ }
+ return;
+ default:
+ if (item.type === "app" && action.actionData) {
+ launchAppAction({
+ parentApp: item.data,
+ actionData: action.actionData
+ });
+ }
+ }
+
+ itemExecuted();
+ }
+
+ function _resolveDesktopEntry(app) {
+ if (!app)
+ return null;
+ if (app.command)
+ return app;
+ var id = app.id || app.execString || app.exec || "";
+ if (!id)
+ return null;
+ return DesktopEntries.heuristicLookup(id);
+ }
+
+ function launchApp(app) {
+ var entry = _resolveDesktopEntry(app);
+ if (!entry)
+ return;
+ SessionService.launchDesktopEntry(entry);
+ AppUsageHistoryData.addAppUsage(entry);
+ }
+
+ function launchAppWithNvidia(app) {
+ var entry = _resolveDesktopEntry(app);
+ if (!entry)
+ return;
+ SessionService.launchDesktopEntry(entry, true);
+ AppUsageHistoryData.addAppUsage(entry);
+ }
+
+ function launchAppAction(actionItem) {
+ if (!actionItem || !actionItem.actionData)
+ return;
+ var entry = _resolveDesktopEntry(actionItem.parentApp);
+ if (!entry)
+ return;
+ SessionService.launchDesktopAction(entry, actionItem.actionData);
+ AppUsageHistoryData.addAppUsage(entry);
+ }
+
+ function openFile(path) {
+ if (!path)
+ return;
+ Qt.openUrlExternally("file://" + path);
+ }
+
+ function openFolder(path) {
+ if (!path)
+ return;
+ var folder = path.substring(0, path.lastIndexOf("/"));
+ Qt.openUrlExternally("file://" + folder);
+ }
+
+ function openTerminal(path) {
+ if (!path)
+ return;
+ var terminal = Quickshell.env("TERMINAL") || "xterm";
+ Quickshell.execDetached({
+ command: [terminal],
+ workingDirectory: path
+ });
+ }
+
+ function copyToClipboard(text) {
+ if (!text)
+ return;
+ Quickshell.execDetached(["dms", "cl", "copy", text]);
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ControllerUtils.js b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ControllerUtils.js
new file mode 100644
index 0000000..a06285f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ControllerUtils.js
@@ -0,0 +1,128 @@
+.pragma library
+
+function getFileIcon(filename) {
+ var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
+
+ switch (ext) {
+ case "pdf":
+ return "picture_as_pdf";
+ case "doc":
+ case "docx":
+ case "odt":
+ return "description";
+ case "xls":
+ case "xlsx":
+ case "ods":
+ return "table_chart";
+ case "ppt":
+ case "pptx":
+ case "odp":
+ return "slideshow";
+ case "txt":
+ case "md":
+ case "rst":
+ return "article";
+ case "jpg":
+ case "jpeg":
+ case "png":
+ case "gif":
+ case "svg":
+ case "webp":
+ return "image";
+ case "mp3":
+ case "wav":
+ case "flac":
+ case "ogg":
+ return "audio_file";
+ case "mp4":
+ case "mkv":
+ case "avi":
+ case "webm":
+ return "video_file";
+ case "zip":
+ case "tar":
+ case "gz":
+ case "7z":
+ case "rar":
+ return "folder_zip";
+ case "js":
+ case "ts":
+ case "py":
+ case "rs":
+ case "go":
+ case "java":
+ case "c":
+ case "cpp":
+ case "h":
+ return "code";
+ case "html":
+ case "css":
+ case "htm":
+ return "web";
+ case "json":
+ case "xml":
+ case "yaml":
+ case "yml":
+ return "data_object";
+ case "sh":
+ case "bash":
+ case "zsh":
+ return "terminal";
+ default:
+ return "insert_drive_file";
+ }
+}
+
+function stripIconPrefix(iconName) {
+ if (!iconName)
+ return "extension";
+ if (iconName.startsWith("unicode:"))
+ return iconName.substring(8);
+ if (iconName.startsWith("material:"))
+ return iconName.substring(9);
+ if (iconName.startsWith("image:"))
+ return iconName.substring(6);
+ return iconName;
+}
+
+function detectIconType(iconName) {
+ if (!iconName)
+ return "material";
+ if (iconName.startsWith("unicode:"))
+ return "unicode";
+ if (iconName.startsWith("material:"))
+ return "material";
+ if (iconName.startsWith("image:"))
+ return "image";
+ if (iconName.indexOf("/") >= 0 || iconName.indexOf(".") >= 0)
+ return "image";
+ if (/^[a-z]+-[a-z]/.test(iconName.toLowerCase()))
+ return "image";
+ return "material";
+}
+
+function sortPluginIdsByOrder(pluginIds, order) {
+ if (!order || order.length === 0)
+ return pluginIds;
+ var orderMap = {};
+ for (var i = 0; i < order.length; i++)
+ orderMap[order[i]] = i;
+ return pluginIds.slice().sort(function (a, b) {
+ var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
+ var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
+ return aOrder - bOrder;
+ });
+}
+
+function sortPluginsOrdered(plugins, order) {
+ if (!order || order.length === 0)
+ return plugins;
+ var orderMap = {};
+ for (var i = 0; i < order.length; i++)
+ orderMap[order[i]] = i;
+ return plugins.sort(function (a, b) {
+ var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
+ var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
+ return aOrder - bOrder;
+ });
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml
new file mode 100644
index 0000000..859022c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml
@@ -0,0 +1,465 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import Quickshell.Hyprland
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ visible: false
+
+ property bool spotlightOpen: false
+ property bool keyboardActive: false
+ property bool contentVisible: false
+ property var spotlightContent: launcherContentLoader.item
+ property bool openedFromOverview: false
+ property bool isClosing: false
+ property bool _pendingInitialize: false
+ property string _pendingQuery: ""
+ property string _pendingMode: ""
+ readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
+
+ readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
+ readonly property var effectiveScreen: launcherWindow.screen
+ readonly property real screenWidth: effectiveScreen?.width ?? 1920
+ readonly property real screenHeight: effectiveScreen?.height ?? 1080
+ readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
+
+ readonly property int baseWidth: {
+ switch (SettingsData.dankLauncherV2Size) {
+ case "micro":
+ return 500;
+ case "medium":
+ return 720;
+ case "large":
+ return 860;
+ default:
+ return 620;
+ }
+ }
+ readonly property int baseHeight: {
+ switch (SettingsData.dankLauncherV2Size) {
+ case "micro":
+ return 480;
+ case "medium":
+ return 720;
+ case "large":
+ return 860;
+ default:
+ return 600;
+ }
+ }
+ readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
+ readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
+ readonly property real modalX: (screenWidth - modalWidth) / 2
+ readonly property real modalY: (screenHeight - modalHeight) / 2
+
+ readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ readonly property real cornerRadius: Theme.cornerRadius
+ readonly property color borderColor: {
+ if (!SettingsData.dankLauncherV2BorderEnabled)
+ return Theme.outlineMedium;
+ switch (SettingsData.dankLauncherV2BorderColor) {
+ case "primary":
+ return Theme.primary;
+ case "secondary":
+ return Theme.secondary;
+ case "outline":
+ return Theme.outline;
+ case "surfaceText":
+ return Theme.surfaceText;
+ default:
+ return Theme.primary;
+ }
+ }
+ readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
+
+ signal dialogClosed
+
+ function _ensureContentLoadedAndInitialize(query, mode) {
+ _pendingQuery = query || "";
+ _pendingMode = mode || "";
+ _pendingInitialize = true;
+ contentVisible = true;
+ launcherContentLoader.active = true;
+
+ if (spotlightContent) {
+ _initializeAndShow(_pendingQuery, _pendingMode);
+ _pendingInitialize = false;
+ }
+ }
+
+ function _initializeAndShow(query, mode) {
+ if (!spotlightContent)
+ return;
+ contentVisible = true;
+ spotlightContent.searchField.forceActiveFocus();
+
+ var targetQuery = "";
+
+ if (query) {
+ targetQuery = query;
+ } else if (SettingsData.rememberLastQuery) {
+ targetQuery = SessionData.launcherLastQuery || "";
+ }
+
+ if (spotlightContent.searchField) {
+ spotlightContent.searchField.text = targetQuery;
+ }
+ if (spotlightContent.controller) {
+ var targetMode = mode || SessionData.launcherLastMode || "all";
+ spotlightContent.controller.searchMode = targetMode;
+ spotlightContent.controller.activePluginId = "";
+ spotlightContent.controller.activePluginName = "";
+ spotlightContent.controller.pluginFilter = "";
+ spotlightContent.controller.fileSearchType = "all";
+ spotlightContent.controller.fileSearchExt = "";
+ spotlightContent.controller.fileSearchFolder = "";
+ spotlightContent.controller.fileSearchSort = "score";
+ spotlightContent.controller.collapsedSections = {};
+ spotlightContent.controller.selectedFlatIndex = 0;
+ spotlightContent.controller.selectedItem = null;
+ spotlightContent.controller.historyIndex = -1;
+ spotlightContent.controller.searchQuery = targetQuery;
+
+ spotlightContent.controller.performSearch();
+ }
+ if (spotlightContent.resetScroll) {
+ spotlightContent.resetScroll();
+ }
+ if (spotlightContent.actionPanel) {
+ spotlightContent.actionPanel.hide();
+ }
+ }
+
+ function _finishShow(query, mode) {
+ spotlightOpen = true;
+ isClosing = false;
+ openedFromOverview = false;
+
+ keyboardActive = true;
+ ModalManager.openModal(root);
+ if (useHyprlandFocusGrab)
+ focusGrab.active = true;
+
+ _ensureContentLoadedAndInitialize(query || "", mode || "");
+ }
+
+ function show() {
+ closeCleanupTimer.stop();
+
+ var focusedScreen = CompositorService.getFocusedScreen();
+ if (focusedScreen && launcherWindow.screen !== focusedScreen) {
+ spotlightOpen = false;
+ isClosing = false;
+ launcherWindow.screen = focusedScreen;
+ Qt.callLater(() => root._finishShow("", ""));
+ return;
+ }
+
+ _finishShow("", "");
+ }
+
+ function showWithQuery(query) {
+ closeCleanupTimer.stop();
+
+ var focusedScreen = CompositorService.getFocusedScreen();
+ if (focusedScreen && launcherWindow.screen !== focusedScreen) {
+ spotlightOpen = false;
+ isClosing = false;
+ launcherWindow.screen = focusedScreen;
+ Qt.callLater(() => root._finishShow(query, ""));
+ return;
+ }
+
+ _finishShow(query, "");
+ }
+
+ function hide() {
+ if (!spotlightOpen)
+ return;
+ openedFromOverview = false;
+ isClosing = true;
+ contentVisible = false;
+
+ keyboardActive = false;
+ spotlightOpen = false;
+ focusGrab.active = false;
+ ModalManager.closeModal(root);
+
+ closeCleanupTimer.start();
+ }
+
+ function toggle() {
+ spotlightOpen ? hide() : show();
+ }
+
+ function showWithMode(mode) {
+ closeCleanupTimer.stop();
+
+ var focusedScreen = CompositorService.getFocusedScreen();
+ if (focusedScreen && launcherWindow.screen !== focusedScreen) {
+ spotlightOpen = false;
+ isClosing = false;
+ launcherWindow.screen = focusedScreen;
+ Qt.callLater(() => root._finishShow("", mode));
+ return;
+ }
+
+ spotlightOpen = true;
+ isClosing = false;
+ openedFromOverview = false;
+
+ keyboardActive = true;
+ ModalManager.openModal(root);
+ if (useHyprlandFocusGrab)
+ focusGrab.active = true;
+
+ _ensureContentLoadedAndInitialize("", mode);
+ }
+
+ function toggleWithMode(mode) {
+ if (spotlightOpen) {
+ hide();
+ } else {
+ showWithMode(mode);
+ }
+ }
+
+ function toggleWithQuery(query) {
+ if (spotlightOpen) {
+ hide();
+ } else {
+ showWithQuery(query);
+ }
+ }
+
+ Timer {
+ id: closeCleanupTimer
+ interval: Theme.modalAnimationDuration + 50
+ repeat: false
+ onTriggered: {
+ isClosing = false;
+ if (root.unloadContentOnClose)
+ launcherContentLoader.active = false;
+ dialogClosed();
+ }
+ }
+
+ Connections {
+ target: spotlightContent?.controller ?? null
+
+ function onModeChanged(mode) {
+ if (spotlightContent.controller.autoSwitchedToFiles)
+ return;
+ SessionData.setLauncherLastMode(mode);
+ }
+ }
+
+ HyprlandFocusGrab {
+ id: focusGrab
+ windows: [launcherWindow]
+ active: false
+
+ onCleared: {
+ if (spotlightOpen) {
+ hide();
+ }
+ }
+ }
+
+ Connections {
+ target: ModalManager
+ function onCloseAllModalsExcept(excludedModal) {
+ if (excludedModal !== root && spotlightOpen) {
+ hide();
+ }
+ }
+ }
+
+ Connections {
+ target: Quickshell
+ function onScreensChanged() {
+ if (Quickshell.screens.length === 0)
+ return;
+
+ const screenName = launcherWindow.screen?.name;
+ if (screenName) {
+ for (let i = 0; i < Quickshell.screens.length; i++) {
+ if (Quickshell.screens[i].name === screenName)
+ return;
+ }
+ }
+
+ if (spotlightOpen)
+ hide();
+
+ const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
+ if (newScreen)
+ launcherWindow.screen = newScreen;
+ }
+ }
+
+ PanelWindow {
+ id: launcherWindow
+ visible: spotlightOpen || isClosing
+ color: "transparent"
+ exclusionMode: ExclusionMode.Ignore
+
+ WindowBlur {
+ targetWindow: launcherWindow
+ readonly property real s: Math.min(1, modalContainer.scale)
+ blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
+ blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
+ blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
+ blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
+ blurRadius: root.cornerRadius
+ }
+
+ WlrLayershell.namespace: "dms:spotlight"
+ WlrLayershell.layer: {
+ switch (Quickshell.env("DMS_MODAL_LAYER")) {
+ case "bottom":
+ console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
+ return WlrLayershell.Top;
+ case "background":
+ console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
+ return WlrLayershell.Top;
+ case "overlay":
+ return WlrLayershell.Overlay;
+ default:
+ return WlrLayershell.Top;
+ }
+ }
+ WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
+
+ anchors {
+ top: true
+ bottom: true
+ left: true
+ right: true
+ }
+
+ mask: Region {
+ item: spotlightOpen ? fullScreenMask : null
+ }
+
+ Item {
+ id: fullScreenMask
+ anchors.fill: parent
+ }
+
+ Rectangle {
+ id: backgroundDarken
+ anchors.fill: parent
+ color: "black"
+ opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
+ visible: contentVisible || opacity > 0
+
+ Behavior on opacity {
+ DankAnim {
+ duration: Theme.modalAnimationDuration
+ easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: spotlightOpen
+ onClicked: mouse => {
+ var contentX = modalContainer.x;
+ var contentY = modalContainer.y;
+ var contentW = modalContainer.width;
+ var contentH = modalContainer.height;
+
+ if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
+ root.hide();
+ }
+ }
+ }
+
+ Item {
+ id: modalContainer
+ x: root.modalX
+ y: root.modalY
+ width: root.modalWidth
+ height: root.modalHeight
+ visible: contentVisible || opacity > 0
+
+ opacity: contentVisible ? 1 : 0
+ scale: contentVisible ? 1 : 0.96
+ transformOrigin: Item.Center
+
+ Behavior on opacity {
+ DankAnim {
+ duration: Theme.modalAnimationDuration
+ easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
+ }
+ }
+
+ Behavior on scale {
+ DankAnim {
+ duration: Theme.modalAnimationDuration
+ easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
+ }
+ }
+
+ ElevationShadow {
+ id: launcherShadowLayer
+ anchors.fill: parent
+ level: Theme.elevationLevel3
+ fallbackOffset: 6
+ targetColor: root.backgroundColor
+ borderColor: root.borderColor
+ borderWidth: root.borderWidth
+ targetRadius: root.cornerRadius
+ shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: mouse => mouse.accepted = true
+ }
+
+ FocusScope {
+ anchors.fill: parent
+ focus: keyboardActive
+
+ Loader {
+ id: launcherContentLoader
+ anchors.fill: parent
+ active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
+ asynchronous: false
+ sourceComponent: LauncherContent {
+ focus: true
+ parentModal: root
+ }
+
+ onLoaded: {
+ if (root._pendingInitialize) {
+ root._initializeAndShow(root._pendingQuery, root._pendingMode);
+ root._pendingInitialize = false;
+ }
+ }
+ }
+
+ Keys.onEscapePressed: event => {
+ root.hide();
+ event.accepted = true;
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: root.cornerRadius
+ color: "transparent"
+ border.color: BlurService.borderColor
+ border.width: BlurService.borderWidth
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/GridItem.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/GridItem.qml
new file mode 100644
index 0000000..a7f39c3
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/GridItem.qml
@@ -0,0 +1,105 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var item: null
+ property bool isSelected: false
+ property bool isHovered: itemArea.containsMouse
+ property var controller: null
+ property int flatIndex: -1
+
+ signal clicked
+ signal rightClicked(real mouseX, real mouseY)
+
+ readonly property string iconValue: {
+ if (!item)
+ return "";
+ switch (item.iconType) {
+ case "material":
+ case "nerd":
+ return "material:" + (item.icon || "apps");
+ case "unicode":
+ return "unicode:" + (item.icon || "");
+ case "composite":
+ return item.iconFull || "";
+ case "image":
+ default:
+ return item.icon || "";
+ }
+ }
+
+ readonly property int computedIconSize: Math.min(48, Math.max(32, width * 0.45))
+
+ radius: Theme.cornerRadius
+ color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: Theme.surfaceText
+ cornerRadius: root.radius
+ }
+
+ Column {
+ anchors.centerIn: parent
+ anchors.margins: Theme.spacingS
+ spacing: Theme.spacingS
+ width: parent.width - Theme.spacingM
+
+ AppIconRenderer {
+ width: root.computedIconSize
+ height: root.computedIconSize
+ anchors.horizontalCenter: parent.horizontalCenter
+ iconValue: root.iconValue
+ iconSize: root.computedIconSize
+ fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
+ iconColor: root.isSelected ? Theme.primary : Theme.surfaceText
+ materialIconSizeAdjustment: root.computedIconSize * 0.3
+ }
+
+ Text {
+ width: parent.width
+ text: root.item?._hName ?? root.item?.name ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ font.family: Theme.fontFamily
+ color: root.isSelected ? Theme.primary : Theme.surfaceText
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignHCenter
+ maximumLineCount: 2
+ wrapMode: Text.Wrap
+ }
+ }
+
+ MouseArea {
+ id: itemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+
+ onPressed: mouse => {
+ if (mouse.button === Qt.LeftButton)
+ rippleLayer.trigger(mouse.x, mouse.y);
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ var scenePos = mapToItem(null, mouse.x, mouse.y);
+ root.rightClicked(scenePos.x, scenePos.y);
+ } else {
+ root.clicked();
+ }
+ }
+
+ onPositionChanged: {
+ if (root.controller) {
+ root.controller.keyboardNavigationActive = false;
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ItemTransformers.js b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ItemTransformers.js
new file mode 100644
index 0000000..61c108c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ItemTransformers.js
@@ -0,0 +1,237 @@
+.pragma library
+
+ .import "ControllerUtils.js" as Utils
+
+function transformApp(app, override, defaultActions, primaryActionLabel) {
+ var appId = app.id || app.execString || app.exec || "";
+
+ var actions = [];
+ if (app.actions && app.actions.length > 0) {
+ for (var i = 0; i < app.actions.length; i++) {
+ actions.push({
+ name: app.actions[i].name,
+ icon: "play_arrow",
+ actionData: app.actions[i]
+ });
+ }
+ }
+
+ return {
+ id: appId,
+ type: "app",
+ name: override?.name || app.name || "",
+ subtitle: override?.comment || app.comment || "",
+ icon: override?.icon || app.icon || "application-x-executable",
+ iconType: "image",
+ section: "apps",
+ data: app,
+ keywords: app.keywords || [],
+ actions: actions,
+ primaryAction: {
+ name: primaryActionLabel,
+ icon: "open_in_new",
+ action: "launch"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ };
+}
+
+function transformCoreApp(app, openLabel) {
+ var iconName = "apps";
+ var iconType = "material";
+
+ if (app.icon) {
+ if (app.icon.startsWith("svg+corner:")) {
+ iconType = "composite";
+ } else if (app.icon.startsWith("material:")) {
+ iconName = app.icon.substring(9);
+ } else {
+ iconName = app.icon;
+ iconType = "image";
+ }
+ }
+
+ return {
+ id: app.builtInPluginId || app.action || "",
+ type: "app",
+ name: app.name || "",
+ subtitle: app.comment || "",
+ icon: iconName,
+ iconType: iconType,
+ iconFull: app.icon,
+ section: "apps",
+ data: app,
+ isCore: true,
+ actions: [],
+ primaryAction: {
+ name: openLabel,
+ icon: "open_in_new",
+ action: "launch"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ };
+}
+
+function transformBuiltInLauncherItem(item, pluginId, openLabel) {
+ var rawIcon = item.icon || "extension";
+ var icon = Utils.stripIconPrefix(rawIcon);
+ var iconType = item.iconType;
+ if (!iconType) {
+ if (rawIcon.startsWith("material:"))
+ iconType = "material";
+ else if (rawIcon.startsWith("unicode:"))
+ iconType = "unicode";
+ else
+ iconType = "image";
+ }
+
+ return {
+ id: item.action || "",
+ type: "plugin",
+ name: item.name || "",
+ subtitle: item.comment || "",
+ icon: icon,
+ iconType: iconType,
+ section: "plugin_" + pluginId,
+ data: item,
+ pluginId: pluginId,
+ isBuiltInLauncher: true,
+ keywords: item.keywords || [],
+ actions: [],
+ primaryAction: {
+ name: openLabel,
+ icon: "open_in_new",
+ action: "execute"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: item._preScored
+ };
+}
+
+function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel, openTerminalLabel) {
+ var filename = file.path ? file.path.split("/").pop() : "";
+ var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
+ var isDir = file.is_dir || false;
+
+ var actions = [];
+ if (isDir) {
+ if (openTerminalLabel) {
+ actions.push({
+ name: openTerminalLabel,
+ icon: "terminal",
+ action: "open_terminal"
+ });
+ }
+ } else {
+ actions.push({
+ name: openFolderLabel,
+ icon: "folder_open",
+ action: "open_folder"
+ });
+ }
+ actions.push({
+ name: copyPathLabel,
+ icon: "content_copy",
+ action: "copy_path"
+ });
+
+ return {
+ id: file.path || "",
+ type: "file",
+ name: filename,
+ subtitle: dirname,
+ icon: isDir ? "folder" : Utils.getFileIcon(filename),
+ iconType: "material",
+ section: "files",
+ data: file,
+ actions: actions,
+ primaryAction: {
+ name: openLabel,
+ icon: "open_in_new",
+ action: "open"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ };
+}
+
+function transformPluginItem(item, pluginId, selectLabel) {
+ var rawIcon = item.icon || "extension";
+ var icon = Utils.stripIconPrefix(rawIcon);
+ var iconType = item.iconType;
+ if (!iconType) {
+ if (rawIcon.startsWith("material:"))
+ iconType = "material";
+ else if (rawIcon.startsWith("unicode:"))
+ iconType = "unicode";
+ else
+ iconType = "image";
+ }
+
+ return {
+ id: item.id || item.name || "",
+ type: "plugin",
+ name: item.name || "",
+ subtitle: item.comment || item.description || "",
+ icon: icon,
+ iconType: iconType,
+ section: "plugin_" + pluginId,
+ data: item,
+ pluginId: pluginId,
+ keywords: item.keywords || [],
+ actions: item.actions || [],
+ primaryAction: item.primaryAction || {
+ name: selectLabel,
+ icon: "check",
+ action: "execute"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: item._preScored
+ };
+}
+
+function createPluginBrowseItem(pluginId, plugin, trigger, isBuiltIn, isAllowed, browseLabel, triggerLabel, noTriggerLabel) {
+ var rawIcon = isBuiltIn ? (plugin.cornerIcon || "extension") : (plugin.icon || "extension");
+ return {
+ id: "browse_" + pluginId,
+ type: "plugin_browse",
+ name: plugin.name || pluginId,
+ subtitle: trigger ? triggerLabel.replace("%1", trigger) : noTriggerLabel,
+ icon: isBuiltIn ? rawIcon : Utils.stripIconPrefix(rawIcon),
+ iconType: isBuiltIn ? "material" : Utils.detectIconType(rawIcon),
+ section: "browse_plugins",
+ data: {
+ pluginId: pluginId,
+ plugin: plugin,
+ isBuiltIn: isBuiltIn
+ },
+ actions: [
+ {
+ name: "All",
+ icon: isAllowed ? "visibility" : "visibility_off",
+ action: "toggle_all_visibility"
+ }
+ ],
+ primaryAction: {
+ name: browseLabel,
+ icon: "arrow_forward",
+ action: "browse_plugin"
+ },
+ _hName: "",
+ _hSub: "",
+ _hRich: false,
+ _preScored: undefined
+ };
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContent.qml
new file mode 100644
index 0000000..4372515
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContent.qml
@@ -0,0 +1,1075 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+FocusScope {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property var parentModal: null
+ property string viewModeContext: "spotlight"
+ property alias searchField: searchField
+ property alias controller: controller
+ property alias resultsList: resultsList
+ property alias actionPanel: actionPanel
+
+ property bool editMode: false
+ property var editingApp: null
+ property string editAppId: ""
+
+ function resetScroll() {
+ resultsList.resetScroll();
+ }
+
+ function focusSearchField() {
+ searchField.forceActiveFocus();
+ }
+
+ function openEditMode(app) {
+ if (!app)
+ return;
+ editingApp = app;
+ editAppId = app.id || app.execString || app.exec || "";
+ var existing = SessionData.getAppOverride(editAppId);
+ editNameField.text = existing?.name || "";
+ editIconField.text = existing?.icon || "";
+ editCommentField.text = existing?.comment || "";
+ editEnvVarsField.text = existing?.envVars || "";
+ editExtraFlagsField.text = existing?.extraFlags || "";
+ editDgpuToggle.checked = existing?.launchOnDgpu || false;
+ editMode = true;
+ Qt.callLater(() => editNameField.forceActiveFocus());
+ }
+
+ function closeEditMode() {
+ editMode = false;
+ editingApp = null;
+ editAppId = "";
+ Qt.callLater(() => searchField.forceActiveFocus());
+ }
+
+ function saveAppOverride() {
+ var override = {};
+ if (editNameField.text.trim())
+ override.name = editNameField.text.trim();
+ if (editIconField.text.trim())
+ override.icon = editIconField.text.trim();
+ if (editCommentField.text.trim())
+ override.comment = editCommentField.text.trim();
+ if (editEnvVarsField.text.trim())
+ override.envVars = editEnvVarsField.text.trim();
+ if (editExtraFlagsField.text.trim())
+ override.extraFlags = editExtraFlagsField.text.trim();
+ if (editDgpuToggle.checked)
+ override.launchOnDgpu = true;
+ SessionData.setAppOverride(editAppId, override);
+ closeEditMode();
+ }
+
+ function resetAppOverride() {
+ SessionData.clearAppOverride(editAppId);
+ closeEditMode();
+ }
+
+ function showContextMenu(item, x, y, fromKeyboard) {
+ if (!item)
+ return;
+ if (!contextMenu.hasContextMenuActions(item))
+ return;
+ contextMenu.show(x, y, item, fromKeyboard);
+ }
+
+ anchors.fill: parent
+ focus: true
+
+ Controller {
+ id: controller
+ active: root.parentModal?.spotlightOpen ?? true
+ viewModeContext: root.viewModeContext
+
+ onItemExecuted: {
+ if (root.parentModal) {
+ root.parentModal.hide();
+ }
+ if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
+ NiriService.toggleOverview();
+ }
+ }
+ }
+
+ LauncherContextMenu {
+ id: contextMenu
+ parent: root
+ controller: root.controller
+ searchField: root.searchField
+ parentHandler: root
+
+ onEditAppRequested: app => {
+ root.openEditMode(app);
+ }
+ }
+
+ Keys.onPressed: event => {
+ if (editMode) {
+ if (event.key === Qt.Key_Escape) {
+ closeEditMode();
+ event.accepted = true;
+ }
+ return;
+ }
+
+ var hasCtrl = event.modifiers & Qt.ControlModifier;
+ event.accepted = true;
+
+ switch (event.key) {
+ case Qt.Key_Escape:
+ if (actionPanel.expanded) {
+ actionPanel.hide();
+ return;
+ }
+ if (controller.clearPluginFilter())
+ return;
+ if (root.parentModal)
+ root.parentModal.hide();
+ return;
+ case Qt.Key_Backspace:
+ if (searchField.text.length === 0) {
+ if (controller.clearPluginFilter())
+ return;
+ if (controller.autoSwitchedToFiles) {
+ controller.restorePreviousMode();
+ return;
+ }
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_Down:
+ if (hasCtrl) {
+ controller.navigateHistory("down");
+ } else {
+ controller.selectNext();
+ }
+ return;
+ case Qt.Key_Up:
+ if (hasCtrl) {
+ controller.navigateHistory("up");
+ } else {
+ controller.selectPrevious();
+ }
+ return;
+ case Qt.Key_PageDown:
+ controller.selectPageDown(8);
+ return;
+ case Qt.Key_PageUp:
+ controller.selectPageUp(8);
+ return;
+ case Qt.Key_Right:
+ if (hasCtrl) {
+ controller.cycleMode();
+ return;
+ }
+ if (controller.getCurrentSectionViewMode() !== "list") {
+ controller.selectRight();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_Left:
+ if (hasCtrl) {
+ const reverse = true;
+ controller.cycleMode(reverse);
+ return;
+ }
+ if (controller.getCurrentSectionViewMode() !== "list") {
+ controller.selectLeft();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_H:
+ if (hasCtrl) {
+ const reverse = true;
+ controller.cycleMode(reverse);
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_J:
+ if (hasCtrl) {
+ controller.selectNext();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_K:
+ if (hasCtrl) {
+ controller.selectPrevious();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_L:
+ if (hasCtrl) {
+ controller.cycleMode();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_N:
+ if (hasCtrl) {
+ controller.selectNextSection();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_P:
+ if (hasCtrl) {
+ controller.selectPreviousSection();
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_Tab:
+ if (hasCtrl && actionPanel.hasActions) {
+ actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
+ return;
+ }
+ controller.selectNext();
+ return;
+ case Qt.Key_Backtab:
+ if (hasCtrl && actionPanel.expanded) {
+ const reverse = true;
+ actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
+ return;
+ }
+ controller.selectPrevious();
+ return;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (event.modifiers & Qt.ShiftModifier) {
+ controller.pasteSelected();
+ return;
+ }
+ if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
+ actionPanel.executeSelectedAction();
+ } else {
+ controller.executeSelected();
+ }
+ return;
+ case Qt.Key_Menu:
+ case Qt.Key_F10:
+ if (contextMenu.hasContextMenuActions(controller.selectedItem)) {
+ var scenePos = resultsList.getSelectedItemPosition();
+ var localPos = root.mapFromItem(null, scenePos.x, scenePos.y);
+ showContextMenu(controller.selectedItem, localPos.x, localPos.y, true);
+ }
+ return;
+ case Qt.Key_1:
+ if (hasCtrl) {
+ controller.setMode("all");
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_2:
+ if (hasCtrl) {
+ controller.setMode("apps");
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_3:
+ if (hasCtrl) {
+ controller.setMode("files");
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_4:
+ if (hasCtrl) {
+ controller.setMode("plugins");
+ return;
+ }
+ event.accepted = false;
+ return;
+ case Qt.Key_Slash:
+ if (event.modifiers === Qt.NoModifier && searchField.text.length === 0) {
+ controller.setMode("files", true);
+ return;
+ }
+ event.accepted = false;
+ return;
+ default:
+ event.accepted = false;
+ }
+ }
+
+ Item {
+ anchors.fill: parent
+ visible: !editMode && !(root.parentModal?.isClosing ?? false)
+
+ Item {
+ id: footerBar
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: root.parentModal?.borderWidth ?? 1
+ anchors.rightMargin: root.parentModal?.borderWidth ?? 1
+ anchors.bottomMargin: root.parentModal?.borderWidth ?? 1
+ readonly property bool showFooter: SettingsData.dankLauncherV2Size !== "micro" && SettingsData.dankLauncherV2ShowFooter
+ height: showFooter ? 36 : 0
+ visible: showFooter
+ clip: true
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.topMargin: -Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ }
+
+ Row {
+ id: modeButtonsRow
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
+ spacing: 2
+
+ Repeater {
+ model: [
+ {
+ id: "all",
+ label: I18n.tr("All"),
+ icon: "search"
+ },
+ {
+ id: "apps",
+ label: I18n.tr("Apps"),
+ icon: "apps"
+ },
+ {
+ id: "files",
+ label: I18n.tr("Files"),
+ icon: "folder"
+ },
+ {
+ id: "plugins",
+ label: I18n.tr("Plugins"),
+ icon: "extension"
+ }
+ ]
+
+ Rectangle {
+ required property var modelData
+ required property int index
+
+ width: buttonContent.width + Theme.spacingM * 2
+ height: 28
+ radius: Theme.cornerRadius
+ color: controller.searchMode === modelData.id || modeArea.containsMouse ? Theme.primaryContainer : "transparent"
+
+ Row {
+ id: buttonContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: modelData.icon
+ size: 14
+ color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ MouseArea {
+ id: modeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: controller.setMode(modelData.id)
+ }
+ }
+ }
+ }
+
+ Row {
+ id: hintsRow
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ layoutDirection: I18n.isRtl ? Qt.RightToLeft : Qt.LeftToRight
+ spacing: Theme.spacingM
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "↑↓ " + I18n.tr("nav")
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "↵ " + I18n.tr("open")
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: "Ctrl-Tab " + I18n.tr("actions")
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ visible: actionPanel.hasActions
+ }
+ }
+ }
+
+ Column {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.bottom: footerBar.top
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ spacing: Theme.spacingXS
+ clip: false
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Rectangle {
+ id: pluginBadge
+ visible: controller.activePluginName.length > 0
+ width: visible ? pluginBadgeContent.implicitWidth + Theme.spacingM : 0
+ height: searchField.height
+ radius: 16
+ color: Theme.primary
+
+ Row {
+ id: pluginBadgeContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: "extension"
+ size: 14
+ color: Theme.primaryText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: controller.activePluginName
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.primaryText
+ }
+ }
+
+ Behavior on width {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ DankTextField {
+ id: searchField
+ width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
+ cornerRadius: Theme.cornerRadius
+ backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ normalBorderColor: Theme.outlineMedium
+ focusedBorderColor: Theme.primary
+ leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
+ leftIconSize: Theme.iconSize
+ leftIconColor: Theme.surfaceVariantText
+ leftIconFocusedColor: Theme.primary
+ showClearButton: true
+ textColor: Theme.surfaceText
+ font.pixelSize: Theme.fontSizeLarge
+ enabled: root.parentModal ? root.parentModal.spotlightOpen : true
+ placeholderText: ""
+ ignoreUpDownKeys: true
+ ignoreTabKeys: true
+ keyForwardTargets: [root]
+
+ onTextChanged: {
+ controller.setSearchQuery(text);
+ if (actionPanel.expanded) {
+ actionPanel.hide();
+ }
+ }
+
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Escape) {
+ if (root.parentModal) {
+ root.parentModal.hide();
+ }
+ event.accepted = true;
+ } else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter)) {
+ if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
+ actionPanel.executeSelectedAction();
+ } else {
+ controller.executeSelected();
+ }
+ event.accepted = true;
+ }
+ }
+ }
+ }
+
+ Row {
+ id: categoryRow
+ width: parent.width
+ readonly property bool showPluginCategories: controller.activePluginCategories.length > 0
+ height: showPluginCategories ? 36 : 0
+ visible: showPluginCategories
+ spacing: Theme.spacingS
+
+ clip: true
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ DankDropdown {
+ id: categoryDropdown
+ visible: categoryRow.showPluginCategories
+ width: Math.min(200, parent.width)
+ compactMode: true
+ dropdownWidth: 200
+ popupWidth: 240
+ maxPopupHeight: 300
+ enableFuzzySearch: controller.activePluginCategories.length > 8
+ currentValue: {
+ const cats = controller.activePluginCategories;
+ const current = controller.activePluginCategory;
+ if (!current)
+ return cats.length > 0 ? cats[0].name : "";
+ for (let i = 0; i < cats.length; i++) {
+ if (cats[i].id === current)
+ return cats[i].name;
+ }
+ return cats.length > 0 ? cats[0].name : "";
+ }
+ options: {
+ const cats = controller.activePluginCategories;
+ const names = [];
+ for (let i = 0; i < cats.length; i++)
+ names.push(cats[i].name);
+ return names;
+ }
+
+ onValueChanged: value => {
+ const cats = controller.activePluginCategories;
+ for (let i = 0; i < cats.length; i++) {
+ if (cats[i].name === value) {
+ controller.setActivePluginCategory(cats[i].id);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ id: fileFilterRow
+ width: parent.width
+ height: showFileFilters ? fileFilterContent.height : 0
+ visible: showFileFilters
+
+ readonly property bool showFileFilters: controller.searchMode === "files"
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Row {
+ id: fileFilterContent
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Row {
+ id: typeChips
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+ visible: DSearchService.supportsTypeFilter
+
+ Repeater {
+ model: [
+ {
+ id: "all",
+ label: I18n.tr("All"),
+ icon: "search"
+ },
+ {
+ id: "file",
+ label: I18n.tr("Files"),
+ icon: "insert_drive_file"
+ },
+ {
+ id: "dir",
+ label: I18n.tr("Folders"),
+ icon: "folder"
+ }
+ ]
+
+ Rectangle {
+ required property var modelData
+ required property int index
+
+ width: chipContent.width + Theme.spacingM * 2
+ height: sortDropdown.height
+ radius: Theme.cornerRadius
+ color: controller.fileSearchType === modelData.id || chipArea.containsMouse ? Theme.primaryContainer : "transparent"
+
+ Row {
+ id: chipContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: modelData.icon
+ size: 14
+ color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: controller.fileSearchType === modelData.id ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ MouseArea {
+ id: chipArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: controller.setFileSearchType(modelData.id)
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: 1
+ height: 20
+ anchors.verticalCenter: parent.verticalCenter
+ color: Theme.outlineMedium
+ visible: typeChips.visible
+ }
+
+ DankDropdown {
+ id: sortDropdown
+ anchors.verticalCenter: parent.verticalCenter
+ width: Math.min(130, parent.width / 3)
+ compactMode: true
+ dropdownWidth: 130
+ popupWidth: 150
+ maxPopupHeight: 200
+ currentValue: {
+ switch (controller.fileSearchSort) {
+ case "score":
+ return I18n.tr("Score");
+ case "name":
+ return I18n.tr("Name");
+ case "modified":
+ return I18n.tr("Modified");
+ case "size":
+ return I18n.tr("Size");
+ default:
+ return I18n.tr("Score");
+ }
+ }
+ options: [I18n.tr("Score"), I18n.tr("Name"), I18n.tr("Modified"), I18n.tr("Size")]
+
+ onValueChanged: value => {
+ var sortMap = {};
+ sortMap[I18n.tr("Score")] = "score";
+ sortMap[I18n.tr("Name")] = "name";
+ sortMap[I18n.tr("Modified")] = "modified";
+ sortMap[I18n.tr("Size")] = "size";
+ controller.setFileSearchSort(sortMap[value] || "score");
+ }
+ }
+
+ DankTextField {
+ id: extFilterField
+ anchors.verticalCenter: parent.verticalCenter
+ width: Math.min(100, parent.width / 4)
+ height: sortDropdown.height
+ placeholderText: I18n.tr("ext")
+ font.pixelSize: Theme.fontSizeSmall
+ showClearButton: text.length > 0
+
+ onTextChanged: {
+ controller.setFileSearchExt(text.trim());
+ }
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
+ ResultsList {
+ id: resultsList
+ anchors.fill: parent
+ controller: root.controller
+
+ onItemRightClicked: (index, item, sceneX, sceneY) => {
+ if (item && contextMenu.hasContextMenuActions(item)) {
+ var localPos = root.mapFromItem(null, sceneX, sceneY);
+ root.showContextMenu(item, localPos.x, localPos.y, false);
+ }
+ }
+ }
+ }
+
+ ActionPanel {
+ id: actionPanel
+ width: parent.width
+ selectedItem: controller.selectedItem
+ controller: controller
+ }
+ }
+ }
+
+ Connections {
+ target: controller
+ function onSelectedItemChanged() {
+ if (actionPanel.expanded && !actionPanel.hasActions) {
+ actionPanel.hide();
+ }
+ }
+ function onSearchQueryRequested(query) {
+ searchField.text = query;
+ searchField.cursorPosition = query.length;
+ }
+ function onModeChanged() {
+ extFilterField.text = "";
+ }
+ }
+
+ FocusScope {
+ id: editView
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ visible: editMode
+ focus: editMode
+
+ Keys.onPressed: event => {
+ if (event.key === Qt.Key_Escape) {
+ closeEditMode();
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ if (event.modifiers & Qt.ControlModifier) {
+ saveAppOverride();
+ event.accepted = true;
+ }
+ } else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) {
+ saveAppOverride();
+ event.accepted = true;
+ }
+ }
+
+ Column {
+ anchors.fill: parent
+ spacing: Theme.spacingM
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: 40
+ height: 40
+ radius: Theme.cornerRadius
+ color: backButtonArea.containsMouse ? Theme.surfaceHover : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "arrow_back"
+ size: 20
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: backButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: closeEditMode()
+ }
+ }
+
+ Image {
+ width: 40
+ height: 40
+ source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable")
+ sourceSize.width: 40
+ sourceSize.height: 40
+ fillMode: Image.PreserveAspectFit
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ StyledText {
+ text: I18n.tr("Edit App")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: editingApp?.name || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ }
+
+ Flickable {
+ width: parent.width
+ height: parent.height - y - buttonsRow.height - Theme.spacingM
+ contentHeight: editFieldsColumn.height
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+
+ Column {
+ id: editFieldsColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Name")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ DankTextField {
+ id: editNameField
+ width: parent.width
+ placeholderText: editingApp?.name || ""
+ keyNavigationTab: editIconField
+ keyNavigationBacktab: editExtraFlagsField
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Icon")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ DankTextField {
+ id: editIconField
+ width: parent.width
+ placeholderText: editingApp?.icon || ""
+ keyNavigationTab: editCommentField
+ keyNavigationBacktab: editNameField
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Description")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ DankTextField {
+ id: editCommentField
+ width: parent.width
+ placeholderText: editingApp?.comment || ""
+ keyNavigationTab: editEnvVarsField
+ keyNavigationBacktab: editIconField
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Environment Variables")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: "KEY=value KEY2=value2"
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ }
+
+ DankTextField {
+ id: editEnvVarsField
+ width: parent.width
+ placeholderText: "VAR=value"
+ keyNavigationTab: editExtraFlagsField
+ keyNavigationBacktab: editCommentField
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Extra Arguments")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ DankTextField {
+ id: editExtraFlagsField
+ width: parent.width
+ placeholderText: "--flag --option=value"
+ keyNavigationTab: editNameField
+ keyNavigationBacktab: editEnvVarsField
+ }
+ }
+
+ DankToggle {
+ id: editDgpuToggle
+ width: parent.width
+ text: I18n.tr("Launch on dGPU by default")
+ visible: SessionService.nvidiaCommand.length > 0
+ checked: false
+ onToggled: checked => editDgpuToggle.checked = checked
+ }
+ }
+ }
+
+ Row {
+ id: buttonsRow
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ id: resetButton
+ width: 90
+ height: 40
+ radius: Theme.cornerRadius
+ color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
+ visible: SessionData.getAppOverride(editAppId) !== null
+
+ StyledText {
+ text: I18n.tr("Reset")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.error
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: resetButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: resetAppOverride()
+ }
+ }
+
+ Rectangle {
+ id: cancelButton
+ width: 90
+ height: 40
+ radius: Theme.cornerRadius
+ color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
+
+ StyledText {
+ text: I18n.tr("Cancel")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: cancelButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: closeEditMode()
+ }
+ }
+
+ Rectangle {
+ id: saveButton
+ width: 90
+ height: 40
+ radius: Theme.cornerRadius
+ color: saveButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.9) : Theme.primary
+
+ StyledText {
+ text: I18n.tr("Save")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.primaryText
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: saveButtonArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: saveAppOverride()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
new file mode 100644
index 0000000..3c55590
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
@@ -0,0 +1,510 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Popup {
+ id: root
+
+ property var item: null
+ property var controller: null
+ property var searchField: null
+ property var parentHandler: null
+
+ signal hideRequested
+ signal editAppRequested(var app)
+
+ function hasContextMenuActions(spotlightItem) {
+ if (!spotlightItem)
+ return false;
+ if (spotlightItem.type === "app")
+ return true;
+ if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
+ var instance = PluginService.pluginInstances[spotlightItem.pluginId];
+ if (!instance)
+ return false;
+ if (typeof instance.getContextMenuActions !== "function")
+ return false;
+ var actions = instance.getContextMenuActions(spotlightItem.data);
+ return Array.isArray(actions) && actions.length > 0;
+ }
+ return false;
+ }
+
+ readonly property bool isCoreApp: item?.type === "app" && !!item?.isCore
+ readonly property var coreAppData: isCoreApp ? item?.data ?? null : null
+ readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null
+ readonly property string appId: {
+ if (isCoreApp) {
+ return item?.id || coreAppData?.builtInPluginId || "";
+ }
+ return desktopEntry?.id || desktopEntry?.execString || "";
+ }
+ readonly property bool isPinned: appId ? SessionData.isPinnedApp(appId) : false
+ readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
+ readonly property bool isPluginItem: item?.type === "plugin"
+
+ function getPluginContextMenuActions() {
+ if (!isPluginItem || !item?.pluginId)
+ return [];
+
+ var instance = PluginService.pluginInstances[item.pluginId];
+ if (!instance)
+ return [];
+ if (typeof instance.getContextMenuActions !== "function")
+ return [];
+
+ var actions = instance.getContextMenuActions(item.data);
+ if (!Array.isArray(actions))
+ return [];
+
+ return actions;
+ }
+
+ function executePluginAction(actionOrObj) {
+ var actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
+ var closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
+
+ if (typeof actionFunc === "function")
+ actionFunc();
+
+ if (closeLauncher) {
+ controller?.itemExecuted();
+ } else {
+ controller?.performSearch();
+ }
+ hide();
+ }
+
+ readonly property var menuItems: {
+ var items = [];
+
+ if (isPluginItem) {
+ var pluginActions = getPluginContextMenuActions();
+ for (var i = 0; i < pluginActions.length; i++) {
+ var act = pluginActions[i];
+ items.push({
+ type: "item",
+ icon: act.icon || "play_arrow",
+ text: act.text || act.name || "",
+ pluginAction: act
+ });
+ }
+ return items;
+ }
+
+ if (item?.type === "app") {
+ items.push({
+ type: "item",
+ icon: isPinned ? "keep_off" : "push_pin",
+ text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
+ action: togglePin
+ });
+ }
+
+ if (isRegularApp) {
+ items.push({
+ type: "item",
+ icon: "visibility_off",
+ text: I18n.tr("Hide App"),
+ action: hideCurrentApp
+ });
+ items.push({
+ type: "item",
+ icon: "edit",
+ text: I18n.tr("Edit App"),
+ action: editCurrentApp
+ });
+ }
+
+ if (item?.actions && item.actions.length > 0) {
+ items.push({
+ type: "separator"
+ });
+ for (var i = 0; i < item.actions.length; i++) {
+ var act = item.actions[i];
+ items.push({
+ type: "item",
+ icon: act.icon || "play_arrow",
+ text: act.name || "",
+ actionData: act
+ });
+ }
+ }
+
+ items.push({
+ type: "separator"
+ });
+
+ if (isRegularApp && SessionService.nvidiaCommand) {
+ items.push({
+ type: "item",
+ icon: "memory",
+ text: I18n.tr("Launch on dGPU"),
+ action: launchWithNvidia
+ });
+ }
+
+ items.push({
+ type: "item",
+ icon: "launch",
+ text: I18n.tr("Launch"),
+ action: launchApp
+ });
+
+ return items;
+ }
+
+ function show(x, y, spotlightItem, fromKeyboard) {
+ if (!spotlightItem?.data)
+ return;
+ item = spotlightItem;
+ selectedMenuIndex = fromKeyboard ? 0 : -1;
+ keyboardNavigation = fromKeyboard;
+
+ if (parentHandler)
+ parentHandler.enabled = false;
+
+ Qt.callLater(() => {
+ var parentW = parent?.width ?? 500;
+ var parentH = parent?.height ?? 600;
+ var menuW = width > 0 ? width : 200;
+ var menuH = height > 0 ? height : 200;
+ var margin = 8;
+
+ var posX = x + 4;
+ var posY = y + 4;
+
+ if (posX + menuW > parentW - margin) {
+ posX = Math.max(margin, parentW - menuW - margin);
+ }
+ if (posY + menuH > parentH - margin) {
+ posY = Math.max(margin, parentH - menuH - margin);
+ }
+
+ root.x = posX;
+ root.y = posY;
+ open();
+ });
+ }
+
+ function hide() {
+ if (parentHandler)
+ parentHandler.enabled = true;
+ close();
+ }
+
+ function togglePin() {
+ if (!appId)
+ return;
+ if (isPinned)
+ SessionData.removePinnedApp(appId);
+ else
+ SessionData.addPinnedApp(appId);
+ hide();
+ }
+
+ function hideCurrentApp() {
+ if (!appId)
+ return;
+ SessionData.hideApp(appId);
+ controller?.performSearch();
+ hide();
+ }
+
+ function editCurrentApp() {
+ if (!desktopEntry)
+ return;
+ editAppRequested(desktopEntry);
+ hide();
+ }
+
+ function launchApp() {
+ if (isCoreApp) {
+ if (!coreAppData)
+ return;
+ AppSearchService.executeCoreApp(coreAppData);
+ controller?.itemExecuted();
+ hide();
+ return;
+ }
+ if (!desktopEntry)
+ return;
+ SessionService.launchDesktopEntry(desktopEntry);
+ AppUsageHistoryData.addAppUsage(desktopEntry);
+ controller?.itemExecuted();
+ hide();
+ }
+
+ function launchWithNvidia() {
+ if (!desktopEntry)
+ return;
+ SessionService.launchDesktopEntry(desktopEntry, true);
+ AppUsageHistoryData.addAppUsage(desktopEntry);
+ controller?.itemExecuted();
+ hide();
+ }
+
+ function executeDesktopAction(actionData) {
+ if (!desktopEntry || !actionData)
+ return;
+ SessionService.launchDesktopAction(desktopEntry, actionData.actionData || actionData);
+ AppUsageHistoryData.addAppUsage(desktopEntry);
+ controller?.itemExecuted();
+ hide();
+ }
+
+ property int selectedMenuIndex: 0
+ property bool keyboardNavigation: false
+
+ readonly property int visibleItemCount: {
+ var count = 0;
+ for (var i = 0; i < menuItems.length; i++) {
+ if (menuItems[i].type === "item")
+ count++;
+ }
+ return count;
+ }
+
+ function selectNext() {
+ if (visibleItemCount > 0)
+ selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
+ }
+
+ function selectPrevious() {
+ if (visibleItemCount > 0)
+ selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
+ }
+
+ function activateSelected() {
+ var itemIndex = 0;
+ for (var i = 0; i < menuItems.length; i++) {
+ if (menuItems[i].type !== "item")
+ continue;
+ if (itemIndex === selectedMenuIndex) {
+ var menuItem = menuItems[i];
+ if (menuItem.action)
+ menuItem.action();
+ else if (menuItem.pluginAction)
+ executePluginAction(menuItem.pluginAction);
+ else if (menuItem.actionData)
+ executeDesktopAction(menuItem.actionData);
+ return;
+ }
+ itemIndex++;
+ }
+ }
+
+ width: menuContainer.implicitWidth
+ height: menuContainer.implicitHeight
+ padding: 0
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+ modal: true
+ dim: false
+ background: Item {}
+
+ onOpened: {
+ Qt.callLater(() => keyboardHandler.forceActiveFocus());
+ }
+
+ onClosed: {
+ if (parentHandler)
+ parentHandler.enabled = true;
+ if (searchField?.visible) {
+ Qt.callLater(() => searchField.forceActiveFocus());
+ }
+ }
+
+ 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
+ }
+ }
+
+ contentItem: Item {
+ id: keyboardHandler
+ focus: true
+ implicitWidth: menuContainer.implicitWidth
+ implicitHeight: menuContainer.implicitHeight
+
+ Keys.onPressed: event => {
+ switch (event.key) {
+ case Qt.Key_Down:
+ root.selectNext();
+ event.accepted = true;
+ return;
+ case Qt.Key_Up:
+ root.selectPrevious();
+ event.accepted = true;
+ return;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ root.activateSelected();
+ event.accepted = true;
+ return;
+ case Qt.Key_Escape:
+ case Qt.Key_Left:
+ root.hide();
+ event.accepted = true;
+ return;
+ }
+ }
+
+ Rectangle {
+ id: menuContainer
+ anchors.fill: parent
+ implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
+ implicitHeight: 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
+
+ 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
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ spacing: 1
+
+ Repeater {
+ model: root.menuItems
+
+ Item {
+ id: menuItemDelegate
+ required property var modelData
+ required property int index
+
+ width: menuColumn.width
+ height: modelData.type === "separator" ? 5 : 32
+
+ readonly property int itemIndex: {
+ var count = 0;
+ for (var i = 0; i < index; i++) {
+ if (root.menuItems[i].type === "item")
+ count++;
+ }
+ return count;
+ }
+
+ Rectangle {
+ visible: menuItemDelegate.modelData.type === "separator"
+ width: parent.width - Theme.spacingS * 2
+ height: parent.height
+ 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: menuItemDelegate.modelData.type === "item"
+ width: parent.width
+ height: parent.height
+ radius: Theme.cornerRadius
+ color: {
+ if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
+ }
+ return itemMouseArea.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.spacingS
+
+ Item {
+ width: Theme.iconSize - 2
+ height: Theme.iconSize - 2
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
+ name: menuItemDelegate.modelData?.icon ?? ""
+ size: Theme.iconSize - 2
+ color: Theme.surfaceText
+ opacity: 0.7
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ StyledText {
+ text: menuItemDelegate.modelData.text || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Normal
+ anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
+ }
+ }
+
+ DankRipple {
+ id: menuItemRipple
+ rippleColor: Theme.surfaceText
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: itemMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onEntered: {
+ root.keyboardNavigation = false;
+ root.selectedMenuIndex = menuItemDelegate.itemIndex;
+ }
+ onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ var menuItem = menuItemDelegate.modelData;
+ if (menuItem.action)
+ menuItem.action();
+ else if (menuItem.pluginAction)
+ root.executePluginAction(menuItem.pluginAction);
+ else if (menuItem.actionData)
+ root.executeDesktopAction(menuItem.actionData);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/NavigationHelpers.js b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/NavigationHelpers.js
new file mode 100644
index 0000000..1e78b11
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/NavigationHelpers.js
@@ -0,0 +1,254 @@
+.pragma library
+
+function getFirstItemIndex(flatModel) {
+ for (var i = 0; i < flatModel.length; i++) {
+ if (!flatModel[i].isHeader)
+ return i;
+ }
+ return 0;
+}
+
+function findNextNonHeaderIndex(flatModel, startIndex) {
+ for (var i = startIndex; i < flatModel.length; i++) {
+ if (!flatModel[i].isHeader)
+ return i;
+ }
+ return -1;
+}
+
+function findPrevNonHeaderIndex(flatModel, startIndex) {
+ for (var i = startIndex; i >= 0; i--) {
+ if (!flatModel[i].isHeader)
+ return i;
+ }
+ return -1;
+}
+
+function getSectionBounds(flatModel, sectionId) {
+ if (flatModel._sectionBounds && flatModel._sectionBounds[sectionId])
+ return flatModel._sectionBounds[sectionId];
+
+ var start = -1, end = -1;
+ for (var i = 0; i < flatModel.length; i++) {
+ if (flatModel[i].isHeader && flatModel[i].section?.id === sectionId) {
+ start = i + 1;
+ } else if (start >= 0 && !flatModel[i].isHeader && flatModel[i].sectionId === sectionId) {
+ end = i;
+ } else if (start >= 0 && end >= 0 && flatModel[i].sectionId !== sectionId) {
+ break;
+ }
+ }
+ return {
+ start: start,
+ end: end,
+ count: end >= start ? end - start + 1 : 0
+ };
+}
+
+function getGridColumns(viewMode, gridColumns) {
+ switch (viewMode) {
+ case "tile":
+ return 3;
+ case "grid":
+ return gridColumns;
+ default:
+ return 1;
+ }
+}
+
+function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader) {
+ var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
+ return next !== -1 ? next : selectedFlatIndex;
+ }
+
+ var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
+ if (actualViewMode === "list") {
+ var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
+ return next !== -1 ? next : selectedFlatIndex;
+ }
+
+ var bounds = getSectionBounds(flatModel, entry.sectionId);
+ var cols = getGridColumns(actualViewMode, gridColumns);
+ var posInSection = selectedFlatIndex - bounds.start;
+ var newPosInSection = posInSection + cols;
+
+ if (newPosInSection < bounds.count) {
+ return bounds.start + newPosInSection;
+ }
+
+ var currentRow = Math.floor(posInSection / cols);
+ var lastRow = Math.floor((bounds.count - 1) / cols);
+ if (currentRow < lastRow) {
+ return bounds.start + bounds.count - 1;
+ }
+
+ var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
+ return nextSection !== -1 ? nextSection : selectedFlatIndex;
+}
+
+function calculatePrevIndex(flatModel, selectedFlatIndex, sectionId, viewMode, gridColumns, getSectionViewModeFn) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader) {
+ var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
+ return prev !== -1 ? prev : selectedFlatIndex;
+ }
+
+ var actualViewMode = viewMode || getSectionViewModeFn(entry.sectionId);
+ if (actualViewMode === "list") {
+ var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
+ return prev !== -1 ? prev : selectedFlatIndex;
+ }
+
+ var bounds = getSectionBounds(flatModel, entry.sectionId);
+ var cols = getGridColumns(actualViewMode, gridColumns);
+ var posInSection = selectedFlatIndex - bounds.start;
+ var newPosInSection = posInSection - cols;
+
+ if (newPosInSection >= 0) {
+ return bounds.start + newPosInSection;
+ }
+
+ var prevItem = findPrevNonHeaderIndex(flatModel, bounds.start - 1);
+ return prevItem !== -1 ? prevItem : selectedFlatIndex;
+}
+
+function calculateRightIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader) {
+ var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
+ return next !== -1 ? next : selectedFlatIndex;
+ }
+
+ var viewMode = getSectionViewModeFn(entry.sectionId);
+ if (viewMode === "list") {
+ var next = findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1);
+ return next !== -1 ? next : selectedFlatIndex;
+ }
+
+ var bounds = getSectionBounds(flatModel, entry.sectionId);
+ var posInSection = selectedFlatIndex - bounds.start;
+ if (posInSection + 1 < bounds.count) {
+ return bounds.start + posInSection + 1;
+ }
+ return selectedFlatIndex;
+}
+
+function calculateLeftIndex(flatModel, selectedFlatIndex, getSectionViewModeFn) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var entry = flatModel[selectedFlatIndex];
+ if (!entry || entry.isHeader) {
+ var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
+ return prev !== -1 ? prev : selectedFlatIndex;
+ }
+
+ var viewMode = getSectionViewModeFn(entry.sectionId);
+ if (viewMode === "list") {
+ var prev = findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1);
+ return prev !== -1 ? prev : selectedFlatIndex;
+ }
+
+ var bounds = getSectionBounds(flatModel, entry.sectionId);
+ var posInSection = selectedFlatIndex - bounds.start;
+ if (posInSection > 0) {
+ return bounds.start + posInSection - 1;
+ }
+ return selectedFlatIndex;
+}
+
+function calculateNextSectionIndex(flatModel, selectedFlatIndex) {
+ var currentSection = null;
+ if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
+ currentSection = flatModel[selectedFlatIndex].sectionId;
+ }
+
+ var foundCurrent = false;
+ for (var i = 0; i < flatModel.length; i++) {
+ if (flatModel[i].isHeader) {
+ if (foundCurrent) {
+ for (var j = i + 1; j < flatModel.length; j++) {
+ if (!flatModel[j].isHeader)
+ return j;
+ }
+ }
+ if (flatModel[i].section.id === currentSection) {
+ foundCurrent = true;
+ }
+ }
+ }
+ return selectedFlatIndex;
+}
+
+function calculatePrevSectionIndex(flatModel, selectedFlatIndex) {
+ var currentSection = null;
+ if (selectedFlatIndex >= 0 && selectedFlatIndex < flatModel.length) {
+ currentSection = flatModel[selectedFlatIndex].sectionId;
+ }
+
+ var lastSectionStart = -1;
+ var prevSectionStart = -1;
+
+ for (var i = 0; i < flatModel.length; i++) {
+ if (flatModel[i].isHeader) {
+ if (flatModel[i].section.id === currentSection) {
+ break;
+ }
+ prevSectionStart = lastSectionStart;
+ lastSectionStart = i;
+ }
+ }
+
+ if (prevSectionStart >= 0) {
+ for (var j = prevSectionStart + 1; j < flatModel.length; j++) {
+ if (!flatModel[j].isHeader)
+ return j;
+ }
+ }
+ return selectedFlatIndex;
+}
+
+function calculatePageDownIndex(flatModel, selectedFlatIndex, visibleItems) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var itemsToSkip = visibleItems || 8;
+ var newIndex = selectedFlatIndex;
+
+ for (var i = 0; i < itemsToSkip; i++) {
+ var next = findNextNonHeaderIndex(flatModel, newIndex + 1);
+ if (next === -1)
+ break;
+ newIndex = next;
+ }
+
+ return newIndex;
+}
+
+function calculatePageUpIndex(flatModel, selectedFlatIndex, visibleItems) {
+ if (flatModel.length === 0)
+ return selectedFlatIndex;
+
+ var itemsToSkip = visibleItems || 8;
+ var newIndex = selectedFlatIndex;
+
+ for (var i = 0; i < itemsToSkip; i++) {
+ var prev = findPrevNonHeaderIndex(flatModel, newIndex - 1);
+ if (prev === -1)
+ break;
+ newIndex = prev;
+ }
+
+ return newIndex;
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultItem.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultItem.qml
new file mode 100644
index 0000000..0a50cd3
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultItem.qml
@@ -0,0 +1,196 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var item: null
+ property bool isSelected: false
+ property bool isHovered: itemArea.containsMouse || allModeToggleArea.containsMouse
+ property var controller: null
+ property int flatIndex: -1
+
+ signal clicked
+ signal rightClicked(real mouseX, real mouseY)
+
+ readonly property string iconValue: {
+ if (!item)
+ return "";
+ switch (item.iconType) {
+ case "material":
+ case "nerd":
+ return "material:" + (item.icon || "apps");
+ case "unicode":
+ return "unicode:" + (item.icon || "");
+ case "composite":
+ return item.iconFull || "";
+ case "image":
+ default:
+ return item.icon || "";
+ }
+ }
+
+ width: parent?.width ?? 200
+ height: 52
+ color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryHoverLight : "transparent"
+ radius: Theme.cornerRadius
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: Theme.surfaceText
+ cornerRadius: root.radius
+ }
+
+ MouseArea {
+ id: itemArea
+ z: 1
+ anchors.fill: parent
+ anchors.rightMargin: root.item?.type === "plugin_browse" ? 40 : 0
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+
+ onPressed: mouse => {
+ if (mouse.button === Qt.LeftButton)
+ rippleLayer.trigger(mouse.x, mouse.y);
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ var scenePos = mapToItem(null, mouse.x, mouse.y);
+ root.rightClicked(scenePos.x, scenePos.y);
+ } else {
+ root.clicked();
+ }
+ }
+
+ onPositionChanged: {
+ if (root.controller)
+ root.controller.keyboardNavigationActive = false;
+ }
+ }
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ AppIconRenderer {
+ width: 36
+ height: 36
+ anchors.verticalCenter: parent.verticalCenter
+ iconValue: root.iconValue
+ iconSize: 36
+ fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
+ materialIconSizeAdjustment: 12
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - 36 - Theme.spacingM * 3 - rightContent.width
+ spacing: 2
+
+ Text {
+ width: parent.width
+ text: root.item?._hName ?? root.item?.name ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ font.family: Theme.fontFamily
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ Text {
+ width: parent.width
+ text: root.item?._hSub ?? root.item?.subtitle ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: Theme.fontFamily
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ clip: true
+ visible: (root.item?.subtitle ?? "").length > 0
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ Row {
+ id: rightContent
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ Rectangle {
+ id: allModeToggle
+ visible: root.item?.type === "plugin_browse"
+ width: 28
+ height: 28
+ radius: 14
+ anchors.verticalCenter: parent.verticalCenter
+ color: allModeToggleArea.containsMouse ? Theme.surfaceHover : "transparent"
+
+ property bool isAllowed: {
+ if (root.item?.type !== "plugin_browse")
+ return false;
+ var pluginId = root.item?.data?.pluginId;
+ if (!pluginId)
+ return false;
+ SettingsData.launcherPluginVisibility;
+ return SettingsData.getPluginAllowWithoutTrigger(pluginId);
+ }
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: allModeToggle.isAllowed ? "visibility" : "visibility_off"
+ size: 18
+ color: allModeToggle.isAllowed ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: allModeToggleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ var pluginId = root.item?.data?.pluginId;
+ if (!pluginId)
+ return;
+ SettingsData.setPluginAllowWithoutTrigger(pluginId, !allModeToggle.isAllowed);
+ }
+ }
+ }
+
+ Rectangle {
+ visible: !!root.item?.type && root.item.type !== "app" && root.item.type !== "plugin_browse"
+ width: typeBadge.implicitWidth + Theme.spacingS * 2
+ height: 20
+ radius: 10
+ color: Theme.surfaceVariantAlpha
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ id: typeBadge
+ anchors.centerIn: parent
+ text: {
+ if (!root.item)
+ return "";
+ switch (root.item.type) {
+ case "plugin":
+ return I18n.tr("Plugin");
+ case "file":
+ return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File");
+ default:
+ return "";
+ }
+ }
+ font.pixelSize: Theme.fontSizeSmall - 2
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultsList.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultsList.qml
new file mode 100644
index 0000000..53a2d45
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/ResultsList.qml
@@ -0,0 +1,498 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property var controller: null
+ property int gridColumns: controller?.gridColumns ?? 4
+ property var _visualRows: []
+ property var _flatIndexToRowMap: ({})
+ property var _cumulativeHeights: []
+
+ signal itemRightClicked(int index, var item, real mouseX, real mouseY)
+
+ function _rebuildVisualModel() {
+ var sections = root.controller?.sections ?? [];
+ var rows = [];
+ var indexMap = {};
+ var cumHeights = [];
+ var cumY = 0;
+
+ for (var s = 0; s < sections.length; s++) {
+ var section = sections[s];
+ var sectionId = section.id;
+
+ cumHeights.push(cumY);
+ rows.push({
+ _rowId: "h_" + sectionId,
+ type: "header",
+ section: section,
+ sectionId: sectionId,
+ height: 32
+ });
+ cumY += 32;
+
+ if (section.collapsed)
+ continue;
+
+ var versionTrigger = root.controller?.viewModeVersion ?? 0;
+ void (versionTrigger);
+ var mode = root.controller?.getSectionViewMode(sectionId) ?? "list";
+ var items = section.items ?? [];
+ var flatStartIndex = section.flatStartIndex ?? 0;
+
+ if (mode === "list") {
+ for (var i = 0; i < items.length; i++) {
+ var flatIdx = flatStartIndex + i;
+ indexMap[flatIdx] = rows.length;
+ cumHeights.push(cumY);
+ rows.push({
+ _rowId: items[i].id,
+ type: "list_item",
+ item: items[i],
+ flatIndex: flatIdx,
+ sectionId: sectionId,
+ height: 52
+ });
+ cumY += 52;
+ }
+ } else {
+ var cols = root.controller?.getGridColumns(sectionId) ?? root.gridColumns;
+ var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / root.gridColumns);
+ var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
+ var numRows = Math.ceil(items.length / cols);
+
+ for (var r = 0; r < numRows; r++) {
+ var rowItems = [];
+ for (var c = 0; c < cols; c++) {
+ var idx = r * cols + c;
+ if (idx >= items.length)
+ break;
+ var fi = flatStartIndex + idx;
+ indexMap[fi] = rows.length;
+ rowItems.push({
+ item: items[idx],
+ flatIndex: fi
+ });
+ }
+ cumHeights.push(cumY);
+ rows.push({
+ _rowId: "gr_" + sectionId + "_" + r,
+ type: "grid_row",
+ items: rowItems,
+ sectionId: sectionId,
+ viewMode: mode,
+ cols: cols,
+ height: cellHeight
+ });
+ cumY += cellHeight;
+ }
+ }
+ }
+
+ root._flatIndexToRowMap = indexMap;
+ root._cumulativeHeights = cumHeights;
+ root._visualRows = rows;
+ }
+
+ onGridColumnsChanged: Qt.callLater(_rebuildVisualModel)
+ onWidthChanged: Qt.callLater(_rebuildVisualModel)
+
+ Connections {
+ target: root.controller
+ function onSectionsChanged() {
+ Qt.callLater(root._rebuildVisualModel);
+ }
+ function onViewModeVersionChanged() {
+ Qt.callLater(root._rebuildVisualModel);
+ }
+ function onSearchModeChanged() {
+ root._visualRows = [];
+ root._cumulativeHeights = [];
+ root._flatIndexToRowMap = {};
+ }
+ }
+
+ function resetScroll() {
+ mainListView.contentY = mainListView.originY;
+ }
+
+ function ensureVisible(index) {
+ if (index < 0 || !controller?.flatModel || index >= controller.flatModel.length)
+ return;
+ var entry = controller.flatModel[index];
+ if (!entry || entry.isHeader)
+ return;
+ var rowIndex = _flatIndexToRowMap[index];
+ if (rowIndex === undefined)
+ return;
+
+ mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
+
+ if (stickyHeader.visible && rowIndex < _cumulativeHeights.length) {
+ var rowY = _cumulativeHeights[rowIndex];
+ var scrollY = mainListView.contentY - mainListView.originY;
+ if (rowY < scrollY + stickyHeader.height) {
+ mainListView.contentY = Math.max(mainListView.originY, rowY - stickyHeader.height + mainListView.originY);
+ }
+ }
+ }
+
+ function getSelectedItemPosition() {
+ var fallback = mapToItem(null, width / 2, height / 2);
+ if (!controller?.flatModel || controller.selectedFlatIndex < 0)
+ return fallback;
+
+ var entry = controller.flatModel[controller.selectedFlatIndex];
+ if (!entry || entry.isHeader)
+ return fallback;
+
+ var rowIndex = _flatIndexToRowMap[controller.selectedFlatIndex];
+ if (rowIndex === undefined)
+ return fallback;
+
+ var rowY = (rowIndex < _cumulativeHeights.length) ? _cumulativeHeights[rowIndex] : 0;
+ var row = _visualRows[rowIndex];
+ if (!row)
+ return fallback;
+
+ var itemX = width / 2;
+ var itemH = row.height;
+
+ if (row.type === "grid_row") {
+ var rowItems = row.items;
+ for (var i = 0; i < rowItems.length; i++) {
+ if (rowItems[i].flatIndex === controller.selectedFlatIndex) {
+ var cellWidth = row.viewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / row.cols);
+ itemX = i * cellWidth + cellWidth / 2;
+ break;
+ }
+ }
+ }
+
+ var visualY = rowY - mainListView.contentY + mainListView.originY + itemH / 2;
+ var clampedY = Math.max(40, Math.min(height - 40, visualY));
+ return mapToItem(null, itemX, clampedY);
+ }
+
+ Connections {
+ target: root.controller
+ function onSelectedFlatIndexChanged() {
+ if (root.controller?.keyboardNavigationActive) {
+ Qt.callLater(() => root.ensureVisible(root.controller.selectedFlatIndex));
+ }
+ }
+ }
+
+ DankListView {
+ id: mainListView
+ anchors.fill: parent
+ clip: true
+ scrollBarTopMargin: (root.controller?.sections?.length > 0) ? 32 : 0
+
+ model: ScriptModel {
+ values: root._visualRows
+ objectProp: "_rowId"
+ }
+
+ add: null
+ remove: null
+ displaced: null
+ move: null
+
+ delegate: Item {
+ id: delegateRoot
+ required property var modelData
+ required property int index
+
+ width: mainListView.width
+ height: modelData?.height ?? 52
+
+ SectionHeader {
+ anchors.fill: parent
+ visible: delegateRoot.modelData?.type === "header"
+ section: delegateRoot.modelData?.section ?? null
+ controller: root.controller
+ viewMode: {
+ var vt = root.controller?.viewModeVersion ?? 0;
+ void (vt);
+ return root.controller?.getSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? "list";
+ }
+ canChangeViewMode: {
+ var vt = root.controller?.viewModeVersion ?? 0;
+ void (vt);
+ return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
+ }
+ canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
+ }
+
+ ResultItem {
+ anchors.fill: parent
+ visible: delegateRoot.modelData?.type === "list_item"
+ item: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.item ?? null) : null
+ isSelected: delegateRoot.modelData?.type === "list_item" && (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: delegateRoot.modelData?.type === "list_item" ? (delegateRoot.modelData?.flatIndex ?? -1) : -1
+
+ onClicked: {
+ if (root.controller && delegateRoot.modelData?.item) {
+ root.controller.executeItem(delegateRoot.modelData.item);
+ }
+ }
+
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
+ }
+ }
+
+ Row {
+ id: gridRowContent
+ anchors.fill: parent
+ visible: delegateRoot.modelData?.type === "grid_row"
+
+ Repeater {
+ model: delegateRoot.modelData?.type === "grid_row" ? (delegateRoot.modelData?.items ?? []) : []
+
+ Item {
+ id: gridCellDelegate
+ required property var modelData
+ required property int index
+
+ readonly property real cellWidth: delegateRoot.modelData?.viewMode === "tile" ? Math.floor(delegateRoot.width / 3) : Math.floor(delegateRoot.width / (delegateRoot.modelData?.cols ?? root.gridColumns))
+
+ width: cellWidth
+ height: delegateRoot.height
+
+ GridItem {
+ width: parent.width - 4
+ height: parent.height - 4
+ anchors.centerIn: parent
+ visible: delegateRoot.modelData?.viewMode === "grid"
+ item: gridCellDelegate.modelData?.item ?? null
+ isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
+
+ onClicked: {
+ if (root.controller && gridCellDelegate.modelData?.item) {
+ root.controller.executeItem(gridCellDelegate.modelData.item);
+ }
+ }
+
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
+ }
+ }
+
+ TileItem {
+ width: parent.width - 4
+ height: parent.height - 4
+ anchors.centerIn: parent
+ visible: delegateRoot.modelData?.viewMode === "tile"
+ item: gridCellDelegate.modelData?.item ?? null
+ isSelected: (gridCellDelegate.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: gridCellDelegate.modelData?.flatIndex ?? -1
+
+ onClicked: {
+ if (root.controller && gridCellDelegate.modelData?.item) {
+ root.controller.executeItem(gridCellDelegate.modelData.item);
+ }
+ }
+
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(gridCellDelegate.modelData?.flatIndex ?? -1, gridCellDelegate.modelData?.item ?? null, mouseX, mouseY);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: bottomShadow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: 24
+ z: 100
+ visible: {
+ if (BlurService.enabled)
+ return false;
+ if (mainListView.contentHeight <= mainListView.height)
+ return false;
+ var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
+ if (atBottom)
+ return false;
+
+ var flatModel = root.controller?.flatModel;
+ if (!flatModel || flatModel.length === 0)
+ return false;
+ var lastItemIdx = -1;
+ for (var i = flatModel.length - 1; i >= 0; i--) {
+ if (!flatModel[i].isHeader) {
+ lastItemIdx = i;
+ break;
+ }
+ }
+ if (lastItemIdx >= 0 && root.controller?.selectedFlatIndex === lastItemIdx)
+ return false;
+ return true;
+ }
+ gradient: Gradient {
+ GradientStop {
+ position: 0.0
+ color: "transparent"
+ }
+ GradientStop {
+ position: 1.0
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ }
+ }
+ }
+
+ Rectangle {
+ id: stickyHeader
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ height: 32
+ z: 101
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ visible: stickyHeaderSection !== null
+
+ readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
+
+ readonly property var stickyHeaderSection: {
+ var scrollY = mainListView.contentY - mainListView.originY;
+ if (scrollY <= 0)
+ return null;
+
+ var rows = root._visualRows;
+ var heights = root._cumulativeHeights;
+ if (rows.length === 0 || heights.length === 0)
+ return null;
+
+ var lo = 0;
+ var hi = rows.length - 1;
+ while (lo < hi) {
+ var mid = (lo + hi + 1) >> 1;
+ if (mid < heights.length && heights[mid] <= scrollY)
+ lo = mid;
+ else
+ hi = mid - 1;
+ }
+
+ for (var i = lo; i >= 0; i--) {
+ if (rows[i].type === "header")
+ return rows[i].section;
+ }
+ return null;
+ }
+
+ SectionHeader {
+ width: parent.width
+ section: stickyHeader.stickyHeaderSection
+ controller: root.controller
+ viewMode: {
+ void (stickyHeader.versionTrigger);
+ return root.controller?.getSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? "list";
+ }
+ canChangeViewMode: {
+ void (stickyHeader.versionTrigger);
+ return root.controller?.canChangeSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? false;
+ }
+ canCollapse: {
+ void (stickyHeader.versionTrigger);
+ return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false;
+ }
+ isSticky: true
+ }
+ }
+
+ Item {
+ anchors.centerIn: parent
+ visible: (!root.controller?.sections || root.controller.sections.length === 0) && !root.controller?.isFileSearching
+ width: emptyColumn.implicitWidth
+ height: emptyColumn.implicitHeight
+
+ Column {
+ id: emptyColumn
+ spacing: Theme.spacingM
+
+ DankIcon {
+ anchors.horizontalCenter: parent.horizontalCenter
+ name: getEmptyIcon()
+ size: 48
+ color: Theme.outlineButton
+
+ function getEmptyIcon() {
+ var mode = root.controller?.searchMode ?? "all";
+ switch (mode) {
+ case "files":
+ var fileType = root.controller?.fileSearchType ?? "all";
+ switch (fileType) {
+ case "dir":
+ return "folder_open";
+ case "file":
+ return "insert_drive_file";
+ default:
+ return "folder_open";
+ }
+ case "plugins":
+ return "extension";
+ case "apps":
+ return "apps";
+ default:
+ return "search_off";
+ }
+ }
+ }
+
+ StyledText {
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: getEmptyText()
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ horizontalAlignment: Text.AlignHCenter
+
+ function getEmptyText() {
+ var mode = root.controller?.searchMode ?? "all";
+ var hasQuery = root.controller?.searchQuery?.length > 0;
+
+ switch (mode) {
+ case "files":
+ if (!DSearchService.dsearchAvailable)
+ return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch");
+ if (!hasQuery)
+ return I18n.tr("Type to search files");
+ if (root.controller.searchQuery.length < 2)
+ return I18n.tr("Type at least 2 characters");
+ var fileType = root.controller?.fileSearchType ?? "all";
+ switch (fileType) {
+ case "dir":
+ return I18n.tr("No folders found");
+ case "file":
+ return I18n.tr("No files found");
+ default:
+ return I18n.tr("No results found");
+ }
+ case "plugins":
+ return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
+ case "apps":
+ return I18n.tr("No apps found");
+ default:
+ return I18n.tr("No results found");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Scorer.js b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Scorer.js
new file mode 100644
index 0000000..7d54df7
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Scorer.js
@@ -0,0 +1,265 @@
+.pragma library
+
+const Weights = {
+ exactMatch: 10000,
+ prefixMatch: 5000,
+ wordBoundary: 3000,
+ substring: 500,
+ fuzzy: 100,
+ frecency: 2000,
+ typeBonus: {
+ app: 1000,
+ plugin: 900,
+ file: 800,
+ action: 600
+ }
+}
+
+function tokenize(text) {
+ return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) { return w.length > 0 })
+}
+
+function hasWordBoundaryMatch(text, query) {
+ var textWords = tokenize(text)
+ var queryWords = tokenize(query)
+
+ if (queryWords.length === 0) return false
+ if (queryWords.length > textWords.length) return false
+
+ for (var i = 0; i <= textWords.length - queryWords.length; i++) {
+ var allMatch = true
+ for (var j = 0; j < queryWords.length; j++) {
+ if (!textWords[i + j].startsWith(queryWords[j])) {
+ allMatch = false
+ break
+ }
+ }
+ if (allMatch) return true
+ }
+ return false
+}
+
+function levenshteinDistance(s1, s2) {
+ var len1 = s1.length
+ var len2 = s2.length
+ var prev = new Array(len2 + 1)
+ var curr = new Array(len2 + 1)
+
+ for (var j = 0; j <= len2; j++)
+ prev[j] = j
+
+ for (var i = 1; i <= len1; i++) {
+ curr[0] = i
+ for (var j = 1; j <= len2; j++) {
+ var cost = s1[i - 1] === s2[j - 1] ? 0 : 1
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost)
+ }
+ var tmp = prev
+ prev = curr
+ curr = tmp
+ }
+ return prev[len2]
+}
+
+function fuzzyScore(text, query) {
+ var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3
+ var bestScore = 0
+
+ if (Math.abs(text.length - query.length) <= maxDistance) {
+ var distance = levenshteinDistance(text, query)
+ if (distance <= maxDistance) {
+ var maxLen = Math.max(text.length, query.length)
+ bestScore = 1 - (distance / maxLen)
+ }
+ }
+
+ var words = tokenize(text)
+ for (var i = 0; i < words.length && bestScore < 0.8; i++) {
+ if (Math.abs(words[i].length - query.length) > maxDistance) continue
+ var wordDistance = levenshteinDistance(words[i], query)
+ if (wordDistance <= maxDistance) {
+ var wordMaxLen = Math.max(words[i].length, query.length)
+ var score = 1 - (wordDistance / wordMaxLen)
+ bestScore = Math.max(bestScore, score)
+ }
+ }
+
+ return bestScore
+}
+
+function getTimeBucketWeight(daysSinceUsed) {
+ for (var i = 0; i < TimeBuckets.length; i++) {
+ if (daysSinceUsed <= TimeBuckets[i].maxDays) {
+ return TimeBuckets[i].weight
+ }
+ }
+ return 10
+}
+
+function calculateTextScore(name, query) {
+ if (name === query) return Weights.exactMatch
+ if (name.startsWith(query)) return Weights.prefixMatch
+ if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary
+ if (name.includes(query)) return Weights.substring
+
+ if (query.length >= 3) {
+ var fs = fuzzyScore(name, query)
+ if (fs > 0) return fs * Weights.fuzzy
+ }
+
+ return 0
+}
+
+function score(item, query, frecencyData) {
+ var typeBonus = Weights.typeBonus[item.type] || 0
+
+ if (!query || query.length === 0) {
+ var usageCount = frecencyData ? frecencyData.usageCount : 0
+ return typeBonus + (usageCount * 100)
+ }
+
+ var name = (item.name || "").toLowerCase()
+ var q = query.toLowerCase()
+
+ var textScore = calculateTextScore(name, q)
+
+ if (textScore === 0 && item.subtitle) {
+ var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q)
+ textScore = subtitleScore * 0.5
+ }
+
+ if (textScore === 0 && item.keywords) {
+ for (var i = 0; i < item.keywords.length; i++) {
+ var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q)
+ if (keywordScore > 0) {
+ textScore = keywordScore * 0.3
+ break
+ }
+ }
+ }
+
+ if (textScore === 0) return 0
+
+ var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0
+
+ return textScore + usageBonus + typeBonus
+}
+
+function scoreItems(items, query, getFrecencyFn) {
+ var scored = []
+
+ for (var i = 0; i < items.length; i++) {
+ var item = items[i]
+ var itemScore
+
+ if (item._preScored !== undefined && (query || item._preScored > 900)) {
+ itemScore = item._preScored
+ } else {
+ var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
+ itemScore = score(item, query, frecencyData)
+ }
+
+ if (itemScore > 0 || !query || query.length === 0) {
+ scored.push({
+ item: item,
+ score: itemScore
+ })
+ }
+ }
+
+ scored.sort(function (a, b) {
+ return b.score - a.score
+ })
+
+ return scored
+}
+
+function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSection) {
+ var sections = {}
+ var result = []
+ var limit = maxPerSection || 50
+
+ for (var i = 0; i < sectionOrder.length; i++) {
+ var sectionId = sectionOrder[i].id
+ sections[sectionId] = {
+ id: sectionId,
+ title: sectionOrder[i].title,
+ icon: sectionOrder[i].icon,
+ priority: sectionOrder[i].priority,
+ items: [],
+ collapsed: false,
+ flatStartIndex: 0
+ }
+ }
+
+ for (var i = 0; i < scoredItems.length; i++) {
+ var scoredItem = scoredItems[i]
+ var item = scoredItem.item
+ var sectionId = item.section || "apps"
+
+ if (sections[sectionId] && sections[sectionId].items.length < limit) {
+ sections[sectionId].items.push(item)
+ } else if (sections["apps"] && sections["apps"].items.length < limit) {
+ sections["apps"].items.push(item)
+ }
+ }
+
+ for (var i = 0; i < sectionOrder.length; i++) {
+ var section = sections[sectionOrder[i].id]
+ if (section && section.items.length > 0) {
+ if (sortAlphabetically && section.id === "apps") {
+ section.items.sort(function (a, b) {
+ return (a.name || "").localeCompare(b.name || "")
+ })
+ }
+ result.push(section)
+ }
+ }
+
+ return result
+}
+
+function flattenSections(sections) {
+ var flat = []
+ flat._sectionBounds = null
+ var bounds = {}
+
+ for (var i = 0; i < sections.length; i++) {
+ var section = sections[i]
+
+ flat.push({
+ isHeader: true,
+ section: section,
+ sectionId: section.id,
+ sectionIndex: i
+ })
+
+ var itemStart = flat.length
+ section.flatStartIndex = itemStart
+
+ if (!section.collapsed) {
+ for (var j = 0; j < section.items.length; j++) {
+ flat.push({
+ isHeader: false,
+ item: section.items[j],
+ sectionId: section.id,
+ sectionIndex: i,
+ indexInSection: j
+ })
+ }
+ }
+
+ var itemEnd = flat.length - 1
+ var itemCount = flat.length - itemStart
+ if (itemCount > 0) {
+ bounds[section.id] = {
+ start: itemStart,
+ end: itemEnd,
+ count: itemCount
+ }
+ }
+ }
+
+ flat._sectionBounds = bounds
+ return flat
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Section.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Section.qml
new file mode 100644
index 0000000..aba4c2f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/Section.qml
@@ -0,0 +1,114 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import qs.Common
+
+Item {
+ id: root
+
+ property var section: null
+ property var controller: null
+ property string viewMode: "list"
+ property int gridColumns: 4
+ property int startIndex: 0
+
+ signal itemClicked(int flatIndex)
+ signal itemRightClicked(int flatIndex, var item, real mouseX, real mouseY)
+
+ height: headerItem.height + (section?.collapsed ? 0 : contentLoader.height + Theme.spacingXS)
+ width: parent?.width ?? 200
+
+ SectionHeader {
+ id: headerItem
+ width: parent.width
+ section: root.section
+ controller: root.controller
+ viewMode: root.viewMode
+ canChangeViewMode: root.controller?.canChangeSectionViewMode(root.section?.id) ?? true
+
+ onViewModeToggled: {
+ if (root.controller && root.section) {
+ var newMode = root.viewMode === "list" ? "grid" : "list";
+ root.controller.setSectionViewMode(root.section.id, newMode);
+ }
+ }
+ }
+
+ Loader {
+ id: contentLoader
+ anchors.top: headerItem.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: Theme.spacingXS
+ active: !root.section?.collapsed
+ visible: active
+
+ sourceComponent: root.viewMode === "grid" ? gridComponent : listComponent
+
+ Component {
+ id: listComponent
+
+ Column {
+ spacing: 2
+ width: contentLoader.width
+
+ Repeater {
+ model: ScriptModel {
+ values: root.section?.items ?? []
+ objectProp: "id"
+ }
+
+ ResultItem {
+ required property var modelData
+ required property int index
+
+ width: parent?.width ?? 200
+ item: modelData
+ isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: root.startIndex + index
+
+ onClicked: root.itemClicked(root.startIndex + index)
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
+ }
+ }
+ }
+ }
+ }
+
+ Component {
+ id: gridComponent
+
+ Flow {
+ width: contentLoader.width
+ spacing: 4
+
+ Repeater {
+ model: ScriptModel {
+ values: root.section?.items ?? []
+ objectProp: "id"
+ }
+
+ GridItem {
+ required property var modelData
+ required property int index
+
+ width: Math.floor(contentLoader.width / root.gridColumns)
+ height: width + 24
+ item: modelData
+ isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
+ controller: root.controller
+ flatIndex: root.startIndex + index
+
+ onClicked: root.itemClicked(root.startIndex + index)
+ onRightClicked: (mouseX, mouseY) => {
+ root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/SectionHeader.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/SectionHeader.qml
new file mode 100644
index 0000000..ee366c7
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/SectionHeader.qml
@@ -0,0 +1,340 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var section: null
+ property var controller: null
+ property string viewMode: "list"
+ property bool canChangeViewMode: true
+ property bool canCollapse: true
+ property bool isSticky: false
+
+ signal viewModeToggled
+
+ width: parent?.width ?? 200
+ height: 32
+ color: isSticky ? "transparent" : (hoverArea.containsMouse ? Theme.surfaceHover : "transparent")
+ radius: Theme.cornerRadius
+
+ MouseArea {
+ id: hoverArea
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.NoButton
+ }
+
+ Row {
+ id: leftContent
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ // Whether the apps category picker should replace the plain title
+ readonly property bool hasAppCategories: root.section?.id === "apps" && (root.controller?.appCategories?.length ?? 0) > 0
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ // Hide section icon when the category chip already shows one
+ visible: !leftContent.hasAppCategories
+ name: root.section?.icon ?? "folder"
+ size: 16
+ color: Theme.surfaceVariantText
+ }
+
+ // Plain title — hidden when the category chip is shown
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ visible: !leftContent.hasAppCategories
+ text: root.section?.title ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ }
+
+ // Compact inline category chip — only visible on the apps section
+ Item {
+ id: categoryChip
+ visible: leftContent.hasAppCategories
+ anchors.verticalCenter: parent.verticalCenter
+ // Size to content with a fixed-min width so it doesn't jump around
+ width: chipRow.implicitWidth + Theme.spacingM * 2
+ height: 24
+
+ readonly property string currentCategory: root.controller?.appCategory || (root.controller?.appCategories?.length > 0 ? root.controller.appCategories[0] : "")
+ readonly property var iconMap: {
+ const cats = root.controller?.appCategories ?? [];
+ const m = {};
+ cats.forEach(c => { m[c] = AppSearchService.getCategoryIcon(c); });
+ return m;
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: chipArea.containsMouse || categoryPopup.visible ? Theme.surfaceContainerHigh : "transparent"
+ border.color: categoryPopup.visible ? Theme.primary : Theme.outlineMedium
+ border.width: categoryPopup.visible ? 2 : 1
+ }
+
+ Row {
+ id: chipRow
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: categoryChip.iconMap[categoryChip.currentCategory] ?? "apps"
+ size: 14
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: categoryChip.currentCategory
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ }
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: categoryPopup.visible ? "expand_less" : "expand_more"
+ size: 14
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ MouseArea {
+ id: chipArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (categoryPopup.visible) {
+ categoryPopup.close();
+ } else {
+ const pos = categoryChip.mapToItem(Overlay.overlay, 0, 0);
+ categoryPopup.x = pos.x;
+ categoryPopup.y = pos.y + categoryChip.height + 4;
+ categoryPopup.open();
+ }
+ }
+ }
+
+ Popup {
+ id: categoryPopup
+ parent: Overlay.overlay
+ width: Math.max(categoryChip.width, 180)
+ padding: 0
+ modal: true
+ dim: false
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ background: Rectangle { color: "transparent" }
+
+ contentItem: Rectangle {
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
+ border.color: Theme.primary
+ border.width: 2
+
+ ElevationShadow {
+ anchors.fill: parent
+ z: -1
+ level: Theme.elevationLevel2
+ fallbackOffset: 4
+ targetRadius: parent.radius
+ targetColor: parent.color
+ borderColor: parent.border.color
+ borderWidth: parent.border.width
+ shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
+ }
+
+ ListView {
+ id: categoryList
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ model: root.controller?.appCategories ?? []
+ spacing: 2
+ clip: true
+ interactive: contentHeight > height
+ implicitHeight: contentHeight
+
+ delegate: Rectangle {
+ id: catDelegate
+ required property string modelData
+ required property int index
+ width: categoryList.width
+ height: 32
+ radius: Theme.cornerRadius
+ readonly property bool isCurrent: categoryChip.currentCategory === modelData
+ color: isCurrent ? Theme.primaryHover : catArea.containsMouse ? Theme.primaryHoverLight : "transparent"
+
+ Row {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ anchors.verticalCenter: parent.verticalCenter
+ name: categoryChip.iconMap[catDelegate.modelData] ?? "apps"
+ size: 16
+ color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: catDelegate.modelData
+ font.pixelSize: Theme.fontSizeMedium
+ color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText
+ font.weight: catDelegate.isCurrent ? Font.Medium : Font.Normal
+ }
+ }
+
+ MouseArea {
+ id: catArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.controller)
+ root.controller.setAppCategory(catDelegate.modelData);
+ categoryPopup.close();
+ }
+ }
+ }
+ }
+ }
+
+ // Size to list content, cap at 10 visible items
+ height: Math.min((root.controller?.appCategories?.length ?? 0) * 34, 10 * 34) + Theme.spacingS * 2 + 4
+ }
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: root.section?.items?.length ?? 0
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.outlineButton
+ }
+ }
+
+ Row {
+ id: rightContent
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ Row {
+ id: viewModeRow
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+ visible: root.canChangeViewMode && !root.section?.collapsed
+
+ Repeater {
+ model: [
+ {
+ mode: "list",
+ icon: "view_list"
+ },
+ {
+ mode: "grid",
+ icon: "grid_view"
+ },
+ {
+ mode: "tile",
+ icon: "view_module"
+ }
+ ]
+
+ Rectangle {
+ required property var modelData
+ required property int index
+
+ width: 20
+ height: 20
+ radius: 4
+ color: root.viewMode === modelData.mode ? Theme.primaryHover : modeArea.containsMouse ? Theme.surfaceHover : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: parent.modelData.icon
+ size: 14
+ color: root.viewMode === parent.modelData.mode ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: modeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.viewMode !== parent.modelData.mode && root.controller && root.section) {
+ root.controller.setSectionViewMode(root.section.id, parent.modelData.mode);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ id: collapseButton
+ width: root.canCollapse ? 24 : 0
+ height: 24
+ visible: root.canCollapse
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.section?.collapsed ? "expand_more" : "expand_less"
+ size: 16
+ color: collapseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: collapseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (root.controller && root.section) {
+ root.controller.toggleSection(root.section.id);
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ anchors.rightMargin: rightContent.width + Theme.spacingS
+ cursorShape: root.canCollapse ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: root.canCollapse
+ onClicked: {
+ if (root.canCollapse && root.controller && root.section) {
+ root.controller.toggleSection(root.section.id);
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: 1
+ color: Theme.outlineMedium
+ visible: root.isSticky
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/TileItem.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/TileItem.qml
new file mode 100644
index 0000000..0b48802
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DankLauncherV2/TileItem.qml
@@ -0,0 +1,199 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell.Wayland
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var item: null
+ property bool isSelected: false
+ property bool isHovered: itemArea.containsMouse
+ property var controller: null
+ property int flatIndex: -1
+
+ signal clicked
+ signal rightClicked(real mouseX, real mouseY)
+
+ radius: Theme.cornerRadius
+ color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
+ border.width: isSelected ? 2 : 0
+ border.color: Theme.primary
+
+ readonly property string toplevelId: item?.data?.toplevelId ?? ""
+ readonly property var waylandToplevel: {
+ if (!toplevelId || !item?.pluginId)
+ return null;
+ const pluginInstance = PluginService.pluginInstances[item.pluginId];
+ if (!pluginInstance?.getToplevelById)
+ return null;
+ return pluginInstance.getToplevelById(toplevelId);
+ }
+ readonly property bool hasScreencopy: waylandToplevel !== null
+
+ readonly property string iconValue: {
+ if (!item)
+ return "";
+ if (hasScreencopy)
+ return "";
+ var data = item.data;
+ if (data?.imageUrl)
+ return "image:" + data.imageUrl;
+ if (data?.imagePath)
+ return "image:" + data.imagePath;
+ if (data?.path && isImageFile(data.path))
+ return "image:" + data.path;
+ switch (item.iconType) {
+ case "material":
+ case "nerd":
+ return "material:" + (item.icon || "image");
+ case "unicode":
+ return "unicode:" + (item.icon || "");
+ case "composite":
+ return item.iconFull || "";
+ case "image":
+ default:
+ return item.icon || "";
+ }
+ }
+
+ function isImageFile(path) {
+ if (!path)
+ return false;
+ var ext = path.split('.').pop().toLowerCase();
+ return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "jxl", "avif", "heif", "exr"].indexOf(ext) >= 0;
+ }
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: Theme.surfaceText
+ cornerRadius: root.radius
+ }
+
+ Item {
+ anchors.fill: parent
+ anchors.margins: 4
+
+ Rectangle {
+ id: imageContainer
+ anchors.fill: parent
+ radius: Theme.cornerRadius - 2
+ color: Theme.surfaceContainerHigh
+ clip: true
+
+ ScreencopyView {
+ id: screencopyView
+ anchors.fill: parent
+ captureSource: root.waylandToplevel
+ live: root.hasScreencopy
+ visible: root.hasScreencopy
+
+ Rectangle {
+ anchors.fill: parent
+ color: root.isHovered ? Theme.withAlpha(Theme.surfaceVariant, 0.2) : "transparent"
+ }
+ }
+
+ AppIconRenderer {
+ anchors.fill: parent
+ iconValue: root.iconValue
+ iconSize: Math.min(parent.width, parent.height)
+ fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
+ materialIconSizeAdjustment: iconSize * 0.3
+ visible: !root.hasScreencopy
+ }
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: labelText.implicitHeight + Theme.spacingS * 2
+ color: Theme.withAlpha(Theme.surfaceContainer, 0.85)
+ visible: root.item?.name?.length > 0
+
+ Text {
+ id: labelText
+ anchors.fill: parent
+ anchors.margins: Theme.spacingXS
+ text: root.item?._hName ?? root.item?.name ?? ""
+ textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: Theme.fontFamily
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+
+ Rectangle {
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingXS
+ width: 20
+ height: 20
+ radius: 10
+ color: Theme.primary
+ visible: root.isSelected
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "check"
+ size: 14
+ color: Theme.primaryText
+ }
+ }
+
+ Rectangle {
+ id: attributionBadge
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.margins: Theme.spacingXS
+ width: root.hasScreencopy ? 28 : 40
+ height: root.hasScreencopy ? 28 : 16
+ radius: root.hasScreencopy ? 14 : 4
+ color: root.hasScreencopy ? Theme.surfaceContainer : "transparent"
+ visible: attributionImage.status === Image.Ready
+ opacity: 0.95
+
+ Image {
+ id: attributionImage
+ anchors.fill: parent
+ anchors.margins: root.hasScreencopy ? 4 : 0
+ fillMode: Image.PreserveAspectFit
+ source: root.item?.data?.attribution || ""
+ mipmap: true
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ id: itemArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+
+ onPressed: mouse => {
+ if (mouse.button === Qt.LeftButton)
+ rippleLayer.trigger(mouse.x, mouse.y);
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ var scenePos = mapToItem(null, mouse.x, mouse.y);
+ root.rightClicked(scenePos.x, scenePos.y);
+ return;
+ }
+ root.clicked();
+ }
+
+ onPositionChanged: {
+ if (root.controller)
+ root.controller.keyboardNavigationActive = false;
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DisplayConfirmationModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DisplayConfirmationModal.qml
new file mode 100644
index 0000000..f8a97de
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/DisplayConfirmationModal.qml
@@ -0,0 +1,212 @@
+import QtQuick
+import qs.Common
+import qs.Modals.Common
+import qs.Widgets
+
+DankModal {
+ id: root
+
+ property string outputName: ""
+ property var changes: []
+ property int countdown: 10
+
+ signal confirmed
+ signal reverted
+
+ shouldBeVisible: false
+ allowStacking: true
+ modalWidth: 420
+ modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
+
+ Timer {
+ id: countdownTimer
+ interval: 1000
+ repeat: true
+ running: root.shouldBeVisible
+ onTriggered: {
+ root.countdown--;
+ if (root.countdown <= 0) {
+ root.reverted();
+ root.close();
+ }
+ }
+ }
+
+ onOpened: {
+ countdown = 10;
+ countdownTimer.start();
+ }
+
+ onDialogClosed: {
+ countdownTimer.stop();
+ }
+
+ onBackgroundClicked: {
+ root.reverted();
+ root.close();
+ }
+
+ content: Component {
+ FocusScope {
+ id: confirmContent
+
+ anchors.fill: parent
+ focus: true
+ implicitHeight: mainColumn.implicitHeight
+
+ Keys.onEscapePressed: event => {
+ root.reverted();
+ root.close();
+ event.accepted = true;
+ }
+
+ Keys.onReturnPressed: event => {
+ root.confirmed();
+ root.close();
+ event.accepted = true;
+ }
+
+ Column {
+ id: mainColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("Confirm Display Changes")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 70
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainerHighest
+
+ StyledText {
+ anchors.centerIn: parent
+ text: root.countdown + "s"
+ font.pixelSize: Theme.fontSizeXLarge * 1.5
+ color: Theme.primary
+ font.weight: Font.Bold
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+ visible: root.changes.length > 0
+
+ Repeater {
+ model: root.changes
+
+ StyledText {
+ required property var modelData
+ text: modelData
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: 36
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ Rectangle {
+ width: Math.max(70, revertText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: revertArea.containsMouse ? Theme.surfaceTextHover : "transparent"
+ border.color: Theme.surfaceVariantAlpha
+ border.width: 1
+
+ StyledText {
+ id: revertText
+
+ anchors.centerIn: parent
+ text: I18n.tr("Revert")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: revertArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.reverted();
+ root.close();
+ }
+ }
+ }
+
+ Rectangle {
+ width: Math.max(80, confirmText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: confirmArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
+
+ StyledText {
+ id: confirmText
+
+ anchors.centerIn: parent
+ text: I18n.tr("Keep Changes")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.background
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: confirmArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.confirmed();
+ root.close();
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+
+ DankActionButton {
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.topMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: {
+ root.reverted();
+ root.close();
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserContent.qml
new file mode 100644
index 0000000..8b96742
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserContent.qml
@@ -0,0 +1,920 @@
+import Qt.labs.folderlistmodel
+import QtCore
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Widgets
+
+FocusScope {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
+ property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
+ property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
+ property string videosDir: StandardPaths.writableLocation(StandardPaths.MoviesLocation)
+ property string picsDir: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
+ property string downloadDir: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
+ property string desktopDir: StandardPaths.writableLocation(StandardPaths.DesktopLocation)
+ property string currentPath: ""
+ property var fileExtensions: ["*.*"]
+ property alias filterExtensions: root.fileExtensions
+ property string browserTitle: "Select File"
+ property string browserIcon: "folder_open"
+ property string browserType: "generic"
+ property bool showHiddenFiles: false
+ property int selectedIndex: -1
+ property bool keyboardNavigationActive: false
+ property bool backButtonFocused: false
+ property bool saveMode: false
+ property string defaultFileName: ""
+ property int keyboardSelectionIndex: -1
+ property bool keyboardSelectionRequested: false
+ property bool showKeyboardHints: false
+ property bool showFileInfo: false
+ property string selectedFilePath: ""
+ property string selectedFileName: ""
+ property bool selectedFileIsDir: false
+ property bool showOverwriteConfirmation: false
+ property string pendingFilePath: ""
+ property bool showSidebar: true
+ property string viewMode: "grid"
+ property string sortBy: "name"
+ property bool sortAscending: true
+ property int iconSizeIndex: 1
+ property var iconSizes: [80, 120, 160, 200]
+ property bool pathEditMode: false
+ property bool pathInputHasFocus: false
+ property int actualGridColumns: 5
+ property bool _initialized: false
+ property bool closeOnEscape: true
+ property var windowControls: null
+
+ signal fileSelected(string path)
+ signal closeRequested
+
+ function encodeFileUrl(path) {
+ if (!path)
+ return "";
+ return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
+ }
+
+ function initialize() {
+ loadSettings();
+ currentPath = getLastPath();
+ _initialized = true;
+ }
+
+ function reset() {
+ currentPath = getLastPath();
+ selectedIndex = -1;
+ keyboardNavigationActive = false;
+ backButtonFocused = false;
+ }
+
+ function loadSettings() {
+ const type = browserType || "default";
+ const settings = CacheData.fileBrowserSettings[type];
+ const isImageBrowser = ["wallpaper", "profile"].includes(browserType);
+
+ if (settings) {
+ viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list");
+ sortBy = settings.sortBy || "name";
+ sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true;
+ iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1;
+ showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true;
+ } else {
+ viewMode = isImageBrowser ? "grid" : "list";
+ }
+ }
+
+ function saveSettings() {
+ if (!_initialized)
+ return;
+ const type = browserType || "default";
+ let settings = CacheData.fileBrowserSettings;
+ if (!settings[type]) {
+ settings[type] = {};
+ }
+ settings[type].viewMode = viewMode;
+ settings[type].sortBy = sortBy;
+ settings[type].sortAscending = sortAscending;
+ settings[type].iconSizeIndex = iconSizeIndex;
+ settings[type].showSidebar = showSidebar;
+ settings[type].lastPath = currentPath;
+ CacheData.fileBrowserSettings = settings;
+
+ if (browserType === "wallpaper") {
+ CacheData.wallpaperLastPath = currentPath;
+ } else if (browserType === "profile") {
+ CacheData.profileLastPath = currentPath;
+ }
+
+ CacheData.saveCache();
+ }
+
+ onViewModeChanged: saveSettings()
+ onSortByChanged: saveSettings()
+ onSortAscendingChanged: saveSettings()
+ onIconSizeIndexChanged: saveSettings()
+ onShowSidebarChanged: saveSettings()
+
+ function isImageFile(fileName) {
+ if (!fileName)
+ return false;
+ const ext = fileName.toLowerCase().split('.').pop();
+ return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
+ }
+
+ function getLastPath() {
+ const type = browserType || "default";
+ const settings = CacheData.fileBrowserSettings[type];
+ const lastPath = settings?.lastPath || "";
+ return (lastPath && lastPath !== "") ? lastPath : homeDir;
+ }
+
+ function saveLastPath(path) {
+ const type = browserType || "default";
+ let settings = CacheData.fileBrowserSettings;
+ if (!settings[type]) {
+ settings[type] = {};
+ }
+ settings[type].lastPath = path;
+ CacheData.fileBrowserSettings = settings;
+ CacheData.saveCache();
+
+ if (browserType === "wallpaper") {
+ CacheData.wallpaperLastPath = path;
+ } else if (browserType === "profile") {
+ CacheData.profileLastPath = path;
+ }
+ }
+
+ function setSelectedFileData(path, name, isDir) {
+ selectedFilePath = path;
+ selectedFileName = name;
+ selectedFileIsDir = isDir;
+ }
+
+ function navigateUp() {
+ const path = currentPath;
+ if (path === homeDir)
+ return;
+ const lastSlash = path.lastIndexOf('/');
+ if (lastSlash <= 0)
+ return;
+ const newPath = path.substring(0, lastSlash);
+ if (newPath.length < homeDir.length) {
+ currentPath = homeDir;
+ saveLastPath(homeDir);
+ } else {
+ currentPath = newPath;
+ saveLastPath(newPath);
+ }
+ }
+
+ function navigateTo(path) {
+ currentPath = path;
+ saveLastPath(path);
+ selectedIndex = -1;
+ backButtonFocused = false;
+ }
+
+ function keyboardFileSelection(index) {
+ if (index < 0)
+ return;
+ keyboardSelectionTimer.targetIndex = index;
+ keyboardSelectionTimer.start();
+ }
+
+ function executeKeyboardSelection(index) {
+ keyboardSelectionIndex = index;
+ keyboardSelectionRequested = true;
+ }
+
+ function handleSaveFile(filePath) {
+ var normalizedPath = filePath;
+ if (!normalizedPath.startsWith("file://")) {
+ normalizedPath = encodeFileUrl(filePath);
+ }
+
+ var exists = false;
+ var fileName = filePath.split('/').pop();
+
+ for (var i = 0; i < folderModel.count; i++) {
+ if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
+ exists = true;
+ break;
+ }
+ }
+
+ if (exists) {
+ pendingFilePath = normalizedPath;
+ showOverwriteConfirmation = true;
+ } else {
+ fileSelected(normalizedPath);
+ closeRequested();
+ }
+ }
+
+ onCurrentPathChanged: {
+ selectedFilePath = "";
+ selectedFileName = "";
+ selectedFileIsDir = false;
+ saveSettings();
+ }
+
+ onSelectedIndexChanged: {
+ if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
+ selectedFilePath = "";
+ selectedFileName = "";
+ selectedFileIsDir = false;
+ }
+ }
+
+ property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"]
+
+ property var quickAccessLocations: [
+ {
+ "name": I18n.tr("Home"),
+ "path": homeDir,
+ "icon": "home"
+ },
+ {
+ "name": I18n.tr("Documents"),
+ "path": docsDir,
+ "icon": "description"
+ },
+ {
+ "name": I18n.tr("Downloads"),
+ "path": downloadDir,
+ "icon": "download"
+ },
+ {
+ "name": I18n.tr("Pictures"),
+ "path": picsDir,
+ "icon": "image"
+ },
+ {
+ "name": I18n.tr("Music"),
+ "path": musicDir,
+ "icon": "music_note"
+ },
+ {
+ "name": I18n.tr("Videos"),
+ "path": videosDir,
+ "icon": "movie"
+ },
+ {
+ "name": I18n.tr("Desktop"),
+ "path": desktopDir,
+ "icon": "computer"
+ }
+ ]
+
+ FolderListModel {
+ id: folderModel
+
+ showDirsFirst: true
+ showDotAndDotDot: false
+ showHidden: root.showHiddenFiles
+ caseSensitive: false
+ nameFilters: fileExtensions
+ showFiles: true
+ showDirs: true
+ folder: encodeFileUrl(currentPath || homeDir)
+ sortField: {
+ switch (sortBy) {
+ case "name":
+ return FolderListModel.Name;
+ case "size":
+ return FolderListModel.Size;
+ case "modified":
+ return FolderListModel.Time;
+ case "type":
+ return FolderListModel.Type;
+ default:
+ return FolderListModel.Name;
+ }
+ }
+ sortReversed: !sortAscending
+ }
+
+ QtObject {
+ id: keyboardController
+
+ property int totalItems: folderModel.count
+ property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
+
+ function handleKey(event) {
+ if (event.key === Qt.Key_Escape && root.closeOnEscape) {
+ closeRequested();
+ event.accepted = true;
+ return;
+ }
+ if (event.key === Qt.Key_F10) {
+ showKeyboardHints = !showKeyboardHints;
+ event.accepted = true;
+ return;
+ }
+ if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
+ showFileInfo = !showFileInfo;
+ event.accepted = true;
+ return;
+ }
+ if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
+ if (currentPath !== homeDir) {
+ navigateUp();
+ event.accepted = true;
+ }
+ return;
+ }
+ if (!keyboardNavigationActive) {
+ const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right || (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier);
+
+ if (isInitKey) {
+ keyboardNavigationActive = true;
+ if (currentPath !== homeDir) {
+ backButtonFocused = true;
+ selectedIndex = -1;
+ } else {
+ backButtonFocused = false;
+ selectedIndex = 0;
+ }
+ event.accepted = true;
+ }
+ return;
+ }
+ switch (event.key) {
+ case Qt.Key_Tab:
+ if (backButtonFocused) {
+ backButtonFocused = false;
+ selectedIndex = 0;
+ } else if (selectedIndex < totalItems - 1) {
+ selectedIndex++;
+ } else if (currentPath !== homeDir) {
+ backButtonFocused = true;
+ selectedIndex = -1;
+ } else {
+ selectedIndex = 0;
+ }
+ event.accepted = true;
+ break;
+ case Qt.Key_Backtab:
+ if (backButtonFocused) {
+ backButtonFocused = false;
+ selectedIndex = totalItems - 1;
+ } else if (selectedIndex > 0) {
+ selectedIndex--;
+ } else if (currentPath !== homeDir) {
+ backButtonFocused = true;
+ selectedIndex = -1;
+ } else {
+ selectedIndex = totalItems - 1;
+ }
+ event.accepted = true;
+ break;
+ case Qt.Key_N:
+ if (event.modifiers & Qt.ControlModifier) {
+ if (backButtonFocused) {
+ backButtonFocused = false;
+ selectedIndex = 0;
+ } else if (selectedIndex < totalItems - 1) {
+ selectedIndex++;
+ }
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_P:
+ if (event.modifiers & Qt.ControlModifier) {
+ if (selectedIndex > 0) {
+ selectedIndex--;
+ } else if (currentPath !== homeDir) {
+ backButtonFocused = true;
+ selectedIndex = -1;
+ }
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_J:
+ if (event.modifiers & Qt.ControlModifier) {
+ if (selectedIndex < totalItems - 1) {
+ selectedIndex++;
+ }
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_K:
+ if (event.modifiers & Qt.ControlModifier) {
+ if (selectedIndex > 0) {
+ selectedIndex--;
+ } else if (currentPath !== homeDir) {
+ backButtonFocused = true;
+ selectedIndex = -1;
+ }
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_H:
+ if (event.modifiers & Qt.ControlModifier) {
+ if (!backButtonFocused && selectedIndex > 0) {
+ selectedIndex--;
+ } else if (currentPath !== homeDir) {
+ backButtonFocused = true;
+ selectedIndex = -1;
+ }
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_L:
+ if (event.modifiers & Qt.ControlModifier) {
+ if (backButtonFocused) {
+ backButtonFocused = false;
+ selectedIndex = 0;
+ } else if (selectedIndex < totalItems - 1) {
+ selectedIndex++;
+ }
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_Left:
+ if (pathInputHasFocus)
+ return;
+ if (backButtonFocused)
+ return;
+ if (selectedIndex > 0) {
+ selectedIndex--;
+ } else if (currentPath !== homeDir) {
+ backButtonFocused = true;
+ selectedIndex = -1;
+ }
+ event.accepted = true;
+ break;
+ case Qt.Key_Right:
+ if (pathInputHasFocus)
+ return;
+ if (backButtonFocused) {
+ backButtonFocused = false;
+ selectedIndex = 0;
+ } else if (selectedIndex < totalItems - 1) {
+ selectedIndex++;
+ }
+ event.accepted = true;
+ break;
+ case Qt.Key_Up:
+ if (backButtonFocused) {
+ backButtonFocused = false;
+ if (gridColumns === 1) {
+ selectedIndex = 0;
+ } else {
+ var col = selectedIndex % gridColumns;
+ selectedIndex = Math.min(col, totalItems - 1);
+ }
+ } else if (selectedIndex >= gridColumns) {
+ selectedIndex -= gridColumns;
+ } else if (selectedIndex > 0 && gridColumns === 1) {
+ selectedIndex--;
+ } else if (currentPath !== homeDir) {
+ backButtonFocused = true;
+ selectedIndex = -1;
+ }
+ event.accepted = true;
+ break;
+ case Qt.Key_Down:
+ if (backButtonFocused) {
+ backButtonFocused = false;
+ selectedIndex = 0;
+ } else if (gridColumns === 1) {
+ if (selectedIndex < totalItems - 1) {
+ selectedIndex++;
+ }
+ } else {
+ var newIndex = selectedIndex + gridColumns;
+ if (newIndex < totalItems) {
+ selectedIndex = newIndex;
+ } else {
+ var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns;
+ var col = selectedIndex % gridColumns;
+ var targetIndex = lastRowStart + col;
+ if (targetIndex < totalItems && targetIndex > selectedIndex) {
+ selectedIndex = targetIndex;
+ }
+ }
+ }
+ event.accepted = true;
+ break;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ case Qt.Key_Space:
+ if (backButtonFocused) {
+ navigateUp();
+ } else if (selectedIndex >= 0 && selectedIndex < totalItems) {
+ root.keyboardFileSelection(selectedIndex);
+ }
+ event.accepted = true;
+ break;
+ }
+ }
+ }
+
+ Timer {
+ id: keyboardSelectionTimer
+
+ property int targetIndex: -1
+
+ interval: 1
+ onTriggered: {
+ executeKeyboardSelection(targetIndex);
+ }
+ }
+
+ focus: true
+
+ Keys.onPressed: event => {
+ keyboardController.handleKey(event);
+ }
+
+ Column {
+ anchors.fill: parent
+ spacing: 0
+
+ Item {
+ width: parent.width
+ height: 48
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: if (windowControls)
+ windowControls.tryStartMove()
+ onDoubleClicked: if (windowControls)
+ windowControls.tryToggleMaximize()
+ }
+
+ Row {
+ spacing: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingL
+
+ DankIcon {
+ name: browserIcon
+ size: Theme.iconSizeLarge
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: browserTitle
+ font.pixelSize: Theme.fontSizeXLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Row {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ DankActionButton {
+ circular: false
+ iconName: showHiddenFiles ? "visibility_off" : "visibility"
+ iconSize: Theme.iconSize - 4
+ iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText
+ onClicked: showHiddenFiles = !showHiddenFiles
+ }
+
+ DankActionButton {
+ circular: false
+ iconName: viewMode === "grid" ? "view_list" : "grid_view"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: viewMode = viewMode === "grid" ? "list" : "grid"
+ }
+
+ DankActionButton {
+ circular: false
+ iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ visible: viewMode === "grid"
+ onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length
+ }
+
+ DankActionButton {
+ circular: false
+ iconName: "info"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: root.showKeyboardHints = !root.showKeyboardHints
+ }
+
+ DankActionButton {
+ visible: windowControls?.supported ?? false
+ circular: false
+ iconName: windowControls?.targetWindow?.maximized ? "fullscreen_exit" : "fullscreen"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: if (windowControls)
+ windowControls.tryToggleMaximize()
+ }
+
+ DankActionButton {
+ circular: false
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: root.closeRequested()
+ }
+ }
+ }
+
+ StyledRect {
+ width: parent.width
+ height: 1
+ color: Theme.outline
+ }
+
+ Item {
+ width: parent.width
+ height: parent.height - 49
+
+ Row {
+ anchors.fill: parent
+ spacing: 0
+
+ Row {
+ width: showSidebar ? 201 : 0
+ height: parent.height
+ spacing: 0
+ visible: showSidebar
+
+ FileBrowserSidebar {
+ height: parent.height
+ quickAccessLocations: root.quickAccessLocations
+ currentPath: root.currentPath
+ onLocationSelected: path => navigateTo(path)
+ }
+
+ StyledRect {
+ width: 1
+ height: parent.height
+ color: Theme.outline
+ }
+ }
+
+ Column {
+ width: parent.width - (showSidebar ? 201 : 0)
+ height: parent.height
+ spacing: 0
+
+ FileBrowserNavigation {
+ width: parent.width
+ currentPath: root.currentPath
+ homeDir: root.homeDir
+ backButtonFocused: root.backButtonFocused
+ keyboardNavigationActive: root.keyboardNavigationActive
+ showSidebar: root.showSidebar
+ pathEditMode: root.pathEditMode
+ onNavigateUp: root.navigateUp()
+ onNavigateTo: path => root.navigateTo(path)
+ onPathInputFocusChanged: hasFocus => {
+ root.pathInputHasFocus = hasFocus;
+ if (hasFocus) {
+ root.pathEditMode = true;
+ }
+ }
+ }
+
+ StyledRect {
+ width: parent.width
+ height: 1
+ color: Theme.outline
+ }
+
+ Item {
+ id: gridContainer
+ width: parent.width
+ height: parent.height - 41
+ clip: true
+
+ property real gridCellWidth: iconSizes[iconSizeIndex] + 24
+ property real gridCellHeight: iconSizes[iconSizeIndex] + 56
+ property real availableGridWidth: width - Theme.spacingM * 2
+ property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth))
+ property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2)
+
+ onGridColumnsChanged: {
+ root.actualGridColumns = gridColumns;
+ }
+ Component.onCompleted: {
+ root.actualGridColumns = gridColumns;
+ }
+
+ DankGridView {
+ id: fileGrid
+ anchors.fill: parent
+ anchors.leftMargin: gridContainer.gridLeftMargin
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingS
+ anchors.bottomMargin: Theme.spacingS
+ visible: viewMode === "grid"
+ cellWidth: gridContainer.gridCellWidth
+ cellHeight: gridContainer.gridCellHeight
+ cacheBuffer: 260
+ model: folderModel
+ currentIndex: selectedIndex
+ onCurrentIndexChanged: {
+ if (keyboardNavigationActive && currentIndex >= 0)
+ positionViewAtIndex(currentIndex, GridView.Contain);
+ }
+
+ ScrollBar.vertical: DankScrollbar {
+ id: gridScrollbar
+ }
+
+ ScrollBar.horizontal: DankScrollbar {
+ policy: ScrollBar.AlwaysOff
+ }
+
+ delegate: FileBrowserGridDelegate {
+ iconSizes: root.iconSizes
+ iconSizeIndex: root.iconSizeIndex
+ selectedIndex: root.selectedIndex
+ keyboardNavigationActive: root.keyboardNavigationActive
+ onItemClicked: (index, path, name, isDir) => {
+ selectedIndex = index;
+ setSelectedFileData(path, name, isDir);
+ if (isDir) {
+ navigateTo(path);
+ } else {
+ fileSelected(path);
+ root.closeRequested();
+ }
+ }
+ onItemSelected: (index, path, name, isDir) => {
+ setSelectedFileData(path, name, isDir);
+ }
+
+ Connections {
+ function onKeyboardSelectionRequestedChanged() {
+ if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
+ root.keyboardSelectionRequested = false;
+ selectedIndex = index;
+ setSelectedFileData(filePath, fileName, fileIsDir);
+ if (fileIsDir) {
+ navigateTo(filePath);
+ } else {
+ fileSelected(filePath);
+ root.closeRequested();
+ }
+ }
+ }
+
+ target: root
+ }
+ }
+ }
+
+ DankListView {
+ id: fileList
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.topMargin: Theme.spacingS
+ anchors.bottomMargin: Theme.spacingS
+ visible: viewMode === "list"
+ spacing: 2
+ model: folderModel
+ currentIndex: selectedIndex
+ onCurrentIndexChanged: {
+ if (keyboardNavigationActive && currentIndex >= 0)
+ positionViewAtIndex(currentIndex, ListView.Contain);
+ }
+
+ ScrollBar.vertical: DankScrollbar {
+ id: listScrollbar
+ }
+
+ delegate: FileBrowserListDelegate {
+ width: fileList.width
+ selectedIndex: root.selectedIndex
+ keyboardNavigationActive: root.keyboardNavigationActive
+ onItemClicked: (index, path, name, isDir) => {
+ selectedIndex = index;
+ setSelectedFileData(path, name, isDir);
+ if (isDir) {
+ navigateTo(path);
+ } else {
+ fileSelected(path);
+ root.closeRequested();
+ }
+ }
+ onItemSelected: (index, path, name, isDir) => {
+ setSelectedFileData(path, name, isDir);
+ }
+
+ Connections {
+ function onKeyboardSelectionRequestedChanged() {
+ if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
+ root.keyboardSelectionRequested = false;
+ selectedIndex = index;
+ setSelectedFileData(filePath, fileName, fileIsDir);
+ if (fileIsDir) {
+ navigateTo(filePath);
+ } else {
+ fileSelected(filePath);
+ root.closeRequested();
+ }
+ }
+ }
+
+ target: root
+ }
+ }
+ }
+ }
+ }
+ }
+
+ FileBrowserSaveRow {
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingL
+ saveMode: root.saveMode
+ defaultFileName: root.defaultFileName
+ currentPath: root.currentPath
+ onSaveRequested: filePath => handleSaveFile(filePath)
+ }
+
+ KeyboardHints {
+ id: keyboardHints
+
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingL
+ showHints: root.showKeyboardHints
+ }
+
+ FileInfo {
+ id: fileInfo
+
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingL
+ width: 300
+ showFileInfo: root.showFileInfo
+ selectedIndex: root.selectedIndex
+ sourceFolderModel: folderModel
+ currentPath: root.currentPath
+ currentFileName: root.selectedFileName
+ currentFileIsDir: root.selectedFileIsDir
+ currentFileExtension: {
+ if (root.selectedFileIsDir || !root.selectedFileName)
+ return "";
+
+ var lastDot = root.selectedFileName.lastIndexOf('.');
+ return lastDot > 0 ? root.selectedFileName.substring(lastDot + 1).toLowerCase() : "";
+ }
+ }
+
+ FileBrowserSortMenu {
+ id: sortMenu
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.topMargin: 120
+ anchors.rightMargin: Theme.spacingL
+ sortBy: root.sortBy
+ sortAscending: root.sortAscending
+ onSortBySelected: value => {
+ root.sortBy = value;
+ }
+ onSortOrderSelected: ascending => {
+ root.sortAscending = ascending;
+ }
+ }
+ }
+
+ FileBrowserOverwriteDialog {
+ anchors.fill: parent
+ showDialog: showOverwriteConfirmation
+ pendingFilePath: root.pendingFilePath
+ onConfirmed: filePath => {
+ showOverwriteConfirmation = false;
+ fileSelected(filePath);
+ pendingFilePath = "";
+ Qt.callLater(() => root.closeRequested());
+ }
+ onCancelled: {
+ showOverwriteConfirmation = false;
+ pendingFilePath = "";
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml
new file mode 100644
index 0000000..effa9bd
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserGridDelegate.qml
@@ -0,0 +1,253 @@
+import QtQuick
+import QtQuick.Effects
+import qs.Common
+import qs.Widgets
+
+StyledRect {
+ id: delegateRoot
+
+ required property bool fileIsDir
+ required property string filePath
+ required property string fileName
+ required property int index
+
+ property bool weMode: false
+ property var iconSizes: [80, 120, 160, 200]
+ property int iconSizeIndex: 1
+ property int selectedIndex: -1
+ property bool keyboardNavigationActive: false
+
+ signal itemClicked(int index, string path, string name, bool isDir)
+ signal itemSelected(int index, string path, string name, bool isDir)
+
+ function getFileExtension(fileName) {
+ const parts = fileName.split('.');
+ if (parts.length > 1) {
+ return parts[parts.length - 1].toLowerCase();
+ }
+ return "";
+ }
+
+ function determineFileType(fileName) {
+ const ext = getFileExtension(fileName);
+
+ const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
+ if (imageExts.includes(ext)) {
+ return "image";
+ }
+
+ const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
+ if (videoExts.includes(ext)) {
+ return "video";
+ }
+
+ const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
+ if (audioExts.includes(ext)) {
+ return "audio";
+ }
+
+ const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
+ if (codeExts.includes(ext)) {
+ return "code";
+ }
+
+ const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
+ if (docExts.includes(ext)) {
+ return "document";
+ }
+
+ const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
+ if (archiveExts.includes(ext)) {
+ return "archive";
+ }
+
+ if (!ext || fileName.indexOf('.') === -1) {
+ return "binary";
+ }
+
+ return "file";
+ }
+
+ function isImageFile(fileName) {
+ if (!fileName) {
+ return false;
+ }
+ return determineFileType(fileName) === "image";
+ }
+
+ function isVideoFile(fileName) {
+ if (!fileName) {
+ return false;
+ }
+ return determineFileType(fileName) === "video";
+ }
+
+ property bool isImage: isImageFile(delegateRoot.fileName)
+ property bool isVideo: isVideoFile(delegateRoot.fileName)
+
+ property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
+ property string _thumbnailSize: iconSizeIndex >= 2 ? "x-large" : "large"
+ property int _thumbnailPx: iconSizeIndex >= 2 ? 512 : 256
+ property string videoThumbnailPath: {
+ if (!delegateRoot.fileIsDir && isVideo) {
+ const hash = Qt.md5("file://" + delegateRoot.filePath);
+ return _xdgCacheHome + "/thumbnails/" + _thumbnailSize + "/" + hash + ".png";
+ }
+ return "";
+ }
+
+ property string _videoThumb: ""
+
+ onVideoThumbnailPathChanged: {
+ _videoThumb = "";
+ if (!videoThumbnailPath)
+ return;
+ const thumbPath = videoThumbnailPath;
+ const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
+ const size = _thumbnailPx;
+ const fp = delegateRoot.filePath;
+ Paths.mkdir(thumbDir);
+ Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
+ if (exitCode === 0) {
+ _videoThumb = thumbPath;
+ } else {
+ Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function(output, exitCode) {
+ if (exitCode === 0)
+ _videoThumb = thumbPath;
+ });
+ }
+ });
+ }
+
+ function getIconForFile(fileName) {
+ const lowerName = fileName.toLowerCase();
+ if (lowerName.startsWith("dockerfile")) {
+ return "docker";
+ }
+ const ext = fileName.split('.').pop();
+ return ext || "";
+ }
+
+ width: weMode ? 245 : iconSizes[iconSizeIndex] + 16
+ height: weMode ? 205 : iconSizes[iconSizeIndex] + 48
+ radius: Theme.cornerRadius
+ color: {
+ if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
+ return Theme.surfacePressed;
+
+ return mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent";
+ }
+ border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : "transparent"
+ border.width: (keyboardNavigationActive && delegateRoot.index === selectedIndex) ? 2 : 0
+
+ Component.onCompleted: {
+ if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
+ itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
+ }
+
+ onSelectedIndexChanged: {
+ if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
+ itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
+ }
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ Item {
+ width: weMode ? 225 : (iconSizes[iconSizeIndex] - 8)
+ height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Image {
+ id: gridPreviewImage
+ anchors.fill: parent
+ anchors.margins: 2
+ property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
+ property int weExtIndex: 0
+ property string imagePath: {
+ if (weMode && delegateRoot.fileIsDir)
+ return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
+ if (!delegateRoot.fileIsDir && isImage)
+ return delegateRoot.filePath;
+ if (_videoThumb)
+ return _videoThumb;
+ return "";
+ }
+ source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
+ onStatusChanged: {
+ if (weMode && delegateRoot.fileIsDir && status === Image.Error) {
+ if (weExtIndex < weExtensions.length - 1) {
+ weExtIndex++;
+ } else {
+ imagePath = "";
+ }
+ }
+ }
+ fillMode: Image.PreserveAspectCrop
+ sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
+ sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
+ asynchronous: true
+ visible: false
+ }
+
+ MultiEffect {
+ anchors.fill: parent
+ anchors.margins: 2
+ source: gridPreviewImage
+ maskEnabled: true
+ maskSource: gridImageMask
+ visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && (isImage || isVideo)) || (weMode && delegateRoot.fileIsDir))
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 1
+ }
+
+ Item {
+ id: gridImageMask
+ anchors.fill: parent
+ anchors.margins: 2
+ layer.enabled: true
+ layer.smooth: true
+ visible: false
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: "black"
+ antialiasing: true
+ }
+ }
+
+ DankNFIcon {
+ anchors.centerIn: parent
+ name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
+ size: iconSizes[iconSizeIndex] * 0.45
+ color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
+ visible: (!delegateRoot.fileIsDir && !isImage && !(isVideo && gridPreviewImage.status === Image.Ready)) || (delegateRoot.fileIsDir && !weMode)
+ }
+ }
+
+ StyledText {
+ text: delegateRoot.fileName || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ width: delegateRoot.width - Theme.spacingM
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ maximumLineCount: 2
+ wrapMode: Text.Wrap
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml
new file mode 100644
index 0000000..2f452b4
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserListDelegate.qml
@@ -0,0 +1,258 @@
+import QtQuick
+import QtQuick.Effects
+import qs.Common
+import qs.Widgets
+
+StyledRect {
+ id: listDelegateRoot
+
+ required property bool fileIsDir
+ required property string filePath
+ required property string fileName
+ required property int index
+ required property var fileModified
+ required property int fileSize
+
+ property int selectedIndex: -1
+ property bool keyboardNavigationActive: false
+
+ signal itemClicked(int index, string path, string name, bool isDir)
+ signal itemSelected(int index, string path, string name, bool isDir)
+
+ function getFileExtension(fileName) {
+ const parts = fileName.split('.');
+ if (parts.length > 1) {
+ return parts[parts.length - 1].toLowerCase();
+ }
+ return "";
+ }
+
+ function determineFileType(fileName) {
+ const ext = getFileExtension(fileName);
+
+ const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
+ if (imageExts.includes(ext)) {
+ return "image";
+ }
+
+ const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
+ if (videoExts.includes(ext)) {
+ return "video";
+ }
+
+ const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
+ if (audioExts.includes(ext)) {
+ return "audio";
+ }
+
+ const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
+ if (codeExts.includes(ext)) {
+ return "code";
+ }
+
+ const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
+ if (docExts.includes(ext)) {
+ return "document";
+ }
+
+ const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
+ if (archiveExts.includes(ext)) {
+ return "archive";
+ }
+
+ if (!ext || fileName.indexOf('.') === -1) {
+ return "binary";
+ }
+
+ return "file";
+ }
+
+ function isImageFile(fileName) {
+ if (!fileName) {
+ return false;
+ }
+ return determineFileType(fileName) === "image";
+ }
+
+ function isVideoFile(fileName) {
+ if (!fileName) {
+ return false;
+ }
+ return determineFileType(fileName) === "video";
+ }
+
+ property bool isImage: isImageFile(listDelegateRoot.fileName)
+ property bool isVideo: isVideoFile(listDelegateRoot.fileName)
+
+ property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
+ property string videoThumbnailPath: {
+ if (!listDelegateRoot.fileIsDir && isVideo) {
+ const hash = Qt.md5("file://" + listDelegateRoot.filePath);
+ return _xdgCacheHome + "/thumbnails/normal/" + hash + ".png";
+ }
+ return "";
+ }
+
+ property string _videoThumb: ""
+
+ onVideoThumbnailPathChanged: {
+ _videoThumb = "";
+ if (!videoThumbnailPath)
+ return;
+ const thumbPath = videoThumbnailPath;
+ const fp = listDelegateRoot.filePath;
+ Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
+ Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
+ if (exitCode === 0) {
+ _videoThumb = thumbPath;
+ } else {
+ Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function(output, exitCode) {
+ if (exitCode === 0)
+ _videoThumb = thumbPath;
+ });
+ }
+ });
+ }
+
+ function getIconForFile(fileName) {
+ const lowerName = fileName.toLowerCase();
+ if (lowerName.startsWith("dockerfile")) {
+ return "docker";
+ }
+ const ext = fileName.split('.').pop();
+ return ext || "";
+ }
+
+ function formatFileSize(size) {
+ if (size < 1024)
+ return size + " B";
+ if (size < 1024 * 1024)
+ return (size / 1024).toFixed(1) + " KB";
+ if (size < 1024 * 1024 * 1024)
+ return (size / (1024 * 1024)).toFixed(1) + " MB";
+ return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB";
+ }
+
+ height: 44
+ radius: Theme.cornerRadius
+ color: {
+ if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
+ return Theme.surfacePressed;
+ return listMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : "transparent";
+ }
+ border.color: keyboardNavigationActive && listDelegateRoot.index === selectedIndex ? Theme.primary : "transparent"
+ border.width: (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) ? 2 : 0
+
+ Component.onCompleted: {
+ if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
+ itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
+ }
+
+ onSelectedIndexChanged: {
+ if (keyboardNavigationActive && selectedIndex === listDelegateRoot.index)
+ itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
+ }
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ Item {
+ width: 28
+ height: 28
+ anchors.verticalCenter: parent.verticalCenter
+
+ Image {
+ id: listPreviewImage
+ anchors.fill: parent
+ property string imagePath: {
+ if (!listDelegateRoot.fileIsDir && isImage)
+ return listDelegateRoot.filePath;
+ if (_videoThumb)
+ return _videoThumb;
+ return "";
+ }
+ source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
+ fillMode: Image.PreserveAspectCrop
+ sourceSize.width: 32
+ sourceSize.height: 32
+ asynchronous: true
+ visible: false
+ }
+
+ MultiEffect {
+ anchors.fill: parent
+ source: listPreviewImage
+ maskEnabled: true
+ maskSource: listImageMask
+ visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && (isImage || isVideo)
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 1
+ }
+
+ Item {
+ id: listImageMask
+ anchors.fill: parent
+ layer.enabled: true
+ layer.smooth: true
+ visible: false
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: "black"
+ antialiasing: true
+ }
+ }
+
+ DankNFIcon {
+ anchors.centerIn: parent
+ name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
+ size: Theme.iconSize - 2
+ color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
+ visible: listDelegateRoot.fileIsDir || (!isImage && !(isVideo && listPreviewImage.status === Image.Ready))
+ }
+ }
+
+ StyledText {
+ text: listDelegateRoot.fileName || ""
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ width: parent.width - 280
+ elide: Text.ElideRight
+ anchors.verticalCenter: parent.verticalCenter
+ maximumLineCount: 1
+ clip: true
+ }
+
+ StyledText {
+ text: listDelegateRoot.fileIsDir ? "" : formatFileSize(listDelegateRoot.fileSize)
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ width: 70
+ horizontalAlignment: Text.AlignRight
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: Qt.formatDateTime(listDelegateRoot.fileModified, "MMM d, yyyy h:mm AP")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ width: 140
+ horizontalAlignment: Text.AlignRight
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: listMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserModal.qml
new file mode 100644
index 0000000..c7686ca
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserModal.qml
@@ -0,0 +1,97 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Widgets
+
+FloatingWindow {
+ id: fileBrowserModal
+
+ property bool disablePopupTransparency: true
+ property string browserTitle: "Select File"
+ property string browserIcon: "folder_open"
+ property string browserType: "generic"
+ property var fileExtensions: ["*.*"]
+ property alias filterExtensions: fileBrowserModal.fileExtensions
+ property bool showHiddenFiles: false
+ property bool saveMode: false
+ property string defaultFileName: ""
+ property var parentModal: null
+ property bool shouldHaveFocus: visible
+ property bool allowFocusOverride: false
+ property bool shouldBeVisible: visible
+ property bool allowStacking: true
+
+ signal fileSelected(string path)
+ signal dialogClosed
+
+ function open() {
+ visible = true;
+ }
+
+ function close() {
+ visible = false;
+ }
+
+ objectName: "fileBrowserModal"
+ title: "Files - " + browserTitle
+ minimumSize: Qt.size(500, 400)
+ implicitWidth: 800
+ implicitHeight: 600
+ color: Theme.surfaceContainer
+ visible: false
+
+ onVisibleChanged: {
+ if (visible) {
+ if (parentModal && "shouldHaveFocus" in parentModal) {
+ parentModal.shouldHaveFocus = false;
+ parentModal.allowFocusOverride = true;
+ }
+ Qt.callLater(() => {
+ if (content) {
+ content.reset();
+ content.forceActiveFocus();
+ }
+ });
+ } else {
+ if (parentModal && "allowFocusOverride" in parentModal) {
+ parentModal.allowFocusOverride = false;
+ parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
+ }
+ dialogClosed();
+ }
+ }
+
+ Loader {
+ id: contentLoader
+ anchors.fill: parent
+ active: fileBrowserModal.visible
+ sourceComponent: FileBrowserContent {
+ id: content
+ anchors.fill: parent
+ focus: true
+ closeOnEscape: false
+ windowControls: fileBrowserModal.windowControlsRef
+
+ browserTitle: fileBrowserModal.browserTitle
+ browserIcon: fileBrowserModal.browserIcon
+ browserType: fileBrowserModal.browserType
+ fileExtensions: fileBrowserModal.fileExtensions
+ showHiddenFiles: fileBrowserModal.showHiddenFiles
+ saveMode: fileBrowserModal.saveMode
+ defaultFileName: fileBrowserModal.defaultFileName
+
+ Component.onCompleted: initialize()
+
+ onFileSelected: path => fileBrowserModal.fileSelected(path)
+ onCloseRequested: fileBrowserModal.close()
+ }
+ }
+
+ property alias content: contentLoader.item
+ property alias windowControlsRef: windowControls
+
+ FloatingWindowControls {
+ id: windowControls
+ targetWindow: fileBrowserModal
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserNavigation.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserNavigation.qml
new file mode 100644
index 0000000..61ce7b6
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserNavigation.qml
@@ -0,0 +1,130 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Row {
+ id: navigation
+
+ property string currentPath: ""
+ property string homeDir: ""
+ property bool backButtonFocused: false
+ property bool keyboardNavigationActive: false
+ property bool showSidebar: true
+ property bool pathEditMode: false
+ property bool pathInputHasFocus: false
+
+ signal navigateUp()
+ signal navigateTo(string path)
+ signal pathInputFocusChanged(bool hasFocus)
+
+ height: 40
+ leftPadding: Theme.spacingM
+ rightPadding: Theme.spacingM
+ spacing: Theme.spacingS
+
+ StyledRect {
+ width: 32
+ height: 32
+ radius: Theme.cornerRadius
+ color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
+ opacity: currentPath !== homeDir ? 1 : 0
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "arrow_back"
+ size: Theme.iconSizeSmall
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: backButtonMouseArea
+
+ anchors.fill: parent
+ hoverEnabled: currentPath !== homeDir
+ cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: currentPath !== homeDir
+ onClicked: navigation.navigateUp()
+ }
+ }
+
+ Item {
+ width: Math.max(0, (parent?.width ?? 0) - 40 - Theme.spacingS - (showSidebar ? 0 : 80))
+ height: 32
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledRect {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: pathEditMode ? Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) : "transparent"
+ border.color: pathEditMode ? Theme.primary : "transparent"
+ border.width: pathEditMode ? 1 : 0
+ visible: !pathEditMode
+
+ StyledText {
+ id: pathDisplay
+ text: currentPath.replace("file://", "")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ elide: Text.ElideMiddle
+ verticalAlignment: Text.AlignVCenter
+ maximumLineCount: 1
+ wrapMode: Text.NoWrap
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.IBeamCursor
+ onClicked: {
+ pathEditMode = true
+ pathInput.text = currentPath.replace("file://", "")
+ Qt.callLater(() => pathInput.forceActiveFocus())
+ }
+ }
+ }
+
+ DankTextField {
+ id: pathInput
+ anchors.fill: parent
+ visible: pathEditMode
+ topPadding: Theme.spacingXS
+ bottomPadding: Theme.spacingXS
+ onAccepted: {
+ const newPath = text.trim()
+ if (newPath !== "") {
+ navigation.navigateTo(newPath)
+ }
+ pathEditMode = false
+ }
+ Keys.onEscapePressed: {
+ pathEditMode = false
+ }
+ Keys.onDownPressed: {
+ pathEditMode = false
+ }
+ onActiveFocusChanged: {
+ navigation.pathInputFocusChanged(activeFocus)
+ if (!activeFocus && pathEditMode) {
+ pathEditMode = false
+ }
+ }
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+ visible: !showSidebar
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankActionButton {
+ circular: false
+ iconName: "sort"
+ iconSize: Theme.iconSize - 6
+ iconColor: Theme.surfaceText
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserOverwriteDialog.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserOverwriteDialog.qml
new file mode 100644
index 0000000..a664be5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserOverwriteDialog.qml
@@ -0,0 +1,127 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: overwriteDialog
+
+ property bool showDialog: false
+ property string pendingFilePath: ""
+
+ signal confirmed(string filePath)
+ signal cancelled()
+
+ visible: showDialog
+ focus: showDialog
+
+ Keys.onEscapePressed: {
+ cancelled()
+ }
+
+ Keys.onReturnPressed: {
+ confirmed(pendingFilePath)
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: Theme.shadowStrong
+ opacity: 0.8
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ cancelled()
+ }
+ }
+ }
+
+ StyledRect {
+ anchors.centerIn: parent
+ width: 400
+ height: 160
+ color: Theme.surfaceContainer
+ radius: Theme.cornerRadius
+ border.color: Theme.outlineMedium
+ border.width: 1
+
+ Column {
+ anchors.centerIn: parent
+ width: parent.width - Theme.spacingL * 2
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("File Already Exists")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("A file with this name already exists. Do you want to overwrite it?")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ Row {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingM
+
+ StyledRect {
+ width: 80
+ height: 36
+ radius: Theme.cornerRadius
+ color: cancelArea.containsMouse ? Theme.surfaceVariantHover : Theme.surfaceVariant
+ border.color: Theme.outline
+ border.width: 1
+
+ StyledText {
+ anchors.centerIn: parent
+ text: I18n.tr("Cancel")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: cancelArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ cancelled()
+ }
+ }
+ }
+
+ StyledRect {
+ width: 90
+ height: 36
+ radius: Theme.cornerRadius
+ color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
+
+ StyledText {
+ anchors.centerIn: parent
+ text: I18n.tr("Overwrite")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.background
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: overwriteArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ confirmed(pendingFilePath)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSaveRow.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSaveRow.qml
new file mode 100644
index 0000000..6360c7f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSaveRow.qml
@@ -0,0 +1,74 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Row {
+ id: saveRow
+
+ property bool saveMode: false
+ property string defaultFileName: ""
+ property string currentPath: ""
+
+ signal saveRequested(string filePath)
+
+ height: saveMode ? 40 : 0
+ visible: saveMode
+ spacing: Theme.spacingM
+
+ DankTextField {
+ id: fileNameInput
+
+ width: parent.width - saveButton.width - Theme.spacingM
+ height: 40
+ text: defaultFileName
+ placeholderText: I18n.tr("Enter filename...")
+ ignoreLeftRightKeys: false
+ focus: saveMode
+ topPadding: Theme.spacingS
+ bottomPadding: Theme.spacingS
+ Component.onCompleted: {
+ if (saveMode)
+ Qt.callLater(() => {
+ forceActiveFocus()
+ })
+ }
+ onAccepted: {
+ if (text.trim() !== "") {
+ var basePath = currentPath.replace(/^file:\/\//, '')
+ var fullPath = basePath + "/" + text.trim()
+ fullPath = fullPath.replace(/\/+/g, '/')
+ saveRequested(fullPath)
+ }
+ }
+ }
+
+ StyledRect {
+ id: saveButton
+
+ width: 80
+ height: 40
+ color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
+ radius: Theme.cornerRadius
+
+ StyledText {
+ anchors.centerIn: parent
+ text: I18n.tr("Save")
+ color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
+ font.pixelSize: Theme.fontSizeMedium
+ }
+
+ StateLayer {
+ stateColor: Theme.primary
+ cornerRadius: Theme.cornerRadius
+ enabled: fileNameInput.text.trim() !== ""
+ onClicked: {
+ if (fileNameInput.text.trim() !== "") {
+ var basePath = currentPath.replace(/^file:\/\//, '')
+ var fullPath = basePath + "/" + fileNameInput.text.trim()
+ fullPath = fullPath.replace(/\/+/g, '/')
+ saveRequested(fullPath)
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSidebar.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSidebar.qml
new file mode 100644
index 0000000..bc02b3e
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSidebar.qml
@@ -0,0 +1,70 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+StyledRect {
+ id: sidebar
+
+ property var quickAccessLocations: []
+ property string currentPath: ""
+ signal locationSelected(string path)
+
+ width: 200
+ color: Theme.surface
+ clip: true
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ spacing: 4
+
+ StyledText {
+ text: I18n.tr("Quick Access")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ font.weight: Font.Medium
+ leftPadding: Theme.spacingS
+ bottomPadding: Theme.spacingXS
+ }
+
+ Repeater {
+ model: quickAccessLocations
+
+ StyledRect {
+ width: parent?.width ?? 0
+ height: 38
+ radius: Theme.cornerRadius
+ color: quickAccessMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : (currentPath === modelData?.path ? Theme.surfacePressed : "transparent")
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: modelData?.icon ?? ""
+ size: Theme.iconSize - 2
+ color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: modelData?.name ?? ""
+ font.pixelSize: Theme.fontSizeMedium
+ color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
+ font.weight: currentPath === modelData?.path ? Font.Medium : Font.Normal
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: quickAccessMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: locationSelected(modelData?.path ?? "")
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSortMenu.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSortMenu.qml
new file mode 100644
index 0000000..e1886f9
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSortMenu.qml
@@ -0,0 +1,183 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+StyledRect {
+ id: sortMenu
+
+ property string sortBy: "name"
+ property bool sortAscending: true
+
+ signal sortBySelected(string value)
+ signal sortOrderSelected(bool ascending)
+
+ width: 200
+ height: sortColumn.height + Theme.spacingM * 2
+ color: Theme.surfaceContainer
+ radius: Theme.cornerRadius
+ border.color: Theme.outlineMedium
+ border.width: 1
+ visible: false
+ z: 100
+
+ Column {
+ id: sortColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: "Sort By"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ font.weight: Font.Medium
+ }
+
+ Repeater {
+ model: [{
+ "name": "Name",
+ "value": "name"
+ }, {
+ "name": "Size",
+ "value": "size"
+ }, {
+ "name": "Modified",
+ "value": "modified"
+ }, {
+ "name": "Type",
+ "value": "type"
+ }]
+
+ StyledRect {
+ width: sortColumn?.width ?? 0
+ height: 32
+ radius: Theme.cornerRadius
+ color: sortMouseArea.containsMouse ? Theme.surfaceVariant : (sortBy === modelData?.value ? Theme.surfacePressed : "transparent")
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: sortBy === modelData?.value ? "check" : ""
+ size: Theme.iconSizeSmall
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ visible: sortBy === modelData?.value
+ }
+
+ StyledText {
+ text: modelData?.name ?? ""
+ font.pixelSize: Theme.fontSizeMedium
+ color: sortBy === modelData?.value ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: sortMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ sortMenu.sortBySelected(modelData?.value ?? "name")
+ sortMenu.visible = false
+ }
+ }
+ }
+ }
+
+ StyledRect {
+ width: sortColumn.width
+ height: 1
+ color: Theme.outline
+ }
+
+ StyledText {
+ text: "Order"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ font.weight: Font.Medium
+ topPadding: Theme.spacingXS
+ }
+
+ StyledRect {
+ width: sortColumn?.width ?? 0
+ height: 32
+ radius: Theme.cornerRadius
+ color: ascMouseArea.containsMouse ? Theme.surfaceVariant : (sortAscending ? Theme.surfacePressed : "transparent")
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "arrow_upward"
+ size: Theme.iconSizeSmall
+ color: sortAscending ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: "Ascending"
+ font.pixelSize: Theme.fontSizeMedium
+ color: sortAscending ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: ascMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ sortMenu.sortOrderSelected(true)
+ sortMenu.visible = false
+ }
+ }
+ }
+
+ StyledRect {
+ width: sortColumn?.width ?? 0
+ height: 32
+ radius: Theme.cornerRadius
+ color: descMouseArea.containsMouse ? Theme.surfaceVariant : (!sortAscending ? Theme.surfacePressed : "transparent")
+
+ Row {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "arrow_downward"
+ size: Theme.iconSizeSmall
+ color: !sortAscending ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: "Descending"
+ font.pixelSize: Theme.fontSizeMedium
+ color: !sortAscending ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: descMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ sortMenu.sortOrderSelected(false)
+ sortMenu.visible = false
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml
new file mode 100644
index 0000000..25b8d5a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileBrowserSurfaceModal.qml
@@ -0,0 +1,66 @@
+import QtQuick
+import Quickshell.Wayland
+import qs.Common
+import qs.Modals.Common
+
+DankModal {
+ id: fileBrowserSurfaceModal
+
+ property string browserTitle: "Select File"
+ property string browserIcon: "folder_open"
+ property string browserType: "generic"
+ property var fileExtensions: ["*.*"]
+ property alias filterExtensions: fileBrowserSurfaceModal.fileExtensions
+ property bool showHiddenFiles: false
+ property bool saveMode: false
+ property string defaultFileName: ""
+ property var parentPopout: null
+
+ signal fileSelected(string path)
+
+ layerNamespace: "dms:filebrowser"
+ modalWidth: 800
+ modalHeight: 600
+ backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ closeOnEscapeKey: true
+ closeOnBackgroundClick: true
+ allowStacking: true
+ keepPopoutsOpen: true
+
+ onBackgroundClicked: close()
+
+ onOpened: {
+ if (parentPopout) {
+ parentPopout.customKeyboardFocus = WlrKeyboardFocus.None;
+ }
+ Qt.callLater(() => {
+ if (contentLoader.item) {
+ contentLoader.item.reset();
+ contentLoader.item.forceActiveFocus();
+ }
+ });
+ }
+
+ onDialogClosed: {
+ if (parentPopout) {
+ parentPopout.customKeyboardFocus = null;
+ }
+ }
+
+ content: FileBrowserContent {
+ focus: true
+
+ browserTitle: fileBrowserSurfaceModal.browserTitle
+ browserIcon: fileBrowserSurfaceModal.browserIcon
+ browserType: fileBrowserSurfaceModal.browserType
+ fileExtensions: fileBrowserSurfaceModal.fileExtensions
+ showHiddenFiles: fileBrowserSurfaceModal.showHiddenFiles
+ saveMode: fileBrowserSurfaceModal.saveMode
+ defaultFileName: fileBrowserSurfaceModal.defaultFileName
+
+ Component.onCompleted: initialize()
+
+ onFileSelected: path => fileBrowserSurfaceModal.fileSelected(path)
+ onCloseRequested: fileBrowserSurfaceModal.close()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileInfo.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileInfo.qml
new file mode 100644
index 0000000..c41f039
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/FileInfo.qml
@@ -0,0 +1,237 @@
+import QtQuick
+import QtCore
+import Quickshell.Io
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property bool showFileInfo: false
+ property int selectedIndex: -1
+ property var sourceFolderModel: null
+ property string currentPath: ""
+
+ height: 200
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
+ border.color: Theme.secondary
+ border.width: 2
+ opacity: showFileInfo ? 1 : 0
+ z: 100
+
+ onShowFileInfoChanged: {
+ if (showFileInfo && currentFileName && currentPath) {
+ const fullPath = currentPath + "/" + currentFileName
+ fileStatProcess.selectedFilePath = fullPath
+ fileStatProcess.running = true
+ }
+ }
+
+ Process {
+ id: fileStatProcess
+ command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath]
+ property string selectedFilePath: ""
+ property var fileStats: null
+ running: false
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ if (text && text.trim()) {
+ const parts = text.trim().split('|')
+ if (parts.length >= 4) {
+ fileStatProcess.fileStats = {
+ "modifiedTime": parts[0],
+ "permissions": parts[1],
+ "size": parseInt(parts[2]) || 0,
+ "fullPath": parts[3]
+ }
+ }
+ }
+ }
+ }
+
+ onExited: function (exitCode) {}
+ }
+
+ property string currentFileName: ""
+ property bool currentFileIsDir: false
+ property string currentFileExtension: ""
+
+ onCurrentFileNameChanged: {
+ if (showFileInfo && currentFileName && currentPath) {
+ const fullPath = currentPath + "/" + currentFileName
+ if (fullPath !== fileStatProcess.selectedFilePath) {
+ fileStatProcess.selectedFilePath = fullPath
+ fileStatProcess.running = true
+ }
+ }
+ }
+
+ function updateFileInfo(filePath, fileName, isDirectory) {
+ if (filePath && filePath !== fileStatProcess.selectedFilePath) {
+ fileStatProcess.selectedFilePath = filePath
+ currentFileName = fileName || ""
+ currentFileIsDir = isDirectory || false
+
+ let ext = ""
+ if (!isDirectory && fileName) {
+ const lastDot = fileName.lastIndexOf('.')
+ if (lastDot > 0) {
+ ext = fileName.substring(lastDot + 1).toLowerCase()
+ }
+ }
+ currentFileExtension = ext
+
+ if (showFileInfo) {
+ fileStatProcess.running = true
+ }
+ }
+ }
+
+ readonly property var currentFileDisplayData: {
+ if (selectedIndex < 0 || !sourceFolderModel) {
+ return {
+ "exists": false,
+ "name": "No selection",
+ "type": "",
+ "size": "",
+ "modified": "",
+ "permissions": "",
+ "extension": "",
+ "position": "N/A"
+ }
+ }
+
+ const hasValidFile = currentFileName !== ""
+ return {
+ "exists": hasValidFile,
+ "name": hasValidFile ? currentFileName : "Loading...",
+ "type": currentFileIsDir ? "Directory" : "File",
+ "size": fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...",
+ "modified": fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...",
+ "permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
+ "extension": currentFileExtension,
+ "position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
+ }
+ }
+
+ Column {
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingXS
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "info"
+ size: Theme.iconSize
+ color: Theme.secondary
+ }
+
+ StyledText {
+ text: I18n.tr("File Information")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: currentFileDisplayData.name
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ width: parent.width
+ elide: Text.ElideMiddle
+ wrapMode: Text.NoWrap
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ }
+
+ StyledText {
+ text: currentFileDisplayData.size
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ visible: currentFileDisplayData.exists && !currentFileIsDir
+ }
+
+ StyledText {
+ text: currentFileDisplayData.modified
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ elide: Text.ElideRight
+ visible: currentFileDisplayData.exists
+ }
+
+ StyledText {
+ text: currentFileDisplayData.permissions
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ visible: currentFileDisplayData.exists
+ }
+
+ StyledText {
+ text: currentFileDisplayData.position
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ }
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("F1/I: Toggle • F10: Help")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingM
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ function formatFileSize(bytes) {
+ if (bytes === 0 || !bytes) {
+ return "0 B"
+ }
+ const k = 1024
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
+ }
+
+ function formatDateTime(dateTimeString) {
+ if (!dateTimeString) {
+ return "Unknown"
+ }
+ const parts = dateTimeString.split(' ')
+ if (parts.length >= 2) {
+ return parts[0] + " " + parts[1].split('.')[0]
+ }
+ return dateTimeString
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/KeyboardHints.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/KeyboardHints.qml
new file mode 100644
index 0000000..1910810
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/FileBrowser/KeyboardHints.qml
@@ -0,0 +1,50 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property bool showHints: false
+
+ height: 80
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
+ border.color: Theme.primary
+ border.width: 2
+ opacity: showHints ? 1 : 0
+ z: 100
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingS
+ spacing: 2
+
+ StyledText {
+ text: I18n.tr("Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterCompletePage.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterCompletePage.qml
new file mode 100644
index 0000000..8a5ca00
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterCompletePage.qml
@@ -0,0 +1,492 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property var greeterRoot: parent ? parent.greeterRoot : null
+
+ readonly property real headerIconContainerSize: Math.round(Theme.iconSize * 2)
+ readonly property real sectionIconSize: Theme.iconSizeSmall + 2
+ readonly property real keybindRowHeight: Math.round(Theme.fontSizeMedium * 2)
+ readonly property real keyBadgeHeight: Math.round(Theme.fontSizeSmall * 1.83)
+
+ readonly property var featureNames: ({
+ "spotlight": "App Launcher",
+ "clipboard": "Clipboard",
+ "processlist": "Task Manager",
+ "settings": "Settings",
+ "notifications": "Notifications",
+ "notepad": "Notepad",
+ "hotkeys": "Keybinds",
+ "lock": "Lock Screen",
+ "dankdash": "Dashboard"
+ })
+
+ function getFeatureDesc(action) {
+ const match = action.match(/dms\s+ipc\s+call\s+(\w+)/);
+ if (match && featureNames[match[1]])
+ return featureNames[match[1]];
+ return null;
+ }
+
+ readonly property var dmsKeybinds: {
+ if (!greeterRoot || !greeterRoot.cheatsheetLoaded || !greeterRoot.cheatsheetData || !greeterRoot.cheatsheetData.binds)
+ return [];
+ const seen = new Set();
+ const binds = [];
+ const allBinds = greeterRoot.cheatsheetData.binds;
+ for (const category in allBinds) {
+ const categoryBinds = allBinds[category];
+ for (let i = 0; i < categoryBinds.length; i++) {
+ const bind = categoryBinds[i];
+ if (!bind.key || !bind.action)
+ continue;
+ if (!bind.action.includes("dms"))
+ continue;
+ if (!(bind.action.includes("spawn") || bind.action.includes("exec")))
+ continue;
+ const feature = getFeatureDesc(bind.action);
+ if (!feature)
+ continue;
+ if (seen.has(feature))
+ continue;
+ seen.add(feature);
+ binds.push({
+ key: bind.key,
+ desc: feature
+ });
+ }
+ }
+ return binds;
+ }
+
+ readonly property bool hasKeybinds: dmsKeybinds.length > 0
+
+ DankFlickable {
+ anchors.fill: parent
+ clip: true
+ contentHeight: mainColumn.height + Theme.spacingL * 2
+ contentWidth: width
+
+ Column {
+ id: mainColumn
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: Math.min(640, parent.width - Theme.spacingXL * 2)
+ topPadding: Theme.spacingL
+ spacing: Theme.spacingL
+
+ Row {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: root.headerIconContainerSize
+ height: root.headerIconContainerSize
+ radius: Math.round(root.headerIconContainerSize * 0.29)
+ color: Theme.withAlpha(Theme.success, 0.15)
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "check_circle"
+ size: Theme.iconSize + 4
+ color: Theme.success
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ StyledText {
+ text: I18n.tr("You're All Set!", "greeter completion page title")
+ font.pixelSize: Theme.fontSizeXLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: I18n.tr("DankMaterialShell is ready to use", "greeter completion page subtitle")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingS
+ visible: root.hasKeybinds
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "keyboard"
+ size: root.sectionIconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("DMS Shortcuts", "greeter keybinds section header")
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ id: keybindsRect
+ width: parent.width
+ height: keybindsGrid.height + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainerHigh
+
+ readonly property bool useTwoColumns: width > 500
+ readonly property int columnCount: useTwoColumns ? 2 : 1
+ readonly property real itemWidth: useTwoColumns ? (width - Theme.spacingM * 3) / 2 : width - Theme.spacingM * 2
+ property real maxKeyWidth: 0
+
+ Grid {
+ id: keybindsGrid
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingM
+ columns: keybindsRect.columnCount
+ rowSpacing: Theme.spacingS
+ columnSpacing: Theme.spacingM
+
+ Repeater {
+ model: root.dmsKeybinds
+
+ Row {
+ width: keybindsRect.itemWidth
+ height: root.keybindRowHeight
+ spacing: Theme.spacingS
+
+ Item {
+ width: keybindsRect.maxKeyWidth
+ height: parent.height
+
+ Row {
+ id: keysRow
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ property real naturalWidth: {
+ let w = 0;
+ for (let i = 0; i < children.length; i++) {
+ if (children[i].visible)
+ w += children[i].width + (i > 0 ? Theme.spacingXS : 0);
+ }
+ return w;
+ }
+
+ Component.onCompleted: {
+ Qt.callLater(() => {
+ if (naturalWidth > keybindsRect.maxKeyWidth)
+ keybindsRect.maxKeyWidth = naturalWidth;
+ });
+ }
+
+ Repeater {
+ model: (modelData.key || "").split("+")
+
+ Rectangle {
+ width: singleKeyText.implicitWidth + Theme.spacingM
+ height: root.keyBadgeHeight
+ radius: Theme.spacingXS
+ color: Theme.surfaceContainerHighest
+ border.width: 1
+ border.color: Theme.outline
+
+ StyledText {
+ id: singleKeyText
+ anchors.centerIn: parent
+ color: Theme.secondary
+ text: modelData
+ font.pixelSize: Theme.fontSizeSmall - 1
+ font.weight: Font.Medium
+ isMonospace: true
+ }
+ }
+ }
+ }
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - keybindsRect.maxKeyWidth - Theme.spacingS
+ text: modelData.desc || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: noKeybindsColumn.height + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainerHigh
+ visible: !root.hasKeybinds
+
+ Column {
+ id: noKeybindsColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ Row {
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "keyboard"
+ size: root.sectionIconSize
+ color: Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("No DMS shortcuts configured", "greeter no keybinds message")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: Math.round(Theme.fontSizeMedium * 2.85)
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainerHighest
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: Theme.primary
+ opacity: noKeybindsLinkMouse.containsMouse ? 0.12 : 0
+ }
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "menu_book"
+ size: root.sectionIconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Configure Keybinds", "greeter configure keybinds link")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ DankIcon {
+ name: "open_in_new"
+ size: Theme.iconSizeSmall - 2
+ color: Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: noKeybindsLinkMouse
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ let url = "https://danklinux.com/docs/dankmaterialshell/keybinds-ipc";
+ if (CompositorService.isNiri)
+ url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings";
+ else if (CompositorService.isHyprland)
+ url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-1";
+ else if (CompositorService.isDwl)
+ url = "https://danklinux.com/docs/dankmaterialshell/compositors#dms-keybindings-2";
+ Qt.openUrlExternally(url);
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ opacity: 0.3
+ visible: root.hasKeybinds
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "settings"
+ size: root.sectionIconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Configure", "greeter settings section header")
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Grid {
+ width: parent.width
+ columns: 2
+ rowSpacing: Theme.spacingS
+ columnSpacing: Theme.spacingS
+
+ GreeterSettingsCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "display_settings"
+ title: I18n.tr("Displays", "greeter settings link")
+ description: I18n.tr("Resolution, position, scale", "greeter displays description")
+ onClicked: PopoutService.openSettingsWithTab("display_config")
+ }
+
+ GreeterSettingsCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "wallpaper"
+ title: I18n.tr("Wallpaper", "greeter settings link")
+ description: I18n.tr("Background image", "greeter wallpaper description")
+ onClicked: PopoutService.openSettingsWithTab("wallpaper")
+ }
+
+ GreeterSettingsCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "format_paint"
+ title: I18n.tr("Theme & Colors", "greeter settings link")
+ description: I18n.tr("Dynamic colors, presets", "greeter theme description")
+ onClicked: PopoutService.openSettingsWithTab("theme")
+ }
+
+ GreeterSettingsCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "notifications"
+ title: I18n.tr("Notifications", "greeter settings link")
+ description: I18n.tr("Popup behavior, position", "greeter notifications description")
+ onClicked: PopoutService.openSettingsWithTab("notifications")
+ }
+
+ GreeterSettingsCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "toolbar"
+ title: I18n.tr("DankBar", "greeter settings link")
+ description: I18n.tr("Widgets, layout, style", "greeter dankbar description")
+ onClicked: PopoutService.openSettingsWithTab("dankbar_settings")
+ }
+
+ GreeterSettingsCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "keyboard"
+ title: I18n.tr("Keybinds", "greeter settings link")
+ description: I18n.tr("niri shortcuts config", "greeter keybinds niri description")
+ visible: KeybindsService.available
+ onClicked: PopoutService.openSettingsWithTab("keybinds")
+ }
+
+ GreeterSettingsCard {
+ width: (parent.width - Theme.spacingS) / 2
+ iconName: "dock_to_bottom"
+ title: I18n.tr("Dock", "greeter settings link")
+ description: I18n.tr("Position, pinned apps", "greeter dock description")
+ visible: !KeybindsService.available
+ onClicked: PopoutService.openSettingsWithTab("dock")
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ opacity: 0.3
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "explore"
+ size: root.sectionIconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Explore", "greeter explore section header")
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ GreeterQuickLink {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "menu_book"
+ title: I18n.tr("Docs", "greeter documentation link")
+ isExternal: true
+ onClicked: Qt.openUrlExternally("https://danklinux.com/docs")
+ }
+
+ GreeterQuickLink {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "extension"
+ title: I18n.tr("Plugins", "greeter plugins link")
+ isExternal: true
+ onClicked: Qt.openUrlExternally("https://danklinux.com/plugins")
+ }
+
+ GreeterQuickLink {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "palette"
+ title: I18n.tr("Themes", "greeter themes link")
+ isExternal: true
+ onClicked: Qt.openUrlExternally("https://danklinux.com/plugins?tab=themes")
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterDoctorPage.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterDoctorPage.qml
new file mode 100644
index 0000000..288cd58
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterDoctorPage.qml
@@ -0,0 +1,427 @@
+import QtQuick
+import Quickshell.Io
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: root
+
+ property bool isRunning: false
+ property bool hasRun: false
+ property var doctorResults: null
+ property int errorCount: 0
+ property int warningCount: 0
+ property int okCount: 0
+ property int infoCount: 0
+ property string selectedFilter: "error"
+
+ readonly property real loadingContainerSize: Math.round(Theme.iconSize * 5)
+ readonly property real pulseRingSize: Math.round(Theme.iconSize * 3.3)
+ readonly property real centerIconContainerSize: Math.round(Theme.iconSize * 2.67)
+ readonly property real headerIconContainerSize: Math.round(Theme.iconSize * 2)
+
+ readonly property var filteredResults: {
+ if (!doctorResults?.results)
+ return [];
+ return doctorResults.results.filter(r => r.status === selectedFilter);
+ }
+
+ function runDoctor() {
+ hasRun = false;
+ isRunning = true;
+ doctorProcess.running = true;
+ }
+
+ Component.onCompleted: runDoctor()
+
+ Item {
+ id: loadingView
+ anchors.fill: parent
+ visible: root.isRunning
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXL
+
+ Item {
+ width: root.loadingContainerSize
+ height: root.loadingContainerSize
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ Rectangle {
+ id: pulseRing1
+ anchors.centerIn: parent
+ width: root.pulseRingSize
+ height: root.pulseRingSize
+ radius: root.pulseRingSize / 2
+ color: "transparent"
+ border.width: Math.round(Theme.spacingXS * 0.75)
+ border.color: Theme.primary
+ opacity: 0
+
+ SequentialAnimation on opacity {
+ running: root.isRunning
+ loops: Animation.Infinite
+ NumberAnimation {
+ from: 0.8
+ to: 0
+ duration: 1500
+ easing.type: Easing.OutQuad
+ }
+ }
+
+ SequentialAnimation on scale {
+ running: root.isRunning
+ loops: Animation.Infinite
+ NumberAnimation {
+ from: 0.5
+ to: 1.5
+ duration: 1500
+ easing.type: Easing.OutQuad
+ }
+ }
+ }
+
+ Rectangle {
+ id: pulseRing2
+ anchors.centerIn: parent
+ width: root.pulseRingSize
+ height: root.pulseRingSize
+ radius: root.pulseRingSize / 2
+ color: "transparent"
+ border.width: Math.round(Theme.spacingXS * 0.75)
+ border.color: Theme.secondary
+ opacity: 0
+
+ SequentialAnimation on opacity {
+ running: root.isRunning
+ loops: Animation.Infinite
+ NumberAnimation {
+ from: 0.8
+ to: 0
+ duration: 1500
+ easing.type: Easing.OutQuad
+ }
+ }
+
+ SequentialAnimation on scale {
+ running: root.isRunning
+ loops: Animation.Infinite
+ NumberAnimation {
+ from: 0.3
+ to: 1.3
+ duration: 1500
+ easing.type: Easing.OutQuad
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: root.centerIconContainerSize
+ height: root.centerIconContainerSize
+ radius: root.centerIconContainerSize / 2
+ color: Theme.primaryContainer
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "vital_signs"
+ size: Theme.iconSizeLarge
+ color: Theme.primary
+ }
+
+ SequentialAnimation on scale {
+ running: root.isRunning
+ loops: Animation.Infinite
+ NumberAnimation {
+ from: 1
+ to: 1.1
+ duration: 750
+ easing.type: Easing.InOutQuad
+ }
+ NumberAnimation {
+ from: 1.1
+ to: 1
+ duration: 750
+ easing.type: Easing.InOutQuad
+ }
+ }
+ }
+ }
+
+ Column {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: I18n.tr("System Check", "greeter doctor page title")
+ font.pixelSize: Theme.fontSizeXLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Analyzing configuration...", "greeter doctor page loading text")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+ }
+
+ Item {
+ id: resultsView
+ anchors.fill: parent
+ visible: root.hasRun && !root.isRunning
+ opacity: (root.hasRun && !root.isRunning) ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Column {
+ id: headerSection
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: Theme.spacingL
+ anchors.leftMargin: Theme.spacingXL
+ anchors.rightMargin: Theme.spacingXL
+ spacing: Theme.spacingL
+
+ Row {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: root.headerIconContainerSize
+ height: root.headerIconContainerSize
+ radius: Math.round(root.headerIconContainerSize * 0.29)
+ color: root.errorCount > 0 ? Theme.errorContainer : Theme.primaryContainer
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.errorCount > 0 ? "warning" : "check_circle"
+ size: Theme.iconSize + 4
+ color: root.errorCount > 0 ? Theme.error : Theme.primary
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ StyledText {
+ text: I18n.tr("System Check", "greeter doctor page title")
+ font.pixelSize: Theme.fontSizeXLarge
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: {
+ if (root.errorCount === 0)
+ return I18n.tr("All checks passed", "greeter doctor page success");
+ return root.errorCount === 1
+ ? I18n.tr("%1 issue found", "greeter doctor page error count").arg(root.errorCount)
+ : I18n.tr("%1 issues found", "greeter doctor page error count").arg(root.errorCount);
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
+ }
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ GreeterStatusCard {
+ width: (parent.width - Theme.spacingS * 3) / 4
+ count: root.errorCount
+ label: I18n.tr("Errors", "greeter doctor page status card")
+ iconName: "error"
+ iconColor: Theme.error
+ bgColor: Theme.errorContainer || Theme.withAlpha(Theme.error, 0.15)
+ selected: root.selectedFilter === "error"
+ onClicked: root.selectedFilter = "error"
+ }
+
+ GreeterStatusCard {
+ width: (parent.width - Theme.spacingS * 3) / 4
+ count: root.warningCount
+ label: I18n.tr("Warnings", "greeter doctor page status card")
+ iconName: "warning"
+ iconColor: Theme.warning
+ bgColor: Theme.withAlpha(Theme.warning, 0.15)
+ selected: root.selectedFilter === "warn"
+ onClicked: root.selectedFilter = "warn"
+ }
+
+ GreeterStatusCard {
+ width: (parent.width - Theme.spacingS * 3) / 4
+ count: root.infoCount
+ label: I18n.tr("Info", "greeter doctor page status card")
+ iconName: "info"
+ iconColor: Theme.secondary
+ bgColor: Theme.withAlpha(Theme.secondary, 0.15)
+ selected: root.selectedFilter === "info"
+ onClicked: root.selectedFilter = "info"
+ }
+
+ GreeterStatusCard {
+ width: (parent.width - Theme.spacingS * 3) / 4
+ count: root.okCount
+ label: I18n.tr("OK", "greeter doctor page status card")
+ iconName: "check_circle"
+ iconColor: Theme.success
+ bgColor: Theme.withAlpha(Theme.success, 0.15)
+ selected: root.selectedFilter === "ok"
+ onClicked: root.selectedFilter = "ok"
+ }
+ }
+ }
+
+ Rectangle {
+ id: resultsContainer
+ anchors.top: headerSection.bottom
+ anchors.bottom: footerSection.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.topMargin: Theme.spacingL
+ anchors.bottomMargin: Theme.spacingM
+ anchors.leftMargin: Theme.spacingXL
+ anchors.rightMargin: Theme.spacingXL
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainerHigh
+ clip: true
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+ visible: root.filteredResults.length === 0
+
+ DankIcon {
+ name: {
+ switch (root.selectedFilter) {
+ case "error":
+ return "check_circle";
+ case "warn":
+ return "thumb_up";
+ case "info":
+ return "info";
+ default:
+ return "verified";
+ }
+ }
+ size: Math.round(Theme.iconSize * 1.67)
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: {
+ switch (root.selectedFilter) {
+ case "error":
+ return I18n.tr("No errors", "greeter doctor page empty state");
+ case "warn":
+ return I18n.tr("No warnings", "greeter doctor page empty state");
+ case "info":
+ return I18n.tr("No info items", "greeter doctor page empty state");
+ default:
+ return I18n.tr("No checks passed", "greeter doctor page empty state");
+ }
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ DankFlickable {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ clip: true
+ contentHeight: resultsColumn.height
+ contentWidth: width
+ visible: root.filteredResults.length > 0
+
+ Column {
+ id: resultsColumn
+ width: parent.width
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: root.filteredResults
+
+ GreeterDoctorResultItem {
+ width: resultsColumn.width
+ resultData: modelData
+ }
+ }
+ }
+ }
+ }
+
+ Row {
+ id: footerSection
+ anchors.bottom: parent.bottom
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottomMargin: Theme.spacingL
+ spacing: Theme.spacingM
+
+ DankButton {
+ text: I18n.tr("Run Again", "greeter doctor page button")
+ iconName: "refresh"
+ backgroundColor: Theme.surfaceContainerHighest
+ textColor: Theme.surfaceText
+ onClicked: root.runDoctor()
+ }
+ }
+ }
+
+ Process {
+ id: doctorProcess
+ command: ["dms", "doctor", "--json"]
+ running: false
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ root.isRunning = false;
+ root.hasRun = true;
+ try {
+ root.doctorResults = JSON.parse(text);
+ if (root.doctorResults?.summary) {
+ root.errorCount = root.doctorResults.summary.errors || 0;
+ root.warningCount = root.doctorResults.summary.warnings || 0;
+ root.okCount = root.doctorResults.summary.ok || 0;
+ root.infoCount = root.doctorResults.summary.info || 0;
+ }
+ if (root.errorCount > 0)
+ root.selectedFilter = "error";
+ else if (root.warningCount > 0)
+ root.selectedFilter = "warn";
+ else if (root.infoCount > 0)
+ root.selectedFilter = "info";
+ else
+ root.selectedFilter = "ok";
+ } catch (e) {
+ console.error("GreeterDoctorPage: Failed to parse doctor output:", e);
+ }
+ }
+ }
+
+ onExited: exitCode => {
+ if (exitCode !== 0) {
+ root.isRunning = false;
+ root.hasRun = true;
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterDoctorResultItem.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterDoctorResultItem.qml
new file mode 100644
index 0000000..b15969a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterDoctorResultItem.qml
@@ -0,0 +1,109 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var resultData: null
+
+ readonly property string status: resultData?.status || "ok"
+ readonly property string statusIcon: {
+ switch (status) {
+ case "error":
+ return "error";
+ case "warn":
+ return "warning";
+ case "info":
+ return "info";
+ default:
+ return "check_circle";
+ }
+ }
+ readonly property color statusColor: {
+ switch (status) {
+ case "error":
+ return Theme.error;
+ case "warn":
+ return Theme.warning;
+ case "info":
+ return Theme.secondary;
+ default:
+ return Theme.success;
+ }
+ }
+
+ height: Math.round(Theme.fontSizeMedium * 3.4)
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(statusColor, 0.08)
+
+ DankIcon {
+ id: statusIcon
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ name: root.statusIcon
+ size: Theme.iconSize - 4
+ color: root.statusColor
+ }
+
+ Column {
+ anchors.left: statusIcon.right
+ anchors.leftMargin: Theme.spacingS
+ anchors.right: categoryChip.visible ? categoryChip.left : (urlButton.visible ? urlButton.left : parent.right)
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 1
+
+ StyledText {
+ width: parent.width
+ text: root.resultData?.name || ""
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ }
+
+ StyledText {
+ width: parent.width
+ text: root.resultData?.message || ""
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ visible: text.length > 0
+ }
+ }
+
+ Rectangle {
+ id: categoryChip
+ anchors.right: urlButton.visible ? urlButton.left : parent.right
+ anchors.rightMargin: urlButton.visible ? Theme.spacingXS : Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ height: Math.round(Theme.fontSizeSmall * 1.67)
+ width: categoryText.implicitWidth + Theme.spacingS
+ radius: Theme.spacingXS
+ color: Theme.surfaceContainerHighest
+ visible: !!(root.resultData?.category)
+
+ StyledText {
+ id: categoryText
+ anchors.centerIn: parent
+ text: root.resultData?.category || ""
+ font.pixelSize: Theme.fontSizeSmall - 2
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ DankActionButton {
+ id: urlButton
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ iconName: "open_in_new"
+ iconSize: Theme.iconSize - 6
+ buttonSize: 24
+ visible: !!(root.resultData?.url)
+ tooltipText: root.resultData?.url || ""
+ onClicked: Qt.openUrlExternally(root.resultData.url)
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterFeatureCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterFeatureCard.qml
new file mode 100644
index 0000000..a5bf4f4
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterFeatureCard.qml
@@ -0,0 +1,74 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property string iconName: ""
+ property string title: ""
+ property string description: ""
+
+ signal clicked
+
+ readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5)
+
+ height: Math.round(Theme.fontSizeMedium * 6.4)
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainerHigh
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: Theme.primary
+ opacity: mouseArea.containsMouse ? 0.12 : 0
+ }
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ Rectangle {
+ width: root.iconContainerSize
+ height: root.iconContainerSize
+ radius: Math.round(root.iconContainerSize * 0.28)
+ color: Theme.primaryContainer
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.iconName
+ size: Theme.iconSize - 4
+ color: Theme.primary
+ }
+ }
+
+ Column {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: 2
+
+ StyledText {
+ text: root.title
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clicked()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterModal.qml
new file mode 100644
index 0000000..973dab2
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterModal.qml
@@ -0,0 +1,333 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+FloatingWindow {
+ id: root
+
+ property bool disablePopupTransparency: true
+ property int currentPage: 0
+ readonly property int totalPages: 3
+ readonly property var pageComponents: [welcomePage, doctorPage, completePage]
+
+ property var cheatsheetData: ({})
+ property bool cheatsheetLoaded: false
+
+ readonly property int modalWidth: 720
+ readonly property int modalHeight: screen ? Math.min(760, screen.height - 80) : 760
+
+ signal greeterCompleted
+
+ Component.onCompleted: Qt.callLater(loadCheatsheet)
+
+ function loadCheatsheet() {
+ const provider = KeybindsService.cheatsheetProvider;
+ if (KeybindsService.cheatsheetAvailable && provider && !cheatsheetLoaded) {
+ cheatsheetProcess.command = ["dms", "keybinds", "show", provider];
+ cheatsheetProcess.running = true;
+ }
+ }
+
+ Connections {
+ target: KeybindsService
+ function onCheatsheetAvailableChanged() {
+ if (KeybindsService.cheatsheetAvailable && !root.cheatsheetLoaded)
+ loadCheatsheet();
+ }
+ }
+
+ function getKeybind(actionPattern) {
+ if (!cheatsheetLoaded || !cheatsheetData.binds)
+ return "";
+ for (const category in cheatsheetData.binds) {
+ const binds = cheatsheetData.binds[category];
+ for (let i = 0; i < binds.length; i++) {
+ const bind = binds[i];
+ if (bind.action && bind.action.includes(actionPattern))
+ return bind.key || "";
+ }
+ }
+ return "";
+ }
+
+ function show() {
+ currentPage = FirstLaunchService.requestedStartPage || 0;
+ visible = true;
+ }
+
+ function showAtPage(page) {
+ currentPage = page;
+ visible = true;
+ }
+
+ function nextPage() {
+ if (currentPage < totalPages - 1)
+ currentPage++;
+ }
+
+ function prevPage() {
+ if (currentPage > 0)
+ currentPage--;
+ }
+
+ function finish() {
+ FirstLaunchService.markFirstLaunchComplete();
+ greeterCompleted();
+ visible = false;
+ }
+
+ function skip() {
+ FirstLaunchService.markFirstLaunchComplete();
+ greeterCompleted();
+ visible = false;
+ }
+
+ objectName: "greeterModal"
+ title: I18n.tr("Welcome", "greeter modal window title")
+ minimumSize: Qt.size(modalWidth, modalHeight)
+ maximumSize: Qt.size(modalWidth, modalHeight)
+ color: Theme.surfaceContainer
+ visible: false
+
+ Process {
+ id: cheatsheetProcess
+ running: false
+
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const trimmed = text.trim();
+ if (trimmed.length === 0)
+ return;
+ try {
+ root.cheatsheetData = JSON.parse(trimmed);
+ root.cheatsheetLoaded = true;
+ } catch (e) {
+ console.warn("Greeter: Failed to parse cheatsheet:", e);
+ }
+ }
+ }
+ }
+
+ FocusScope {
+ id: contentFocusScope
+ anchors.fill: parent
+ focus: true
+
+ Keys.onEscapePressed: event => {
+ root.skip();
+ event.accepted = true;
+ }
+
+ Keys.onPressed: event => {
+ switch (event.key) {
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (root.currentPage < root.totalPages - 1)
+ root.nextPage();
+ else
+ root.finish();
+ event.accepted = true;
+ break;
+ case Qt.Key_Left:
+ if (root.currentPage > 0)
+ root.prevPage();
+ event.accepted = true;
+ break;
+ case Qt.Key_Right:
+ if (root.currentPage < root.totalPages - 1)
+ root.nextPage();
+ event.accepted = true;
+ break;
+ }
+ }
+
+ MouseArea {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ height: headerRow.height + Theme.spacingM
+ onPressed: windowControls.tryStartMove()
+ onDoubleClicked: windowControls.tryToggleMaximize()
+ }
+
+ Item {
+ id: headerRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingM
+ height: Math.round(Theme.fontSizeMedium * 2.85)
+
+ Rectangle {
+ id: pageIndicatorContainer
+ readonly property real indicatorHeight: Math.round(Theme.fontSizeMedium * 2)
+
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ width: pageIndicatorRow.width + Theme.spacingM * 2
+ height: indicatorHeight
+ radius: indicatorHeight / 2
+ color: Theme.surfaceContainerHigh
+
+ Row {
+ id: pageIndicatorRow
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: root.totalPages
+
+ Rectangle {
+ required property int index
+ property bool isActive: index === root.currentPage
+ readonly property real dotSize: Math.round(Theme.spacingS * 1.3)
+
+ width: isActive ? dotSize * 3 : dotSize
+ height: dotSize
+ radius: dotSize / 2
+ color: isActive ? Theme.primary : Theme.surfaceTextAlpha
+ anchors.verticalCenter: parent.verticalCenter
+
+ Behavior on width {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ visible: windowControls.supported && windowControls.canMaximize
+ iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: windowControls.tryToggleMaximize()
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: root.skip()
+
+ DankTooltip {
+ text: I18n.tr("Skip setup", "greeter skip button tooltip")
+ }
+ }
+ }
+ }
+
+ Item {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: headerRow.bottom
+ anchors.bottom: footerRow.top
+ anchors.topMargin: Theme.spacingS
+
+ Loader {
+ id: pageLoader
+ anchors.fill: parent
+ sourceComponent: root.pageComponents[root.currentPage]
+
+ property var greeterRoot: root
+ }
+ }
+
+ Rectangle {
+ id: footerRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: Math.round(Theme.fontSizeMedium * 4.5)
+ color: Theme.surfaceContainerHigh
+
+ Rectangle {
+ anchors.top: parent.top
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ opacity: 0.5
+ }
+
+ Row {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingL
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankButton {
+ visible: root.currentPage < root.totalPages - 1
+ text: I18n.tr("Skip", "greeter skip button")
+ backgroundColor: "transparent"
+ textColor: Theme.surfaceVariantText
+ onClicked: root.skip()
+ }
+
+ DankButton {
+ visible: root.currentPage > 0
+ text: I18n.tr("Back", "greeter back button")
+ iconName: "arrow_back"
+ backgroundColor: Theme.surfaceContainerHighest
+ textColor: Theme.surfaceText
+ onClicked: root.prevPage()
+ }
+
+ DankButton {
+ visible: root.currentPage < root.totalPages - 1
+ enabled: !(root.currentPage === 1 && pageLoader.item && pageLoader.item.isRunning)
+ text: root.currentPage === 0 ? I18n.tr("Get Started", "greeter first page button") : I18n.tr("Next", "greeter next button")
+ iconName: "arrow_forward"
+ backgroundColor: Theme.primary
+ textColor: Theme.primaryText
+ onClicked: root.nextPage()
+ }
+
+ DankButton {
+ visible: root.currentPage === root.totalPages - 1
+ text: I18n.tr("Finish", "greeter finish button")
+ iconName: "check"
+ backgroundColor: Theme.primary
+ textColor: Theme.primaryText
+ onClicked: root.finish()
+ }
+ }
+ }
+ }
+
+ FloatingWindowControls {
+ id: windowControls
+ targetWindow: root
+ }
+
+ Component {
+ id: welcomePage
+ GreeterWelcomePage {}
+ }
+
+ Component {
+ id: doctorPage
+ GreeterDoctorPage {}
+ }
+
+ Component {
+ id: completePage
+ GreeterCompletePage {}
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterQuickLink.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterQuickLink.qml
new file mode 100644
index 0000000..d064275
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterQuickLink.qml
@@ -0,0 +1,60 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property string iconName: ""
+ property string title: ""
+ property bool isExternal: false
+
+ signal clicked
+
+ height: Math.round(Theme.fontSizeMedium * 3.1)
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainerHigh
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: Theme.primary
+ opacity: mouseArea.containsMouse ? 0.12 : 0
+ }
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: root.iconName
+ size: Theme.iconSizeSmall + 2
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: root.title
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ DankIcon {
+ visible: root.isExternal
+ name: "open_in_new"
+ size: Theme.iconSizeSmall - 2
+ color: Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clicked()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterSettingsCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterSettingsCard.qml
new file mode 100644
index 0000000..85e6b1c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterSettingsCard.qml
@@ -0,0 +1,78 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property string iconName: ""
+ property string title: ""
+ property string description: ""
+
+ signal clicked
+
+ readonly property real iconContainerSize: Math.round(Theme.iconSize * 1.5)
+
+ height: Math.round(Theme.fontSizeMedium * 4.5)
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainerHigh
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: Theme.primary
+ opacity: mouseArea.containsMouse ? 0.12 : 0
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: root.iconContainerSize
+ height: root.iconContainerSize
+ radius: Math.round(root.iconContainerSize * 0.28)
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.iconName
+ size: Theme.iconSize - 4
+ color: Theme.primaryText
+ }
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+ width: parent.width - root.iconContainerSize - Theme.spacingM
+
+ StyledText {
+ text: root.title
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Theme.surfaceVariantText
+ width: parent.width
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clicked()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterStatusCard.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterStatusCard.qml
new file mode 100644
index 0000000..af0d5d6
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterStatusCard.qml
@@ -0,0 +1,75 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property int count: 0
+ property string label: ""
+ property string iconName: ""
+ property color iconColor: Theme.surfaceText
+ property color bgColor: Theme.surfaceContainerHigh
+ property bool selected: false
+
+ signal clicked
+
+ height: Math.round(Theme.fontSizeMedium * 5)
+ radius: Theme.cornerRadius
+ color: bgColor
+ border.width: selected ? 2 : 0
+ border.color: selected ? iconColor : "transparent"
+ scale: mouseArea.pressed ? 0.97 : 1
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+
+ Behavior on border.width {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.clicked()
+ }
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ Row {
+ anchors.horizontalCenter: parent.horizontalCenter
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: root.iconName
+ size: Theme.iconSize - 4
+ color: root.iconColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: root.count.toString()
+ font.pixelSize: Theme.fontSizeXLarge
+ font.weight: Font.Bold
+ color: root.iconColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ StyledText {
+ text: root.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterWelcomePage.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterWelcomePage.qml
new file mode 100644
index 0000000..fcb4fc5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Greeter/GreeterWelcomePage.qml
@@ -0,0 +1,165 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ readonly property real logoSize: Math.round(Theme.iconSize * 5.3)
+
+ Column {
+ id: mainColumn
+ anchors.centerIn: parent
+ width: Math.min(Math.round(Theme.fontSizeMedium * 43), parent.width - Theme.spacingXL * 2)
+ spacing: Theme.spacingXL
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Image {
+ width: root.logoSize
+ height: width * (569.94629 / 506.50931)
+ anchors.horizontalCenter: parent.horizontalCenter
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ mipmap: true
+ asynchronous: true
+ source: "file://" + Theme.shellDir + "/assets/danklogonormal.svg"
+ layer.enabled: true
+ layer.smooth: true
+ layer.mipmap: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.primary
+ }
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Welcome to DankMaterialShell", "greeter welcome page title")
+ font.pixelSize: Theme.fontSizeXLarge + 4
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("A modern desktop shell for Wayland compositors", "greeter welcome page tagline")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.outlineMedium
+ opacity: 0.3
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("Features", "greeter welcome page section header")
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ Grid {
+ width: parent.width
+ columns: 3
+ rowSpacing: Theme.spacingS
+ columnSpacing: Theme.spacingS
+
+ GreeterFeatureCard {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "auto_awesome"
+ title: I18n.tr("Dynamic Theming", "greeter feature card title")
+ description: I18n.tr("Colors from wallpaper", "greeter feature card description")
+ onClicked: PopoutService.openSettingsWithTab("theme")
+ }
+
+ GreeterFeatureCard {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "format_paint"
+ title: I18n.tr("App Theming", "greeter feature card title")
+ description: I18n.tr("GTK, Qt, IDEs, more", "greeter feature card description")
+ onClicked: PopoutService.openSettingsWithTab("theme")
+ }
+
+ GreeterFeatureCard {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "download"
+ title: I18n.tr("Theme Registry", "greeter feature card title")
+ description: I18n.tr("Community themes", "greeter feature card description")
+ onClicked: PopoutService.openSettingsWithTab("theme")
+ }
+
+ GreeterFeatureCard {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "view_carousel"
+ title: I18n.tr("DankBar", "greeter feature card title")
+ description: I18n.tr("Modular widget bar", "greeter feature card description")
+ onClicked: PopoutService.openSettingsWithTab("dankbar_settings")
+ }
+
+ GreeterFeatureCard {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "extension"
+ title: I18n.tr("Plugins", "greeter feature card title")
+ description: I18n.tr("Extensible architecture", "greeter feature card description")
+ onClicked: PopoutService.openSettingsWithTab("plugins")
+ }
+
+ GreeterFeatureCard {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "layers"
+ title: I18n.tr("Multi-Monitor", "greeter feature card title")
+ description: I18n.tr("Per-screen config", "greeter feature card description")
+ onClicked: {
+ const hasDisplayConfig = CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl;
+ PopoutService.openSettingsWithTab(hasDisplayConfig ? "display_config" : "display_widgets");
+ }
+ }
+
+ GreeterFeatureCard {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "nightlight"
+ title: I18n.tr("Display Control", "greeter feature card title")
+ description: I18n.tr("Night mode & gamma", "greeter feature card description")
+ onClicked: PopoutService.openSettingsWithTab("display_gamma")
+ }
+
+ GreeterFeatureCard {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "tune"
+ title: I18n.tr("Control Center", "greeter feature card title")
+ description: I18n.tr("Quick system toggles", "greeter feature card description")
+ // This is doing an IPC since its just easier and lazier to access the bar ref
+ onClicked: Quickshell.execDetached(["dms", "ipc", "call", "control-center", "open"])
+ }
+
+ GreeterFeatureCard {
+ width: (parent.width - Theme.spacingS * 2) / 3
+ iconName: "lock"
+ title: I18n.tr("Lock Screen", "greeter feature card title")
+ description: I18n.tr("Security & privacy", "greeter feature card description")
+ onClicked: PopoutService.openSettingsWithTab("lock_screen")
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/KeybindsModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/KeybindsModal.qml
new file mode 100644
index 0000000..e1d6765
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/KeybindsModal.qml
@@ -0,0 +1,347 @@
+import QtQml
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Hyprland
+import qs.Common
+import qs.Modals.Common
+import qs.Services
+import qs.Widgets
+
+DankModal {
+ id: root
+
+ layerNamespace: "dms:keybinds"
+ useOverlayLayer: true
+ property real scrollStep: 60
+ property var activeFlickable: null
+ property real _maxW: Math.min(Screen.width * 0.92, 1200)
+ property real _maxH: Math.min(Screen.height * 0.92, 900)
+ modalWidth: _maxW
+ modalHeight: _maxH
+ onBackgroundClicked: close()
+ onOpened: {
+ Qt.callLater(() => {
+ modalFocusScope.forceActiveFocus();
+ if (contentLoader.item?.searchField)
+ contentLoader.item.searchField.forceActiveFocus();
+ });
+ if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
+ KeybindsService.loadCheatsheet();
+ }
+
+ HyprlandFocusGrab {
+ windows: [root.contentWindow]
+ active: root.useHyprlandFocusGrab && root.shouldHaveFocus
+ }
+
+ function scrollDown() {
+ if (!root.activeFlickable)
+ return;
+ let newY = root.activeFlickable.contentY + scrollStep;
+ newY = Math.min(newY, root.activeFlickable.contentHeight - root.activeFlickable.height);
+ root.activeFlickable.contentY = newY;
+ }
+
+ function scrollUp() {
+ if (!root.activeFlickable)
+ return;
+ let newY = root.activeFlickable.contentY - root.scrollStep;
+ newY = Math.max(0, newY);
+ root.activeFlickable.contentY = newY;
+ }
+
+ modalFocusScope.Keys.onPressed: event => {
+ if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
+ scrollDown();
+ event.accepted = true;
+ } else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
+ scrollUp();
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Down) {
+ scrollDown();
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Up) {
+ scrollUp();
+ event.accepted = true;
+ }
+ }
+
+ content: Component {
+ Item {
+ anchors.fill: parent
+ property alias searchField: searchField
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingL
+
+ RowLayout {
+ width: parent.width
+
+ StyledText {
+ Layout.alignment: Qt.AlignLeft
+ text: KeybindsService.cheatsheet.title || i18n("Keybinds")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: Theme.primary
+ }
+
+ DankTextField {
+ id: searchField
+ Layout.alignment: Qt.AlignRight
+ leftIconName: "search"
+ keyForwardTargets: [root.modalFocusScope]
+ onTextEdited: searchDebounce.restart()
+ Keys.onEscapePressed: event => {
+ root.close();
+ event.accepted = true;
+ }
+ }
+ }
+
+ Timer {
+ id: searchDebounce
+ interval: 50
+ repeat: false
+ onTriggered: {
+ mainFlickable.categories = mainFlickable.generateCategories(searchField.text);
+ }
+ }
+
+ DankFlickable {
+ id: mainFlickable
+ width: parent.width
+ height: parent.height - parent.spacing - 40
+ contentWidth: rowLayout.implicitWidth
+ contentHeight: rowLayout.implicitHeight
+ clip: true
+
+ Component.onCompleted: root.activeFlickable = mainFlickable
+
+ property var rawBinds: KeybindsService.cheatsheet.binds || {}
+
+ function generateCategories(query) {
+ const lowerQuery = query ? query.toLowerCase().trim() : "";
+ const lowerQueryWords = query.split(/\s+/);
+ const processed = {};
+
+ for (const cat in rawBinds) {
+ const binds = rawBinds[cat];
+ const catLower = cat.toLowerCase();
+ const subcats = {};
+ let hasSubcats = false;
+ for (let i = 0; i < binds.length; i++) {
+ const bind = binds[i];
+ const keyLower = bind.key.toLowerCase();
+ const descLower = bind.desc.toLowerCase();
+ const actionLower = bind.action.toLowerCase();
+
+ if (bind.hideOnOverlay)
+ continue;
+ let shouldContinue = false;
+ for (let j = 0; j < lowerQueryWords.length; j++) {
+ const word = lowerQueryWords[j];
+ if (!(
+ word.length === 0 ||
+ keyLower.includes(word) ||
+ descLower.includes(word) ||
+ catLower.includes(word) ||
+ actionLower.includes(word)
+ )) {
+ shouldContinue = true;
+ break;
+ }
+ }
+ if (shouldContinue)
+ continue;
+
+ if (bind.subcat) {
+ hasSubcats = true;
+ if (!subcats[bind.subcat])
+ subcats[bind.subcat] = [];
+ subcats[bind.subcat].push(bind);
+ } else {
+ if (!subcats["_root"])
+ subcats["_root"] = [];
+ subcats["_root"].push(bind);
+ }
+ }
+
+ if (Object.keys(subcats).length === 0)
+ continue;
+
+ processed[cat] = {
+ hasSubcats: hasSubcats,
+ subcats: subcats,
+ subcatKeys: Object.keys(subcats)
+ };
+ }
+
+ return processed;
+ }
+
+ property var categories: generateCategories("");
+
+ function estimateCategoryHeight(catName) {
+ const catData = categories[catName];
+ if (!catData)
+ return 0;
+ let bindCount = 0;
+ for (const key of catData.subcatKeys) {
+ bindCount += catData.subcats[key]?.length || 0;
+ if (key !== "_root")
+ bindCount += 1;
+ }
+ return 40 + bindCount * 28;
+ }
+
+ property var categoryKeys: Object.keys(categories);
+
+ function distributeCategories(cols) {
+ const columns = [];
+ const heights = [];
+ for (let i = 0; i < cols; i++) {
+ columns.push([]);
+ heights.push(0);
+ }
+ const sorted = [...categoryKeys].sort((a, b) => estimateCategoryHeight(b) - estimateCategoryHeight(a));
+ for (const cat of sorted) {
+ let minIdx = 0;
+ for (let i = 1; i < cols; i++) {
+ if (heights[i] < heights[minIdx])
+ minIdx = i;
+ }
+ columns[minIdx].push(cat);
+ heights[minIdx] += estimateCategoryHeight(cat);
+ }
+ return columns;
+ }
+
+ Row {
+ id: rowLayout
+ width: mainFlickable.width
+ spacing: Theme.spacingM
+
+ property int numColumns: Math.max(1, Math.min(3, Math.floor(width / 350)))
+ property var columnCategories: mainFlickable.distributeCategories(numColumns)
+
+ Repeater {
+ model: rowLayout.numColumns
+
+ Column {
+ id: masonryColumn
+ width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
+ spacing: Theme.spacingXL
+
+ Repeater {
+ model: rowLayout.columnCategories[index] || []
+
+ Column {
+ id: categoryColumn
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ property string catName: modelData
+ property var catData: mainFlickable.categories[catName]
+
+ StyledText {
+ text: categoryColumn.catName
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Bold
+ color: Theme.primary
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.primary
+ opacity: 0.3
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingXS
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Repeater {
+ model: categoryColumn.catData?.subcatKeys || []
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ property string subcatName: modelData
+ property var subcatBinds: categoryColumn.catData?.subcats?.[subcatName] || []
+
+ StyledText {
+ visible: parent.subcatName !== "_root"
+ text: parent.subcatName
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.DemiBold
+ color: Theme.primary
+ opacity: 0.7
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: parent.parent.subcatBinds
+
+ Item {
+ width: parent.width
+ height: 24
+
+ StyledRect {
+ id: keyBadge
+ width: Math.min(keyText.implicitWidth + 12, 160)
+ height: 22
+ radius: 4
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ id: keyText
+ anchors.centerIn: parent
+ color: Theme.secondary
+ text: (modelData.key || "").replace(/\+/g, " + ")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ isMonospace: true
+ elide: Text.ElideRight
+ width: Math.min(implicitWidth, 148)
+ }
+ }
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.leftMargin: 170
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ text: modelData.desc || modelData.action || ""
+ font.pixelSize: Theme.fontSizeSmall
+ opacity: 0.9
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/MuxModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/MuxModal.qml
new file mode 100644
index 0000000..9d19388
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/MuxModal.qml
@@ -0,0 +1,633 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Quickshell.Hyprland
+import Quickshell.Io
+import Quickshell
+import qs.Common
+import qs.Modals.Common
+import qs.Services
+import qs.Widgets
+
+DankModal {
+ id: muxModal
+
+ layerNamespace: "dms:mux"
+
+ property int selectedIndex: -1
+ property string searchText: ""
+ property var filteredSessions: []
+
+ function updateFilteredSessions() {
+ var filtered = []
+ var lowerSearch = searchText.trim().toLowerCase()
+ for (var i = 0; i < MuxService.sessions.length; i++) {
+ var session = MuxService.sessions[i]
+ if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
+ continue
+ filtered.push(session)
+ }
+ filteredSessions = filtered
+
+ if (selectedIndex >= filteredSessions.length) {
+ selectedIndex = Math.max(0, filteredSessions.length - 1)
+ }
+ }
+
+ onSearchTextChanged: updateFilteredSessions()
+
+ Connections {
+ target: MuxService
+ function onSessionsChanged() {
+ updateFilteredSessions()
+ }
+ }
+
+ HyprlandFocusGrab {
+ id: grab
+ windows: [muxModal.contentWindow]
+ active: CompositorService.isHyprland && muxModal.shouldHaveFocus
+ }
+
+ function toggle() {
+ if (shouldBeVisible) {
+ hide()
+ } else {
+ show()
+ }
+ }
+
+ function show() {
+ open()
+ selectedIndex = -1
+ searchText = ""
+ MuxService.refreshSessions()
+ shouldHaveFocus = true
+
+ Qt.callLater(() => {
+ if (muxPanel && muxPanel.searchField) {
+ muxPanel.searchField.forceActiveFocus();
+ }
+ })
+ }
+
+ function hide() {
+ close()
+ selectedIndex = -1
+ searchText = ""
+ }
+
+ function attachToSession(name) {
+ MuxService.attachToSession(name)
+ hide()
+ }
+
+ function renameSession(name) {
+ inputModal.showWithOptions({
+ title: I18n.tr("Rename Session"),
+ message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
+ initialText: name,
+ onConfirm: function (newName) {
+ MuxService.renameSession(name, newName)
+ }
+ })
+ }
+
+ function killSession(name) {
+ confirmModal.showWithOptions({
+ title: I18n.tr("Kill Session"),
+ message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
+ confirmText: I18n.tr("Kill"),
+ confirmColor: Theme.primary,
+ onConfirm: function () {
+ MuxService.killSession(name)
+ }
+ })
+ }
+
+ function createNewSession() {
+ inputModal.showWithOptions({
+ title: I18n.tr("New Session"),
+ message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
+ onConfirm: function (name) {
+ MuxService.createSession(name)
+ hide()
+ }
+ })
+ }
+
+ function selectNext() {
+ selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
+ }
+
+ function selectPrevious() {
+ selectedIndex = Math.max(selectedIndex - 1, -1)
+ }
+
+ function activateSelected() {
+ if (selectedIndex === -1) {
+ createNewSession()
+ } else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
+ attachToSession(filteredSessions[selectedIndex].name)
+ }
+ }
+
+ visible: false
+ modalWidth: 600
+ modalHeight: 600
+ backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ cornerRadius: Theme.cornerRadius
+ borderColor: Theme.outlineMedium
+ borderWidth: 1
+ enableShadow: true
+ keepContentLoaded: true
+
+ onBackgroundClicked: hide()
+
+ Timer {
+ interval: 3000
+ running: muxModal.shouldBeVisible
+ repeat: true
+ onTriggered: MuxService.refreshSessions()
+ }
+
+ IpcHandler {
+ function open(): string {
+ muxModal.show()
+ return "MUX_OPEN_SUCCESS"
+ }
+
+ function close(): string {
+ muxModal.hide()
+ return "MUX_CLOSE_SUCCESS"
+ }
+
+ function toggle(): string {
+ muxModal.toggle()
+ return "MUX_TOGGLE_SUCCESS"
+ }
+
+ target: "mux"
+ }
+
+ // Backwards compatibility
+ IpcHandler {
+ function open(): string {
+ muxModal.show()
+ return "TMUX_OPEN_SUCCESS"
+ }
+
+ function close(): string {
+ muxModal.hide()
+ return "TMUX_CLOSE_SUCCESS"
+ }
+
+ function toggle(): string {
+ muxModal.toggle()
+ return "TMUX_TOGGLE_SUCCESS"
+ }
+
+ target: "tmux"
+ }
+
+ InputModal {
+ id: inputModal
+ onShouldBeVisibleChanged: {
+ if (shouldBeVisible) {
+ muxModal.shouldHaveFocus = false;
+ muxModal.contentWindow.visible = false;
+ return;
+ }
+ if (muxModal.shouldBeVisible) {
+ muxModal.contentWindow.visible = true;
+ }
+ Qt.callLater(function () {
+ if (!muxModal.shouldBeVisible) {
+ return;
+ }
+ muxModal.shouldHaveFocus = true;
+ muxModal.modalFocusScope.forceActiveFocus();
+ if (muxPanel.searchField) {
+ muxPanel.searchField.forceActiveFocus();
+ }
+ });
+ }
+ }
+
+ ConfirmModal {
+ id: confirmModal
+ onShouldBeVisibleChanged: {
+ if (shouldBeVisible) {
+ muxModal.shouldHaveFocus = false;
+ muxModal.contentWindow.visible = false;
+ return;
+ }
+ if (muxModal.shouldBeVisible) {
+ muxModal.contentWindow.visible = true;
+ }
+ Qt.callLater(function () {
+ if (!muxModal.shouldBeVisible) {
+ return;
+ }
+ muxModal.shouldHaveFocus = true;
+ muxModal.modalFocusScope.forceActiveFocus();
+ if (muxPanel.searchField) {
+ muxPanel.searchField.forceActiveFocus();
+ }
+ });
+ }
+ }
+
+ directContent: Item {
+ id: muxPanel
+
+ clip: false
+
+ property alias searchField: searchField
+
+ Keys.onPressed: event => {
+ if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
+ (event.key === Qt.Key_Down)) {
+ selectNext()
+ event.accepted = true
+ } else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
+ (event.key === Qt.Key_Up)) {
+ selectPrevious()
+ event.accepted = true
+ } else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
+ createNewSession()
+ event.accepted = true
+ } else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
+ if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
+ renameSession(filteredSessions[selectedIndex].name)
+ }
+ event.accepted = true
+ } else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
+ if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
+ killSession(filteredSessions[selectedIndex].name)
+ }
+ event.accepted = true
+ } else if (event.key === Qt.Key_Escape) {
+ hide()
+ event.accepted = true
+ } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ activateSelected()
+ event.accepted = true
+ }
+ }
+
+ Column {
+ width: parent.width - Theme.spacingM * 2
+ height: parent.height - Theme.spacingM * 2
+ x: Theme.spacingM
+ y: Theme.spacingM
+ spacing: Theme.spacingS
+
+ // Header
+ Item {
+ width: parent.width
+ height: 40
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
+ 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: {
+ const total = MuxService.sessions.length;
+ const filtered = muxModal.filteredSessions.length;
+ const activePart = total === 1
+ ? I18n.tr("%1 active session").arg(total)
+ : I18n.tr("%1 active sessions").arg(total);
+ const filteredPart = filtered === 1
+ ? I18n.tr("%1 filtered").arg(filtered)
+ : I18n.tr("%1 filtered").arg(filtered);
+ return activePart + ", " + filteredPart;
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ // Search field
+ DankTextField {
+ id: searchField
+
+ width: parent.width
+ height: 48
+ cornerRadius: Theme.cornerRadius
+ backgroundColor: Theme.surfaceContainerHigh
+ normalBorderColor: Theme.outlineMedium
+ focusedBorderColor: Theme.primary
+ leftIconName: "search"
+ leftIconSize: Theme.iconSize
+ leftIconColor: Theme.surfaceVariantText
+ leftIconFocusedColor: Theme.primary
+ showClearButton: true
+ font.pixelSize: Theme.fontSizeMedium
+ placeholderText: I18n.tr("Search sessions...")
+ keyForwardTargets: [muxPanel]
+
+ onTextEdited: {
+ muxModal.searchText = text
+ muxModal.selectedIndex = 0
+ }
+ }
+
+ // New Session Button
+ Rectangle {
+ width: parent.width
+ height: 56
+ radius: Theme.cornerRadius
+ color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
+ (newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Rectangle {
+ Layout.preferredWidth: 40
+ Layout.preferredHeight: 40
+ radius: 20
+ color: Theme.primaryContainer
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "add"
+ size: Theme.iconSize
+ color: Theme.primary
+ }
+ }
+
+ Column {
+ Layout.fillWidth: true
+ spacing: 2
+
+ StyledText {
+ text: I18n.tr("New Session")
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ MouseArea {
+ id: newMouse
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: muxModal.createNewSession()
+ }
+ }
+
+ // Sessions List
+ Rectangle {
+ width: parent.width
+ height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
+ radius: Theme.cornerRadius
+ color: "transparent"
+
+ ScrollView {
+ anchors.fill: parent
+ clip: true
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: ScriptModel {
+ values: muxModal.filteredSessions
+ }
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ width: parent.width
+ height: 64
+ radius: Theme.cornerRadius
+ color: muxModal.selectedIndex === index ? Theme.primaryContainer :
+ (sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
+
+ MouseArea {
+ id: sessionMouse
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: muxModal.attachToSession(modelData.name)
+ }
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ // Avatar
+ Rectangle {
+ Layout.preferredWidth: 40
+ Layout.preferredHeight: 40
+ radius: 20
+ color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
+
+ StyledText {
+ anchors.centerIn: parent
+ text: modelData.name.charAt(0).toUpperCase()
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: modelData.attached ? Theme.primary : Theme.surfaceText
+ }
+ }
+
+ // Info
+ Column {
+ Layout.fillWidth: true
+ spacing: 2
+
+ StyledText {
+ text: modelData.name
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ }
+
+ StyledText {
+ text: {
+ var parts = []
+ if (modelData.windows !== "N/A")
+ parts.push(modelData.windows === 1
+ ? I18n.tr("%1 window").arg(modelData.windows)
+ : I18n.tr("%1 windows").arg(modelData.windows))
+ parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
+ return parts.join(" \u2022 ")
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ // Rename button (tmux only)
+ Rectangle {
+ Layout.preferredWidth: 36
+ Layout.preferredHeight: 36
+ radius: 18
+ visible: MuxService.supportsRename
+ color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "edit"
+ size: Theme.iconSizeSmall
+ color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: renameMouse
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: muxModal.renameSession(modelData.name)
+ }
+ }
+
+ // Delete button
+ Rectangle {
+ Layout.preferredWidth: 36
+ Layout.preferredHeight: 36
+ radius: 18
+ color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "delete"
+ size: Theme.iconSizeSmall
+ color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: deleteMouse
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ muxModal.killSession(modelData.name)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Empty state
+ Item {
+ width: parent.width
+ height: muxModal.filteredSessions.length === 0 ? 200 : 0
+ visible: muxModal.filteredSessions.length === 0
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
+ size: 48
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Shortcuts bar
+ Row {
+ id: shortcutsBar
+ width: parent.width
+ spacing: Theme.spacingM
+ bottomPadding: Theme.spacingS
+
+ Repeater {
+ model: {
+ var shortcuts = [
+ { key: "↑↓", label: I18n.tr("Navigate") },
+ { key: "↵", label: I18n.tr("Attach") },
+ { key: "^N", label: I18n.tr("New") },
+ { key: "^D", label: I18n.tr("Kill") },
+ { key: "Esc", label: I18n.tr("Close") }
+ ]
+ if (MuxService.supportsRename)
+ shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
+ return shortcuts
+ }
+
+ delegate: Row {
+ required property var modelData
+ spacing: 4
+
+ Rectangle {
+ width: keyText.width + Theme.spacingS
+ height: keyText.height + 4
+ radius: 4
+ color: Theme.surfaceContainerHighest
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ id: keyText
+ anchors.centerIn: parent
+ text: modelData.key
+ font.pixelSize: Theme.fontSizeSmall - 1
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ StyledText {
+ text: modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NetworkInfoModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NetworkInfoModal.qml
new file mode 100644
index 0000000..79dbada
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NetworkInfoModal.qml
@@ -0,0 +1,155 @@
+import QtQuick
+import qs.Common
+import qs.Modals.Common
+import qs.Services
+import qs.Widgets
+
+DankModal {
+ id: root
+
+ layerNamespace: "dms:network-info"
+
+ keepPopoutsOpen: true
+
+ property bool networkInfoModalVisible: false
+ property string networkSSID: ""
+ property var networkData: null
+
+ function showNetworkInfo(ssid, data) {
+ networkSSID = ssid;
+ networkData = data;
+ networkInfoModalVisible = true;
+ open();
+ NetworkService.fetchNetworkInfo(ssid);
+ }
+
+ function hideDialog() {
+ networkInfoModalVisible = false;
+ close();
+ networkSSID = "";
+ networkData = null;
+ }
+
+ visible: networkInfoModalVisible
+ modalWidth: 600
+ modalHeight: 500
+ enableShadow: true
+ onBackgroundClicked: hideDialog()
+ onVisibleChanged: {
+ if (!visible) {
+ networkSSID = "";
+ networkData = null;
+ }
+ }
+
+ content: Component {
+ Item {
+ anchors.fill: parent
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingL
+
+ Row {
+ width: parent.width
+
+ Column {
+ width: parent.width - 40
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Network Information")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: I18n.tr("Details for \"%1\"").arg(networkSSID)
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ elide: Text.ElideRight
+ }
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: root.hideDialog()
+ }
+ }
+
+ Rectangle {
+ id: detailsRect
+
+ width: parent.width
+ height: parent.height - 140
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: Theme.outlineStrong
+ border.width: 1
+ clip: true
+
+ DankFlickable {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ contentHeight: detailsText.contentHeight
+
+ StyledText {
+ id: detailsText
+
+ width: parent.width
+ text: NetworkService.networkInfoDetails && NetworkService.networkInfoDetails.replace(/\\n/g, '\n') || I18n.tr("No information available")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: 40
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
+
+ StyledText {
+ id: closeText
+
+ anchors.centerIn: parent
+ text: I18n.tr("Close")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.background
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: closeArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.hideDialog()
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NetworkWiredInfoModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NetworkWiredInfoModal.qml
new file mode 100644
index 0000000..4b42fdc
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NetworkWiredInfoModal.qml
@@ -0,0 +1,155 @@
+import QtQuick
+import qs.Common
+import qs.Modals.Common
+import qs.Services
+import qs.Widgets
+
+DankModal {
+ id: root
+
+ layerNamespace: "dms:network-info-wired"
+
+ keepPopoutsOpen: true
+
+ property bool networkWiredInfoModalVisible: false
+ property string networkID: ""
+ property var networkData: null
+
+ function showNetworkInfo(id, data) {
+ networkID = id;
+ networkData = data;
+ networkWiredInfoModalVisible = true;
+ open();
+ NetworkService.fetchWiredNetworkInfo(data.uuid);
+ }
+
+ function hideDialog() {
+ networkWiredInfoModalVisible = false;
+ close();
+ networkID = "";
+ networkData = null;
+ }
+
+ visible: networkWiredInfoModalVisible
+ modalWidth: 600
+ modalHeight: 500
+ enableShadow: true
+ onBackgroundClicked: hideDialog()
+ onVisibleChanged: {
+ if (!visible) {
+ networkID = "";
+ networkData = null;
+ }
+ }
+
+ content: Component {
+ Item {
+ anchors.fill: parent
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingL
+
+ Row {
+ width: parent.width
+
+ Column {
+ width: parent.width - 40
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Network Information")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: I18n.tr("Details for \"%1\"").arg(networkSSID)
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ elide: Text.ElideRight
+ }
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: root.hideDialog()
+ }
+ }
+
+ Rectangle {
+ id: detailsRect
+
+ width: parent.width
+ height: parent.height - 140
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: Theme.outlineStrong
+ border.width: 1
+ clip: true
+
+ DankFlickable {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ contentHeight: detailsText.contentHeight
+
+ StyledText {
+ id: detailsText
+
+ width: parent.width
+ text: NetworkService.networkWiredInfoDetails && NetworkService.networkWiredInfoDetails.replace(/\\n/g, '\n') || I18n.tr("No information available")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: 40
+
+ Rectangle {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ width: Math.max(70, closeText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: closeArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
+
+ StyledText {
+ id: closeText
+
+ anchors.centerIn: parent
+ text: I18n.tr("Close")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.background
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: closeArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.hideDialog()
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NotificationModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NotificationModal.qml
new file mode 100644
index 0000000..8653344
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/NotificationModal.qml
@@ -0,0 +1,225 @@
+import QtQuick
+import Quickshell.Hyprland
+import Quickshell.Io
+import qs.Common
+import qs.Modals.Common
+import qs.Modules.Notifications.Center
+import qs.Services
+
+DankModal {
+ id: notificationModal
+
+ layerNamespace: "dms:notification-center-modal"
+
+ HyprlandFocusGrab {
+ windows: [notificationModal.contentWindow]
+ active: notificationModal.useHyprlandFocusGrab && notificationModal.shouldHaveFocus
+ }
+
+ property bool notificationModalOpen: false
+ property var notificationListRef: null
+ property var historyListRef: null
+ property int currentTab: 0
+
+ property var notificationHeaderRef: null
+
+ function show() {
+ notificationModalOpen = true;
+ currentTab = 0;
+ NotificationService.onOverlayOpen();
+ open();
+ modalKeyboardController.reset();
+ if (modalKeyboardController && notificationListRef) {
+ modalKeyboardController.listView = notificationListRef;
+ modalKeyboardController.rebuildFlatNavigation();
+
+ Qt.callLater(() => {
+ modalKeyboardController.keyboardNavigationActive = true;
+ modalKeyboardController.selectedFlatIndex = 0;
+ modalKeyboardController.updateSelectedIdFromIndex();
+ if (notificationListRef) {
+ notificationListRef.keyboardActive = true;
+ notificationListRef.currentIndex = 0;
+ }
+ modalKeyboardController.selectionVersion++;
+ modalKeyboardController.ensureVisible();
+ });
+ }
+ }
+
+ function hide() {
+ notificationModalOpen = false;
+ NotificationService.onOverlayClose();
+ close();
+ modalKeyboardController.reset();
+ }
+
+ function toggle() {
+ if (shouldBeVisible) {
+ hide();
+ } else {
+ show();
+ }
+ }
+
+ function clearAll() {
+ NotificationService.clearAllNotifications();
+ }
+
+ function dismissAllPopups() {
+ NotificationService.dismissAllPopups();
+ }
+
+ modalWidth: Math.min(500, screenWidth - 48)
+ modalHeight: Math.min(700, screenHeight * 0.85)
+ backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ visible: false
+ onBackgroundClicked: hide()
+ onOpened: () => {
+ Qt.callLater(() => modalFocusScope.forceActiveFocus());
+ }
+ onShouldBeVisibleChanged: shouldBeVisible => {
+ if (!shouldBeVisible) {
+ notificationModalOpen = false;
+ modalKeyboardController.reset();
+ NotificationService.onOverlayClose();
+ }
+ }
+ modalFocusScope.Keys.onPressed: event => {
+ if (event.key === Qt.Key_Escape) {
+ hide();
+ event.accepted = true;
+ return;
+ }
+
+ if (event.key === Qt.Key_Left) {
+ if (notificationHeaderRef && notificationHeaderRef.currentTab > 0) {
+ notificationHeaderRef.currentTab = 0;
+ event.accepted = true;
+ }
+ return;
+ }
+ if (event.key === Qt.Key_Right) {
+ if (notificationHeaderRef && notificationHeaderRef.currentTab === 0 && SettingsData.notificationHistoryEnabled) {
+ notificationHeaderRef.currentTab = 1;
+ event.accepted = true;
+ }
+ return;
+ }
+
+ if (currentTab === 1 && historyListRef) {
+ historyListRef.handleKey(event);
+ return;
+ }
+ modalKeyboardController.handleKey(event);
+ }
+
+ NotificationKeyboardController {
+ id: modalKeyboardController
+
+ listView: null
+ isOpen: notificationModal.notificationModalOpen
+ onClose: () => notificationModal.hide()
+ }
+
+ IpcHandler {
+ function open(): string {
+ notificationModal.show();
+ return "NOTIFICATION_MODAL_OPEN_SUCCESS";
+ }
+
+ function close(): string {
+ notificationModal.hide();
+ return "NOTIFICATION_MODAL_CLOSE_SUCCESS";
+ }
+
+ function toggle(): string {
+ notificationModal.toggle();
+ return "NOTIFICATION_MODAL_TOGGLE_SUCCESS";
+ }
+
+ function toggleDoNotDisturb(): string {
+ SessionData.setDoNotDisturb(!SessionData.doNotDisturb);
+
+ return "NOTIFICATION_MODAL_TOGGLE_DND_SUCCESS";
+ }
+
+ function getDoNotDisturb(): bool {
+ return SessionData.doNotDisturb;
+ }
+
+ function clearAll(): string {
+ notificationModal.clearAll();
+ return "NOTIFICATION_MODAL_CLEAR_ALL_SUCCESS";
+ }
+
+ function dismissAllPopups(): string {
+ notificationModal.dismissAllPopups();
+ return "NOTIFICATION_MODAL_DISMISS_ALL_POPUPS_SUCCESS";
+ }
+
+ target: "notifications"
+ }
+
+ content: Component {
+ Item {
+ id: notificationKeyHandler
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ anchors.fill: parent
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingM
+
+ NotificationHeader {
+ id: notificationHeader
+ keyboardController: modalKeyboardController
+ onCurrentTabChanged: notificationModal.currentTab = currentTab
+ Component.onCompleted: notificationModal.notificationHeaderRef = notificationHeader
+ }
+
+ NotificationSettings {
+ id: notificationSettings
+ expanded: notificationHeader.showSettings
+ }
+
+ KeyboardNavigatedNotificationList {
+ id: notificationList
+ width: parent.width
+ height: parent.height - y
+ visible: notificationHeader.currentTab === 0
+ keyboardController: modalKeyboardController
+ Component.onCompleted: {
+ notificationModal.notificationListRef = notificationList;
+ if (modalKeyboardController) {
+ modalKeyboardController.listView = notificationList;
+ modalKeyboardController.rebuildFlatNavigation();
+ }
+ }
+ }
+
+ HistoryNotificationList {
+ id: historyList
+ width: parent.width
+ height: parent.height - y
+ visible: notificationHeader.currentTab === 1
+ Component.onCompleted: notificationModal.historyListRef = historyList
+ }
+ }
+
+ NotificationKeyboardHints {
+ id: keyboardHints
+
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingL
+ showHints: notificationHeader.currentTab === 0 ? modalKeyboardController.showKeyboardHints : historyList.showKeyboardHints
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/PolkitAuthModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/PolkitAuthModal.qml
new file mode 100644
index 0000000..21ddf8f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/PolkitAuthModal.qml
@@ -0,0 +1,412 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+FloatingWindow {
+ id: root
+
+ property bool disablePopupTransparency: true
+ property string passwordInput: ""
+ property var currentFlow: PolkitService.agent?.flow
+ property bool isLoading: false
+ property bool awaitingFprintForPassword: false
+ readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
+
+ property string polkitEtcPamText: ""
+ property string polkitLibPamText: ""
+ property string systemAuthPamText: ""
+ property string commonAuthPamText: ""
+ property string passwordAuthPamText: ""
+ readonly property bool polkitPamHasFprint: {
+ const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
+ if (!polkitText)
+ return false;
+ return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
+ }
+
+ function stripPamComment(line) {
+ if (!line)
+ return "";
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith("#"))
+ return "";
+ const hashIdx = trimmed.indexOf("#");
+ if (hashIdx >= 0)
+ return trimmed.substring(0, hashIdx).trim();
+ return trimmed;
+ }
+
+ function pamModuleEnabled(pamText, moduleName) {
+ if (!pamText || !moduleName)
+ return false;
+ const lines = pamText.split(/\r?\n/);
+ for (let i = 0; i < lines.length; i++) {
+ const line = stripPamComment(lines[i]);
+ if (line && line.includes(moduleName))
+ return true;
+ }
+ return false;
+ }
+
+ function focusPasswordField() {
+ passwordField.forceActiveFocus();
+ }
+
+ function show() {
+ passwordInput = "";
+ isLoading = false;
+ awaitingFprintForPassword = false;
+ visible = true;
+ Qt.callLater(focusPasswordField);
+ }
+
+ function hide() {
+ visible = false;
+ }
+
+ function _commitSubmit() {
+ isLoading = true;
+ awaitingFprintForPassword = false;
+ currentFlow.submit(passwordInput);
+ passwordInput = "";
+ }
+
+ function submitAuth() {
+ if (!currentFlow || isLoading)
+ return;
+ if (!currentFlow.isResponseRequired) {
+ awaitingFprintForPassword = true;
+ return;
+ }
+ _commitSubmit();
+ }
+
+ function cancelAuth() {
+ if (isLoading)
+ return;
+ awaitingFprintForPassword = false;
+ if (currentFlow) {
+ currentFlow.cancelAuthenticationRequest();
+ return;
+ }
+ hide();
+ }
+
+ objectName: "polkitAuthModal"
+ title: I18n.tr("Authentication")
+ minimumSize: Qt.size(460, 220)
+ maximumSize: Qt.size(460, 220)
+ color: Theme.surfaceContainer
+ visible: false
+
+ onVisibleChanged: {
+ if (visible) {
+ Qt.callLater(focusPasswordField);
+ return;
+ }
+ passwordInput = "";
+ isLoading = false;
+ awaitingFprintForPassword = false;
+ }
+
+ Connections {
+ target: PolkitService.agent
+ enabled: PolkitService.polkitAvailable
+
+ function onAuthenticationRequestStarted() {
+ show();
+ }
+
+ function onIsActiveChanged() {
+ if (!(PolkitService.agent?.isActive ?? false))
+ hide();
+ }
+ }
+
+ Connections {
+ target: currentFlow
+ enabled: currentFlow !== null
+
+ function onIsResponseRequiredChanged() {
+ if (!currentFlow.isResponseRequired)
+ return;
+ if (awaitingFprintForPassword && passwordInput !== "") {
+ _commitSubmit();
+ return;
+ }
+ awaitingFprintForPassword = false;
+ isLoading = false;
+ passwordInput = "";
+ passwordField.forceActiveFocus();
+ }
+
+ function onAuthenticationSucceeded() {
+ hide();
+ }
+
+ function onAuthenticationFailed() {
+ isLoading = false;
+ }
+
+ function onAuthenticationRequestCancelled() {
+ hide();
+ }
+ }
+
+ FileView {
+ path: "/etc/pam.d/polkit-1"
+ printErrors: false
+ onLoaded: root.polkitEtcPamText = text()
+ onLoadFailed: root.polkitEtcPamText = ""
+ }
+
+ FileView {
+ path: "/usr/lib/pam.d/polkit-1"
+ printErrors: false
+ onLoaded: root.polkitLibPamText = text()
+ onLoadFailed: root.polkitLibPamText = ""
+ }
+
+ FileView {
+ path: "/etc/pam.d/system-auth"
+ printErrors: false
+ onLoaded: root.systemAuthPamText = text()
+ onLoadFailed: root.systemAuthPamText = ""
+ }
+
+ FileView {
+ path: "/etc/pam.d/common-auth"
+ printErrors: false
+ onLoaded: root.commonAuthPamText = text()
+ onLoadFailed: root.commonAuthPamText = ""
+ }
+
+ FileView {
+ path: "/etc/pam.d/password-auth"
+ printErrors: false
+ onLoaded: root.passwordAuthPamText = text()
+ onLoadFailed: root.passwordAuthPamText = ""
+ }
+
+ FocusScope {
+ id: contentFocusScope
+
+ anchors.fill: parent
+ focus: true
+
+ Keys.onEscapePressed: event => {
+ cancelAuth();
+ event.accepted = true;
+ }
+
+ Item {
+ id: headerSection
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingM
+ height: Math.max(titleColumn.implicitHeight, windowButtonRow.implicitHeight)
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: windowControls.tryStartMove()
+ onDoubleClicked: windowControls.tryToggleMaximize()
+ }
+
+ Column {
+ id: titleColumn
+ anchors.left: parent.left
+ anchors.right: windowButtonRow.left
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Authentication Required")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ StyledText {
+ text: currentFlow?.message ?? ""
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ wrapMode: Text.Wrap
+ maximumLineCount: 2
+ elide: Text.ElideRight
+ visible: text !== ""
+ }
+
+ StyledText {
+ text: currentFlow?.supplementaryMessage ?? ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: (currentFlow?.supplementaryIsError ?? false) ? Theme.error : Theme.surfaceTextMedium
+ width: parent.width
+ wrapMode: Text.Wrap
+ maximumLineCount: 2
+ elide: Text.ElideRight
+ opacity: (currentFlow?.supplementaryIsError ?? false) ? 1 : 0.8
+ visible: text !== ""
+ }
+ }
+
+ Row {
+ id: windowButtonRow
+ anchors.right: parent.right
+ anchors.top: parent.top
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ visible: windowControls.supported && windowControls.canMaximize
+ iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: windowControls.tryToggleMaximize()
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ enabled: !isLoading
+ opacity: enabled ? 1 : 0.5
+ onClicked: cancelAuth()
+ }
+ }
+ }
+
+ Column {
+ id: bottomSection
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: currentFlow?.inputPrompt ?? ""
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ width: parent.width
+ visible: text !== ""
+ }
+
+ DankTextField {
+ id: passwordField
+
+ width: parent.width
+ height: inputFieldHeight
+ backgroundColor: Theme.surfaceHover
+ normalBorderColor: Theme.outlineStrong
+ focusedBorderColor: Theme.primary
+ borderWidth: 1
+ focusedBorderWidth: 2
+ leftIconName: polkitPamHasFprint ? "fingerprint" : ""
+ leftIconSize: 20
+ leftIconColor: Theme.primary
+ leftIconFocusedColor: Theme.primary
+ opacity: isLoading ? 0.5 : 1
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ text: passwordInput
+ showPasswordToggle: !(currentFlow?.responseVisible ?? false)
+ echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
+ placeholderText: ""
+ enabled: !isLoading
+ onTextEdited: passwordInput = text
+ onAccepted: submitAuth()
+ }
+
+ StyledText {
+ text: I18n.tr("Authentication failed, please try again")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.error
+ width: parent.width
+ visible: currentFlow?.failed ?? false
+ }
+
+ Item {
+ width: parent.width
+ height: 36
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
+ border.color: Theme.surfaceVariantAlpha
+ border.width: 1
+ enabled: !isLoading
+ opacity: enabled ? 1 : 0.5
+
+ StyledText {
+ id: cancelText
+ anchors.centerIn: parent
+ text: I18n.tr("Cancel")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: cancelArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: parent.enabled
+ onClicked: cancelAuth()
+ }
+ }
+
+ Rectangle {
+ width: Math.max(80, authText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: authArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
+ enabled: !isLoading
+ opacity: enabled ? 1 : 0.5
+
+ StyledText {
+ id: authText
+ anchors.centerIn: parent
+ text: I18n.tr("Authenticate")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.background
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: authArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: parent.enabled
+ onClicked: submitAuth()
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ FloatingWindowControls {
+ id: windowControls
+ targetWindow: root
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/PowerMenuModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/PowerMenuModal.qml
new file mode 100644
index 0000000..c59037f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/PowerMenuModal.qml
@@ -0,0 +1,809 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Hyprland
+import qs.Common
+import qs.Modals.Common
+import qs.Services
+import qs.Widgets
+
+DankModal {
+ id: root
+
+ layerNamespace: "dms:power-menu"
+ keepPopoutsOpen: true
+
+ HyprlandFocusGrab {
+ windows: [root.contentWindow]
+ active: root.useHyprlandFocusGrab && root.shouldHaveFocus
+ }
+
+ property int selectedIndex: 0
+ property int selectedRow: 0
+ property int selectedCol: 0
+ property rect parentBounds: Qt.rect(0, 0, 0, 0)
+ property var parentScreen: null
+ property var visibleActions: []
+ property int gridColumns: 3
+ property int gridRows: 2
+
+ property string holdAction: ""
+ property int holdActionIndex: -1
+ property real holdProgress: 0
+ property bool showHoldHint: false
+
+ readonly property bool needsConfirmation: SettingsData.powerActionConfirm
+ readonly property int holdDurationMs: SettingsData.powerActionHoldDuration * 1000
+
+ signal powerActionRequested(string action, string title, string message)
+ signal lockRequested
+
+ function actionNeedsConfirm(action) {
+ return action !== "lock" && action !== "restart";
+ }
+
+ function startHold(action, actionIndex) {
+ if (!needsConfirmation || !actionNeedsConfirm(action)) {
+ executeAction(action);
+ return;
+ }
+ holdAction = action;
+ holdActionIndex = actionIndex;
+ holdProgress = 0;
+ showHoldHint = false;
+ holdTimer.start();
+ }
+
+ function cancelHold() {
+ if (holdAction === "")
+ return;
+ const wasHolding = holdProgress > 0;
+ holdTimer.stop();
+ if (wasHolding && holdProgress < 1) {
+ showHoldHint = true;
+ hintTimer.restart();
+ }
+ holdAction = "";
+ holdActionIndex = -1;
+ holdProgress = 0;
+ }
+
+ function completeHold() {
+ if (holdProgress < 1) {
+ cancelHold();
+ return;
+ }
+ const action = holdAction;
+ holdTimer.stop();
+ holdAction = "";
+ holdActionIndex = -1;
+ holdProgress = 0;
+ executeAction(action);
+ }
+
+ function executeAction(action) {
+ if (action === "lock") {
+ close();
+ lockRequested();
+ return;
+ }
+ if (action === "restart") {
+ close();
+ Quickshell.execDetached(["dms", "restart"]);
+ return;
+ }
+ close();
+ root.powerActionRequested(action, "", "");
+ }
+
+ Timer {
+ id: holdTimer
+ interval: 16
+ repeat: true
+ onTriggered: {
+ root.holdProgress = Math.min(1, root.holdProgress + (interval / root.holdDurationMs));
+ if (root.holdProgress >= 1) {
+ stop();
+ root.completeHold();
+ }
+ }
+ }
+
+ Timer {
+ id: hintTimer
+ interval: 2000
+ onTriggered: root.showHoldHint = false
+ }
+
+ function openCentered() {
+ parentBounds = Qt.rect(0, 0, 0, 0);
+ parentScreen = null;
+ open();
+ }
+
+ function openFromControlCenter(bounds, targetScreen) {
+ parentBounds = bounds;
+ parentScreen = targetScreen;
+ open();
+ }
+
+ function updateVisibleActions() {
+ const allActions = SettingsData.powerMenuActions || ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
+ visibleActions = allActions.filter(action => {
+ if (action === "hibernate" && !SessionService.hibernateSupported)
+ return false;
+ return true;
+ });
+
+ if (!SettingsData.powerMenuGridLayout)
+ return;
+ const count = visibleActions.length;
+ if (count === 0) {
+ gridColumns = 1;
+ gridRows = 1;
+ return;
+ }
+
+ if (count <= 3) {
+ gridColumns = 1;
+ gridRows = count;
+ return;
+ }
+
+ if (count === 4) {
+ gridColumns = 2;
+ gridRows = 2;
+ return;
+ }
+
+ gridColumns = 3;
+ gridRows = Math.ceil(count / 3);
+ }
+
+ function getDefaultActionIndex() {
+ const defaultAction = SettingsData.powerMenuDefaultAction || "logout";
+ const index = visibleActions.indexOf(defaultAction);
+ return index >= 0 ? index : 0;
+ }
+
+ function getActionAtIndex(index) {
+ if (index < 0 || index >= visibleActions.length)
+ return "";
+ return visibleActions[index];
+ }
+
+ function getActionData(action) {
+ switch (action) {
+ case "reboot":
+ return {
+ "icon": "restart_alt",
+ "label": I18n.tr("Reboot"),
+ "key": "R"
+ };
+ case "logout":
+ return {
+ "icon": "logout",
+ "label": I18n.tr("Log Out"),
+ "key": "X"
+ };
+ case "poweroff":
+ return {
+ "icon": "power_settings_new",
+ "label": I18n.tr("Power Off"),
+ "key": "P"
+ };
+ case "lock":
+ return {
+ "icon": "lock",
+ "label": I18n.tr("Lock"),
+ "key": "L"
+ };
+ case "suspend":
+ return {
+ "icon": "bedtime",
+ "label": I18n.tr("Suspend"),
+ "key": "S"
+ };
+ case "hibernate":
+ return {
+ "icon": "ac_unit",
+ "label": I18n.tr("Hibernate"),
+ "key": "H"
+ };
+ case "restart":
+ return {
+ "icon": "refresh",
+ "label": I18n.tr("Restart DMS"),
+ "key": "D"
+ };
+ default:
+ return {
+ "icon": "help",
+ "label": action,
+ "key": "?"
+ };
+ }
+ }
+
+ function selectOption(action, actionIndex) {
+ startHold(action, actionIndex !== undefined ? actionIndex : -1);
+ }
+
+ shouldBeVisible: false
+ modalWidth: SettingsData.powerMenuGridLayout ? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) : 400
+ modalHeight: contentLoader.item ? contentLoader.item.implicitHeight : 300
+ enableShadow: true
+ targetScreen: parentScreen
+ positioning: parentBounds.width > 0 ? "custom" : "center"
+ customPosition: {
+ if (parentBounds.width > 0) {
+ const effectiveBarThickness = Math.max(26 + (SettingsData.barConfigs[0]?.innerPadding ?? 4) * 0.6 + (SettingsData.barConfigs[0]?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (SettingsData.barConfigs[0]?.innerPadding ?? 4)));
+ const barExclusionZone = effectiveBarThickness + (SettingsData.barConfigs[0]?.spacing ?? 4) + (SettingsData.barConfigs[0]?.bottomGap ?? 0);
+ const screenW = parentScreen?.width ?? 1920;
+ const screenH = parentScreen?.height ?? 1080;
+ const margin = Theme.spacingL;
+
+ let targetX = parentBounds.x + (parentBounds.width - modalWidth) / 2;
+ let targetY = parentBounds.y + (parentBounds.height - modalHeight) / 2;
+
+ const minY = (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Top ? barExclusionZone + margin : margin;
+ const maxY = (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Bottom ? screenH - modalHeight - barExclusionZone - margin : screenH - modalHeight - margin;
+
+ targetY = Math.max(minY, Math.min(maxY, targetY));
+
+ return Qt.point(targetX, targetY);
+ }
+ return Qt.point(0, 0);
+ }
+ onBackgroundClicked: () => {
+ cancelHold();
+ close();
+ }
+ onOpened: () => {
+ holdAction = "";
+ holdActionIndex = -1;
+ holdProgress = 0;
+ showHoldHint = false;
+ updateVisibleActions();
+ const defaultIndex = getDefaultActionIndex();
+ if (SettingsData.powerMenuGridLayout) {
+ selectedRow = Math.floor(defaultIndex / gridColumns);
+ selectedCol = defaultIndex % gridColumns;
+ selectedIndex = defaultIndex;
+ } else {
+ selectedIndex = defaultIndex;
+ }
+ Qt.callLater(() => modalFocusScope.forceActiveFocus());
+ }
+ onDialogClosed: () => {
+ cancelHold();
+ }
+ Component.onCompleted: updateVisibleActions()
+ modalFocusScope.Keys.onPressed: event => {
+ if (event.isAutoRepeat) {
+ event.accepted = true;
+ return;
+ }
+ if (SettingsData.powerMenuGridLayout) {
+ handleGridNavigation(event, true);
+ } else {
+ handleListNavigation(event, true);
+ }
+ }
+ modalFocusScope.Keys.onReleased: event => {
+ if (event.isAutoRepeat) {
+ event.accepted = true;
+ return;
+ }
+ if (SettingsData.powerMenuGridLayout) {
+ handleGridNavigation(event, false);
+ } else {
+ handleListNavigation(event, false);
+ }
+ }
+
+ function handleListNavigation(event, isPressed) {
+ if (!isPressed) {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
+ cancelHold();
+ event.accepted = true;
+ }
+ return;
+ }
+
+ switch (event.key) {
+ case Qt.Key_Up:
+ case Qt.Key_Backtab:
+ selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
+ event.accepted = true;
+ break;
+ case Qt.Key_Down:
+ case Qt.Key_Tab:
+ selectedIndex = (selectedIndex + 1) % visibleActions.length;
+ event.accepted = true;
+ break;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ startHold(getActionAtIndex(selectedIndex), selectedIndex);
+ event.accepted = true;
+ break;
+ case Qt.Key_N:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedIndex = (selectedIndex + 1) % visibleActions.length;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_P:
+ if (!(event.modifiers & Qt.ControlModifier)) {
+ const idx = visibleActions.indexOf("poweroff");
+ startHold("poweroff", idx);
+ event.accepted = true;
+ } else {
+ selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_J:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedIndex = (selectedIndex + 1) % visibleActions.length;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_K:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_R:
+ startHold("reboot", visibleActions.indexOf("reboot"));
+ event.accepted = true;
+ break;
+ case Qt.Key_X:
+ startHold("logout", visibleActions.indexOf("logout"));
+ event.accepted = true;
+ break;
+ case Qt.Key_L:
+ startHold("lock", visibleActions.indexOf("lock"));
+ event.accepted = true;
+ break;
+ case Qt.Key_S:
+ startHold("suspend", visibleActions.indexOf("suspend"));
+ event.accepted = true;
+ break;
+ case Qt.Key_H:
+ startHold("hibernate", visibleActions.indexOf("hibernate"));
+ event.accepted = true;
+ break;
+ case Qt.Key_D:
+ startHold("restart", visibleActions.indexOf("restart"));
+ event.accepted = true;
+ break;
+ }
+ }
+
+ function handleGridNavigation(event, isPressed) {
+ if (!isPressed) {
+ if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
+ cancelHold();
+ event.accepted = true;
+ }
+ return;
+ }
+
+ switch (event.key) {
+ case Qt.Key_Left:
+ selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ break;
+ case Qt.Key_Right:
+ selectedCol = (selectedCol + 1) % gridColumns;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ break;
+ case Qt.Key_Up:
+ case Qt.Key_Backtab:
+ selectedRow = (selectedRow - 1 + gridRows) % gridRows;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ break;
+ case Qt.Key_Down:
+ case Qt.Key_Tab:
+ selectedRow = (selectedRow + 1) % gridRows;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ break;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ startHold(getActionAtIndex(selectedIndex), selectedIndex);
+ event.accepted = true;
+ break;
+ case Qt.Key_N:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedCol = (selectedCol + 1) % gridColumns;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_P:
+ if (!(event.modifiers & Qt.ControlModifier)) {
+ const idx = visibleActions.indexOf("poweroff");
+ startHold("poweroff", idx);
+ event.accepted = true;
+ } else {
+ selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_J:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedRow = (selectedRow + 1) % gridRows;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_K:
+ if (event.modifiers & Qt.ControlModifier) {
+ selectedRow = (selectedRow - 1 + gridRows) % gridRows;
+ selectedIndex = selectedRow * gridColumns + selectedCol;
+ event.accepted = true;
+ }
+ break;
+ case Qt.Key_R:
+ startHold("reboot", visibleActions.indexOf("reboot"));
+ event.accepted = true;
+ break;
+ case Qt.Key_X:
+ startHold("logout", visibleActions.indexOf("logout"));
+ event.accepted = true;
+ break;
+ case Qt.Key_L:
+ startHold("lock", visibleActions.indexOf("lock"));
+ event.accepted = true;
+ break;
+ case Qt.Key_S:
+ startHold("suspend", visibleActions.indexOf("suspend"));
+ event.accepted = true;
+ break;
+ case Qt.Key_H:
+ startHold("hibernate", visibleActions.indexOf("hibernate"));
+ event.accepted = true;
+ break;
+ case Qt.Key_D:
+ startHold("restart", visibleActions.indexOf("restart"));
+ event.accepted = true;
+ break;
+ }
+ }
+
+ content: Component {
+ Item {
+ anchors.fill: parent
+ implicitHeight: (SettingsData.powerMenuGridLayout ? buttonGrid.implicitHeight : buttonColumn.implicitHeight) + Theme.spacingL * 2 + (root.needsConfirmation ? hintRow.height + Theme.spacingM : 0)
+
+ Grid {
+ id: buttonGrid
+ visible: SettingsData.powerMenuGridLayout
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.top: parent.top
+ anchors.topMargin: Theme.spacingL
+ columns: root.gridColumns
+ columnSpacing: Theme.spacingS
+ rowSpacing: Theme.spacingS
+
+ Repeater {
+ model: root.visibleActions
+
+ Rectangle {
+ id: gridButtonRect
+ required property int index
+ required property string modelData
+
+ readonly property var actionData: root.getActionData(modelData)
+ readonly property bool isSelected: root.selectedIndex === index
+ readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
+ readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
+
+ width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
+ height: 100
+ radius: Theme.cornerRadius
+ color: {
+ if (isSelected)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
+ if (mouseArea.containsMouse)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
+ return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
+ }
+ border.color: isSelected ? Theme.primary : "transparent"
+ border.width: isSelected ? 2 : 0
+
+ Rectangle {
+ id: gridProgressMask
+ anchors.fill: parent
+ radius: parent.radius
+ visible: false
+ layer.enabled: true
+ }
+
+ Item {
+ anchors.fill: parent
+ visible: gridButtonRect.isHolding
+ layer.enabled: gridButtonRect.isHolding
+ layer.effect: MultiEffect {
+ maskEnabled: true
+ maskSource: gridProgressMask
+ maskSpreadAtMin: 1
+ maskThresholdMin: 0.5
+ }
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: parent.width * root.holdProgress
+ color: {
+ if (gridButtonRect.modelData === "poweroff")
+ return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
+ if (gridButtonRect.modelData === "reboot")
+ return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3);
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
+ }
+ }
+ }
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: gridButtonRect.actionData.icon
+ size: Theme.iconSize + 8
+ color: {
+ if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
+ return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
+ }
+ return Theme.surfaceText;
+ }
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: gridButtonRect.actionData.label
+ font.pixelSize: Theme.fontSizeMedium
+ color: {
+ if (gridButtonRect.showWarning && (mouseArea.containsMouse || gridButtonRect.isHolding)) {
+ return gridButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
+ }
+ return Theme.surfaceText;
+ }
+ font.weight: Font.Medium
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ Rectangle {
+ width: 20
+ height: 16
+ radius: 4
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.1)
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ StyledText {
+ text: gridButtonRect.actionData.key
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: {
+ root.selectedRow = Math.floor(index / root.gridColumns);
+ root.selectedCol = index % root.gridColumns;
+ root.selectedIndex = index;
+ root.startHold(modelData, index);
+ }
+ onReleased: root.cancelHold()
+ onCanceled: root.cancelHold()
+ }
+ }
+ }
+ }
+
+ Column {
+ id: buttonColumn
+ visible: !SettingsData.powerMenuGridLayout
+ anchors {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ leftMargin: Theme.spacingL
+ rightMargin: Theme.spacingL
+ topMargin: Theme.spacingL
+ }
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: root.visibleActions
+
+ Rectangle {
+ id: listButtonRect
+ required property int index
+ required property string modelData
+
+ readonly property var actionData: root.getActionData(modelData)
+ readonly property bool isSelected: root.selectedIndex === index
+ readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
+ readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
+
+ width: parent.width
+ height: 56
+ radius: Theme.cornerRadius
+ color: {
+ if (isSelected)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
+ if (listMouseArea.containsMouse)
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
+ return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
+ }
+ border.color: isSelected ? Theme.primary : "transparent"
+ border.width: isSelected ? 2 : 0
+
+ Rectangle {
+ id: listProgressMask
+ anchors.fill: parent
+ radius: parent.radius
+ visible: false
+ layer.enabled: true
+ }
+
+ Item {
+ anchors.fill: parent
+ visible: listButtonRect.isHolding
+ layer.enabled: listButtonRect.isHolding
+ layer.effect: MultiEffect {
+ maskEnabled: true
+ maskSource: listProgressMask
+ maskSpreadAtMin: 1
+ maskThresholdMin: 0.5
+ }
+
+ Rectangle {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: parent.width * root.holdProgress
+ color: {
+ if (listButtonRect.modelData === "poweroff")
+ return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
+ if (listButtonRect.modelData === "reboot")
+ return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3);
+ return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
+ }
+ }
+ }
+
+ Row {
+ anchors {
+ left: parent.left
+ right: parent.right
+ leftMargin: Theme.spacingM
+ rightMargin: Theme.spacingM
+ verticalCenter: parent.verticalCenter
+ }
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: listButtonRect.actionData.icon
+ size: Theme.iconSize + 4
+ color: {
+ if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
+ return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
+ }
+ return Theme.surfaceText;
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: listButtonRect.actionData.label
+ font.pixelSize: Theme.fontSizeMedium
+ color: {
+ if (listButtonRect.showWarning && (listMouseArea.containsMouse || listButtonRect.isHolding)) {
+ return listButtonRect.modelData === "poweroff" ? Theme.error : Theme.warning;
+ }
+ return Theme.surfaceText;
+ }
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Rectangle {
+ width: 28
+ height: 20
+ radius: 4
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.1)
+ anchors {
+ right: parent.right
+ rightMargin: Theme.spacingM
+ verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: listButtonRect.actionData.key
+ font.pixelSize: Theme.fontSizeSmall
+ color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ }
+ }
+
+ MouseArea {
+ id: listMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: {
+ root.selectedIndex = index;
+ root.startHold(modelData, index);
+ }
+ onReleased: root.cancelHold()
+ onCanceled: root.cancelHold()
+ }
+ }
+ }
+ }
+
+ Row {
+ id: hintRow
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: Theme.spacingS
+ spacing: Theme.spacingXS
+ visible: root.needsConfirmation
+ opacity: root.showHoldHint ? 1 : 0.5
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 150
+ }
+ }
+
+ DankIcon {
+ name: root.showHoldHint ? "warning" : "touch_app"
+ size: Theme.fontSizeSmall
+ color: root.showHoldHint ? Theme.warning : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000
+ readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
+ text: {
+ if (root.showHoldHint)
+ return I18n.tr("Hold longer to confirm");
+ if (root.holdProgress > 0) {
+ if (totalMs < 1000)
+ return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
+ return I18n.tr("Hold to confirm (%1s)").arg(Math.ceil(remainingMs / 1000));
+ }
+ if (totalMs < 1000)
+ return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
+ return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: root.showHoldHint ? Theme.warning : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/ProcessListModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/ProcessListModal.qml
new file mode 100644
index 0000000..8cfc705
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/ProcessListModal.qml
@@ -0,0 +1,609 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Wayland
+import qs.Common
+import qs.Modules.ProcessList
+import qs.Services
+import qs.Widgets
+
+FloatingWindow {
+ id: processListModal
+
+ property bool disablePopupTransparency: true
+ property int currentTab: 0
+ property string searchText: ""
+ property string expandedPid: ""
+ property string processFilter: "all"
+ property bool shouldHaveFocus: visible
+ property alias shouldBeVisible: processListModal.visible
+
+ signal closingModal
+
+ function show() {
+ if (!DgopService.dgopAvailable) {
+ console.warn("ProcessListModal: dgop is not available");
+ return;
+ }
+ visible = true;
+ }
+
+ function hide() {
+ visible = false;
+ if (processContextMenu.visible)
+ processContextMenu.close();
+ }
+
+ function toggle() {
+ if (!DgopService.dgopAvailable) {
+ console.warn("ProcessListModal: dgop is not available");
+ return;
+ }
+ visible = !visible;
+ }
+
+ function focusOrToggle() {
+ if (!DgopService.dgopAvailable) {
+ console.warn("ProcessListModal: dgop is not available");
+ return;
+ }
+ if (visible) {
+ const modalTitle = I18n.tr("System Monitor", "sysmon window title");
+ for (const toplevel of ToplevelManager.toplevels.values) {
+ if (toplevel.title !== "System Monitor" && toplevel.title !== modalTitle)
+ continue;
+ if (toplevel.activated) {
+ hide();
+ return;
+ }
+ toplevel.activate();
+ return;
+ }
+ }
+ show();
+ }
+
+ function formatBytes(bytes) {
+ if (bytes < 1024)
+ return bytes.toFixed(0) + " B/s";
+ if (bytes < 1024 * 1024)
+ return (bytes / 1024).toFixed(1) + " KB/s";
+ if (bytes < 1024 * 1024 * 1024)
+ return (bytes / (1024 * 1024)).toFixed(1) + " MB/s";
+ return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB/s";
+ }
+
+ function nextTab() {
+ currentTab = (currentTab + 1) % 4;
+ }
+
+ function previousTab() {
+ currentTab = (currentTab - 1 + 4) % 4;
+ }
+
+ objectName: "processListModal"
+ title: I18n.tr("System Monitor", "sysmon window title")
+ minimumSize: Qt.size(Math.min(Math.round(Theme.fontSizeMedium * 48), Screen.width), Math.min(Math.round(Theme.fontSizeMedium * 34), Screen.height))
+ implicitWidth: Math.round(Theme.fontSizeMedium * 71)
+ implicitHeight: Math.round(Theme.fontSizeMedium * 51)
+ color: Theme.surfaceContainer
+ visible: false
+
+ onCurrentTabChanged: {
+ if (visible && currentTab === 0 && searchField.visible)
+ searchField.forceActiveFocus();
+ }
+
+ onVisibleChanged: {
+ if (!visible) {
+ closingModal();
+ searchText = "";
+ expandedPid = "";
+ processFilter = "all";
+ processFilterGroup.currentIndex = 0;
+ if (processesTabLoader.item)
+ processesTabLoader.item.reset();
+ DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]);
+ } else {
+ DgopService.addRef(["cpu", "memory", "network", "disk", "system"]);
+ Qt.callLater(() => {
+ if (currentTab === 0 && searchField.visible)
+ searchField.forceActiveFocus();
+ else if (contentFocusScope)
+ contentFocusScope.forceActiveFocus();
+ });
+ }
+ }
+
+ ProcessContextMenu {
+ id: processContextMenu
+ parentFocusItem: contentFocusScope
+ onProcessKilled: {
+ if (processesTabLoader.item)
+ processesTabLoader.item.forceRefresh(3);
+ }
+ }
+
+ FocusScope {
+ id: contentFocusScope
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ anchors.fill: parent
+ focus: true
+
+ Keys.onPressed: event => {
+ if (processContextMenu.visible)
+ return;
+
+ switch (event.key) {
+ case Qt.Key_1:
+ currentTab = 0;
+ event.accepted = true;
+ return;
+ case Qt.Key_2:
+ currentTab = 1;
+ event.accepted = true;
+ return;
+ case Qt.Key_3:
+ currentTab = 2;
+ event.accepted = true;
+ return;
+ case Qt.Key_4:
+ currentTab = 3;
+ event.accepted = true;
+ return;
+ case Qt.Key_Tab:
+ nextTab();
+ event.accepted = true;
+ return;
+ case Qt.Key_Backtab:
+ previousTab();
+ event.accepted = true;
+ return;
+ case Qt.Key_Escape:
+ if (searchText.length > 0) {
+ searchText = "";
+ event.accepted = true;
+ return;
+ }
+ if (currentTab === 0 && processesTabLoader.item?.keyboardNavigationActive) {
+ processesTabLoader.item.reset();
+ event.accepted = true;
+ return;
+ }
+ hide();
+ event.accepted = true;
+ return;
+ case Qt.Key_F:
+ if (event.modifiers & Qt.ControlModifier) {
+ searchField.forceActiveFocus();
+ event.accepted = true;
+ return;
+ }
+ break;
+ }
+
+ if (currentTab === 0 && processesTabLoader.item)
+ processesTabLoader.item.handleKey(event);
+ }
+
+ Rectangle {
+ anchors.centerIn: parent
+ width: 400
+ height: 200
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
+ border.color: Theme.error
+ border.width: 2
+ visible: !DgopService.dgopAvailable
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingL
+
+ DankIcon {
+ name: "error"
+ size: 48
+ color: Theme.error
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("System Monitor Unavailable")
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Bold
+ color: Theme.error
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.", "dgop unavailable error message")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ spacing: 0
+ visible: DgopService.dgopAvailable
+
+ Item {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.4)
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: windowControls.tryStartMove()
+ onDoubleClicked: windowControls.tryToggleMaximize()
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingL
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "analytics"
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("System Monitor")
+ font.pixelSize: Theme.fontSizeXLarge
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Row {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ visible: windowControls.supported
+ circular: false
+ iconName: processListModal.maximized ? "fullscreen_exit" : "fullscreen"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: windowControls.tryToggleMaximize()
+ }
+
+ DankActionButton {
+ circular: false
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: processListModal.hide()
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.7)
+ Layout.leftMargin: Theme.spacingL
+ Layout.rightMargin: Theme.spacingL
+ spacing: Theme.spacingM
+
+ Row {
+ spacing: 2
+
+ Repeater {
+ model: [
+ {
+ text: I18n.tr("Processes"),
+ icon: "list_alt"
+ },
+ {
+ text: I18n.tr("Performance"),
+ icon: "analytics"
+ },
+ {
+ text: I18n.tr("Disks"),
+ icon: "storage"
+ },
+ {
+ text: I18n.tr("System"),
+ icon: "computer"
+ }
+ ]
+
+ Rectangle {
+ width: tabRowContent.implicitWidth + Theme.spacingM * 2
+ height: Math.round(Theme.fontSizeMedium * 3.1)
+ radius: Theme.cornerRadius
+ color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
+ border.color: currentTab === index ? Theme.primary : "transparent"
+ border.width: currentTab === index ? 1 : 0
+
+ Row {
+ id: tabRowContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: modelData.icon
+ size: Theme.iconSize - 2
+ color: currentTab === index ? Theme.primary : Theme.surfaceText
+ opacity: currentTab === index ? 1 : 0.7
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: modelData.text
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: currentTab === index ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: tabMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: currentTab = index
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ }
+ }
+ }
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ DankButtonGroup {
+ id: processFilterGroup
+ model: [I18n.tr("All"), I18n.tr("User"), I18n.tr("System")]
+ currentIndex: 0
+ checkEnabled: false
+ buttonHeight: Math.round(Theme.fontSizeSmall * 2.6)
+ minButtonWidth: 0
+ buttonPadding: Theme.spacingS
+ textSize: Theme.fontSizeSmall
+ visible: currentTab === 0
+ onSelectionChanged: (index, selected) => {
+ if (!selected)
+ return;
+ currentIndex = index;
+ switch (index) {
+ case 0:
+ processListModal.processFilter = "all";
+ return;
+ case 1:
+ processListModal.processFilter = "user";
+ return;
+ case 2:
+ processListModal.processFilter = "system";
+ return;
+ }
+ }
+ }
+
+ DankTextField {
+ id: searchField
+ Layout.fillWidth: true
+ Layout.maximumWidth: Math.round(Theme.fontSizeMedium * 18)
+ Layout.minimumWidth: Theme.fontSizeMedium * 4
+ Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 2.8)
+ placeholderText: I18n.tr("Search processes...", "process search placeholder")
+ leftIconName: "search"
+ showClearButton: true
+ text: searchText
+ visible: currentTab === 0
+ onTextChanged: searchText = text
+ ignoreUpDownKeys: true
+ keyForwardTargets: [contentFocusScope]
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ Layout.margins: Theme.spacingL
+ Layout.topMargin: Theme.spacingM
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ border.color: Theme.outlineLight
+ border.width: 1
+ clip: true
+
+ Loader {
+ id: processesTabLoader
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ active: processListModal.visible && currentTab === 0
+ visible: currentTab === 0
+ sourceComponent: ProcessesView {
+ searchText: processListModal.searchText
+ expandedPid: processListModal.expandedPid
+ processFilter: processListModal.processFilter
+ contextMenu: processContextMenu
+ onExpandedPidChanged: processListModal.expandedPid = expandedPid
+ }
+ }
+
+ Loader {
+ id: performanceTabLoader
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ active: processListModal.visible && currentTab === 1
+ visible: currentTab === 1
+ sourceComponent: PerformanceView {}
+ }
+
+ Loader {
+ id: disksTabLoader
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ active: processListModal.visible && currentTab === 2
+ visible: currentTab === 2
+ sourceComponent: DisksView {}
+ }
+
+ Loader {
+ id: systemTabLoader
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ active: processListModal.visible && currentTab === 3
+ visible: currentTab === 3
+ sourceComponent: SystemView {}
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.round(Theme.fontSizeSmall * 2.7)
+ Layout.leftMargin: Theme.spacingL
+ Layout.rightMargin: Theme.spacingL
+ Layout.bottomMargin: Theme.spacingM
+ color: "transparent"
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingL
+
+ Row {
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Processes:", "process count label in footer")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: DgopService.processCount.toString()
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Uptime:", "uptime label in footer")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: DgopService.shortUptime || "--"
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Bold
+ color: Theme.surfaceText
+ }
+ }
+ }
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingL
+
+ Row {
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "swap_horiz"
+ size: 14
+ color: Theme.info
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: "↓" + formatBytes(DgopService.networkRxRate) + " ↑" + formatBytes(DgopService.networkTxRate)
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceText
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "storage"
+ size: 14
+ color: Theme.warning
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: "↓" + formatBytes(DgopService.diskReadRate) + " ↑" + formatBytes(DgopService.diskWriteRate)
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ color: Theme.surfaceText
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "memory"
+ size: 14
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: DgopService.cpuUsage.toFixed(1) + "%"
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Bold
+ color: DgopService.cpuUsage > 80 ? Theme.error : Theme.surfaceText
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "sd_card"
+ size: 14
+ color: Theme.secondary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: DgopService.formatSystemMemory(DgopService.usedMemoryKB) + " / " + DgopService.formatSystemMemory(DgopService.totalMemoryKB)
+ font.pixelSize: Theme.fontSizeSmall
+ font.family: SettingsData.monoFontFamily
+ font.weight: Font.Bold
+ color: DgopService.memoryUsage > 90 ? Theme.error : Theme.surfaceText
+ }
+ }
+ }
+ }
+ }
+ }
+
+ FloatingWindowControls {
+ id: windowControls
+ targetWindow: processListModal
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/ProfileSection.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/ProfileSection.qml
new file mode 100644
index 0000000..29b4961
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/ProfileSection.qml
@@ -0,0 +1,144 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property var parentModal: null
+
+ width: parent.width - Theme.spacingS * 2
+ height: 110
+ radius: Theme.cornerRadius
+ color: "transparent"
+ border.width: 0
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Item {
+ id: profileImageContainer
+
+ width: 80
+ height: 80
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankCircularImage {
+ id: profileImage
+
+ anchors.fill: parent
+ imageSource: {
+ if (PortalService.profileImage === "") {
+ return "";
+ }
+ if (PortalService.profileImage.startsWith("/")) {
+ return "file://" + PortalService.profileImage;
+ }
+ return PortalService.profileImage;
+ }
+ fallbackIcon: "person"
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: width / 2
+ color: Qt.rgba(0, 0, 0, 0.7)
+ visible: profileMouseArea.containsMouse
+
+ Row {
+ anchors.centerIn: parent
+ spacing: 4
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: 14
+ color: Qt.rgba(255, 255, 255, 0.9)
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "edit"
+ size: 16
+ color: "black"
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: () => {
+ if (root.parentModal) {
+ root.parentModal.allowFocusOverride = true;
+ root.parentModal.shouldHaveFocus = false;
+ root.parentModal.openProfileBrowser();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: 14
+ color: Qt.rgba(255, 255, 255, 0.9)
+ visible: profileImage.hasImage
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "close"
+ size: 16
+ color: "black"
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: () => {
+ return PortalService.setProfileImage("");
+ }
+ }
+ }
+ }
+ }
+
+ MouseArea {
+ id: profileMouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ propagateComposedEvents: true
+ acceptedButtons: Qt.NoButton
+ }
+ }
+
+ Column {
+ width: 120
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: UserInfoService.fullName || "User"
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: DgopService.hostname || "DMS"
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsContent.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsContent.qml
new file mode 100644
index 0000000..34cd51a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsContent.qml
@@ -0,0 +1,522 @@
+import QtQuick
+import qs.Common
+import qs.Modules.Settings
+
+FocusScope {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property int currentIndex: 0
+ property var parentModal: null
+
+ focus: true
+
+ Rectangle {
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: (parentModal && parentModal.isCompactMode) ? Theme.spacingS : (32 + Theme.spacingS)
+ anchors.bottomMargin: 0
+ anchors.topMargin: 0
+ color: "transparent"
+
+ Loader {
+ id: wallpaperLoader
+ anchors.fill: parent
+ active: root.currentIndex === 0
+ visible: active
+ focus: active
+
+ sourceComponent: Component {
+ WallpaperTab {
+ parentModal: root.parentModal
+ }
+ }
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: timeWeatherLoader
+ anchors.fill: parent
+ active: root.currentIndex === 1
+ visible: active
+ focus: active
+
+ sourceComponent: TimeWeatherTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: keybindsLoader
+ anchors.fill: parent
+ active: root.currentIndex === 2
+ visible: active
+ focus: active
+
+ sourceComponent: KeybindsTab {
+ parentModal: root.parentModal
+ }
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: topBarLoader
+ anchors.fill: parent
+ active: root.currentIndex === 3
+ visible: active
+ focus: active
+
+ sourceComponent: DankBarTab {
+ parentModal: root.parentModal
+ }
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: workspacesLoader
+ anchors.fill: parent
+ active: root.currentIndex === 4
+ visible: active
+ focus: active
+
+ sourceComponent: WorkspacesTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: dockLoader
+ anchors.fill: parent
+ active: root.currentIndex === 5
+ visible: active
+ focus: active
+
+ sourceComponent: Component {
+ DockTab {}
+ }
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: displayConfigLoader
+ anchors.fill: parent
+ active: root.currentIndex === 24
+ visible: active
+ focus: active
+
+ sourceComponent: DisplayConfigTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: gammaControlLoader
+ anchors.fill: parent
+ active: root.currentIndex === 25
+ visible: active
+ focus: active
+
+ sourceComponent: GammaControlTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: displayWidgetsLoader
+ anchors.fill: parent
+ active: root.currentIndex === 26
+ visible: active
+ focus: active
+
+ sourceComponent: DisplayWidgetsTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: networkLoader
+ anchors.fill: parent
+ active: root.currentIndex === 7
+ visible: active
+ focus: active
+
+ sourceComponent: NetworkTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: printerLoader
+ anchors.fill: parent
+ active: root.currentIndex === 8
+ visible: active
+ focus: active
+
+ sourceComponent: PrinterTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: launcherLoader
+ anchors.fill: parent
+ active: root.currentIndex === 9
+ visible: active
+ focus: active
+
+ sourceComponent: LauncherTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: themeColorsLoader
+ anchors.fill: parent
+ active: root.currentIndex === 10
+ visible: active
+ focus: active
+
+ sourceComponent: ThemeColorsTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: lockScreenLoader
+ anchors.fill: parent
+ active: root.currentIndex === 11
+ visible: active
+ focus: active
+
+ sourceComponent: LockScreenTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: greeterLoader
+ anchors.fill: parent
+ active: root.currentIndex === 31
+ visible: active
+ focus: active
+
+ sourceComponent: GreeterTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: pluginsLoader
+ anchors.fill: parent
+ active: root.currentIndex === 12
+ visible: active
+ focus: active
+
+ sourceComponent: PluginsTab {
+ parentModal: root.parentModal
+ }
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: aboutLoader
+ anchors.fill: parent
+ active: root.currentIndex === 13
+ visible: active
+ focus: active
+
+ sourceComponent: AboutTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: typographyMotionLoader
+ anchors.fill: parent
+ active: root.currentIndex === 14
+ visible: active
+ focus: active
+
+ sourceComponent: TypographyMotionTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: soundsLoader
+ anchors.fill: parent
+ active: root.currentIndex === 15
+ visible: active
+ focus: active
+
+ sourceComponent: SoundsTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: mediaPlayerLoader
+ anchors.fill: parent
+ active: root.currentIndex === 16
+ visible: active
+ focus: active
+
+ sourceComponent: MediaPlayerTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: notificationsLoader
+ anchors.fill: parent
+ active: root.currentIndex === 17
+ visible: active
+ focus: active
+
+ sourceComponent: NotificationsTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: osdLoader
+ anchors.fill: parent
+ active: root.currentIndex === 18
+ visible: active
+ focus: active
+
+ sourceComponent: OSDTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: runningAppsLoader
+ anchors.fill: parent
+ active: root.currentIndex === 19
+ visible: active
+ focus: active
+
+ sourceComponent: RunningAppsTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: systemUpdaterLoader
+ anchors.fill: parent
+ active: root.currentIndex === 20
+ visible: active
+ focus: active
+
+ sourceComponent: SystemUpdaterTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: powerSleepLoader
+ anchors.fill: parent
+ active: root.currentIndex === 21
+ visible: active
+ focus: active
+
+ sourceComponent: PowerSleepTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: widgetsLoader
+ anchors.fill: parent
+ active: root.currentIndex === 22
+ visible: active
+ focus: active
+
+ sourceComponent: WidgetsTab {
+ parentModal: root.parentModal
+ }
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: clipboardLoader
+ anchors.fill: parent
+ active: root.currentIndex === 23
+ visible: active
+ focus: active
+
+ sourceComponent: ClipboardTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: desktopWidgetsLoader
+ anchors.fill: parent
+ active: root.currentIndex === 27
+ visible: active
+ focus: active
+
+ sourceComponent: DesktopWidgetsTab {
+ parentModal: root.parentModal
+ }
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: windowRulesLoader
+ anchors.fill: parent
+ active: root.currentIndex === 28
+ visible: active
+ focus: active
+
+ sourceComponent: WindowRulesTab {
+ parentModal: root.parentModal
+ }
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: audioLoader
+ anchors.fill: parent
+ active: root.currentIndex === 29
+ visible: active
+ focus: active
+
+ sourceComponent: AudioTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: localeLoader
+ anchors.fill: parent
+ active: root.currentIndex === 30
+ visible: active
+ focus: active
+
+ sourceComponent: LocaleTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+
+ Loader {
+ id: muxLoader
+ anchors.fill: parent
+ active: root.currentIndex === 32
+ visible: active
+ focus: active
+
+ sourceComponent: MuxTab {}
+
+ onActiveChanged: {
+ if (active && item)
+ Qt.callLater(() => item.forceActiveFocus());
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsModal.qml
new file mode 100644
index 0000000..9ce9153
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsModal.qml
@@ -0,0 +1,382 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Modals.FileBrowser
+import qs.Services
+import qs.Widgets
+
+FloatingWindow {
+ id: settingsModal
+
+ property bool disablePopupTransparency: true
+ property var profileBrowser: profileBrowserLoader.item
+ property var wallpaperBrowser: wallpaperBrowserLoader.item
+
+ function openProfileBrowser(allowStacking) {
+ profileBrowserLoader.active = true;
+ if (!profileBrowserLoader.item)
+ return;
+ if (allowStacking !== undefined)
+ profileBrowserLoader.item.allowStacking = allowStacking;
+ profileBrowserLoader.item.open();
+ }
+
+ function openWallpaperBrowser(allowStacking) {
+ wallpaperBrowserLoader.active = true;
+ if (!wallpaperBrowserLoader.item)
+ return;
+ if (allowStacking !== undefined)
+ wallpaperBrowserLoader.item.allowStacking = allowStacking;
+ wallpaperBrowserLoader.item.open();
+ }
+ property alias sidebar: sidebar
+ property int currentTabIndex: 0
+ property bool shouldHaveFocus: visible
+ property bool allowFocusOverride: false
+ property alias shouldBeVisible: settingsModal.visible
+ property bool isCompactMode: width < 700
+ property bool menuVisible: !isCompactMode
+ property bool enableAnimations: true
+
+ signal closingModal
+
+ function show() {
+ visible = true;
+ }
+
+ function hide() {
+ visible = false;
+ }
+
+ function toggle() {
+ visible = !visible;
+ }
+
+ function showWithTab(tabIndex: int) {
+ if (tabIndex >= 0) {
+ currentTabIndex = tabIndex;
+ sidebar.autoExpandForTab(tabIndex);
+ }
+ visible = true;
+ }
+
+ function showWithTabName(tabName: string) {
+ var idx = sidebar.resolveTabIndex(tabName);
+ if (idx >= 0) {
+ currentTabIndex = idx;
+ sidebar.autoExpandForTab(idx);
+ }
+ visible = true;
+ }
+
+ function resolveTabIndex(tabName: string): int {
+ return sidebar.resolveTabIndex(tabName);
+ }
+
+ function toggleMenu() {
+ enableAnimations = true;
+ menuVisible = !menuVisible;
+ }
+
+ objectName: "settingsModal"
+ title: I18n.tr("Settings", "settings window title")
+ minimumSize: Qt.size(500, 400)
+ implicitWidth: 900
+ implicitHeight: screen ? Math.min(940, screen.height - 100) : 940
+ color: Theme.surfaceContainer
+ visible: false
+
+ onIsCompactModeChanged: {
+ enableAnimations = false;
+ if (!isCompactMode) {
+ menuVisible = true;
+ }
+ Qt.callLater(() => {
+ enableAnimations = true;
+ });
+ }
+
+ onVisibleChanged: {
+ if (!visible) {
+ closingModal();
+ } else {
+ Qt.callLater(() => {
+ sidebar.focusSearch();
+ });
+ }
+ }
+
+ Loader {
+ active: settingsModal.visible
+ sourceComponent: Component {
+ Ref {
+ service: CupsService
+ }
+ }
+ }
+
+ LazyLoader {
+ id: profileBrowserLoader
+ active: false
+
+ FileBrowserModal {
+ id: profileBrowserItem
+
+ allowStacking: true
+ parentModal: settingsModal
+ browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
+ browserIcon: "person"
+ browserType: "profile"
+ showHiddenFiles: true
+ fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
+ onFileSelected: path => {
+ PortalService.setProfileImage(path);
+ close();
+ }
+ onDialogClosed: () => {
+ allowStacking = true;
+ }
+ }
+ }
+
+ LazyLoader {
+ id: wallpaperBrowserLoader
+ active: false
+
+ FileBrowserModal {
+ id: wallpaperBrowserItem
+
+ allowStacking: true
+ parentModal: settingsModal
+ browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
+ browserIcon: "wallpaper"
+ browserType: "wallpaper"
+ showHiddenFiles: true
+ fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
+ onFileSelected: path => {
+ SessionData.setWallpaper(path);
+ close();
+ }
+ onDialogClosed: () => {
+ allowStacking = true;
+ }
+ }
+ }
+
+ FocusScope {
+ id: contentFocusScope
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ anchors.fill: parent
+ focus: true
+
+ Column {
+ anchors.fill: parent
+ spacing: 0
+
+ Item {
+ width: parent.width
+ height: 48
+ z: 10
+
+ MouseArea {
+ anchors.fill: parent
+ onPressed: windowControls.tryStartMove()
+ onDoubleClicked: windowControls.tryToggleMaximize()
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: Theme.surfaceContainer
+ opacity: 0.5
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingL
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankActionButton {
+ visible: settingsModal.isCompactMode
+ circular: false
+ iconName: "menu"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ onClicked: () => {
+ settingsModal.toggleMenu();
+ }
+ }
+
+ DankIcon {
+ name: "settings"
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Settings")
+ font.pixelSize: Theme.fontSizeXLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Row {
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.top: parent.top
+ anchors.topMargin: Theme.spacingM
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ visible: windowControls.supported
+ circular: false
+ iconName: settingsModal.maximized ? "fullscreen_exit" : "fullscreen"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: windowControls.tryToggleMaximize()
+ }
+
+ DankActionButton {
+ circular: false
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: settingsModal.hide()
+ }
+ }
+ }
+
+ Rectangle {
+ id: readOnlyBanner
+
+ property bool showBanner: (SettingsData._isReadOnly && SettingsData._hasUnsavedChanges) || (SessionData._isReadOnly && SessionData._hasUnsavedChanges)
+
+ width: parent.width
+ height: showBanner ? bannerContent.implicitHeight + Theme.spacingM * 2 : 0
+ color: Theme.surfaceContainerHigh
+ visible: showBanner
+ clip: true
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Row {
+ id: bannerContent
+
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingL
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "info"
+ size: Theme.iconSize
+ color: Theme.warning
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ id: bannerText
+
+ text: I18n.tr("Settings are read-only. Changes will not persist.", "read-only settings warning for NixOS home-manager users")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ width: Math.max(100, parent.width - (copySettingsButton.visible ? copySettingsButton.width + Theme.spacingM : 0) - (copySessionButton.visible ? copySessionButton.width + Theme.spacingM : 0) - Theme.spacingM * 2 - Theme.iconSize)
+ wrapMode: Text.WordWrap
+ }
+
+ DankButton {
+ id: copySettingsButton
+
+ visible: SettingsData._isReadOnly && SettingsData._hasUnsavedChanges
+ text: "settings.json"
+ iconName: "content_copy"
+ backgroundColor: Theme.primary
+ textColor: Theme.primaryText
+ buttonHeight: 32
+ horizontalPadding: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ onClicked: {
+ Quickshell.execDetached(["dms", "cl", "copy", SettingsData.getCurrentSettingsJson()]);
+ ToastService.showInfo(I18n.tr("Copied to clipboard"));
+ }
+ }
+
+ DankButton {
+ id: copySessionButton
+
+ visible: SessionData._isReadOnly && SessionData._hasUnsavedChanges
+ text: "session.json"
+ iconName: "content_copy"
+ backgroundColor: Theme.primary
+ textColor: Theme.primaryText
+ buttonHeight: 32
+ horizontalPadding: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ onClicked: {
+ Quickshell.execDetached(["dms", "cl", "copy", SessionData.getCurrentSessionJson()]);
+ ToastService.showInfo(I18n.tr("Copied to clipboard"));
+ }
+ }
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: parent.height - 48 - readOnlyBanner.height
+ clip: true
+
+ SettingsSidebar {
+ id: sidebar
+
+ anchors.left: parent.left
+ width: settingsModal.isCompactMode ? parent.width : sidebar.implicitWidth
+ visible: settingsModal.isCompactMode ? settingsModal.menuVisible : true
+ parentModal: settingsModal
+ currentIndex: settingsModal.currentTabIndex
+ onTabChangeRequested: tabIndex => {
+ settingsModal.currentTabIndex = tabIndex;
+ if (settingsModal.isCompactMode) {
+ settingsModal.enableAnimations = true;
+ settingsModal.menuVisible = false;
+ }
+ }
+ }
+
+ Item {
+ anchors.left: settingsModal.isCompactMode ? (settingsModal.menuVisible ? sidebar.right : parent.left) : sidebar.right
+ anchors.right: parent.right
+ height: parent.height
+ clip: true
+
+ SettingsContent {
+ id: content
+
+ anchors.fill: parent
+ parentModal: settingsModal
+ currentIndex: settingsModal.currentTabIndex
+ }
+ }
+ }
+ }
+ }
+
+ FloatingWindowControls {
+ id: windowControls
+ targetWindow: settingsModal
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsSidebar.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsSidebar.qml
new file mode 100644
index 0000000..092881c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/Settings/SettingsSidebar.qml
@@ -0,0 +1,1023 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Modals.Settings
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property int currentIndex: 0
+ property var parentModal: null
+
+ signal tabChangeRequested(int tabIndex)
+ property string _expandedIds: ","
+ property string _collapsedIds: ","
+ property string _autoExpandedIds: ","
+ property bool searchActive: searchField.text.length > 0
+ property int searchSelectedIndex: 0
+ property int keyboardHighlightIndex: -1
+
+ function focusSearch() {
+ searchField.forceActiveFocus();
+ }
+
+ function highlightNext() {
+ var flatItems = getFlatNavigableItems();
+ if (flatItems.length === 0)
+ return;
+ var currentPos = flatItems.findIndex(item => item.tabIndex === keyboardHighlightIndex);
+ if (currentPos === -1) {
+ currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex);
+ }
+ var nextPos = (currentPos + 1) % flatItems.length;
+ keyboardHighlightIndex = flatItems[nextPos].tabIndex;
+ autoExpandForTab(keyboardHighlightIndex);
+ }
+
+ function highlightPrevious() {
+ var flatItems = getFlatNavigableItems();
+ if (flatItems.length === 0)
+ return;
+ var currentPos = flatItems.findIndex(item => item.tabIndex === keyboardHighlightIndex);
+ if (currentPos === -1) {
+ currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex);
+ }
+ var prevPos = (currentPos - 1 + flatItems.length) % flatItems.length;
+ keyboardHighlightIndex = flatItems[prevPos].tabIndex;
+ autoExpandForTab(keyboardHighlightIndex);
+ }
+
+ function selectHighlighted() {
+ if (keyboardHighlightIndex < 0)
+ return;
+ var oldIndex = currentIndex;
+ var newIndex = keyboardHighlightIndex;
+ tabChangeRequested(newIndex);
+ autoCollapseIfNeeded(oldIndex, newIndex);
+ keyboardHighlightIndex = -1;
+ Qt.callLater(searchField.forceActiveFocus);
+ }
+
+ readonly property var categoryStructure: [
+ {
+ "id": "personalization",
+ "text": I18n.tr("Personalization"),
+ "icon": "palette",
+ "children": [
+ {
+ "id": "wallpaper",
+ "text": I18n.tr("Wallpaper"),
+ "icon": "wallpaper",
+ "tabIndex": 0
+ },
+ {
+ "id": "theme",
+ "text": I18n.tr("Theme & Colors"),
+ "icon": "format_paint",
+ "tabIndex": 10
+ },
+ {
+ "id": "typography",
+ "text": I18n.tr("Typography & Motion"),
+ "icon": "text_fields",
+ "tabIndex": 14
+ },
+ {
+ "id": "time_weather",
+ "text": I18n.tr("Time & Weather"),
+ "icon": "schedule",
+ "tabIndex": 1
+ },
+ {
+ "id": "sounds",
+ "text": I18n.tr("Sounds"),
+ "icon": "volume_up",
+ "tabIndex": 15,
+ "soundsOnly": true
+ }
+ ]
+ },
+ {
+ "id": "dankbar",
+ "text": I18n.tr("Dank Bar"),
+ "icon": "toolbar",
+ "children": [
+ {
+ "id": "dankbar_settings",
+ "text": I18n.tr("Settings"),
+ "icon": "tune",
+ "tabIndex": 3
+ },
+ {
+ "id": "dankbar_widgets",
+ "text": I18n.tr("Widgets"),
+ "icon": "widgets",
+ "tabIndex": 22
+ }
+ ]
+ },
+ {
+ "id": "workspaces_widgets",
+ "text": I18n.tr("Workspaces & Widgets"),
+ "icon": "dashboard",
+ "collapsedByDefault": true,
+ "children": [
+ {
+ "id": "workspaces",
+ "text": I18n.tr("Workspaces"),
+ "icon": "view_module",
+ "tabIndex": 4
+ },
+ {
+ "id": "media_player",
+ "text": I18n.tr("Media Player"),
+ "icon": "music_note",
+ "tabIndex": 16
+ },
+ {
+ "id": "notifications",
+ "text": I18n.tr("Notifications"),
+ "icon": "notifications",
+ "tabIndex": 17
+ },
+ {
+ "id": "osd",
+ "text": I18n.tr("On-screen Displays"),
+ "icon": "tune",
+ "tabIndex": 18
+ },
+ {
+ "id": "running_apps",
+ "text": I18n.tr("Running Apps"),
+ "icon": "app_registration",
+ "tabIndex": 19,
+ "hyprlandNiriOnly": true
+ },
+ {
+ "id": "updater",
+ "text": I18n.tr("System Updater"),
+ "icon": "refresh",
+ "tabIndex": 20
+ },
+ {
+ "id": "desktop_widgets",
+ "text": I18n.tr("Desktop Widgets"),
+ "icon": "widgets",
+ "tabIndex": 27
+ }
+ ]
+ },
+ {
+ "id": "dock_launcher",
+ "text": I18n.tr("Dock & Launcher"),
+ "icon": "apps",
+ "collapsedByDefault": true,
+ "children": [
+ {
+ "id": "dock",
+ "text": I18n.tr("Dock"),
+ "icon": "dock_to_bottom",
+ "tabIndex": 5
+ },
+ {
+ "id": "launcher",
+ "text": I18n.tr("Launcher"),
+ "icon": "grid_view",
+ "tabIndex": 9
+ }
+ ]
+ },
+ {
+ "id": "keybinds",
+ "text": I18n.tr("Keyboard Shortcuts"),
+ "icon": "keyboard",
+ "tabIndex": 2,
+ "shortcutsOnly": true
+ },
+ {
+ "id": "displays",
+ "text": I18n.tr("Displays"),
+ "icon": "monitor",
+ "collapsedByDefault": true,
+ "children": [
+ {
+ "id": "display_config",
+ "text": I18n.tr("Configuration"),
+ "icon": "display_settings",
+ "tabIndex": 24
+ },
+ {
+ "id": "display_gamma",
+ "text": I18n.tr("Gamma Control"),
+ "icon": "brightness_6",
+ "tabIndex": 25
+ },
+ {
+ "id": "display_widgets",
+ "text": I18n.tr("Widgets", "settings_displays"),
+ "icon": "widgets",
+ "tabIndex": 26
+ }
+ ]
+ },
+ {
+ "id": "network",
+ "text": I18n.tr("Network"),
+ "icon": "wifi",
+ "tabIndex": 7,
+ "dmsOnly": true
+ },
+ {
+ "id": "system",
+ "text": I18n.tr("System"),
+ "icon": "memory",
+ "collapsedByDefault": true,
+ "children": [
+ {
+ "id": "audio",
+ "text": I18n.tr("Audio"),
+ "icon": "headphones",
+ "tabIndex": 29
+ },
+ {
+ "id": "locale",
+ "text": I18n.tr("Locale"),
+ "icon": "language",
+ "tabIndex": 30
+ },
+ {
+ "id": "clipboard",
+ "text": I18n.tr("Clipboard"),
+ "icon": "content_paste",
+ "tabIndex": 23,
+ "clipboardOnly": true
+ },
+ {
+ "id": "printers",
+ "text": I18n.tr("Printers"),
+ "icon": "print",
+ "tabIndex": 8,
+ "cupsOnly": true
+ },
+ {
+ "id": "multiplexers",
+ "text": I18n.tr("Multiplexers"),
+ "icon": "terminal",
+ "tabIndex": 32
+ },
+ {
+ "id": "window_rules",
+ "text": I18n.tr("Window Rules"),
+ "icon": "select_window",
+ "tabIndex": 28,
+ "niriOnly": true
+ }
+ ]
+ },
+ {
+ "id": "power_security",
+ "text": I18n.tr("Power & Security"),
+ "icon": "security",
+ "collapsedByDefault": true,
+ "children": [
+ {
+ "id": "lock_screen",
+ "text": I18n.tr("Lock Screen"),
+ "icon": "lock",
+ "tabIndex": 11
+ },
+ {
+ "id": "greeter",
+ "text": I18n.tr("Greeter"),
+ "icon": "login",
+ "tabIndex": 31
+ },
+ {
+ "id": "power_sleep",
+ "text": I18n.tr("Power & Sleep"),
+ "icon": "power_settings_new",
+ "tabIndex": 21
+ }
+ ]
+ },
+ {
+ "id": "plugins",
+ "text": I18n.tr("Plugins"),
+ "icon": "extension",
+ "tabIndex": 12
+ },
+ {
+ "id": "separator",
+ "separator": true
+ },
+ {
+ "id": "about",
+ "text": I18n.tr("About"),
+ "icon": "info",
+ "tabIndex": 13
+ }
+ ]
+
+ function isItemVisible(item) {
+ if (item.dmsOnly && NetworkService.usingLegacy)
+ return false;
+ if (item.cupsOnly && !CupsService.cupsAvailable)
+ return false;
+ if (item.shortcutsOnly && !KeybindsService.available)
+ return false;
+ if (item.soundsOnly && !AudioService.soundsAvailable)
+ return false;
+ if (item.hyprlandNiriOnly && !CompositorService.isNiri && !CompositorService.isHyprland)
+ return false;
+ if (item.niriOnly && !CompositorService.isNiri)
+ return false;
+ if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
+ return false;
+ return true;
+ }
+
+ function hasVisibleChildren(category) {
+ if (!category.children)
+ return false;
+ return category.children.some(child => isItemVisible(child));
+ }
+
+ function isCategoryVisible(category) {
+ if (category.separator)
+ return true;
+ if (!isItemVisible(category))
+ return false;
+ if (category.children && !hasVisibleChildren(category))
+ return false;
+ return true;
+ }
+
+ function _setExpanded(id, expanded) {
+ var marker = "," + id + ",";
+ if (expanded) {
+ if (_expandedIds.indexOf(marker) < 0)
+ _expandedIds = _expandedIds + id + ",";
+ _collapsedIds = _collapsedIds.replace(marker, ",");
+ } else {
+ _expandedIds = _expandedIds.replace(marker, ",");
+ if (_collapsedIds.indexOf(marker) < 0)
+ _collapsedIds = _collapsedIds + id + ",";
+ }
+ SessionData.setSettingsSidebarState(_expandedIds, _collapsedIds);
+ }
+
+ function _setAutoExpanded(id, value) {
+ var marker = "," + id + ",";
+ if (value) {
+ if (_autoExpandedIds.indexOf(marker) < 0)
+ _autoExpandedIds = _autoExpandedIds + id + ",";
+ } else {
+ _autoExpandedIds = _autoExpandedIds.replace(marker, ",");
+ }
+ }
+
+ function _isAutoExpanded(id) {
+ return _autoExpandedIds.indexOf("," + id + ",") >= 0;
+ }
+
+ function toggleCategory(categoryId) {
+ _setExpanded(categoryId, !isCategoryExpanded(categoryId));
+ _setAutoExpanded(categoryId, false);
+ }
+
+ function isCategoryExpanded(categoryId) {
+ if (_collapsedIds.indexOf("," + categoryId + ",") >= 0)
+ return false;
+ if (_expandedIds.indexOf("," + categoryId + ",") >= 0)
+ return true;
+ var category = categoryStructure.find(cat => cat.id === categoryId);
+ if (category && category.collapsedByDefault)
+ return false;
+ return true;
+ }
+
+ function isChildActive(category) {
+ if (!category.children)
+ return false;
+ return category.children.some(child => child.tabIndex === currentIndex);
+ }
+
+ function findParentCategory(tabIndex) {
+ for (var i = 0; i < categoryStructure.length; i++) {
+ var cat = categoryStructure[i];
+ if (cat.children) {
+ for (var j = 0; j < cat.children.length; j++) {
+ if (cat.children[j].tabIndex === tabIndex) {
+ return cat;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ function autoExpandForTab(tabIndex) {
+ var parent = findParentCategory(tabIndex);
+ if (!parent)
+ return;
+
+ if (!isCategoryExpanded(parent.id)) {
+ _setExpanded(parent.id, true);
+ _setAutoExpanded(parent.id, true);
+ }
+ }
+
+ function autoCollapseIfNeeded(oldTabIndex, newTabIndex) {
+ var oldParent = findParentCategory(oldTabIndex);
+ var newParent = findParentCategory(newTabIndex);
+
+ if (oldParent && oldParent !== newParent && _isAutoExpanded(oldParent.id)) {
+ _setExpanded(oldParent.id, false);
+ _setAutoExpanded(oldParent.id, false);
+ }
+ }
+
+ function navigateNext() {
+ var flatItems = getFlatNavigableItems();
+ var currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex);
+ var oldIndex = currentIndex;
+ var newIndex;
+ if (currentPos === -1) {
+ newIndex = flatItems[0]?.tabIndex ?? 0;
+ } else {
+ var nextPos = (currentPos + 1) % flatItems.length;
+ newIndex = flatItems[nextPos].tabIndex;
+ }
+ tabChangeRequested(newIndex);
+ autoCollapseIfNeeded(oldIndex, newIndex);
+ autoExpandForTab(newIndex);
+ }
+
+ function navigatePrevious() {
+ var flatItems = getFlatNavigableItems();
+ var currentPos = flatItems.findIndex(item => item.tabIndex === currentIndex);
+ var oldIndex = currentIndex;
+ var newIndex;
+ if (currentPos === -1) {
+ newIndex = flatItems[0]?.tabIndex ?? 0;
+ } else {
+ var prevPos = (currentPos - 1 + flatItems.length) % flatItems.length;
+ newIndex = flatItems[prevPos].tabIndex;
+ }
+ tabChangeRequested(newIndex);
+ autoCollapseIfNeeded(oldIndex, newIndex);
+ autoExpandForTab(newIndex);
+ }
+
+ function getFlatNavigableItems() {
+ var items = [];
+ for (var i = 0; i < categoryStructure.length; i++) {
+ var cat = categoryStructure[i];
+ if (cat.separator || !isCategoryVisible(cat))
+ continue;
+
+ if (cat.tabIndex !== undefined && !cat.children) {
+ items.push(cat);
+ }
+
+ if (cat.children) {
+ for (var j = 0; j < cat.children.length; j++) {
+ var child = cat.children[j];
+ if (isItemVisible(child)) {
+ items.push(child);
+ }
+ }
+ }
+ }
+ return items;
+ }
+
+ function resolveTabIndex(name: string): int {
+ if (!name)
+ return -1;
+
+ var normalized = name.toLowerCase().replace(/[_\-\s]/g, "");
+
+ for (var i = 0; i < categoryStructure.length; i++) {
+ var cat = categoryStructure[i];
+ if (cat.separator)
+ continue;
+
+ var catId = (cat.id || "").toLowerCase().replace(/[_\-\s]/g, "");
+ if (catId === normalized) {
+ if (cat.tabIndex !== undefined)
+ return cat.tabIndex;
+ if (cat.children && cat.children.length > 0)
+ return cat.children[0].tabIndex;
+ }
+
+ if (cat.children) {
+ for (var j = 0; j < cat.children.length; j++) {
+ var child = cat.children[j];
+ var childId = (child.id || "").toLowerCase().replace(/[_\-\s]/g, "");
+ if (childId === normalized)
+ return child.tabIndex;
+ }
+ }
+ }
+ return -1;
+ }
+
+ property real __maxTextWidth: Math.max(__m1.advanceWidth, __m2.advanceWidth, __m3.advanceWidth, __m4.advanceWidth, __m5.advanceWidth, __m6.advanceWidth)
+ property real __calculatedWidth: Math.max(270, __maxTextWidth + Theme.iconSize * 2 + Theme.spacingM * 4 + Theme.spacingS * 2)
+
+ implicitWidth: __calculatedWidth
+ width: __calculatedWidth
+ height: parent.height
+ color: Theme.surfaceContainer
+ radius: Theme.cornerRadius
+
+ Component.onCompleted: {
+ root._expandedIds = SessionData.settingsSidebarExpandedIds;
+ root._collapsedIds = SessionData.settingsSidebarCollapsedIds;
+ }
+
+ StyledTextMetrics {
+ id: __m1
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ text: I18n.tr("Workspaces & Widgets")
+ }
+ StyledTextMetrics {
+ id: __m2
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ text: I18n.tr("Typography & Motion")
+ }
+ StyledTextMetrics {
+ id: __m3
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ text: I18n.tr("Keyboard Shortcuts")
+ }
+ StyledTextMetrics {
+ id: __m4
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ text: I18n.tr("Power & Security")
+ }
+ StyledTextMetrics {
+ id: __m5
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ text: I18n.tr("Dock & Launcher")
+ }
+ StyledTextMetrics {
+ id: __m6
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ text: I18n.tr("Personalization")
+ }
+
+ function selectSearchResult(result) {
+ if (!result)
+ return;
+ if (result.section) {
+ SettingsSearchService.navigateToSection(result.section);
+ }
+ var oldIndex = root.currentIndex;
+ tabChangeRequested(result.tabIndex);
+ autoCollapseIfNeeded(oldIndex, result.tabIndex);
+ autoExpandForTab(result.tabIndex);
+ searchField.text = "";
+ SettingsSearchService.clear();
+ searchSelectedIndex = 0;
+ keyboardHighlightIndex = -1;
+ Qt.callLater(searchField.forceActiveFocus);
+ }
+
+ function navigateSearchResults(delta) {
+ if (SettingsSearchService.results.length === 0)
+ return;
+ searchSelectedIndex = Math.max(0, Math.min(searchSelectedIndex + delta, SettingsSearchService.results.length - 1));
+ }
+
+ DankFlickable {
+ anchors.fill: parent
+ clip: true
+ contentHeight: sidebarColumn.height
+
+ Column {
+ id: sidebarColumn
+ width: parent.width
+ leftPadding: Theme.spacingS
+ rightPadding: Theme.spacingS
+ bottomPadding: Theme.spacingL
+ topPadding: Theme.spacingM + 2
+ spacing: 2
+
+ ProfileSection {
+ width: parent.width - parent.leftPadding - parent.rightPadding
+ parentModal: root.parentModal
+ }
+
+ Rectangle {
+ width: parent.width - parent.leftPadding - parent.rightPadding
+ height: 1
+ color: Theme.outline
+ opacity: 0.2
+ }
+
+ Item {
+ width: parent.width - parent.leftPadding - parent.rightPadding
+ height: Theme.spacingXS
+ }
+
+ DankTextField {
+ id: searchField
+ width: parent.width - parent.leftPadding - parent.rightPadding
+ placeholderText: I18n.tr("Search...")
+ normalBorderColor: Theme.outlineMedium
+ focusedBorderColor: Theme.primary
+ leftIconName: "search"
+ leftIconSize: Theme.iconSize - 4
+ showClearButton: text.length > 0
+ usePopupTransparency: false
+ onTextChanged: {
+ SettingsSearchService.search(text);
+ root.searchSelectedIndex = 0;
+ }
+ keyForwardTargets: [keyHandler]
+
+ Item {
+ id: keyHandler
+ function navNext() {
+ if (root.searchActive) {
+ root.navigateSearchResults(1);
+ } else {
+ root.highlightNext();
+ }
+ }
+ function navPrev() {
+ if (root.searchActive) {
+ root.navigateSearchResults(-1);
+ } else {
+ root.highlightPrevious();
+ }
+ }
+ function navSelect() {
+ if (root.searchActive && SettingsSearchService.results.length > 0) {
+ root.selectSearchResult(SettingsSearchService.results[root.searchSelectedIndex]);
+ } else if (root.keyboardHighlightIndex >= 0) {
+ root.selectHighlighted();
+ }
+ }
+ Keys.onDownPressed: event => {
+ navNext();
+ event.accepted = true;
+ }
+ Keys.onUpPressed: event => {
+ navPrev();
+ event.accepted = true;
+ }
+ Keys.onTabPressed: event => {
+ navNext();
+ event.accepted = true;
+ }
+ Keys.onBacktabPressed: event => {
+ navPrev();
+ event.accepted = true;
+ }
+ Keys.onReturnPressed: event => {
+ navSelect();
+ event.accepted = true;
+ }
+ Keys.onEscapePressed: event => {
+ if (root.searchActive) {
+ searchField.text = "";
+ SettingsSearchService.clear();
+ } else {
+ root.keyboardHighlightIndex = -1;
+ }
+ event.accepted = true;
+ }
+ }
+ }
+
+ Column {
+ id: searchResultsColumn
+ width: parent.width - parent.leftPadding - parent.rightPadding
+ spacing: 2
+ visible: root.searchActive
+
+ Item {
+ width: parent.width
+ height: Theme.spacingS
+ }
+
+ Repeater {
+ model: ScriptModel {
+ values: SettingsSearchService.results
+ }
+
+ Rectangle {
+ id: resultDelegate
+ required property int index
+ required property var modelData
+
+ width: searchResultsColumn.width
+ height: resultContent.height + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: {
+ if (root.searchSelectedIndex === index)
+ return Theme.buttonBg;
+ if (resultMouseArea.containsMouse)
+ return Theme.surfaceHover;
+ return "transparent";
+ }
+
+ DankRipple {
+ id: resultRipple
+ rippleColor: root.searchSelectedIndex === resultDelegate.index ? Theme.buttonText : Theme.surfaceText
+ cornerRadius: resultDelegate.radius
+ }
+
+ Row {
+ id: resultContent
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: resultDelegate.modelData.icon || "settings"
+ size: Theme.iconSize - 2
+ color: root.searchSelectedIndex === resultDelegate.index ? Theme.buttonText : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ width: parent.width - Theme.iconSize - Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 2
+
+ StyledText {
+ text: resultDelegate.modelData.label
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: root.searchSelectedIndex === resultDelegate.index ? Theme.buttonText : Theme.surfaceText
+ width: parent.width
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: resultDelegate.modelData.category
+ font.pixelSize: Theme.fontSizeSmall - 1
+ color: root.searchSelectedIndex === resultDelegate.index ? Theme.withAlpha(Theme.buttonText, 0.7) : Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.Wrap
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+
+ MouseArea {
+ id: resultMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => resultRipple.trigger(mouse.x, mouse.y)
+ onClicked: root.selectSearchResult(resultDelegate.modelData)
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ StyledText {
+ width: parent.width
+ text: I18n.tr("No matches")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ horizontalAlignment: Text.AlignHCenter
+ visible: searchField.text.length > 0 && SettingsSearchService.results.length === 0
+ topPadding: Theme.spacingM
+ }
+ }
+
+ Item {
+ width: parent.width - parent.leftPadding - parent.rightPadding
+ height: Theme.spacingXS
+ visible: !root.searchActive
+ }
+
+ Repeater {
+ model: root.categoryStructure
+
+ delegate: Column {
+ id: categoryDelegate
+ required property int index
+ required property var modelData
+
+ width: parent.width - parent.leftPadding - parent.rightPadding
+ visible: !root.searchActive && root.isCategoryVisible(modelData)
+ spacing: 2
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.outline
+ opacity: 0.15
+ visible: categoryDelegate.modelData.separator === true
+ }
+
+ Item {
+ width: parent.width
+ height: Theme.spacingS
+ visible: categoryDelegate.modelData.separator === true
+ }
+
+ Rectangle {
+ id: categoryRow
+ width: parent.width
+ height: Math.max(Theme.iconSize, Theme.fontSizeMedium) + Theme.spacingS * 2
+ radius: Theme.cornerRadius
+ visible: categoryDelegate.modelData.separator !== true
+
+ readonly property bool hasTab: categoryDelegate.modelData.tabIndex !== undefined && !categoryDelegate.modelData.children
+ readonly property bool isActive: hasTab && root.currentIndex === categoryDelegate.modelData.tabIndex
+ readonly property bool isHighlighted: hasTab && root.keyboardHighlightIndex === categoryDelegate.modelData.tabIndex
+
+ color: {
+ if (isActive)
+ return Theme.buttonBg;
+ if (isHighlighted)
+ return Theme.buttonHover;
+ if (categoryMouseArea.containsMouse)
+ return Theme.surfaceHover;
+ return "transparent";
+ }
+
+ DankRipple {
+ id: categoryRipple
+ rippleColor: categoryRow.isActive ? Theme.buttonText : Theme.surfaceText
+ cornerRadius: categoryRow.radius
+ }
+
+ Row {
+ id: categoryRowContent
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: categoryDelegate.modelData.icon || ""
+ size: Theme.iconSize - 2
+ color: categoryRow.isActive ? Theme.buttonText : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: categoryDelegate.modelData.text || ""
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: (categoryRow.isActive || root.isChildActive(categoryDelegate.modelData)) ? Font.Medium : Font.Normal
+ color: categoryRow.isActive ? Theme.buttonText : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ DankIcon {
+ id: expandIcon
+ name: root.isCategoryExpanded(categoryDelegate.modelData.id) ? "expand_less" : "expand_more"
+ size: Theme.iconSize - 4
+ color: Theme.surfaceVariantText
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ visible: categoryDelegate.modelData.children !== undefined && categoryDelegate.modelData.children.length > 0
+ }
+
+ MouseArea {
+ id: categoryMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => categoryRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ root.keyboardHighlightIndex = -1;
+ if (categoryDelegate.modelData.children) {
+ root.toggleCategory(categoryDelegate.modelData.id);
+ } else if (categoryDelegate.modelData.tabIndex !== undefined) {
+ root.tabChangeRequested(categoryDelegate.modelData.tabIndex);
+ }
+ Qt.callLater(searchField.forceActiveFocus);
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Column {
+ id: childrenColumn
+ width: parent.width
+ spacing: 2
+ visible: categoryDelegate.modelData.children !== undefined && root.isCategoryExpanded(categoryDelegate.modelData.id)
+ clip: true
+
+ Repeater {
+ model: categoryDelegate.modelData.children || []
+
+ delegate: Rectangle {
+ id: childDelegate
+ required property int index
+ required property var modelData
+
+ readonly property bool isActive: root.currentIndex === modelData.tabIndex
+ readonly property bool isHighlighted: root.keyboardHighlightIndex === modelData.tabIndex
+
+ width: childrenColumn.width
+ height: Math.max(Theme.iconSize - 4, Theme.fontSizeSmall + 1) + Theme.spacingS * 2
+ radius: Theme.cornerRadius
+ visible: root.isItemVisible(modelData)
+ color: {
+ if (isActive)
+ return Theme.buttonBg;
+ if (isHighlighted)
+ return Theme.buttonHover;
+ if (childMouseArea.containsMouse)
+ return Theme.surfaceHover;
+ return "transparent";
+ }
+
+ DankRipple {
+ id: childRipple
+ rippleColor: childDelegate.isActive ? Theme.buttonText : Theme.surfaceText
+ cornerRadius: childDelegate.radius
+ }
+
+ Row {
+ id: childRowContent
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingL + Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: childDelegate.modelData.icon || ""
+ size: Theme.iconSize - 4
+ color: childDelegate.isActive ? Theme.buttonText : Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: childDelegate.modelData.text || ""
+ font.pixelSize: Theme.fontSizeSmall + 1
+ font.weight: childDelegate.isActive ? Font.Medium : Font.Normal
+ color: childDelegate.isActive ? Theme.buttonText : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: childMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => childRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ root.keyboardHighlightIndex = -1;
+ root.tabChangeRequested(childDelegate.modelData.tabIndex);
+ Qt.callLater(searchField.forceActiveFocus);
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WifiPasswordModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WifiPasswordModal.qml
new file mode 100644
index 0000000..92689f2
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WifiPasswordModal.qml
@@ -0,0 +1,764 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+FloatingWindow {
+ id: root
+
+ property bool disablePopupTransparency: true
+ property string wifiPasswordSSID: ""
+ property string wifiPasswordInput: ""
+ property string wifiUsernameInput: ""
+ property bool requiresEnterprise: false
+ property bool isHiddenNetwork: false
+
+ property string wifiAnonymousIdentityInput: ""
+ property string wifiDomainInput: ""
+
+ property bool isPromptMode: false
+ property string promptToken: ""
+ property string promptReason: ""
+ property var promptFields: []
+ property string promptSetting: ""
+
+ property bool isVpnPrompt: false
+ property string connectionName: ""
+ property string vpnServiceType: ""
+ property string connectionType: ""
+ property var fieldsInfo: []
+ property var secretValues: ({})
+
+ readonly property bool showUsernameField: requiresEnterprise && !isVpnPrompt && fieldsInfo.length === 0
+ readonly property bool showPasswordField: fieldsInfo.length === 0
+ readonly property bool showAnonField: requiresEnterprise && !isVpnPrompt
+ readonly property bool showDomainField: requiresEnterprise && !isVpnPrompt
+ readonly property bool showSavePasswordCheckbox: (isVpnPrompt || fieldsInfo.length > 0) && promptReason !== "pkcs11"
+
+ readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
+ readonly property int inputFieldWithSpacing: inputFieldHeight + Theme.spacingM
+ readonly property int checkboxRowHeight: Theme.fontSizeMedium + Theme.spacingS
+ readonly property int headerHeight: Theme.fontSizeLarge + Theme.fontSizeMedium + Theme.spacingM * 2
+ readonly property int buttonRowHeight: 36 + Theme.spacingM
+
+ property int calculatedHeight: {
+ let h = headerHeight + buttonRowHeight + Theme.spacingL * 2;
+ h += fieldsInfo.length * inputFieldWithSpacing;
+ if (isHiddenNetwork)
+ h += inputFieldWithSpacing;
+ if (showUsernameField)
+ h += inputFieldWithSpacing;
+ if (showPasswordField)
+ h += inputFieldWithSpacing;
+ if (showAnonField)
+ h += inputFieldWithSpacing;
+ if (showDomainField)
+ h += inputFieldWithSpacing;
+ if (showSavePasswordCheckbox)
+ h += checkboxRowHeight;
+ return h;
+ }
+
+ function focusFirstField() {
+ if (fieldsInfo.length > 0) {
+ if (dynamicFieldsRepeater.count > 0) {
+ const firstItem = dynamicFieldsRepeater.itemAt(0);
+ if (firstItem)
+ firstItem.children[0].forceActiveFocus();
+ }
+ return;
+ }
+ if (isHiddenNetwork) {
+ ssidInput.forceActiveFocus();
+ return;
+ }
+ if (requiresEnterprise && !isVpnPrompt) {
+ usernameInput.forceActiveFocus();
+ return;
+ }
+ passwordInput.forceActiveFocus();
+ }
+
+ function show(ssid) {
+ wifiPasswordSSID = ssid;
+ wifiPasswordInput = "";
+ wifiUsernameInput = "";
+ wifiAnonymousIdentityInput = "";
+ wifiDomainInput = "";
+ isPromptMode = false;
+ isHiddenNetwork = false;
+ promptToken = "";
+ promptReason = "";
+ promptFields = [];
+ promptSetting = "";
+ isVpnPrompt = false;
+ connectionName = "";
+ vpnServiceType = "";
+ connectionType = "";
+ fieldsInfo = [];
+ secretValues = {};
+
+ const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid);
+ requiresEnterprise = network?.enterprise || false;
+
+ visible = true;
+ Qt.callLater(focusFirstField);
+ }
+
+ function showHidden() {
+ wifiPasswordSSID = "";
+ wifiPasswordInput = "";
+ wifiUsernameInput = "";
+ wifiAnonymousIdentityInput = "";
+ wifiDomainInput = "";
+ isPromptMode = false;
+ isHiddenNetwork = true;
+ promptToken = "";
+ promptReason = "";
+ promptFields = [];
+ promptSetting = "";
+ isVpnPrompt = false;
+ connectionName = "";
+ vpnServiceType = "";
+ connectionType = "";
+ fieldsInfo = [];
+ secretValues = {};
+ requiresEnterprise = false;
+
+ visible = true;
+ Qt.callLater(focusFirstField);
+ }
+
+ function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService, fInfo) {
+ isPromptMode = true;
+ promptToken = token;
+ promptReason = reason;
+ promptFields = fields || [];
+ promptSetting = setting || "802-11-wireless-security";
+ connectionType = connType || "802-11-wireless";
+ connectionName = connName || ssid || "";
+ vpnServiceType = vpnService || "";
+ fieldsInfo = fInfo || [];
+ secretValues = {};
+
+ isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard");
+ wifiPasswordSSID = isVpnPrompt ? connectionName : ssid;
+
+ requiresEnterprise = setting === "802-1x";
+
+ wifiPasswordInput = "";
+ wifiUsernameInput = "";
+ wifiAnonymousIdentityInput = "";
+ wifiDomainInput = "";
+
+ visible = true;
+ Qt.callLater(() => {
+ if (reason === "wrong-password" && fieldsInfo.length === 0) {
+ passwordInput.text = "";
+ }
+ focusFirstField();
+ });
+ }
+
+ function hide() {
+ visible = false;
+ }
+
+ function getFieldLabel(fieldName) {
+ switch (fieldName) {
+ case "username":
+ case "identity":
+ return I18n.tr("Username");
+ case "password":
+ return I18n.tr("Password");
+ case "cert-pass":
+ case "certpass":
+ return I18n.tr("Certificate Password");
+ case "private-key-password":
+ return I18n.tr("Private Key Password");
+ case "pin":
+ case "key_pass":
+ return I18n.tr("PIN");
+ case "psk":
+ return I18n.tr("Password");
+ case "anonymous-identity":
+ return I18n.tr("Anonymous Identity");
+ default:
+ return fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/-/g, " ");
+ }
+ }
+
+ function submitCredentialsAndClose() {
+ if (fieldsInfo.length > 0) {
+ NetworkService.submitCredentials(promptToken, secretValues, savePasswordCheckbox.checked);
+ hide();
+ secretValues = {};
+ return;
+ }
+
+ if (isPromptMode) {
+ const secrets = {};
+ if (isVpnPrompt) {
+ if (passwordInput.text)
+ secrets["password"] = passwordInput.text;
+ } else if (promptSetting === "802-11-wireless-security") {
+ secrets["psk"] = passwordInput.text;
+ } else if (promptSetting === "802-1x") {
+ if (usernameInput.text)
+ secrets["identity"] = usernameInput.text;
+ if (passwordInput.text)
+ secrets["password"] = passwordInput.text;
+ if (wifiAnonymousIdentityInput)
+ secrets["anonymous-identity"] = wifiAnonymousIdentityInput;
+ }
+ NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
+ } else {
+ const ssid = isHiddenNetwork ? ssidInput.text : wifiPasswordSSID;
+ const username = requiresEnterprise ? usernameInput.text : "";
+ NetworkService.connectToWifi(ssid, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput, isHiddenNetwork);
+ }
+
+ hide();
+ wifiPasswordInput = "";
+ wifiUsernameInput = "";
+ wifiAnonymousIdentityInput = "";
+ wifiDomainInput = "";
+ passwordInput.text = "";
+ if (requiresEnterprise)
+ usernameInput.text = "";
+ if (isHiddenNetwork)
+ ssidInput.text = "";
+ }
+
+ function clearAndClose() {
+ if (isPromptMode)
+ NetworkService.cancelCredentials(promptToken);
+ hide();
+ wifiPasswordInput = "";
+ wifiUsernameInput = "";
+ wifiAnonymousIdentityInput = "";
+ wifiDomainInput = "";
+ secretValues = {};
+ }
+
+ objectName: "wifiPasswordModal"
+ title: {
+ if (promptReason === "pkcs11")
+ return I18n.tr("Smartcard PIN");
+ if (isVpnPrompt)
+ return I18n.tr("VPN Password");
+ if (isHiddenNetwork)
+ return I18n.tr("Hidden Network");
+ return I18n.tr("Wi-Fi Password");
+ }
+ minimumSize: Qt.size(420, calculatedHeight)
+ maximumSize: Qt.size(420, calculatedHeight)
+ color: Theme.surfaceContainer
+ visible: false
+
+ onVisibleChanged: {
+ if (visible) {
+ Qt.callLater(focusFirstField);
+ return;
+ }
+ wifiPasswordInput = "";
+ wifiUsernameInput = "";
+ wifiAnonymousIdentityInput = "";
+ wifiDomainInput = "";
+ secretValues = {};
+ passwordInput.text = "";
+ usernameInput.text = "";
+ anonInput.text = "";
+ domainMatchInput.text = "";
+ ssidInput.text = "";
+ for (var i = 0; i < dynamicFieldsRepeater.count; i++) {
+ const item = dynamicFieldsRepeater.itemAt(i);
+ if (item?.children[0])
+ item.children[0].text = "";
+ }
+ }
+
+ Connections {
+ target: NetworkService
+
+ function onPasswordDialogShouldReopenChanged() {
+ if (!NetworkService.passwordDialogShouldReopen || NetworkService.connectingSSID === "")
+ return;
+ wifiPasswordSSID = NetworkService.connectingSSID;
+ wifiPasswordInput = "";
+ visible = true;
+ NetworkService.passwordDialogShouldReopen = false;
+ }
+ }
+
+ FocusScope {
+ id: contentFocusScope
+
+ anchors.fill: parent
+ focus: true
+
+ Keys.onEscapePressed: event => {
+ clearAndClose();
+ event.accepted = true;
+ }
+
+ Column {
+ id: contentCol
+ anchors.centerIn: parent
+ width: parent.width - Theme.spacingL * 2
+ spacing: Theme.spacingM
+
+ Item {
+ width: contentCol.width
+ height: Math.max(headerCol.height, buttonRow.height)
+
+ MouseArea {
+ anchors.left: parent.left
+ anchors.right: buttonRow.left
+ anchors.rightMargin: Theme.spacingM
+ height: headerCol.height
+ onPressed: windowControls.tryStartMove()
+ onDoubleClicked: windowControls.tryToggleMaximize()
+
+ Column {
+ id: headerCol
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: {
+ if (promptReason === "pkcs11")
+ return I18n.tr("Smartcard Authentication");
+ if (isVpnPrompt)
+ return I18n.tr("Connect to VPN");
+ if (isHiddenNetwork)
+ return I18n.tr("Connect to Hidden Network");
+ return I18n.tr("Connect to Wi-Fi");
+ }
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ Column {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: {
+ if (promptReason === "pkcs11")
+ return I18n.tr("Enter PIN for ") + wifiPasswordSSID;
+ if (fieldsInfo.length > 0)
+ return I18n.tr("Enter credentials for ") + wifiPasswordSSID;
+ if (isVpnPrompt)
+ return I18n.tr("Enter password for ") + wifiPasswordSSID;
+ if (isHiddenNetwork)
+ return I18n.tr("Enter network name and password");
+ const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ");
+ return prefix + wifiPasswordSSID;
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ elide: Text.ElideRight
+ }
+
+ StyledText {
+ visible: isPromptMode && promptReason === "wrong-password"
+ text: I18n.tr("Incorrect password")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.error
+ width: parent.width
+ }
+ }
+ }
+ }
+
+ Row {
+ id: buttonRow
+ anchors.right: parent.right
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ visible: windowControls.supported && windowControls.canMaximize
+ iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: windowControls.tryToggleMaximize()
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: clearAndClose()
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: inputFieldHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: ssidInput.activeFocus ? Theme.primary : Theme.outlineStrong
+ border.width: ssidInput.activeFocus ? 2 : 1
+ visible: isHiddenNetwork
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: ssidInput.forceActiveFocus()
+ }
+
+ DankTextField {
+ id: ssidInput
+
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ placeholderText: I18n.tr("Network Name (SSID)")
+ backgroundColor: "transparent"
+ enabled: root.visible
+ keyNavigationTab: passwordInput
+ onAccepted: passwordInput.forceActiveFocus()
+ }
+ }
+
+ Repeater {
+ id: dynamicFieldsRepeater
+ model: fieldsInfo
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ width: contentCol.width
+ height: inputFieldHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: fieldInput.activeFocus ? Theme.primary : Theme.outlineStrong
+ border.width: fieldInput.activeFocus ? 2 : 1
+
+ DankTextField {
+ id: fieldInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ showPasswordToggle: modelData.isSecret
+ echoMode: modelData.isSecret && !passwordVisible ? TextInput.Password : TextInput.Normal
+ placeholderText: getFieldLabel(modelData.name)
+ backgroundColor: "transparent"
+ enabled: root.visible
+
+ Keys.onTabPressed: event => {
+ if (index < fieldsInfo.length - 1) {
+ const nextItem = dynamicFieldsRepeater.itemAt(index + 1);
+ if (nextItem)
+ nextItem.children[0].forceActiveFocus();
+ } else {
+ const firstItem = dynamicFieldsRepeater.itemAt(0);
+ if (firstItem)
+ firstItem.children[0].forceActiveFocus();
+ }
+ event.accepted = true;
+ }
+
+ Keys.onBacktabPressed: event => {
+ if (index > 0) {
+ const prevItem = dynamicFieldsRepeater.itemAt(index - 1);
+ if (prevItem)
+ prevItem.children[0].forceActiveFocus();
+ } else {
+ const lastItem = dynamicFieldsRepeater.itemAt(fieldsInfo.length - 1);
+ if (lastItem)
+ lastItem.children[0].forceActiveFocus();
+ }
+ event.accepted = true;
+ }
+
+ onTextEdited: {
+ let updated = Object.assign({}, root.secretValues);
+ updated[modelData.name] = text;
+ root.secretValues = updated;
+ }
+
+ onAccepted: {
+ if (index < fieldsInfo.length - 1) {
+ const nextItem = dynamicFieldsRepeater.itemAt(index + 1);
+ if (nextItem)
+ nextItem.children[0].forceActiveFocus();
+ return;
+ }
+ submitCredentialsAndClose();
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: inputFieldHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: usernameInput.activeFocus ? Theme.primary : Theme.outlineStrong
+ border.width: usernameInput.activeFocus ? 2 : 1
+ visible: showUsernameField
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: usernameInput.forceActiveFocus()
+ }
+
+ DankTextField {
+ id: usernameInput
+
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ text: wifiUsernameInput
+ placeholderText: I18n.tr("Username")
+ backgroundColor: "transparent"
+ enabled: root.visible
+ keyNavigationTab: passwordInput
+ keyNavigationBacktab: domainMatchInput
+ onTextEdited: wifiUsernameInput = text
+ onAccepted: passwordInput.forceActiveFocus()
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: inputFieldHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: passwordInput.activeFocus ? Theme.primary : Theme.outlineStrong
+ border.width: passwordInput.activeFocus ? 2 : 1
+ visible: showPasswordField
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: passwordInput.forceActiveFocus()
+ }
+
+ DankTextField {
+ id: passwordInput
+
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ text: wifiPasswordInput
+ showPasswordToggle: true
+ echoMode: passwordVisible ? TextInput.Normal : TextInput.Password
+ placeholderText: (requiresEnterprise && !isVpnPrompt) ? I18n.tr("Password") : ""
+ backgroundColor: "transparent"
+ enabled: root.visible
+ keyNavigationTab: (requiresEnterprise && !isVpnPrompt) ? anonInput : null
+ keyNavigationBacktab: (requiresEnterprise && !isVpnPrompt) ? usernameInput : null
+ onTextEdited: wifiPasswordInput = text
+ onAccepted: {
+ if (requiresEnterprise && !isVpnPrompt) {
+ anonInput.forceActiveFocus();
+ return;
+ }
+ submitCredentialsAndClose();
+ }
+ }
+ }
+
+ Rectangle {
+ visible: showAnonField
+ width: parent.width
+ height: inputFieldHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: anonInput.activeFocus ? Theme.primary : Theme.outlineStrong
+ border.width: anonInput.activeFocus ? 2 : 1
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: anonInput.forceActiveFocus()
+ }
+
+ DankTextField {
+ id: anonInput
+
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ text: wifiAnonymousIdentityInput
+ placeholderText: I18n.tr("Anonymous Identity (optional)")
+ backgroundColor: "transparent"
+ enabled: root.visible
+ keyNavigationTab: domainMatchInput
+ keyNavigationBacktab: passwordInput
+ onTextEdited: wifiAnonymousIdentityInput = text
+ onAccepted: domainMatchInput.forceActiveFocus()
+ }
+ }
+
+ Rectangle {
+ visible: showDomainField
+ width: parent.width
+ height: inputFieldHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: domainMatchInput.activeFocus ? Theme.primary : Theme.outlineStrong
+ border.width: domainMatchInput.activeFocus ? 2 : 1
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: domainMatchInput.forceActiveFocus()
+ }
+
+ DankTextField {
+ id: domainMatchInput
+
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ text: wifiDomainInput
+ placeholderText: I18n.tr("Domain (optional)")
+ backgroundColor: "transparent"
+ enabled: root.visible
+ keyNavigationTab: usernameInput
+ keyNavigationBacktab: anonInput
+ onTextEdited: wifiDomainInput = text
+ onAccepted: submitCredentialsAndClose()
+ }
+ }
+
+ Row {
+ spacing: Theme.spacingS
+ visible: showSavePasswordCheckbox
+
+ Rectangle {
+ id: savePasswordCheckbox
+
+ property bool checked: !isVpnPrompt
+
+ width: 20
+ height: 20
+ radius: 4
+ color: checked ? Theme.primary : "transparent"
+ border.color: checked ? Theme.primary : Theme.outlineButton
+ border.width: 2
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "check"
+ size: 12
+ color: Theme.background
+ visible: parent.checked
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: savePasswordCheckbox.checked = !savePasswordCheckbox.checked
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Save password")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: 40
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
+ border.color: Theme.surfaceVariantAlpha
+ border.width: 1
+
+ StyledText {
+ id: cancelText
+ anchors.centerIn: parent
+ text: I18n.tr("Cancel")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: cancelArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: clearAndClose()
+ }
+ }
+
+ Rectangle {
+ width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
+ enabled: {
+ if (fieldsInfo.length > 0) {
+ for (var i = 0; i < fieldsInfo.length; i++) {
+ if (!fieldsInfo[i].isSecret)
+ continue;
+ const fieldName = fieldsInfo[i].name;
+ if (!secretValues[fieldName] || secretValues[fieldName].length === 0)
+ return false;
+ }
+ return true;
+ }
+ if (isVpnPrompt)
+ return passwordInput.text.length > 0;
+ if (isHiddenNetwork)
+ return ssidInput.text.length > 0;
+ return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0;
+ }
+ opacity: enabled ? 1 : 0.5
+
+ StyledText {
+ id: connectText
+ anchors.centerIn: parent
+ text: I18n.tr("Connect")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.background
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: connectArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: parent.enabled
+ onClicked: submitCredentialsAndClose()
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ FloatingWindowControls {
+ id: windowControls
+ targetWindow: root
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WifiQRCodeModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WifiQRCodeModal.qml
new file mode 100644
index 0000000..06ffab7
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WifiQRCodeModal.qml
@@ -0,0 +1,170 @@
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Io
+import qs.Modals.Common
+import qs.Modals.FileBrowser
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+DankModal {
+ id: root
+ visible: false
+ layerNamespace: "dms:wifi-qrcode"
+
+ property bool disablePopupTransparency: true
+ property string wifiSSID: ""
+ property string themedQrCodePath: ""
+ property string normalQrCodePath: ""
+ modalWidth: 420
+ modalHeight: 480
+ onBackgroundClicked: hide()
+ onOpened: {
+ Qt.callLater(() => {
+ modalFocusScope.forceActiveFocus();
+ contentLoader.item.wifiSSID = wifiSSID;
+ contentLoader.item.themedQrCodePath = themedQrCodePath;
+ contentLoader.item.saveBrowserLoader = saveBrowserLoader;
+ });
+ }
+
+ function show(ssid) {
+ wifiSSID = ssid;
+ fetchNetworkQRCode(ssid);
+ }
+
+ function hide() {
+ if (themedQrCodePath !== "") {
+ deleteQRCodeFile(themedQrCodePath);
+ }
+ if (normalQrCodePath !== "") {
+ deleteQRCodeFile(normalQrCodePath);
+ }
+ close();
+ }
+
+ function fetchNetworkQRCode(ssid) {
+ // TODO: Add loading UI?
+
+ DMSService.sendRequest("network.qrcode", {
+ ssid: ssid
+ }, response => {
+ if (response.error) {
+ ToastService.showError("Failed to fetch network QR code: ", JSON.stringify(response.error));
+ } else if (response.result) {
+ themedQrCodePath = response.result[0];
+ normalQrCodePath = response.result[1];
+ open();
+ }
+ });
+ }
+
+ function deleteQRCodeFile(path) {
+ DMSService.sendRequest("network.delete-qrcode", {
+ path: path
+ }, response => {
+ if (response.error) {
+ ToastService.showError(`Failed to remove QR code at ${path}: `, JSON.stringify(response.error));
+ }
+ })
+ }
+
+ LazyLoader {
+ id: saveBrowserLoader
+ active: false
+
+ FileBrowserSurfaceModal {
+ id: saveBrowser
+
+ browserTitle: I18n.tr("Save QR Code")
+ browserIcon: "qr_code"
+ browserType: "default"
+ fileExtensions: ["*.png"]
+ allowStacking: true
+ saveMode: true
+ defaultFileName: `${root.wifiSSID ?? "wifi-qrcode"}.png`
+ onFileSelected: path => {
+ const cleanPath = decodeURI(path.toString().replace(/^file:\/\//, ''));
+ const fileName = cleanPath.split('/').pop();
+ const fileUrl = "file://" + cleanPath;
+
+ copyQrCodeProcess.exec(["cp", root.normalQrCodePath, cleanPath, "-f"])
+ }
+
+ Process {
+ id: copyQrCodeProcess
+ stdout: StdioCollector {
+ onStreamFinished: {
+ saveBrowser.close();
+ }
+ }
+ }
+ }
+ }
+
+ content: Component {
+ Item {
+ id: theItem
+ property alias themedQrCodePath: qrCodeImg.source
+ property var saveBrowserLoader: null
+ property string wifiSSID: ""
+ anchors.fill: parent
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingL
+
+ RowLayout {
+ id: modalTitle
+ width: parent.width
+
+ StyledText {
+ text: I18n.tr("WiFi QR code for ") + theItem.wifiSSID
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Bold
+ Layout.alignment: Qt.AlignLeft
+ }
+
+ DankActionButton {
+ iconName: "save"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: {
+ saveBrowserLoader.active = true;
+ if (saveBrowserLoader.item) {
+ saveBrowserLoader.item.open();
+ }
+ }
+ Layout.alignment: Qt.AlignRight
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: root.hide()
+ Layout.alignment: Qt.AlignRight
+ }
+ }
+
+ Image {
+ id: qrCodeImg
+ height: parent.height - parent.spacing - modalTitle.height
+ width: height
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ MultiEffect {
+ source: qrCodeImg
+ anchors.fill: source
+ colorization: 1.0
+ colorizationColor: Theme.primary
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WindowRuleModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WindowRuleModal.qml
new file mode 100644
index 0000000..6c9583a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WindowRuleModal.qml
@@ -0,0 +1,1184 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+FloatingWindow {
+ id: root
+
+ property bool disablePopupTransparency: true
+ property var editingRule: null
+ property bool isEditMode: editingRule !== null
+ property bool isNiri: CompositorService.isNiri
+ property bool isHyprland: CompositorService.isHyprland
+ property bool submitting: false
+ property var targetWindow: null
+
+ signal ruleSubmitted
+
+ readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
+ readonly property int sectionSpacing: Theme.spacingL
+
+ objectName: "windowRuleModal"
+ title: isEditMode ? I18n.tr("Edit Window Rule") : I18n.tr("Create Window Rule")
+ minimumSize: Qt.size(500, 600)
+ maximumSize: Qt.size(500, 600)
+ color: Theme.surfaceContainer
+ visible: false
+
+ function resetForm() {
+ nameInput.text = "";
+ appIdInput.text = "";
+ titleInput.text = "";
+ opacityEnabled.checked = false;
+ opacitySlider.value = 100;
+ floatingToggle.checked = false;
+ maximizedToggle.checked = false;
+ maximizedToEdgesToggle.checked = false;
+ fullscreenToggle.checked = false;
+ openFocusedToggle.checked = false;
+ outputInput.text = "";
+ workspaceInput.text = "";
+ columnWidthInput.text = "";
+ windowHeightInput.text = "";
+ vrrToggle.checked = false;
+ blockOutDropdown.currentValue = "";
+ columnDisplayDropdown.currentValue = "";
+ scrollFactorEnabled.checked = false;
+ scrollFactorSlider.value = 100;
+ cornerRadiusEnabled.checked = false;
+ cornerRadiusSlider.value = 12;
+ clipToGeometryToggle.checked = false;
+ tiledStateToggle.checked = false;
+ drawBorderBgToggle.checked = false;
+ minWidthInput.text = "";
+ maxWidthInput.text = "";
+ minHeightInput.text = "";
+ maxHeightInput.text = "";
+ tileToggle.checked = false;
+ noFocusToggle.checked = false;
+ noBorderToggle.checked = false;
+ noShadowToggle.checked = false;
+ noDimToggle.checked = false;
+ noBlurToggle.checked = false;
+ noAnimToggle.checked = false;
+ noRoundingToggle.checked = false;
+ pinToggle.checked = false;
+ opaqueToggle.checked = false;
+ sizeInput.text = "";
+ moveInput.text = "";
+ monitorInput.text = "";
+ hyprWorkspaceInput.text = "";
+ }
+
+ function show(window) {
+ editingRule = null;
+ targetWindow = window || null;
+ resetForm();
+ if (targetWindow) {
+ nameInput.text = targetWindow.appId || "";
+ appIdInput.text = targetWindow.appId ? "^" + targetWindow.appId + "$" : "";
+ }
+ visible = true;
+ Qt.callLater(() => nameInput.forceActiveFocus());
+ }
+
+ function showEdit(rule) {
+ if (!rule) {
+ show();
+ return;
+ }
+ editingRule = rule;
+ resetForm();
+
+ nameInput.text = rule.name || "";
+ const match = rule.matchCriteria || {};
+ appIdInput.text = match.appId || "";
+ titleInput.text = match.title || "";
+
+ const actions = rule.actions || {};
+ const hasOpacity = actions.opacity !== undefined && actions.opacity !== null;
+ opacityEnabled.checked = hasOpacity;
+ opacitySlider.value = hasOpacity ? Math.round(actions.opacity * 100) : 100;
+
+ floatingToggle.checked = actions.openFloating || false;
+ maximizedToggle.checked = actions.openMaximized || false;
+ maximizedToEdgesToggle.checked = actions.openMaximizedToEdges || false;
+ fullscreenToggle.checked = actions.openFullscreen || false;
+
+ openFocusedToggle.checked = actions.openFocused || false;
+
+ outputInput.text = actions.openOnOutput || "";
+ workspaceInput.text = actions.openOnWorkspace || "";
+ columnWidthInput.text = actions.defaultColumnWidth || "";
+ windowHeightInput.text = actions.defaultWindowHeight || "";
+ vrrToggle.checked = actions.variableRefreshRate || false;
+
+ blockOutDropdown.currentValue = actions.blockOutFrom || "";
+ columnDisplayDropdown.currentValue = actions.defaultColumnDisplay || "";
+
+ const hasScrollFactor = actions.scrollFactor !== undefined && actions.scrollFactor !== null;
+ scrollFactorEnabled.checked = hasScrollFactor;
+ scrollFactorSlider.value = hasScrollFactor ? Math.round(actions.scrollFactor * 100) : 100;
+
+ const hasCornerRadius = actions.cornerRadius !== undefined && actions.cornerRadius !== null;
+ cornerRadiusEnabled.checked = hasCornerRadius;
+ cornerRadiusSlider.value = hasCornerRadius ? actions.cornerRadius : 12;
+
+ clipToGeometryToggle.checked = actions.clipToGeometry || false;
+ tiledStateToggle.checked = actions.tiledState || false;
+
+ drawBorderBgToggle.checked = actions.drawBorderWithBackground || false;
+
+ minWidthInput.text = actions.minWidth !== undefined ? String(actions.minWidth) : "";
+ maxWidthInput.text = actions.maxWidth !== undefined ? String(actions.maxWidth) : "";
+ minHeightInput.text = actions.minHeight !== undefined ? String(actions.minHeight) : "";
+ maxHeightInput.text = actions.maxHeight !== undefined ? String(actions.maxHeight) : "";
+
+ tileToggle.checked = actions.tile || false;
+ noFocusToggle.checked = actions.nofocus || false;
+ noBorderToggle.checked = actions.noborder || false;
+ noShadowToggle.checked = actions.noshadow || false;
+ noDimToggle.checked = actions.nodim || false;
+ noBlurToggle.checked = actions.noblur || false;
+ noAnimToggle.checked = actions.noanim || false;
+ noRoundingToggle.checked = actions.norounding || false;
+ pinToggle.checked = actions.pin || false;
+ opaqueToggle.checked = actions.opaque || false;
+ sizeInput.text = actions.size || "";
+ moveInput.text = actions.move || "";
+ monitorInput.text = actions.monitor || "";
+ hyprWorkspaceInput.text = actions.workspace || "";
+
+ visible = true;
+ Qt.callLater(() => nameInput.forceActiveFocus());
+ }
+
+ function hide() {
+ visible = false;
+ editingRule = null;
+ targetWindow = null;
+ }
+
+ function submitAndClose() {
+ const matchCriteria = {};
+ if (appIdInput.text.trim())
+ matchCriteria.appId = appIdInput.text.trim();
+ if (titleInput.text.trim())
+ matchCriteria.title = titleInput.text.trim();
+
+ const actions = {};
+
+ if (opacityEnabled.checked)
+ actions.opacity = opacitySlider.value / 100;
+ if (floatingToggle.checked)
+ actions.openFloating = true;
+ if (maximizedToggle.checked)
+ actions.openMaximized = true;
+ if (maximizedToEdgesToggle.checked && isNiri)
+ actions.openMaximizedToEdges = true;
+ if (fullscreenToggle.checked)
+ actions.openFullscreen = true;
+ if (openFocusedToggle.checked && isNiri)
+ actions.openFocused = true;
+ if (outputInput.text.trim())
+ actions.openOnOutput = outputInput.text.trim();
+ if (workspaceInput.text.trim())
+ actions.openOnWorkspace = workspaceInput.text.trim();
+ if (columnWidthInput.text.trim() && isNiri)
+ actions.defaultColumnWidth = columnWidthInput.text.trim();
+ if (windowHeightInput.text.trim() && isNiri)
+ actions.defaultWindowHeight = windowHeightInput.text.trim();
+ if (vrrToggle.checked && isNiri)
+ actions.variableRefreshRate = true;
+ if (blockOutDropdown.currentValue && isNiri)
+ actions.blockOutFrom = blockOutDropdown.currentValue;
+ if (columnDisplayDropdown.currentValue && isNiri)
+ actions.defaultColumnDisplay = columnDisplayDropdown.currentValue;
+ if (scrollFactorEnabled.checked && isNiri)
+ actions.scrollFactor = scrollFactorSlider.value / 100;
+ if (cornerRadiusEnabled.checked)
+ actions.cornerRadius = cornerRadiusSlider.value;
+ if (clipToGeometryToggle.checked && isNiri)
+ actions.clipToGeometry = true;
+ if (tiledStateToggle.checked && isNiri)
+ actions.tiledState = true;
+ if (drawBorderBgToggle.checked && isNiri)
+ actions.drawBorderWithBackground = true;
+
+ const minW = parseInt(minWidthInput.text);
+ const maxW = parseInt(maxWidthInput.text);
+ const minH = parseInt(minHeightInput.text);
+ const maxH = parseInt(maxHeightInput.text);
+ if (!isNaN(minW))
+ actions.minWidth = minW;
+ if (!isNaN(maxW))
+ actions.maxWidth = maxW;
+ if (!isNaN(minH))
+ actions.minHeight = minH;
+ if (!isNaN(maxH))
+ actions.maxHeight = maxH;
+
+ if (isHyprland) {
+ if (tileToggle.checked)
+ actions.tile = true;
+ if (noFocusToggle.checked)
+ actions.nofocus = true;
+ if (noBorderToggle.checked)
+ actions.noborder = true;
+ if (noShadowToggle.checked)
+ actions.noshadow = true;
+ if (noDimToggle.checked)
+ actions.nodim = true;
+ if (noBlurToggle.checked)
+ actions.noblur = true;
+ if (noAnimToggle.checked)
+ actions.noanim = true;
+ if (noRoundingToggle.checked)
+ actions.norounding = true;
+ if (pinToggle.checked)
+ actions.pin = true;
+ if (opaqueToggle.checked)
+ actions.opaque = true;
+ if (sizeInput.text.trim())
+ actions.size = sizeInput.text.trim();
+ if (moveInput.text.trim())
+ actions.move = moveInput.text.trim();
+ if (monitorInput.text.trim())
+ actions.monitor = monitorInput.text.trim();
+ if (hyprWorkspaceInput.text.trim())
+ actions.workspace = hyprWorkspaceInput.text.trim();
+ }
+
+ const name = nameInput.text.trim() || matchCriteria.appId || I18n.tr("Rule");
+ const compositor = CompositorService.compositor;
+
+ const ruleData = {
+ name: name,
+ matchCriteria: matchCriteria,
+ actions: actions,
+ enabled: true
+ };
+
+ submitting = true;
+
+ const shouldValidate = CompositorService.isNiri;
+
+ if (isEditMode) {
+ const ruleJson = JSON.stringify(ruleData);
+ Proc.runCommand("update-windowrule", ["dms", "config", "windowrules", "update", compositor, editingRule.id, ruleJson], (output, exitCode) => {
+ root.submitting = false;
+ if (exitCode !== 0)
+ return;
+ if (shouldValidate)
+ NiriService.validate();
+ root.ruleSubmitted();
+ root.hide();
+ });
+ } else {
+ const ruleJson = JSON.stringify(ruleData);
+ Proc.runCommand("add-windowrule", ["dms", "config", "windowrules", "add", compositor, ruleJson], (output, exitCode) => {
+ root.submitting = false;
+ if (exitCode !== 0)
+ return;
+ if (shouldValidate)
+ NiriService.validate();
+ root.ruleSubmitted();
+ root.hide();
+ });
+ }
+ }
+
+ onVisibleChanged: {
+ if (!visible) {
+ editingRule = null;
+ targetWindow = null;
+ }
+ }
+
+ component SectionHeader: StyledText {
+ property string title
+ text: title
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: Theme.primary
+ topPadding: Theme.spacingM
+ bottomPadding: Theme.spacingXS
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ component CheckboxRow: Row {
+ property alias checked: checkbox.checked
+ property alias label: labelText.text
+ property bool indeterminate: false
+ spacing: Theme.spacingS
+ height: 24
+
+ Rectangle {
+ id: checkbox
+ property bool checked: false
+ width: 20
+ height: 20
+ radius: 4
+ color: parent.indeterminate ? Theme.surfaceVariant : (checked ? Theme.primary : "transparent")
+ border.color: parent.indeterminate ? Theme.outlineButton : (checked ? Theme.primary : Theme.outlineButton)
+ border.width: 2
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: parent.parent.indeterminate ? "remove" : "check"
+ size: 12
+ color: parent.parent.indeterminate ? Theme.surfaceVariantText : Theme.background
+ visible: parent.checked || parent.parent.indeterminate
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (parent.parent.indeterminate) {
+ parent.parent.indeterminate = false;
+ parent.checked = true;
+ } else {
+ parent.checked = !parent.checked;
+ }
+ }
+ }
+ }
+
+ StyledText {
+ id: labelText
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ component InputField: Rectangle {
+ id: inputFieldRect
+ default property alias contentData: inputFieldRect.data
+ property bool hasFocus: false
+ width: parent.width
+ height: root.inputFieldHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: hasFocus ? Theme.primary : Theme.outlineStrong
+ border.width: hasFocus ? 2 : 1
+ }
+
+ FocusScope {
+ anchors.fill: parent
+ focus: true
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ Keys.onEscapePressed: event => {
+ hide();
+ event.accepted = true;
+ }
+
+ Item {
+ id: header
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingL
+ height: Math.max(headerCol.height, closeBtn.height)
+
+ MouseArea {
+ anchors.left: parent.left
+ anchors.right: closeBtn.left
+ anchors.rightMargin: Theme.spacingM
+ height: headerCol.height
+ onPressed: windowControls.tryStartMove()
+
+ Column {
+ id: headerCol
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: root.isEditMode ? I18n.tr("Edit Window Rule") : I18n.tr("New Window Rule")
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: I18n.tr("Configure match criteria and actions")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+
+ DankActionButton {
+ id: closeBtn
+ anchors.right: parent.right
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: hide()
+ }
+ }
+
+ DankFlickable {
+ id: flickable
+ anchors.top: header.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: footer.top
+ anchors.margins: Theme.spacingL
+ anchors.topMargin: Theme.spacingM
+ contentWidth: width
+ contentHeight: contentCol.implicitHeight
+ clip: true
+
+ Column {
+ id: contentCol
+ width: flickable.width - Theme.spacingM
+ spacing: Theme.spacingXS
+
+ InputField {
+ hasFocus: nameInput.activeFocus
+ DankTextField {
+ id: nameInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: I18n.tr("Rule Name")
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+
+ SectionHeader {
+ title: I18n.tr("Match Criteria")
+ }
+
+ InputField {
+ hasFocus: appIdInput.activeFocus
+ DankTextField {
+ id: appIdInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: isNiri ? I18n.tr("App ID regex (e.g. ^firefox$)") : I18n.tr("Class regex (e.g. ^firefox$)")
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ InputField {
+ width: addTitleBtn.visible ? parent.width - addTitleBtn.width - Theme.spacingS : parent.width
+ hasFocus: titleInput.activeFocus
+ DankTextField {
+ id: titleInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: I18n.tr("Title regex (optional)")
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+
+ DankActionButton {
+ id: addTitleBtn
+ width: root.inputFieldHeight
+ height: root.inputFieldHeight
+ circular: false
+ iconName: "add"
+ iconSize: 16
+ iconColor: Theme.surfaceVariantText
+ visible: !root.isEditMode && !!root.targetWindow?.title
+ tooltipText: I18n.tr("Add Title")
+ tooltipSide: "left"
+ onClicked: {
+ if (!root.targetWindow?.title)
+ return;
+ titleInput.text = "^" + root.targetWindow.title + "$";
+ }
+ }
+ }
+
+ SectionHeader {
+ title: I18n.tr("Window Opening")
+ }
+
+ Flow {
+ width: parent.width
+ spacing: Theme.spacingL
+
+ CheckboxRow {
+ id: floatingToggle
+ label: I18n.tr("Float")
+ }
+ CheckboxRow {
+ id: maximizedToggle
+ label: I18n.tr("Maximize")
+ }
+ CheckboxRow {
+ id: fullscreenToggle
+ label: I18n.tr("Fullscreen")
+ }
+ CheckboxRow {
+ id: maximizedToEdgesToggle
+ label: I18n.tr("Max to Edges")
+ visible: isNiri
+ }
+ CheckboxRow {
+ id: openFocusedToggle
+ label: I18n.tr("Focus")
+ visible: isNiri
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+ visible: true
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Output")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: outputInput.activeFocus
+ DankTextField {
+ id: outputInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "HDMI-A-1"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Workspace")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: workspaceInput.activeFocus
+ DankTextField {
+ id: workspaceInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "chat"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+ visible: isNiri
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Column Width")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: columnWidthInput.activeFocus
+ DankTextField {
+ id: columnWidthInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "800"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Window Height")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: windowHeightInput.activeFocus
+ DankTextField {
+ id: windowHeightInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "600"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+ }
+
+ SectionHeader {
+ title: I18n.tr("Dynamic Properties")
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ CheckboxRow {
+ id: opacityEnabled
+ label: I18n.tr("Opacity")
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ DankSlider {
+ id: opacitySlider
+ width: parent.width - 100
+ minimum: 10
+ maximum: 100
+ value: 100
+ enabled: opacityEnabled.checked
+ opacity: enabled ? 1 : 0.4
+ }
+ }
+
+ Flow {
+ width: parent.width
+ spacing: Theme.spacingL
+ visible: isNiri
+
+ CheckboxRow {
+ id: vrrToggle
+ label: I18n.tr("VRR On-Demand")
+ }
+ CheckboxRow {
+ id: clipToGeometryToggle
+ label: I18n.tr("Clip to Geometry")
+ }
+ CheckboxRow {
+ id: tiledStateToggle
+ label: I18n.tr("Tiled State")
+ }
+ CheckboxRow {
+ id: drawBorderBgToggle
+ label: I18n.tr("Border with BG")
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+ visible: isNiri
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Block Out From")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ DankDropdown {
+ id: blockOutDropdown
+ width: parent.width
+ dropdownWidth: parent.width
+ compactMode: true
+ options: ["", "screencast", "screen-capture"]
+ emptyText: I18n.tr("None")
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Column Display")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ DankDropdown {
+ id: columnDisplayDropdown
+ width: parent.width
+ dropdownWidth: parent.width
+ compactMode: true
+ options: ["", "tabbed"]
+ emptyText: I18n.tr("Normal")
+ }
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+ visible: isNiri
+
+ CheckboxRow {
+ id: scrollFactorEnabled
+ label: I18n.tr("Scroll Factor")
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ DankSlider {
+ id: scrollFactorSlider
+ width: parent.width - 120
+ minimum: 10
+ maximum: 200
+ value: 100
+ enabled: scrollFactorEnabled.checked
+ opacity: enabled ? 1 : 0.4
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ CheckboxRow {
+ id: cornerRadiusEnabled
+ label: I18n.tr("Corner Radius")
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ DankSlider {
+ id: cornerRadiusSlider
+ width: parent.width - 130
+ minimum: 0
+ maximum: 24
+ value: 12
+ enabled: cornerRadiusEnabled.checked
+ opacity: enabled ? 1 : 0.4
+ }
+ }
+
+ SectionHeader {
+ title: I18n.tr("Size Constraints")
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Column {
+ width: (parent.width - Theme.spacingM * 3) / 4
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Min W")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: minWidthInput.activeFocus
+ DankTextField {
+ id: minWidthInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "px"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM * 3) / 4
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Max W")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: maxWidthInput.activeFocus
+ DankTextField {
+ id: maxWidthInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "px"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM * 3) / 4
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Min H")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: minHeightInput.activeFocus
+ DankTextField {
+ id: minHeightInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "px"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM * 3) / 4
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Max H")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: maxHeightInput.activeFocus
+ DankTextField {
+ id: maxHeightInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "px"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+ }
+
+ SectionHeader {
+ title: I18n.tr("Hyprland Options")
+ visible: isHyprland
+ }
+
+ Flow {
+ width: parent.width
+ spacing: Theme.spacingL
+ visible: isHyprland
+
+ CheckboxRow {
+ id: tileToggle
+ label: I18n.tr("Tile")
+ }
+ CheckboxRow {
+ id: noFocusToggle
+ label: I18n.tr("No Focus")
+ }
+ CheckboxRow {
+ id: noBorderToggle
+ label: I18n.tr("No Border")
+ }
+ CheckboxRow {
+ id: noShadowToggle
+ label: I18n.tr("No Shadow")
+ }
+ CheckboxRow {
+ id: noDimToggle
+ label: I18n.tr("No Dim")
+ }
+ CheckboxRow {
+ id: noBlurToggle
+ label: I18n.tr("No Blur")
+ }
+ CheckboxRow {
+ id: noAnimToggle
+ label: I18n.tr("No Anim")
+ }
+ CheckboxRow {
+ id: noRoundingToggle
+ label: I18n.tr("No Rounding")
+ }
+ CheckboxRow {
+ id: pinToggle
+ label: I18n.tr("Pin")
+ }
+ CheckboxRow {
+ id: opaqueToggle
+ label: I18n.tr("Opaque")
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+ visible: isHyprland
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Size")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: sizeInput.activeFocus
+ DankTextField {
+ id: sizeInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "800 600"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Move")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: moveInput.activeFocus
+ DankTextField {
+ id: moveInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "100 100"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingM
+ visible: isHyprland
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Monitor")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: monitorInput.activeFocus
+ DankTextField {
+ id: monitorInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "DP-1"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+
+ Column {
+ width: (parent.width - Theme.spacingM) / 2
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: I18n.tr("Workspace")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ InputField {
+ width: parent.width
+ hasFocus: hyprWorkspaceInput.activeFocus
+ DankTextField {
+ id: hyprWorkspaceInput
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeSmall
+ textColor: Theme.surfaceText
+ placeholderText: "1"
+ backgroundColor: "transparent"
+ enabled: root.visible
+ }
+ }
+ }
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingM
+ }
+ }
+ }
+
+ Item {
+ id: footer
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingL
+ height: 44
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
+ border.color: Theme.surfaceVariantAlpha
+ border.width: 1
+
+ StyledText {
+ id: cancelText
+ anchors.centerIn: parent
+ text: I18n.tr("Cancel")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: cancelArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: hide()
+ }
+ }
+
+ Rectangle {
+ width: Math.max(80, createText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: root.submitting ? Theme.surfaceVariant : (createArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary)
+
+ StyledText {
+ id: createText
+ anchors.centerIn: parent
+ text: root.submitting ? I18n.tr("Saving...") : (root.isEditMode ? I18n.tr("Update") : I18n.tr("Create"))
+ font.pixelSize: Theme.fontSizeMedium
+ color: root.submitting ? Theme.surfaceVariantText : Theme.background
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: createArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: root.submitting ? Qt.ArrowCursor : Qt.PointingHandCursor
+ enabled: !root.submitting
+ onClicked: submitAndClose()
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+
+ FloatingWindowControls {
+ id: windowControls
+ targetWindow: root
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WorkspaceRenameModal.qml b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WorkspaceRenameModal.qml
new file mode 100644
index 0000000..6519ac3
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/quickshell/Modals/WorkspaceRenameModal.qml
@@ -0,0 +1,230 @@
+import QtQuick
+import Quickshell
+import Quickshell.Io
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+FloatingWindow {
+ id: root
+
+ property bool disablePopupTransparency: true
+ readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
+
+ objectName: "workspaceRenameModal"
+ title: I18n.tr("Rename Workspace")
+ minimumSize: Qt.size(400, 160)
+ maximumSize: Qt.size(400, 160)
+ color: Theme.surfaceContainer
+ visible: false
+
+ function show(name) {
+ nameInput.text = name;
+ visible = true;
+ Qt.callLater(() => nameInput.forceActiveFocus());
+ }
+
+ function hide() {
+ visible = false;
+ }
+
+ function submitAndClose() {
+ renameWorkspace(nameInput.text);
+ hide();
+ }
+
+ function renameWorkspace(name) {
+ if (CompositorService.isNiri) {
+ NiriService.renameWorkspace(name);
+ } else if (CompositorService.isHyprland) {
+ HyprlandService.renameWorkspace(name);
+ } else {
+ console.warn("WorkspaceRenameModal: rename not supported for this compositor");
+ }
+ }
+
+ onVisibleChanged: {
+ if (visible) {
+ Qt.callLater(() => nameInput.forceActiveFocus());
+ return;
+ }
+ nameInput.text = "";
+ }
+
+ FocusScope {
+ id: contentFocusScope
+
+ anchors.fill: parent
+ focus: true
+
+ Keys.onEscapePressed: event => {
+ hide();
+ event.accepted = true;
+ }
+
+ Column {
+ id: contentCol
+ anchors.centerIn: parent
+ width: parent.width - Theme.spacingL * 2
+ spacing: Theme.spacingM
+
+ Item {
+ width: contentCol.width
+ height: Math.max(headerText.height, buttonRow.height)
+
+ MouseArea {
+ anchors.left: parent.left
+ anchors.right: buttonRow.left
+ anchors.rightMargin: Theme.spacingM
+ height: parent.height
+ onPressed: windowControls.tryStartMove()
+ onDoubleClicked: windowControls.tryToggleMaximize()
+ }
+
+ StyledText {
+ id: headerText
+ text: I18n.tr("Enter a new name for this workspace")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceTextMedium
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - buttonRow.width - Theme.spacingM
+ }
+
+ Row {
+ id: buttonRow
+ anchors.right: parent.right
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ visible: windowControls.supported && windowControls.canMaximize
+ iconName: root.maximized ? "fullscreen_exit" : "fullscreen"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: windowControls.tryToggleMaximize()
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: hide()
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: inputFieldHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceHover
+ border.color: nameInput.activeFocus ? Theme.primary : Theme.outlineStrong
+ border.width: nameInput.activeFocus ? 2 : 1
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: nameInput.forceActiveFocus()
+ }
+
+ DankTextField {
+ id: nameInput
+
+ anchors.fill: parent
+ font.pixelSize: Theme.fontSizeMedium
+ textColor: Theme.surfaceText
+ placeholderText: I18n.tr("Workspace name")
+ backgroundColor: "transparent"
+ enabled: root.visible
+ onAccepted: submitAndClose()
+ }
+ }
+
+ Item {
+ width: parent.width
+ height: 40
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ Rectangle {
+ width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: cancelArea.containsMouse ? Theme.surfaceTextHover : "transparent"
+ border.color: Theme.surfaceVariantAlpha
+ border.width: 1
+
+ StyledText {
+ id: cancelText
+ anchors.centerIn: parent
+ text: I18n.tr("Cancel")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: cancelArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: hide()
+ }
+ }
+
+ Rectangle {
+ width: Math.max(80, renameText.contentWidth + Theme.spacingM * 2)
+ height: 36
+ radius: Theme.cornerRadius
+ color: renameArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
+
+ StyledText {
+ id: renameText
+ anchors.centerIn: parent
+ text: I18n.tr("Rename")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.background
+ font.weight: Font.Medium
+ }
+
+ MouseArea {
+ id: renameArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: submitAndClose()
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ FloatingWindowControls {
+ id: windowControls
+ targetWindow: root
+ }
+
+ IpcHandler {
+ target: "workspace-rename"
+
+ function open(): string {
+ const ws = NiriService.workspaces[NiriService.focusedWorkspaceId];
+ show(ws?.name || "");
+ return "WORKSPACE_RENAME_MODAL_OPENED";
+ }
+
+ function close(): string {
+ hide();
+ return "WORKSPACE_RENAME_MODAL_CLOSED";
+ }
+ }
+}