summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard
diff options
context:
space:
mode:
authornippy <you@example.com>2026-04-18 13:49:56 +0200
committernippy <you@example.com>2026-04-18 13:49:56 +0200
commit5d94c0a7d44a2255b81815a52a7056a94a39842d (patch)
tree759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard
parente79cdf210b267f21a186255ce1a4d50029439d54 (diff)
downloadRaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.tar.gz
RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.zip
update Raveos themes
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml21
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml260
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml155
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml87
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml174
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml197
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml166
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml51
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml178
9 files changed, 1289 insertions, 0 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml
new file mode 100644
index 0000000..f796ffe
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardConstants.qml
@@ -0,0 +1,21 @@
+pragma Singleton
+
+import QtQuick
+import Quickshell
+
+Singleton {
+ id: root
+ readonly property int previewLength: 100
+ readonly property int longTextThreshold: 200
+ readonly property int modalWidth: 650
+ readonly property int modalHeight: 550
+ readonly property int popoutWidth: 550
+ readonly property int popoutHeight: 500
+ readonly property int itemHeight: 72
+ readonly property int thumbnailSize: 100
+ readonly property int retryInterval: 50
+ readonly property int viewportBuffer: 100
+ readonly property int extendedBuffer: 200
+ readonly property int keyboardHintsHeight: 80
+ readonly property int headerHeight: 32
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml
new file mode 100644
index 0000000..e00bc22
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardContent.qml
@@ -0,0 +1,260 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: clipboardContent
+
+ required property var modal
+ required property var clearConfirmDialog
+
+ property alias searchField: searchField
+ property alias clipboardListView: clipboardListView
+
+ anchors.fill: parent
+
+ Column {
+ id: headerColumn
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingM
+ focus: false
+
+ ClipboardHeader {
+ id: header
+ width: parent.width
+ totalCount: modal.totalCount
+ showKeyboardHints: modal.showKeyboardHints
+ activeTab: modal.activeTab
+ pinnedCount: modal.pinnedCount
+ onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
+ onTabChanged: tabName => modal.activeTab = tabName
+ onClearAllClicked: {
+ const hasPinned = modal.pinnedCount > 0;
+ const message = hasPinned ? I18n.tr("This will delete all unpinned entries. %1 pinned entries will be kept.").arg(modal.pinnedCount) : I18n.tr("This will permanently delete all clipboard history.");
+ clearConfirmDialog.show(I18n.tr("Clear History?"), message, function () {
+ modal.clearAll();
+ modal.hide();
+ }, function () {});
+ }
+ onCloseClicked: modal.hide()
+ }
+
+ DankTextField {
+ id: searchField
+ width: parent.width
+ placeholderText: ""
+ leftIconName: "search"
+ showClearButton: true
+ focus: true
+ ignoreTabKeys: true
+ keyForwardTargets: [modal.modalFocusScope]
+ onTextChanged: {
+ modal.searchText = text;
+ modal.updateFilteredModel();
+ }
+ Keys.onEscapePressed: function (event) {
+ modal.hide();
+ event.accepted = true;
+ }
+ Component.onCompleted: {
+ Qt.callLater(function () {
+ forceActiveFocus();
+ });
+ }
+
+ Connections {
+ target: modal
+ function onOpened() {
+ Qt.callLater(function () {
+ searchField.forceActiveFocus();
+ });
+ }
+ }
+ }
+ }
+
+ Item {
+ id: listContainer
+ anchors.top: headerColumn.bottom
+ anchors.topMargin: Theme.spacingM
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.bottomMargin: (modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0) + Theme.spacingXS
+ clip: true
+
+ DankListView {
+ id: clipboardListView
+ anchors.fill: parent
+ model: ScriptModel {
+ values: clipboardContent.modal.unpinnedEntries
+ objectProp: "id"
+ }
+ visible: modal.activeTab === "recents"
+
+ currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
+ spacing: Theme.spacingXS
+ interactive: true
+ flickDeceleration: 1500
+ maximumFlickVelocity: 2000
+ boundsBehavior: Flickable.DragAndOvershootBounds
+ boundsMovement: Flickable.FollowBoundsBehavior
+ pressDelay: 0
+ flickableDirection: Flickable.VerticalFlick
+
+ function ensureVisible(index) {
+ if (index < 0 || index >= count) {
+ return;
+ }
+ positionViewAtIndex(index, ListView.Contain);
+ }
+
+ onCurrentIndexChanged: {
+ if (clipboardContent.modal?.keyboardNavigationActive && currentIndex >= 0) {
+ ensureVisible(currentIndex);
+ }
+ }
+
+ StyledText {
+ text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No recent clipboard entries found") : I18n.tr("Connecting to clipboard service…")
+ anchors.centerIn: parent
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ visible: clipboardContent.modal.unpinnedEntries.length === 0
+ }
+
+ delegate: ClipboardEntry {
+ required property int index
+ required property var modelData
+
+ width: clipboardListView.width
+ height: ClipboardConstants.itemHeight
+ entry: modelData
+ entryIndex: index + 1
+ itemIndex: index
+ isSelected: clipboardContent.modal?.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
+ modal: clipboardContent.modal
+ listView: clipboardListView
+ onCopyRequested: clipboardContent.modal.copyEntry(modelData)
+ onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
+ onPinRequested: clipboardContent.modal.pinEntry(modelData)
+ onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
+ }
+ }
+
+ DankListView {
+ id: savedListView
+ anchors.fill: parent
+ model: ScriptModel {
+ values: clipboardContent.modal.pinnedEntries
+ objectProp: "id"
+ }
+ visible: modal.activeTab === "saved"
+
+ currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
+ spacing: Theme.spacingXS
+ interactive: true
+ flickDeceleration: 1500
+ maximumFlickVelocity: 2000
+ boundsBehavior: Flickable.DragAndOvershootBounds
+ boundsMovement: Flickable.FollowBoundsBehavior
+ pressDelay: 0
+ flickableDirection: Flickable.VerticalFlick
+
+ function ensureVisible(index) {
+ if (index < 0 || index >= count) {
+ return;
+ }
+ positionViewAtIndex(index, ListView.Contain);
+ }
+
+ onCurrentIndexChanged: {
+ if (clipboardContent.modal?.keyboardNavigationActive && currentIndex >= 0) {
+ ensureVisible(currentIndex);
+ }
+ }
+
+ StyledText {
+ text: clipboardContent.modal.clipboardAvailable ? I18n.tr("No saved clipboard entries") : I18n.tr("Connecting to clipboard service…")
+ anchors.centerIn: parent
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ visible: clipboardContent.modal.pinnedEntries.length === 0
+ }
+
+ delegate: ClipboardEntry {
+ required property int index
+ required property var modelData
+
+ width: savedListView.width
+ height: ClipboardConstants.itemHeight
+ entry: modelData
+ entryIndex: index + 1
+ itemIndex: index
+ isSelected: clipboardContent.modal?.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
+ modal: clipboardContent.modal
+ listView: savedListView
+ onCopyRequested: clipboardContent.modal.copyEntry(modelData)
+ onDeleteRequested: clipboardContent.modal.deletePinnedEntry(modelData)
+ onPinRequested: clipboardContent.modal.pinEntry(modelData)
+ onUnpinRequested: clipboardContent.modal.unpinEntry(modelData)
+ }
+ }
+
+ Rectangle {
+ id: bottomFade
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: 24
+ z: 100
+ visible: {
+ const listView = modal.activeTab === "recents" ? clipboardListView : savedListView;
+ if (listView.contentHeight <= listView.height)
+ return false;
+ const atBottom = listView.contentY >= listView.contentHeight - listView.height - 5;
+ return !atBottom;
+ }
+ gradient: Gradient {
+ GradientStop {
+ position: 0.0
+ color: "transparent"
+ }
+ GradientStop {
+ position: 1.0
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ }
+ }
+ }
+ }
+
+ Loader {
+ id: keyboardHintsLoader
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ anchors.bottomMargin: active ? Theme.spacingM : 0
+ active: modal.showKeyboardHints
+ height: active ? ClipboardConstants.keyboardHintsHeight : 0
+
+ Behavior on height {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ sourceComponent: ClipboardKeyboardHints {
+ wtypeAvailable: modal.wtypeAvailable
+ enterToPaste: SettingsData.clipboardEnterToPaste
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml
new file mode 100644
index 0000000..53a2ece
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardEntry.qml
@@ -0,0 +1,155 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ required property var entry
+ required property int entryIndex
+ required property int itemIndex
+ required property bool isSelected
+ required property var modal
+ required property var listView
+
+ signal copyRequested
+ signal deleteRequested
+ signal pinRequested
+ signal unpinRequested
+
+ readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
+ readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
+ readonly property bool hasPinnedDuplicate: !entry.pinned && ClipboardService.hashedPinnedEntry(entry.hash)
+
+ radius: Theme.cornerRadius
+ color: {
+ if (isSelected) {
+ return Theme.primaryPressed;
+ }
+ return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
+ }
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: Theme.surfaceText
+ cornerRadius: root.radius
+ }
+
+ Rectangle {
+ id: indexBadge
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ width: 24
+ height: 24
+ radius: 12
+ color: Theme.primarySelected
+
+ StyledText {
+ anchors.centerIn: parent
+ text: entryIndex.toString()
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Bold
+ color: Theme.primary
+ }
+ }
+
+ Row {
+ id: actionButtons
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ iconName: "push_pin"
+ iconSize: Theme.iconSize - 6
+ iconColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primary : Theme.surfaceText
+ backgroundColor: (entry.pinned || hasPinnedDuplicate) ? Theme.primarySelected : "transparent"
+ onClicked: entry.pinned ? unpinRequested() : pinRequested()
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 6
+ iconColor: Theme.surfaceText
+ onClicked: deleteRequested()
+ }
+ }
+
+ Item {
+ anchors.left: indexBadge.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: actionButtons.left
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ // height: contentColumn.implicitHeight
+ height: ClipboardConstants.itemHeight
+ clip: true
+
+ ClipboardThumbnail {
+ id: thumbnail
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
+ height: entryType === "image" ? ClipboardConstants.itemHeight - 4 : Theme.iconSize // 100 - 4 = 96, 96:72 = 4:3
+ entry: root.entry
+ entryType: root.entryType
+ modal: root.modal
+ listView: root.listView
+ itemIndex: root.itemIndex
+ }
+
+ Column {
+ id: contentColumn
+ anchors.left: thumbnail.right
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: {
+ switch (entryType) {
+ case "image":
+ return I18n.tr("Image") + " • " + entryPreview;
+ case "long_text":
+ return I18n.tr("Long Text");
+ default:
+ return I18n.tr("Text");
+ }
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ font.weight: Font.Medium
+ width: parent.width
+ elide: Text.ElideRight
+ }
+
+ StyledText {
+ text: entryPreview
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ maximumLineCount: entryType === "long_text" ? 3 : 1
+ elide: Text.ElideRight
+ textFormat: Text.PlainText
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ anchors.rightMargin: 80
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => {
+ const pos = mouseArea.mapToItem(root, mouse.x, mouse.y);
+ rippleLayer.trigger(pos.x, pos.y);
+ }
+ onClicked: copyRequested()
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml
new file mode 100644
index 0000000..fedba15
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHeader.qml
@@ -0,0 +1,87 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+import qs.Modals.Clipboard
+
+Item {
+ id: header
+
+ property int totalCount: 0
+ property bool showKeyboardHints: false
+ property string activeTab: "recents"
+ property int pinnedCount: 0
+
+ signal keyboardHintsToggled
+ signal clearAllClicked
+ signal closeClicked
+ signal tabChanged(string tabName)
+
+ height: ClipboardConstants.headerHeight
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: "content_paste"
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Clipboard History") + ` (${totalCount})`
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ Row {
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ DankActionButton {
+ iconName: "push_pin"
+ iconSize: Theme.iconSize - 4
+ iconColor: header.activeTab === "saved" ? Theme.primary : Theme.surfaceText
+ visible: header.pinnedCount > 0
+ tooltipText: I18n.tr("Saved")
+ onClicked: tabChanged("saved")
+ }
+
+ DankActionButton {
+ iconName: "history"
+ iconSize: Theme.iconSize - 4
+ iconColor: header.activeTab === "recents" ? Theme.primary : Theme.surfaceText
+ tooltipText: I18n.tr("History")
+ onClicked: tabChanged("recents")
+ }
+
+ DankActionButton {
+ iconName: "info"
+ iconSize: Theme.iconSize - 4
+ iconColor: showKeyboardHints ? Theme.primary : Theme.surfaceText
+ tooltipText: I18n.tr("Keyboard Shortcuts")
+ onClicked: keyboardHintsToggled()
+ }
+
+ DankActionButton {
+ iconName: "delete_sweep"
+ iconSize: Theme.iconSize
+ iconColor: Theme.surfaceText
+ tooltipText: I18n.tr("Clear All")
+ onClicked: clearAllClicked()
+ }
+
+ DankActionButton {
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: closeClicked()
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml
new file mode 100644
index 0000000..2f76cc7
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryModal.qml
@@ -0,0 +1,174 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import Quickshell.Hyprland
+import qs.Common
+import qs.Modals.Clipboard
+import qs.Modals.Common
+import qs.Services
+
+DankModal {
+ id: clipboardHistoryModal
+
+ layerNamespace: "dms:clipboard"
+
+ HyprlandFocusGrab {
+ windows: [clipboardHistoryModal.contentWindow]
+ active: clipboardHistoryModal.useHyprlandFocusGrab && clipboardHistoryModal.shouldHaveFocus
+ }
+
+ property string activeTab: "recents"
+ onActiveTabChanged: {
+ ClipboardService.selectedIndex = 0;
+ ClipboardService.keyboardNavigationActive = false;
+ }
+ property bool showKeyboardHints: false
+ property Component clipboardContent
+ property int activeImageLoads: 0
+ readonly property int maxConcurrentLoads: 3
+
+ readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
+ readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
+ readonly property int totalCount: ClipboardService.totalCount
+ readonly property var clipboardEntries: ClipboardService.clipboardEntries
+ readonly property var pinnedEntries: ClipboardService.pinnedEntries
+ readonly property int pinnedCount: ClipboardService.pinnedCount
+ readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
+ readonly property int selectedIndex: ClipboardService.selectedIndex
+ readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
+ property string searchText: ClipboardService.searchText
+ onSearchTextChanged: ClipboardService.searchText = searchText
+
+ Ref {
+ service: ClipboardService
+ }
+
+ function updateFilteredModel() {
+ ClipboardService.updateFilteredModel();
+ }
+
+ function pasteSelected() {
+ ClipboardService.pasteSelected(instantClose);
+ }
+
+ function toggle() {
+ if (shouldBeVisible) {
+ hide();
+ } else {
+ show();
+ }
+ }
+
+ function show() {
+ open();
+ activeImageLoads = 0;
+ shouldHaveFocus = true;
+ ClipboardService.reset();
+ if (clipboardAvailable)
+ ClipboardService.refresh();
+ keyboardController.reset();
+
+ Qt.callLater(function () {
+ if (contentLoader.item?.searchField) {
+ contentLoader.item.searchField.text = "";
+ contentLoader.item.searchField.forceActiveFocus();
+ }
+ });
+ }
+
+ function hide() {
+ close();
+ }
+
+ onDialogClosed: {
+ activeImageLoads = 0;
+ ClipboardService.reset();
+ keyboardController.reset();
+ }
+
+ function refreshClipboard() {
+ ClipboardService.refresh();
+ }
+
+ function copyEntry(entry) {
+ ClipboardService.copyEntry(entry, hide);
+ }
+
+ function deleteEntry(entry) {
+ ClipboardService.deleteEntry(entry);
+ }
+
+ function deletePinnedEntry(entry) {
+ ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
+ }
+
+ function pinEntry(entry) {
+ ClipboardService.pinEntry(entry);
+ }
+
+ function unpinEntry(entry) {
+ ClipboardService.unpinEntry(entry);
+ }
+
+ function clearAll() {
+ ClipboardService.clearAll();
+ }
+
+ function getEntryPreview(entry) {
+ return ClipboardService.getEntryPreview(entry);
+ }
+
+ function getEntryType(entry) {
+ return ClipboardService.getEntryType(entry);
+ }
+
+ visible: false
+ modalWidth: ClipboardConstants.modalWidth
+ modalHeight: ClipboardConstants.modalHeight
+ backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ cornerRadius: Theme.cornerRadius
+ borderColor: Theme.outlineMedium
+ borderWidth: 1
+ enableShadow: true
+ onBackgroundClicked: hide()
+ modalFocusScope.Keys.onPressed: function (event) {
+ keyboardController.handleKey(event);
+ }
+ content: clipboardContent
+
+ ClipboardKeyboardController {
+ id: keyboardController
+ modal: clipboardHistoryModal
+ }
+
+ ConfirmModal {
+ id: clearConfirmDialog
+ confirmButtonText: I18n.tr("Clear All")
+ confirmButtonColor: Theme.primary
+ onVisibleChanged: {
+ if (visible) {
+ clipboardHistoryModal.shouldHaveFocus = false;
+ return;
+ }
+ Qt.callLater(function () {
+ if (!clipboardHistoryModal.shouldBeVisible) {
+ return;
+ }
+ clipboardHistoryModal.shouldHaveFocus = true;
+ clipboardHistoryModal.modalFocusScope.forceActiveFocus();
+ if (clipboardHistoryModal.contentLoader.item?.searchField) {
+ clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
+ }
+ });
+ }
+ }
+
+ property var confirmDialog: clearConfirmDialog
+
+ clipboardContent: Component {
+ ClipboardContent {
+ modal: clipboardHistoryModal
+ clearConfirmDialog: clipboardHistoryModal.confirmDialog
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml
new file mode 100644
index 0000000..c4f0296
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardHistoryPopout.qml
@@ -0,0 +1,197 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import qs.Common
+import qs.Modals.Clipboard
+import qs.Modals.Common
+import qs.Services
+import qs.Widgets
+
+DankPopout {
+ id: root
+
+ layerNamespace: "dms:clipboard-popout"
+
+ property var parentWidget: null
+ property var triggerScreen: null
+ property string activeTab: "recents"
+ property bool showKeyboardHints: false
+ property int activeImageLoads: 0
+ readonly property int maxConcurrentLoads: 3
+
+ readonly property bool clipboardAvailable: ClipboardService.clipboardAvailable
+ readonly property bool wtypeAvailable: ClipboardService.wtypeAvailable
+ readonly property int totalCount: ClipboardService.totalCount
+ readonly property var clipboardEntries: ClipboardService.clipboardEntries
+ readonly property var pinnedEntries: ClipboardService.pinnedEntries
+ readonly property int pinnedCount: ClipboardService.pinnedCount
+ readonly property var unpinnedEntries: ClipboardService.unpinnedEntries
+ readonly property int selectedIndex: ClipboardService.selectedIndex
+ readonly property bool keyboardNavigationActive: ClipboardService.keyboardNavigationActive
+ property string searchText: ClipboardService.searchText
+ onSearchTextChanged: ClipboardService.searchText = searchText
+
+ readonly property var modalFocusScope: contentLoader.item ?? null
+
+ Ref {
+ service: ClipboardService
+ }
+
+ function updateFilteredModel() {
+ ClipboardService.updateFilteredModel();
+ }
+
+ function pasteSelected() {
+ ClipboardService.pasteSelected(instantClose);
+ }
+
+ function instantClose() {
+ close();
+ }
+
+ function show() {
+ open();
+ activeImageLoads = 0;
+ ClipboardService.reset();
+ if (clipboardAvailable)
+ ClipboardService.refresh();
+ keyboardController.reset();
+
+ Qt.callLater(function () {
+ if (contentLoader.item?.searchField) {
+ contentLoader.item.searchField.text = "";
+ contentLoader.item.searchField.forceActiveFocus();
+ }
+ });
+ }
+
+ function hide() {
+ close();
+ activeImageLoads = 0;
+ ClipboardService.reset();
+ keyboardController.reset();
+ }
+
+ function refreshClipboard() {
+ ClipboardService.refresh();
+ }
+
+ function copyEntry(entry) {
+ ClipboardService.copyEntry(entry, hide);
+ }
+
+ function deleteEntry(entry) {
+ ClipboardService.deleteEntry(entry);
+ }
+
+ function deletePinnedEntry(entry) {
+ ClipboardService.deletePinnedEntry(entry, clearConfirmDialog);
+ }
+
+ function pinEntry(entry) {
+ ClipboardService.pinEntry(entry);
+ }
+
+ function unpinEntry(entry) {
+ ClipboardService.unpinEntry(entry);
+ }
+
+ function clearAll() {
+ ClipboardService.clearAll();
+ }
+
+ function getEntryPreview(entry) {
+ return ClipboardService.getEntryPreview(entry);
+ }
+
+ function getEntryType(entry) {
+ return ClipboardService.getEntryType(entry);
+ }
+
+ popupWidth: ClipboardConstants.popoutWidth
+ popupHeight: ClipboardConstants.popoutHeight
+ triggerWidth: 55
+ positioning: ""
+ screen: triggerScreen
+ shouldBeVisible: false
+ contentHandlesKeys: true
+
+ onBackgroundClicked: hide()
+
+ onShouldBeVisibleChanged: {
+ if (!shouldBeVisible)
+ return;
+ if (clipboardAvailable)
+ ClipboardService.refresh();
+ keyboardController.reset();
+ Qt.callLater(function () {
+ if (contentLoader.item?.searchField) {
+ contentLoader.item.searchField.text = "";
+ contentLoader.item.searchField.forceActiveFocus();
+ }
+ });
+ }
+
+ onPopoutClosed: {
+ activeImageLoads = 0;
+ ClipboardService.reset();
+ keyboardController.reset();
+ }
+
+ ClipboardKeyboardController {
+ id: keyboardController
+ modal: root
+ }
+
+ ConfirmModal {
+ id: clearConfirmDialog
+ confirmButtonText: I18n.tr("Clear All")
+ confirmButtonColor: Theme.primary
+ }
+
+ property var confirmDialog: clearConfirmDialog
+
+ content: Component {
+ FocusScope {
+ id: contentFocusScope
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ focus: true
+
+ property alias searchField: clipboardContentItem.searchField
+
+ Keys.onPressed: function (event) {
+ keyboardController.handleKey(event);
+ }
+
+ Component.onCompleted: {
+ if (root.shouldBeVisible)
+ forceActiveFocus();
+ }
+
+ Connections {
+ target: root
+ function onShouldBeVisibleChanged() {
+ if (root.shouldBeVisible) {
+ Qt.callLater(() => contentFocusScope.forceActiveFocus());
+ }
+ }
+ function onOpened() {
+ Qt.callLater(() => {
+ if (clipboardContentItem.searchField) {
+ clipboardContentItem.searchField.forceActiveFocus();
+ }
+ });
+ }
+ }
+
+ ClipboardContent {
+ id: clipboardContentItem
+ modal: root
+ clearConfirmDialog: root.confirmDialog
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml
new file mode 100644
index 0000000..f2a457e
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardController.qml
@@ -0,0 +1,166 @@
+import QtQuick
+import qs.Common
+import qs.Services
+
+QtObject {
+ id: keyboardController
+
+ required property var modal
+
+ function reset() {
+ ClipboardService.selectedIndex = 0;
+ ClipboardService.keyboardNavigationActive = false;
+ modal.showKeyboardHints = false;
+ }
+
+ function selectNext() {
+ const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
+ if (!entries || entries.length === 0) {
+ return;
+ }
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = Math.min(ClipboardService.selectedIndex + 1, entries.length - 1);
+ }
+
+ function selectPrevious() {
+ const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
+ if (!entries || entries.length === 0) {
+ return;
+ }
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = Math.max(ClipboardService.selectedIndex - 1, 0);
+ }
+
+ function copySelected() {
+ const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
+ if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) {
+ return;
+ }
+ const selectedEntry = entries[ClipboardService.selectedIndex];
+ modal.copyEntry(selectedEntry);
+ }
+
+ function deleteSelected() {
+ const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
+ if (!entries || entries.length === 0 || ClipboardService.selectedIndex < 0 || ClipboardService.selectedIndex >= entries.length) {
+ return;
+ }
+ const selectedEntry = entries[ClipboardService.selectedIndex];
+ if (modal.activeTab === "saved") {
+ modal.deletePinnedEntry(selectedEntry);
+ } else {
+ modal.deleteEntry(selectedEntry);
+ }
+ }
+
+ function handleKey(event) {
+ switch (event.key) {
+ case Qt.Key_Escape:
+ if (ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = false;
+ } else {
+ modal.hide();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_Down:
+ case Qt.Key_Tab:
+ if (!ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = 0;
+ } else {
+ selectNext();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_Up:
+ case Qt.Key_Backtab:
+ if (!ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = 0;
+ } else if (ClipboardService.selectedIndex === 0) {
+ ClipboardService.keyboardNavigationActive = false;
+ } else {
+ selectPrevious();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_F10:
+ modal.showKeyboardHints = !modal.showKeyboardHints;
+ event.accepted = true;
+ return;
+ }
+
+ if (event.modifiers & Qt.ControlModifier) {
+ switch (event.key) {
+ case Qt.Key_N:
+ case Qt.Key_J:
+ if (!ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = 0;
+ } else {
+ selectNext();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_P:
+ case Qt.Key_K:
+ if (!ClipboardService.keyboardNavigationActive) {
+ ClipboardService.keyboardNavigationActive = true;
+ ClipboardService.selectedIndex = 0;
+ } else if (ClipboardService.selectedIndex === 0) {
+ ClipboardService.keyboardNavigationActive = false;
+ } else {
+ selectPrevious();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_C:
+ if (ClipboardService.keyboardNavigationActive) {
+ copySelected();
+ event.accepted = true;
+ }
+ return;
+ }
+ }
+
+ if (event.modifiers & Qt.ShiftModifier) {
+ switch (event.key) {
+ case Qt.Key_Delete:
+ modal.clearAll();
+ modal.hide();
+ event.accepted = true;
+ return;
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (ClipboardService.keyboardNavigationActive) {
+ if (SettingsData.clipboardEnterToPaste) {
+ copySelected();
+ } else {
+ modal.pasteSelected();
+ }
+ event.accepted = true;
+ }
+ return;
+ }
+ }
+
+ if (ClipboardService.keyboardNavigationActive) {
+ switch (event.key) {
+ case Qt.Key_Return:
+ case Qt.Key_Enter:
+ if (SettingsData.clipboardEnterToPaste) {
+ modal.pasteSelected();
+ } else {
+ copySelected();
+ }
+ event.accepted = true;
+ return;
+ case Qt.Key_Delete:
+ deleteSelected();
+ event.accepted = true;
+ return;
+ }
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml
new file mode 100644
index 0000000..0285952
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardKeyboardHints.qml
@@ -0,0 +1,51 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: keyboardHints
+
+ property bool wtypeAvailable: false
+ property bool enterToPaste: false
+ readonly property string hintsText: {
+ if (!wtypeAvailable)
+ return I18n.tr("Shift+Del: Clear All • Esc: Close");
+ return enterToPaste ? I18n.tr("Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close");
+ }
+
+ height: ClipboardConstants.keyboardHintsHeight
+ radius: Theme.cornerRadius
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
+ border.color: Theme.primary
+ border.width: 2
+ opacity: visible ? 1 : 0
+ z: 100
+
+ Column {
+ anchors.centerIn: parent
+ spacing: 2
+
+ StyledText {
+ text: keyboardHints.enterToPaste
+ ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled")
+ : I18n.tr("↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: keyboardHints.hintsText
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+}
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml
new file mode 100644
index 0000000..26a9210
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/quickshell/Modals/Clipboard/ClipboardThumbnail.qml
@@ -0,0 +1,178 @@
+import QtQuick
+import QtQuick.Effects
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: thumbnail
+
+ required property var entry
+ required property string entryType
+ required property var modal
+ required property var listView
+ required property int itemIndex
+
+ Image {
+ id: thumbnailImage
+
+ property bool isVisible: false
+ property string cachedImageData: ""
+ property bool loadQueued: false
+
+ anchors.fill: parent
+ source: cachedImageData ? `data:image/png;base64,${cachedImageData}` : ""
+ fillMode: Image.PreserveAspectCrop
+ smooth: true
+ cache: false
+ visible: false
+ asynchronous: true
+ sourceSize.width: 128
+ sourceSize.height: 128
+
+ function tryLoadImage() {
+ if (thumbnailImage.loadQueued || entryType !== "image" || thumbnailImage.cachedImageData) {
+ return;
+ }
+ thumbnailImage.loadQueued = true;
+ if (modal.activeImageLoads < modal.maxConcurrentLoads) {
+ modal.activeImageLoads++;
+ thumbnailImage.loadImage();
+ } else {
+ retryTimer.restart();
+ }
+ }
+
+ function loadImage() {
+ DMSService.sendRequest("clipboard.getEntry", {
+ "id": entry.id
+ }, function (response) {
+ thumbnailImage.loadQueued = false;
+ if (modal.activeImageLoads > 0) {
+ modal.activeImageLoads--;
+ }
+ if (response.error) {
+ console.warn("ClipboardThumbnail: Failed to load image:", entry.id);
+ return;
+ }
+ const data = response.result?.data;
+ if (data) {
+ thumbnailImage.cachedImageData = data;
+ }
+ });
+ }
+
+ Timer {
+ id: retryTimer
+ interval: ClipboardConstants.retryInterval
+ onTriggered: {
+ if (!thumbnailImage.loadQueued) {
+ return;
+ }
+ if (modal.activeImageLoads < modal.maxConcurrentLoads) {
+ modal.activeImageLoads++;
+ thumbnailImage.loadImage();
+ } else {
+ retryTimer.restart();
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ if (entryType !== "image" || listView.height <= 0) {
+ return;
+ }
+
+ const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing);
+ const viewTop = listView.contentY;
+ const viewBottom = viewTop + listView.height;
+ isVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom);
+
+ if (isVisible) {
+ tryLoadImage();
+ }
+ }
+
+ Timer {
+ id: visibilityTimer
+ interval: 100
+ onTriggered: thumbnailImage.checkVisibility()
+ }
+
+ function checkVisibility() {
+ if (entryType !== "image" || listView.height <= 0 || isVisible) {
+ return;
+ }
+ const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing);
+ const viewTop = listView.contentY - ClipboardConstants.viewportBuffer;
+ const viewBottom = viewTop + listView.height + ClipboardConstants.extendedBuffer;
+ const nowVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom);
+ if (nowVisible) {
+ isVisible = true;
+ tryLoadImage();
+ }
+ }
+
+ Connections {
+ target: listView
+
+ function onContentYChanged() {
+ if (thumbnailImage.isVisible || entryType !== "image") {
+ return;
+ }
+ visibilityTimer.restart();
+ }
+
+ function onHeightChanged() {
+ if (thumbnailImage.isVisible || entryType !== "image") {
+ return;
+ }
+ visibilityTimer.restart();
+ }
+ }
+ }
+
+ MultiEffect {
+ anchors.fill: parent
+ anchors.margins: 2
+ source: thumbnailImage
+ maskEnabled: true
+ maskSource: clipboardRoundedRectangularMask
+ visible: entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != ""
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 1
+ }
+
+ Item {
+ id: clipboardRoundedRectangularMask
+ width: ClipboardConstants.thumbnailSize
+ height: ClipboardConstants.itemHeight - 4
+ layer.enabled: true
+ layer.smooth: true
+ visible: false
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius / 2 // Thumbnail corner radius is divided by 2 so it doesnt look weird on large corner radius (eg: 32px)
+ color: "black"
+ antialiasing: true
+ }
+ }
+
+ DankIcon {
+ visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
+ name: {
+ switch (entryType) {
+ case "image":
+ return "image";
+ case "long_text":
+ return "subject";
+ default:
+ return "content_copy";
+ }
+ }
+ size: Theme.iconSize
+ color: Theme.primary
+ anchors.centerIn: parent
+ }
+}