diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Modals')
73 files changed, 23391 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Modals/AppPickerModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/AppPickerModal.qml new file mode 100644 index 0000000..2188111 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/BluetoothPairingModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/BluetoothPairingModal.qml new file mode 100644 index 0000000..cd3c9b7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/BrowserPickerModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/BrowserPickerModal.qml new file mode 100644 index 0000000..e4afcb8 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Changelog/ChangelogContent.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Changelog/ChangelogContent.qml new file mode 100644 index 0000000..8fd3129 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Changelog/ChangelogFeatureCard.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Changelog/ChangelogFeatureCard.qml new file mode 100644 index 0000000..eab4f70 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Changelog/ChangelogModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Changelog/ChangelogModal.qml new file mode 100644 index 0000000..524298b --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Changelog/ChangelogUpgradeNote.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Changelog/ChangelogUpgradeNote.qml new file mode 100644 index 0000000..545eb9a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Clipboard/ClipboardConstants.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Clipboard/ClipboardConstants.qml new file mode 100644 index 0000000..f796ffe --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Clipboard/ClipboardContent.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Clipboard/ClipboardContent.qml new file mode 100644 index 0000000..e00bc22 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Clipboard/ClipboardEntry.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Clipboard/ClipboardEntry.qml new file mode 100644 index 0000000..53a2ece --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Clipboard/ClipboardHeader.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Clipboard/ClipboardHeader.qml new file mode 100644 index 0000000..fedba15 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Clipboard/ClipboardHistoryModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Clipboard/ClipboardHistoryModal.qml new file mode 100644 index 0000000..2f76cc7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Clipboard/ClipboardHistoryPopout.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Clipboard/ClipboardHistoryPopout.qml new file mode 100644 index 0000000..c4f0296 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Clipboard/ClipboardKeyboardController.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Clipboard/ClipboardKeyboardController.qml new file mode 100644 index 0000000..f2a457e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Clipboard/ClipboardKeyboardHints.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Clipboard/ClipboardKeyboardHints.qml new file mode 100644 index 0000000..0285952 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Clipboard/ClipboardThumbnail.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Clipboard/ClipboardThumbnail.qml new file mode 100644 index 0000000..26a9210 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Common/ConfirmModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Common/ConfirmModal.qml new file mode 100644 index 0000000..b9c9b90 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Common/DankModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Common/DankModal.qml new file mode 100644 index 0000000..cafa133 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Common/InputModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Common/InputModal.qml new file mode 100644 index 0000000..c65f3e0 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankColorPickerModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankColorPickerModal.qml new file mode 100644 index 0000000..8a33878 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/ActionPanel.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ActionPanel.qml new file mode 100644 index 0000000..31ada32 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/Controller.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Controller.qml new file mode 100644 index 0000000..67cb606 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/ControllerUtils.js b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ControllerUtils.js new file mode 100644 index 0000000..a06285f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/DankLauncherV2Modal.qml new file mode 100644 index 0000000..859022c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/GridItem.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/GridItem.qml new file mode 100644 index 0000000..a7f39c3 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/ItemTransformers.js b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ItemTransformers.js new file mode 100644 index 0000000..61c108c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/LauncherContent.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContent.qml new file mode 100644 index 0000000..4372515 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/LauncherContextMenu.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/LauncherContextMenu.qml new file mode 100644 index 0000000..3c55590 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/NavigationHelpers.js b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/NavigationHelpers.js new file mode 100644 index 0000000..1e78b11 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/ResultItem.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultItem.qml new file mode 100644 index 0000000..0a50cd3 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/ResultsList.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/ResultsList.qml new file mode 100644 index 0000000..53a2d45 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/Scorer.js b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Scorer.js new file mode 100644 index 0000000..7d54df7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/Section.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/Section.qml new file mode 100644 index 0000000..aba4c2f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/SectionHeader.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/SectionHeader.qml new file mode 100644 index 0000000..ee366c7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DankLauncherV2/TileItem.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DankLauncherV2/TileItem.qml new file mode 100644 index 0000000..0b48802 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/DisplayConfirmationModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/DisplayConfirmationModal.qml new file mode 100644 index 0000000..f8a97de --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserContent.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserContent.qml new file mode 100644 index 0000000..8b96742 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserGridDelegate.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserGridDelegate.qml new file mode 100644 index 0000000..effa9bd --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserListDelegate.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserListDelegate.qml new file mode 100644 index 0000000..2f452b4 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserModal.qml new file mode 100644 index 0000000..c7686ca --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserNavigation.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserNavigation.qml new file mode 100644 index 0000000..61ce7b6 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserOverwriteDialog.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserOverwriteDialog.qml new file mode 100644 index 0000000..a664be5 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserSaveRow.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSaveRow.qml new file mode 100644 index 0000000..6360c7f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserSidebar.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSidebar.qml new file mode 100644 index 0000000..bc02b3e --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserSortMenu.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSortMenu.qml new file mode 100644 index 0000000..e1886f9 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileBrowserSurfaceModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileBrowserSurfaceModal.qml new file mode 100644 index 0000000..25b8d5a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/FileInfo.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/FileInfo.qml new file mode 100644 index 0000000..c41f039 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/FileBrowser/KeyboardHints.qml b/raveos-hyprland-theme/theme-data/dms/Modals/FileBrowser/KeyboardHints.qml new file mode 100644 index 0000000..1910810 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Greeter/GreeterCompletePage.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Greeter/GreeterCompletePage.qml new file mode 100644 index 0000000..8a5ca00 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Greeter/GreeterDoctorPage.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Greeter/GreeterDoctorPage.qml new file mode 100644 index 0000000..288cd58 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Greeter/GreeterDoctorResultItem.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Greeter/GreeterDoctorResultItem.qml new file mode 100644 index 0000000..b15969a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Greeter/GreeterFeatureCard.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Greeter/GreeterFeatureCard.qml new file mode 100644 index 0000000..a5bf4f4 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Greeter/GreeterModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Greeter/GreeterModal.qml new file mode 100644 index 0000000..973dab2 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Greeter/GreeterQuickLink.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Greeter/GreeterQuickLink.qml new file mode 100644 index 0000000..d064275 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Greeter/GreeterSettingsCard.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Greeter/GreeterSettingsCard.qml new file mode 100644 index 0000000..85e6b1c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Greeter/GreeterStatusCard.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Greeter/GreeterStatusCard.qml new file mode 100644 index 0000000..af0d5d6 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Greeter/GreeterWelcomePage.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Greeter/GreeterWelcomePage.qml new file mode 100644 index 0000000..fcb4fc5 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/KeybindsModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/KeybindsModal.qml new file mode 100644 index 0000000..e1d6765 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/MuxModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/MuxModal.qml new file mode 100644 index 0000000..9d19388 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/NetworkInfoModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/NetworkInfoModal.qml new file mode 100644 index 0000000..79dbada --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/NetworkWiredInfoModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/NetworkWiredInfoModal.qml new file mode 100644 index 0000000..4b42fdc --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/NotificationModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/NotificationModal.qml new file mode 100644 index 0000000..8653344 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/PolkitAuthModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/PolkitAuthModal.qml new file mode 100644 index 0000000..21ddf8f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/PowerMenuModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/PowerMenuModal.qml new file mode 100644 index 0000000..c59037f --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/ProcessListModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/ProcessListModal.qml new file mode 100644 index 0000000..8cfc705 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Settings/ProfileSection.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Settings/ProfileSection.qml new file mode 100644 index 0000000..29b4961 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Settings/SettingsContent.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Settings/SettingsContent.qml new file mode 100644 index 0000000..34cd51a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Settings/SettingsModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Settings/SettingsModal.qml new file mode 100644 index 0000000..9ce9153 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/Settings/SettingsSidebar.qml b/raveos-hyprland-theme/theme-data/dms/Modals/Settings/SettingsSidebar.qml new file mode 100644 index 0000000..092881c --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/WifiPasswordModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/WifiPasswordModal.qml new file mode 100644 index 0000000..92689f2 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/WifiQRCodeModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/WifiQRCodeModal.qml new file mode 100644 index 0000000..06ffab7 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/WindowRuleModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/WindowRuleModal.qml new file mode 100644 index 0000000..6c9583a --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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/dms/Modals/WorkspaceRenameModal.qml b/raveos-hyprland-theme/theme-data/dms/Modals/WorkspaceRenameModal.qml new file mode 100644 index 0000000..6519ac3 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/dms/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"; + } + } +} |