mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
clipboard: introduce native clipboard, clip-persist, clip-storage functionality
This commit is contained in:
@@ -865,4 +865,32 @@ Item {
|
||||
|
||||
target: "plugins"
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
if (!PopoutService.clipboardHistoryModal) {
|
||||
return "CLIPBOARD_NOT_AVAILABLE";
|
||||
}
|
||||
PopoutService.clipboardHistoryModal.show();
|
||||
return "CLIPBOARD_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
if (!PopoutService.clipboardHistoryModal) {
|
||||
return "CLIPBOARD_NOT_AVAILABLE";
|
||||
}
|
||||
PopoutService.clipboardHistoryModal.hide();
|
||||
return "CLIPBOARD_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
if (!PopoutService.clipboardHistoryModal) {
|
||||
return "CLIPBOARD_NOT_AVAILABLE";
|
||||
}
|
||||
PopoutService.clipboardHistoryModal.toggle();
|
||||
return "CLIPBOARD_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "clipboard"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modals.Clipboard
|
||||
|
||||
Item {
|
||||
id: clipboardContent
|
||||
|
||||
required property var modal
|
||||
required property var filteredModel
|
||||
required property var clearConfirmDialog
|
||||
|
||||
property alias searchField: searchField
|
||||
@@ -22,7 +20,6 @@ Item {
|
||||
spacing: Theme.spacingL
|
||||
focus: false
|
||||
|
||||
// Header
|
||||
ClipboardHeader {
|
||||
id: header
|
||||
width: parent.width
|
||||
@@ -31,14 +28,13 @@ Item {
|
||||
onKeyboardHintsToggled: modal.showKeyboardHints = !modal.showKeyboardHints
|
||||
onClearAllClicked: {
|
||||
clearConfirmDialog.show(I18n.tr("Clear All History?"), I18n.tr("This will permanently delete all clipboard history."), function () {
|
||||
modal.clearAll()
|
||||
modal.hide()
|
||||
}, function () {})
|
||||
modal.clearAll();
|
||||
modal.hide();
|
||||
}, function () {});
|
||||
}
|
||||
onCloseClicked: modal.hide()
|
||||
}
|
||||
|
||||
// Search Field
|
||||
DankTextField {
|
||||
id: searchField
|
||||
width: parent.width
|
||||
@@ -49,30 +45,29 @@ Item {
|
||||
ignoreTabKeys: true
|
||||
keyForwardTargets: [modal.modalFocusScope]
|
||||
onTextChanged: {
|
||||
modal.searchText = text
|
||||
modal.updateFilteredModel()
|
||||
modal.searchText = text;
|
||||
modal.updateFilteredModel();
|
||||
}
|
||||
Keys.onEscapePressed: function (event) {
|
||||
modal.hide()
|
||||
event.accepted = true
|
||||
modal.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(function () {
|
||||
forceActiveFocus()
|
||||
})
|
||||
forceActiveFocus();
|
||||
});
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: modal
|
||||
function onOpened() {
|
||||
Qt.callLater(function () {
|
||||
searchField.forceActiveFocus()
|
||||
})
|
||||
searchField.forceActiveFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List Container
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - ClipboardConstants.headerHeight - 70
|
||||
@@ -83,7 +78,10 @@ Item {
|
||||
DankListView {
|
||||
id: clipboardListView
|
||||
anchors.fill: parent
|
||||
model: filteredModel
|
||||
model: ScriptModel {
|
||||
values: clipboardContent.modal.clipboardEntries
|
||||
objectProp: "id"
|
||||
}
|
||||
|
||||
currentIndex: clipboardContent.modal ? clipboardContent.modal.selectedIndex : 0
|
||||
spacing: Theme.spacingXS
|
||||
@@ -97,21 +95,21 @@ Item {
|
||||
|
||||
function ensureVisible(index) {
|
||||
if (index < 0 || index >= count) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const itemHeight = ClipboardConstants.itemHeight + spacing
|
||||
const itemY = index * itemHeight
|
||||
const itemBottom = itemY + itemHeight
|
||||
const itemHeight = ClipboardConstants.itemHeight + spacing;
|
||||
const itemY = index * itemHeight;
|
||||
const itemBottom = itemY + itemHeight;
|
||||
if (itemY < contentY) {
|
||||
contentY = itemY
|
||||
contentY = itemY;
|
||||
} else if (itemBottom > contentY + height) {
|
||||
contentY = itemBottom - height
|
||||
contentY = itemBottom - height;
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && currentIndex >= 0) {
|
||||
ensureVisible(currentIndex)
|
||||
if (clipboardContent.modal?.keyboardNavigationActive && currentIndex >= 0) {
|
||||
ensureVisible(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,28 +118,27 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: filteredModel.count === 0
|
||||
visible: clipboardContent.modal.clipboardEntries.length === 0
|
||||
}
|
||||
|
||||
delegate: ClipboardEntry {
|
||||
required property int index
|
||||
required property var model
|
||||
required property var modelData
|
||||
|
||||
width: clipboardListView.width
|
||||
height: ClipboardConstants.itemHeight
|
||||
entryData: model.entry
|
||||
entry: modelData
|
||||
entryIndex: index + 1
|
||||
itemIndex: index
|
||||
isSelected: clipboardContent.modal && clipboardContent.modal.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
|
||||
isSelected: clipboardContent.modal?.keyboardNavigationActive && index === clipboardContent.modal.selectedIndex
|
||||
modal: clipboardContent.modal
|
||||
listView: clipboardListView
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(model.entry)
|
||||
onDeleteRequested: clipboardContent.modal.deleteEntry(model.entry)
|
||||
onCopyRequested: clipboardContent.modal.copyEntry(modelData)
|
||||
onDeleteRequested: clipboardContent.modal.deleteEntry(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spacer for keyboard hints
|
||||
Item {
|
||||
width: parent.width
|
||||
height: modal.showKeyboardHints ? ClipboardConstants.keyboardHintsHeight + Theme.spacingL : 0
|
||||
@@ -155,7 +152,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard Hints Overlay
|
||||
ClipboardKeyboardHints {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modals.Clipboard
|
||||
|
||||
Rectangle {
|
||||
id: entry
|
||||
id: root
|
||||
|
||||
required property string entryData
|
||||
required property var entry
|
||||
required property int entryIndex
|
||||
required property int itemIndex
|
||||
required property bool isSelected
|
||||
@@ -18,15 +15,15 @@ Rectangle {
|
||||
signal copyRequested
|
||||
signal deleteRequested
|
||||
|
||||
readonly property string entryType: modal ? modal.getEntryType(entryData) : "text"
|
||||
readonly property string entryPreview: modal ? modal.getEntryPreview(entryData) : entryData
|
||||
readonly property string entryType: modal ? modal.getEntryType(entry) : "text"
|
||||
readonly property string entryPreview: modal ? modal.getEntryPreview(entry) : ""
|
||||
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (isSelected) {
|
||||
return Theme.primaryPressed
|
||||
return Theme.primaryPressed;
|
||||
}
|
||||
return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||
return mouseArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -35,7 +32,6 @@ Rectangle {
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Index indicator
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
@@ -52,25 +48,22 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Content area
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 68
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Thumbnail/Icon
|
||||
ClipboardThumbnail {
|
||||
width: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
|
||||
height: entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
entryData: entry.entryData
|
||||
entryType: entry.entryType
|
||||
modal: entry.modal
|
||||
listView: entry.listView
|
||||
itemIndex: entry.itemIndex
|
||||
entry: root.entry
|
||||
entryType: root.entryType
|
||||
modal: root.modal
|
||||
listView: root.listView
|
||||
itemIndex: root.itemIndex
|
||||
}
|
||||
|
||||
// Text content
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (entryType === "image" ? ClipboardConstants.thumbnailSize : Theme.iconSize) - Theme.spacingM
|
||||
@@ -80,11 +73,11 @@ Rectangle {
|
||||
text: {
|
||||
switch (entryType) {
|
||||
case "image":
|
||||
return I18n.tr("Image") + " • " + entryPreview
|
||||
return I18n.tr("Image") + " • " + entryPreview;
|
||||
case "long_text":
|
||||
return I18n.tr("Long Text")
|
||||
return I18n.tr("Long Text");
|
||||
default:
|
||||
return I18n.tr("Text")
|
||||
return I18n.tr("Text");
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
@@ -107,7 +100,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete button
|
||||
DankActionButton {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
@@ -118,7 +110,6 @@ Rectangle {
|
||||
onClicked: deleteRequested()
|
||||
}
|
||||
|
||||
// Click area
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
@@ -27,33 +25,27 @@ DankModal {
|
||||
property Component clipboardContent
|
||||
property int activeImageLoads: 0
|
||||
readonly property int maxConcurrentLoads: 3
|
||||
readonly property bool clipboardAvailable: DMSService.isConnected && DMSService.capabilities.includes("clipboard")
|
||||
|
||||
function updateFilteredModel() {
|
||||
filteredClipboardModel.clear();
|
||||
for (var i = 0; i < clipboardModel.count; i++) {
|
||||
const entry = clipboardModel.get(i).entry;
|
||||
if (searchText.trim().length === 0) {
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
});
|
||||
} else {
|
||||
const content = getEntryPreview(entry).toLowerCase();
|
||||
if (content.includes(searchText.toLowerCase())) {
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
});
|
||||
}
|
||||
}
|
||||
const query = searchText.trim();
|
||||
if (query.length === 0) {
|
||||
clipboardEntries = internalEntries;
|
||||
} else {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
clipboardEntries = internalEntries.filter(entry => entry.preview.toLowerCase().includes(lowerQuery));
|
||||
}
|
||||
clipboardHistoryModal.totalCount = filteredClipboardModel.count;
|
||||
if (filteredClipboardModel.count === 0) {
|
||||
totalCount = clipboardEntries.length;
|
||||
if (clipboardEntries.length === 0) {
|
||||
keyboardNavigationActive = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex >= filteredClipboardModel.count) {
|
||||
selectedIndex = filteredClipboardModel.count - 1;
|
||||
} else if (selectedIndex >= clipboardEntries.length) {
|
||||
selectedIndex = clipboardEntries.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
property var internalEntries: []
|
||||
|
||||
function toggle() {
|
||||
if (shouldBeVisible) {
|
||||
hide();
|
||||
@@ -63,15 +55,19 @@ DankModal {
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (!clipboardAvailable) {
|
||||
ToastService.showError(I18n.tr("Clipboard service not available"));
|
||||
return;
|
||||
}
|
||||
open();
|
||||
clipboardHistoryModal.searchText = "";
|
||||
clipboardHistoryModal.activeImageLoads = 0;
|
||||
clipboardHistoryModal.shouldHaveFocus = true;
|
||||
searchText = "";
|
||||
activeImageLoads = 0;
|
||||
shouldHaveFocus = true;
|
||||
refreshClipboard();
|
||||
keyboardController.reset();
|
||||
|
||||
Qt.callLater(function () {
|
||||
if (contentLoader.item && contentLoader.item.searchField) {
|
||||
if (contentLoader.item?.searchField) {
|
||||
contentLoader.item.searchField.text = "";
|
||||
contentLoader.item.searchField.forceActiveFocus();
|
||||
}
|
||||
@@ -80,60 +76,90 @@ DankModal {
|
||||
|
||||
function hide() {
|
||||
close();
|
||||
clipboardHistoryModal.searchText = "";
|
||||
clipboardHistoryModal.activeImageLoads = 0;
|
||||
updateFilteredModel();
|
||||
searchText = "";
|
||||
activeImageLoads = 0;
|
||||
internalEntries = [];
|
||||
clipboardEntries = [];
|
||||
keyboardController.reset();
|
||||
cleanupTempFiles();
|
||||
}
|
||||
|
||||
function cleanupTempFiles() {
|
||||
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"]);
|
||||
}
|
||||
|
||||
function refreshClipboard() {
|
||||
clipboardProcesses.refresh();
|
||||
DMSService.sendRequest("clipboard.getHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to get history:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = response.result || [];
|
||||
updateFilteredModel();
|
||||
});
|
||||
}
|
||||
|
||||
function copyEntry(entry) {
|
||||
const entryId = entry.split('\t')[0];
|
||||
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`]);
|
||||
ToastService.showInfo(I18n.tr("Copied to clipboard"));
|
||||
hide();
|
||||
DMSService.sendRequest("clipboard.getEntry", {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to copy entry"));
|
||||
return;
|
||||
}
|
||||
const fullEntry = response.result;
|
||||
if (fullEntry.isImage) {
|
||||
ToastService.showInfo(I18n.tr("Image copied to clipboard"));
|
||||
} else {
|
||||
DMSService.sendRequest("clipboard.copy", {
|
||||
"text": fullEntry.data
|
||||
}, function (copyResponse) {
|
||||
if (copyResponse.error) {
|
||||
ToastService.showError(I18n.tr("Failed to copy"));
|
||||
return;
|
||||
}
|
||||
ToastService.showInfo(I18n.tr("Copied to clipboard"));
|
||||
});
|
||||
}
|
||||
hide();
|
||||
});
|
||||
}
|
||||
|
||||
function deleteEntry(entry) {
|
||||
clipboardProcesses.deleteEntry(entry);
|
||||
DMSService.sendRequest("clipboard.deleteEntry", {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to delete entry:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = internalEntries.filter(e => e.id !== entry.id);
|
||||
updateFilteredModel();
|
||||
if (clipboardEntries.length === 0) {
|
||||
keyboardNavigationActive = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex >= clipboardEntries.length) {
|
||||
selectedIndex = clipboardEntries.length - 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
clipboardProcesses.clearAll();
|
||||
DMSService.sendRequest("clipboard.clearHistory", null, function (response) {
|
||||
if (response.error) {
|
||||
console.warn("ClipboardHistoryModal: Failed to clear history:", response.error);
|
||||
return;
|
||||
}
|
||||
internalEntries = [];
|
||||
clipboardEntries = [];
|
||||
totalCount = 0;
|
||||
});
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
let content = entry.replace(/^\s*\d+\s+/, "");
|
||||
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
||||
const dimensionMatch = content.match(/(\d+)x(\d+)/);
|
||||
if (dimensionMatch) {
|
||||
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`;
|
||||
}
|
||||
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i);
|
||||
if (typeMatch) {
|
||||
return `Image (${typeMatch[1].toUpperCase()})`;
|
||||
}
|
||||
return "Image";
|
||||
}
|
||||
if (content.length > ClipboardConstants.previewLength) {
|
||||
return content.substring(0, ClipboardConstants.previewLength) + "...";
|
||||
}
|
||||
return content;
|
||||
return entry.preview || "";
|
||||
}
|
||||
|
||||
function getEntryType(entry) {
|
||||
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) {
|
||||
if (entry.isImage) {
|
||||
return "image";
|
||||
}
|
||||
if (entry.length > ClipboardConstants.longTextThreshold) {
|
||||
if (entry.size > ClipboardConstants.longTextThreshold) {
|
||||
return "long_text";
|
||||
}
|
||||
return "text";
|
||||
@@ -168,55 +194,18 @@ DankModal {
|
||||
} else if (clipboardHistoryModal.shouldBeVisible) {
|
||||
clipboardHistoryModal.shouldHaveFocus = true;
|
||||
clipboardHistoryModal.modalFocusScope.forceActiveFocus();
|
||||
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
|
||||
if (clipboardHistoryModal.contentLoader.item?.searchField) {
|
||||
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property alias filteredClipboardModel: filteredClipboardModel
|
||||
property alias clipboardModel: clipboardModel
|
||||
property var confirmDialog: clearConfirmDialog
|
||||
|
||||
ListModel {
|
||||
id: clipboardModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredClipboardModel
|
||||
}
|
||||
|
||||
ClipboardProcesses {
|
||||
id: clipboardProcesses
|
||||
modal: clipboardHistoryModal
|
||||
clipboardModel: clipboardModel
|
||||
filteredClipboardModel: filteredClipboardModel
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
clipboardHistoryModal.show();
|
||||
return "CLIPBOARD_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
clipboardHistoryModal.hide();
|
||||
return "CLIPBOARD_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
clipboardHistoryModal.toggle();
|
||||
return "CLIPBOARD_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "clipboard"
|
||||
}
|
||||
|
||||
clipboardContent: Component {
|
||||
ClipboardContent {
|
||||
modal: clipboardHistoryModal
|
||||
filteredModel: filteredClipboardModel
|
||||
clearConfirmDialog: clipboardHistoryModal.confirmDialog
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
QtObject {
|
||||
id: keyboardController
|
||||
@@ -7,125 +6,133 @@ QtObject {
|
||||
required property var modal
|
||||
|
||||
function reset() {
|
||||
modal.selectedIndex = 0
|
||||
modal.keyboardNavigationActive = false
|
||||
modal.showKeyboardHints = false
|
||||
modal.selectedIndex = 0;
|
||||
modal.keyboardNavigationActive = false;
|
||||
modal.showKeyboardHints = false;
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
|
||||
return
|
||||
if (!modal.clipboardEntries || modal.clipboardEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = Math.min(modal.selectedIndex + 1, modal.filteredClipboardModel.count - 1)
|
||||
modal.keyboardNavigationActive = true;
|
||||
modal.selectedIndex = Math.min(modal.selectedIndex + 1, modal.clipboardEntries.length - 1);
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0) {
|
||||
return
|
||||
if (!modal.clipboardEntries || modal.clipboardEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = Math.max(modal.selectedIndex - 1, 0)
|
||||
modal.keyboardNavigationActive = true;
|
||||
modal.selectedIndex = Math.max(modal.selectedIndex - 1, 0);
|
||||
}
|
||||
|
||||
function copySelected() {
|
||||
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
|
||||
return
|
||||
if (!modal.clipboardEntries || modal.clipboardEntries.length === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.clipboardEntries.length) {
|
||||
return;
|
||||
}
|
||||
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
|
||||
modal.copyEntry(selectedEntry)
|
||||
const selectedEntry = modal.clipboardEntries[modal.selectedIndex];
|
||||
modal.copyEntry(selectedEntry);
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
if (!modal.filteredClipboardModel || modal.filteredClipboardModel.count === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.filteredClipboardModel.count) {
|
||||
return
|
||||
if (!modal.clipboardEntries || modal.clipboardEntries.length === 0 || modal.selectedIndex < 0 || modal.selectedIndex >= modal.clipboardEntries.length) {
|
||||
return;
|
||||
}
|
||||
const selectedEntry = modal.filteredClipboardModel.get(modal.selectedIndex).entry
|
||||
modal.deleteEntry(selectedEntry)
|
||||
const selectedEntry = modal.clipboardEntries[modal.selectedIndex];
|
||||
modal.deleteEntry(selectedEntry);
|
||||
}
|
||||
|
||||
function handleKey(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Escape:
|
||||
if (modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = false
|
||||
event.accepted = true
|
||||
modal.keyboardNavigationActive = false;
|
||||
} else {
|
||||
modal.hide()
|
||||
event.accepted = true
|
||||
modal.hide();
|
||||
}
|
||||
} else if (event.key === Qt.Key_Down || event.key === Qt.Key_Tab) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Down:
|
||||
case Qt.Key_Tab:
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
event.accepted = true
|
||||
modal.keyboardNavigationActive = true;
|
||||
modal.selectedIndex = 0;
|
||||
} else {
|
||||
selectNext()
|
||||
event.accepted = true
|
||||
selectNext();
|
||||
}
|
||||
} else if (event.key === Qt.Key_Up || event.key === Qt.Key_Backtab) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Up:
|
||||
case Qt.Key_Backtab:
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
event.accepted = true
|
||||
modal.keyboardNavigationActive = true;
|
||||
modal.selectedIndex = 0;
|
||||
} else if (modal.selectedIndex === 0) {
|
||||
modal.keyboardNavigationActive = false
|
||||
event.accepted = true
|
||||
modal.keyboardNavigationActive = false;
|
||||
} else {
|
||||
selectPrevious()
|
||||
event.accepted = true
|
||||
selectPrevious();
|
||||
}
|
||||
} else if (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
} else {
|
||||
selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_P && event.modifiers & Qt.ControlModifier) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
} else if (modal.selectedIndex === 0) {
|
||||
modal.keyboardNavigationActive = false
|
||||
} else {
|
||||
selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
} else {
|
||||
selectNext()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_K && event.modifiers & Qt.ControlModifier) {
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true
|
||||
modal.selectedIndex = 0
|
||||
} else if (modal.selectedIndex === 0) {
|
||||
modal.keyboardNavigationActive = false
|
||||
} else {
|
||||
selectPrevious()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Delete && (event.modifiers & Qt.ShiftModifier)) {
|
||||
modal.clearAll()
|
||||
modal.hide()
|
||||
event.accepted = true
|
||||
} else if (modal.keyboardNavigationActive) {
|
||||
if ((event.key === Qt.Key_C && (event.modifiers & Qt.ControlModifier)) || event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
copySelected()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Delete) {
|
||||
deleteSelected()
|
||||
event.accepted = true
|
||||
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 (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true;
|
||||
modal.selectedIndex = 0;
|
||||
} else {
|
||||
selectNext();
|
||||
}
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_P:
|
||||
case Qt.Key_K:
|
||||
if (!modal.keyboardNavigationActive) {
|
||||
modal.keyboardNavigationActive = true;
|
||||
modal.selectedIndex = 0;
|
||||
} else if (modal.selectedIndex === 0) {
|
||||
modal.keyboardNavigationActive = false;
|
||||
} else {
|
||||
selectPrevious();
|
||||
}
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_C:
|
||||
if (modal.keyboardNavigationActive) {
|
||||
copySelected();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (event.key === Qt.Key_F10) {
|
||||
modal.showKeyboardHints = !modal.showKeyboardHints
|
||||
event.accepted = true
|
||||
|
||||
if (event.modifiers & Qt.ShiftModifier && event.key === Qt.Key_Delete) {
|
||||
modal.clearAll();
|
||||
modal.hide();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (modal.keyboardNavigationActive) {
|
||||
switch (event.key) {
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
copySelected();
|
||||
event.accepted = true;
|
||||
return;
|
||||
case Qt.Key_Delete:
|
||||
deleteSelected();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modals.Clipboard
|
||||
|
||||
Rectangle {
|
||||
id: keyboardHints
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
|
||||
QtObject {
|
||||
id: clipboardProcesses
|
||||
|
||||
required property var modal
|
||||
required property var clipboardModel
|
||||
required property var filteredClipboardModel
|
||||
|
||||
// Load clipboard entries
|
||||
property var loadProcess: Process {
|
||||
id: loadProcess
|
||||
command: ["cliphist", "list"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
clipboardModel.clear()
|
||||
const lines = text.trim().split('\n')
|
||||
for (const line of lines) {
|
||||
if (line.trim().length > 0) {
|
||||
clipboardModel.append({
|
||||
"entry": line
|
||||
})
|
||||
}
|
||||
}
|
||||
modal.updateFilteredModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete single entry
|
||||
property var deleteProcess: Process {
|
||||
id: deleteProcess
|
||||
property string deletedEntry: ""
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
for (var i = 0; i < clipboardModel.count; i++) {
|
||||
if (clipboardModel.get(i).entry === deleteProcess.deletedEntry) {
|
||||
clipboardModel.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
for (var j = 0; j < filteredClipboardModel.count; j++) {
|
||||
if (filteredClipboardModel.get(j).entry === deleteProcess.deletedEntry) {
|
||||
filteredClipboardModel.remove(j)
|
||||
break
|
||||
}
|
||||
}
|
||||
modal.totalCount = filteredClipboardModel.count
|
||||
if (filteredClipboardModel.count === 0) {
|
||||
modal.keyboardNavigationActive = false
|
||||
modal.selectedIndex = 0
|
||||
} else if (modal.selectedIndex >= filteredClipboardModel.count) {
|
||||
modal.selectedIndex = filteredClipboardModel.count - 1
|
||||
}
|
||||
} else {
|
||||
console.warn("Failed to delete clipboard entry")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all entries
|
||||
property var clearProcess: Process {
|
||||
id: clearProcess
|
||||
command: ["cliphist", "wipe"]
|
||||
running: false
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
clipboardModel.clear()
|
||||
filteredClipboardModel.clear()
|
||||
modal.totalCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
loadProcess.running = true
|
||||
}
|
||||
|
||||
function deleteEntry(entry) {
|
||||
deleteProcess.deletedEntry = entry
|
||||
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`]
|
||||
deleteProcess.running = true
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
clearProcess.running = true
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modals.Clipboard
|
||||
|
||||
Item {
|
||||
id: thumbnail
|
||||
|
||||
required property string entryData
|
||||
required property var entry
|
||||
required property string entryType
|
||||
required property var modal
|
||||
required property var listView
|
||||
@@ -17,13 +16,12 @@ Item {
|
||||
Image {
|
||||
id: thumbnailImage
|
||||
|
||||
property string entryId: entryData.split('\t')[0]
|
||||
property bool isVisible: false
|
||||
property string cachedImageData: ""
|
||||
property bool loadQueued: false
|
||||
|
||||
anchors.fill: parent
|
||||
source: ""
|
||||
source: cachedImageData ? `data:image/png;base64,${cachedImageData}` : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
cache: false
|
||||
@@ -32,53 +30,66 @@ Item {
|
||||
sourceSize.width: 128
|
||||
sourceSize.height: 128
|
||||
|
||||
onCachedImageDataChanged: {
|
||||
if (cachedImageData) {
|
||||
source = ""
|
||||
source = `data:image/png;base64,${cachedImageData}`
|
||||
function tryLoadImage() {
|
||||
if (loadQueued || entryType !== "image" || cachedImageData) {
|
||||
return;
|
||||
}
|
||||
loadQueued = true;
|
||||
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||
modal.activeImageLoads++;
|
||||
loadImage();
|
||||
} else {
|
||||
retryTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
function tryLoadImage() {
|
||||
if (!loadQueued && entryType === "image" && !cachedImageData) {
|
||||
loadQueued = true
|
||||
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||
modal.activeImageLoads++
|
||||
imageLoader.running = true
|
||||
} else {
|
||||
retryTimer.restart()
|
||||
function loadImage() {
|
||||
DMSService.sendRequest("clipboard.getEntry", {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
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) {
|
||||
cachedImageData = data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: retryTimer
|
||||
interval: ClipboardConstants.retryInterval
|
||||
onTriggered: {
|
||||
if (thumbnailImage.loadQueued && !imageLoader.running) {
|
||||
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||
modal.activeImageLoads++
|
||||
imageLoader.running = true
|
||||
} else {
|
||||
retryTimer.restart()
|
||||
}
|
||||
if (!thumbnailImage.loadQueued) {
|
||||
return;
|
||||
}
|
||||
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||
modal.activeImageLoads++;
|
||||
thumbnailImage.loadImage();
|
||||
} else {
|
||||
retryTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (entryType !== "image") {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if item is visible on screen initially
|
||||
const itemY = itemIndex * (ClipboardConstants.itemHeight + listView.spacing)
|
||||
const viewTop = listView.contentY
|
||||
const viewBottom = viewTop + listView.height
|
||||
isVisible = (itemY + ClipboardConstants.itemHeight >= viewTop && itemY <= viewBottom)
|
||||
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()
|
||||
tryLoadImage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,48 +97,22 @@ Item {
|
||||
target: listView
|
||||
function onContentYChanged() {
|
||||
if (entryType !== "image") {
|
||||
return
|
||||
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)
|
||||
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 && !thumbnailImage.isVisible) {
|
||||
thumbnailImage.isVisible = true
|
||||
thumbnailImage.tryLoadImage()
|
||||
thumbnailImage.isVisible = true;
|
||||
thumbnailImage.tryLoadImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: imageLoader
|
||||
running: false
|
||||
command: ["sh", "-c", `cliphist decode ${thumbnailImage.entryId} | base64 -w 0`]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const imageData = text.trim()
|
||||
if (imageData && imageData.length > 0) {
|
||||
thumbnailImage.cachedImageData = imageData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: exitCode => {
|
||||
thumbnailImage.loadQueued = false
|
||||
if (modal.activeImageLoads > 0) {
|
||||
modal.activeImageLoads--
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Failed to load clipboard image:", thumbnailImage.entryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rounded mask effect for images
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
@@ -155,17 +140,17 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback icon
|
||||
DankIcon {
|
||||
visible: !(entryType === "image" && thumbnailImage.status === Image.Ready && thumbnailImage.source != "")
|
||||
name: {
|
||||
if (entryType === "image") {
|
||||
return "image"
|
||||
switch (entryType) {
|
||||
case "image":
|
||||
return "image";
|
||||
case "long_text":
|
||||
return "subject";
|
||||
default:
|
||||
return "content_copy";
|
||||
}
|
||||
if (entryType === "long_text") {
|
||||
return "subject"
|
||||
}
|
||||
return "content_copy"
|
||||
}
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
|
||||
@@ -399,5 +399,21 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: clipboardLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 23
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: ClipboardTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item) {
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,12 +144,6 @@ Rectangle {
|
||||
"tabIndex": 2,
|
||||
"shortcutsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "displays",
|
||||
"text": I18n.tr("Displays"),
|
||||
"icon": "monitor",
|
||||
"tabIndex": 6
|
||||
},
|
||||
{
|
||||
"id": "network",
|
||||
"text": I18n.tr("Network"),
|
||||
@@ -158,11 +152,32 @@ Rectangle {
|
||||
"dmsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "printers",
|
||||
"text": I18n.tr("Printers"),
|
||||
"icon": "print",
|
||||
"tabIndex": 8,
|
||||
"cupsOnly": true
|
||||
"id": "system",
|
||||
"text": I18n.tr("System"),
|
||||
"icon": "computer",
|
||||
"collapsedByDefault": true,
|
||||
"children": [
|
||||
{
|
||||
"id": "displays",
|
||||
"text": I18n.tr("Displays"),
|
||||
"icon": "monitor",
|
||||
"tabIndex": 6
|
||||
},
|
||||
{
|
||||
"id": "printers",
|
||||
"text": I18n.tr("Printers"),
|
||||
"icon": "print",
|
||||
"tabIndex": 8,
|
||||
"cupsOnly": true
|
||||
},
|
||||
{
|
||||
"id": "clipboard",
|
||||
"text": I18n.tr("Clipboard"),
|
||||
"icon": "content_paste",
|
||||
"tabIndex": 23,
|
||||
"clipboardOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "power_security",
|
||||
@@ -213,6 +228,8 @@ Rectangle {
|
||||
return false;
|
||||
if (item.hyprlandNiriOnly && !CompositorService.isNiri && !CompositorService.isHyprland)
|
||||
return false;
|
||||
if (item.clipboardOnly && (!DMSService.isConnected || DMSService.apiVersion < 23))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
273
quickshell/Modules/Settings/ClipboardTab.qml
Normal file
273
quickshell/Modules/Settings/ClipboardTab.qml
Normal file
@@ -0,0 +1,273 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var config: ({})
|
||||
property bool configLoaded: false
|
||||
property bool configError: false
|
||||
property bool saving: false
|
||||
|
||||
readonly property var maxHistoryOptions: [
|
||||
{ text: "25", value: 25 },
|
||||
{ text: "50", value: 50 },
|
||||
{ text: "100", value: 100 },
|
||||
{ text: "200", value: 200 },
|
||||
{ text: "500", value: 500 },
|
||||
{ text: "1000", value: 1000 }
|
||||
]
|
||||
|
||||
readonly property var maxEntrySizeOptions: [
|
||||
{ text: "1 MB", value: 1048576 },
|
||||
{ text: "2 MB", value: 2097152 },
|
||||
{ text: "5 MB", value: 5242880 },
|
||||
{ text: "10 MB", value: 10485760 },
|
||||
{ text: "20 MB", value: 20971520 },
|
||||
{ text: "50 MB", value: 52428800 }
|
||||
]
|
||||
|
||||
readonly property var autoClearOptions: [
|
||||
{ text: I18n.tr("Never"), value: 0 },
|
||||
{ text: I18n.tr("1 day"), value: 1 },
|
||||
{ text: I18n.tr("3 days"), value: 3 },
|
||||
{ text: I18n.tr("7 days"), value: 7 },
|
||||
{ text: I18n.tr("14 days"), value: 14 },
|
||||
{ text: I18n.tr("30 days"), value: 30 },
|
||||
{ text: I18n.tr("90 days"), value: 90 }
|
||||
]
|
||||
|
||||
function getMaxHistoryText(value) {
|
||||
for (let opt of maxHistoryOptions) {
|
||||
if (opt.value === value)
|
||||
return opt.text;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function getMaxEntrySizeText(value) {
|
||||
for (let opt of maxEntrySizeOptions) {
|
||||
if (opt.value === value)
|
||||
return opt.text;
|
||||
}
|
||||
const mb = Math.round(value / 1048576);
|
||||
return mb + " MB";
|
||||
}
|
||||
|
||||
function getAutoClearText(value) {
|
||||
for (let opt of autoClearOptions) {
|
||||
if (opt.value === value)
|
||||
return opt.text;
|
||||
}
|
||||
return value + " " + I18n.tr("days");
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
configLoaded = false;
|
||||
configError = false;
|
||||
DMSService.sendRequest("clipboard.getConfig", null, response => {
|
||||
if (response.error) {
|
||||
configError = true;
|
||||
return;
|
||||
}
|
||||
config = response.result || {};
|
||||
configLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
function saveConfig(key, value) {
|
||||
const params = {};
|
||||
params[key] = value;
|
||||
saving = true;
|
||||
DMSService.sendRequest("clipboard.setConfig", params, response => {
|
||||
saving = false;
|
||||
if (response.error) {
|
||||
ToastService.showError(I18n.tr("Failed to save clipboard setting"));
|
||||
return;
|
||||
}
|
||||
loadConfig();
|
||||
});
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (DMSService.isConnected)
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: DMSService
|
||||
function onIsConnectedChanged() {
|
||||
if (DMSService.isConnected)
|
||||
loadConfig();
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: mainColumn.height + Theme.spacingXL
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: warningContent.implicitHeight + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
|
||||
visible: !DMSService.isConnected || configError
|
||||
|
||||
Row {
|
||||
id: warningContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "info"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.warning
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
text: !DMSService.isConnected
|
||||
? I18n.tr("DMS service is not connected. Clipboard settings are unavailable.")
|
||||
: I18n.tr("Failed to load clipboard configuration.")
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width - Theme.iconSizeSmall - Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "history", "limit"]
|
||||
title: I18n.tr("History Settings")
|
||||
iconName: "history"
|
||||
visible: configLoaded
|
||||
|
||||
SettingsDropdownRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "history", "max", "limit"]
|
||||
settingKey: "maxHistory"
|
||||
text: I18n.tr("Maximum History")
|
||||
description: I18n.tr("Maximum number of clipboard entries to keep")
|
||||
currentValue: root.getMaxHistoryText(root.config.maxHistory ?? 100)
|
||||
options: root.maxHistoryOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let opt of root.maxHistoryOptions) {
|
||||
if (opt.text === value) {
|
||||
root.saveConfig("maxHistory", opt.value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "entry", "size", "limit"]
|
||||
settingKey: "maxEntrySize"
|
||||
text: I18n.tr("Maximum Entry Size")
|
||||
description: I18n.tr("Maximum size per clipboard entry")
|
||||
currentValue: root.getMaxEntrySizeText(root.config.maxEntrySize ?? 5242880)
|
||||
options: root.maxEntrySizeOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let opt of root.maxEntrySizeOptions) {
|
||||
if (opt.text === value) {
|
||||
root.saveConfig("maxEntrySize", opt.value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "auto", "clear", "days"]
|
||||
settingKey: "autoClearDays"
|
||||
text: I18n.tr("Auto-Clear After")
|
||||
description: I18n.tr("Automatically delete entries older than this")
|
||||
currentValue: root.getAutoClearText(root.config.autoClearDays ?? 0)
|
||||
options: root.autoClearOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
for (let opt of root.autoClearOptions) {
|
||||
if (opt.text === value) {
|
||||
root.saveConfig("autoClearDays", opt.value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "behavior"]
|
||||
title: I18n.tr("Behavior")
|
||||
iconName: "settings"
|
||||
visible: configLoaded
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "clear", "startup"]
|
||||
settingKey: "clearAtStartup"
|
||||
text: I18n.tr("Clear at Startup")
|
||||
description: I18n.tr("Clear all history when server starts")
|
||||
checked: root.config.clearAtStartup ?? false
|
||||
onToggled: checked => root.saveConfig("clearAtStartup", checked)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "advanced", "disable"]
|
||||
title: I18n.tr("Advanced")
|
||||
iconName: "tune"
|
||||
collapsible: true
|
||||
expanded: false
|
||||
visible: configLoaded
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "disable", "manager"]
|
||||
settingKey: "disabled"
|
||||
text: I18n.tr("Disable Clipboard Manager")
|
||||
description: I18n.tr("Disable clipboard manager entirely (requires restart)")
|
||||
checked: root.config.disabled ?? false
|
||||
onToggled: checked => root.saveConfig("disabled", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "disable", "history"]
|
||||
settingKey: "disableHistory"
|
||||
text: I18n.tr("Disable History Persistence")
|
||||
description: I18n.tr("Clipboard works but nothing saved to disk")
|
||||
checked: root.config.disableHistory ?? false
|
||||
onToggled: checked => root.saveConfig("disableHistory", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
tab: "clipboard"
|
||||
tags: ["clipboard", "disable", "persist", "ownership"]
|
||||
settingKey: "disablePersist"
|
||||
text: I18n.tr("Disable Clipboard Ownership")
|
||||
description: I18n.tr("Don't preserve clipboard when apps close")
|
||||
checked: root.config.disablePersist ?? false
|
||||
onToggled: checked => root.saveConfig("disablePersist", checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ The `PopoutService` singleton provides plugins with access to all DankMaterialSh
|
||||
## Automatic Injection
|
||||
|
||||
The `popoutService` property is automatically injected into:
|
||||
|
||||
- Widget plugins (loaded in DankBar)
|
||||
- Daemon plugins (background services)
|
||||
- Plugin settings components
|
||||
@@ -23,36 +24,36 @@ property var popoutService: null
|
||||
|
||||
### Popouts (DankPopout-based)
|
||||
|
||||
| Component | Open | Close | Toggle |
|
||||
|-----------|------|-------|--------|
|
||||
| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |
|
||||
| Component | Open | Close | Toggle |
|
||||
| ------------------- | -------------------------- | --------------------------- | ---------------------------- |
|
||||
| Control Center | `openControlCenter()` | `closeControlCenter()` | `toggleControlCenter()` |
|
||||
| Notification Center | `openNotificationCenter()` | `closeNotificationCenter()` | `toggleNotificationCenter()` |
|
||||
| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |
|
||||
| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |
|
||||
| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |
|
||||
| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |
|
||||
| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |
|
||||
| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |
|
||||
| App Drawer | `openAppDrawer()` | `closeAppDrawer()` | `toggleAppDrawer()` |
|
||||
| Process List | `openProcessList()` | `closeProcessList()` | `toggleProcessList()` |
|
||||
| DankDash | `openDankDash(tab)` | `closeDankDash()` | `toggleDankDash(tab)` |
|
||||
| Battery | `openBattery()` | `closeBattery()` | `toggleBattery()` |
|
||||
| VPN | `openVpn()` | `closeVpn()` | `toggleVpn()` |
|
||||
| System Update | `openSystemUpdate()` | `closeSystemUpdate()` | `toggleSystemUpdate()` |
|
||||
|
||||
### Modals (DankModal-based)
|
||||
|
||||
| Modal | Show | Hide | Notes |
|
||||
|-------|------|------|-------|
|
||||
| Settings | `openSettings()` | `closeSettings()` | Full settings interface |
|
||||
| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Cliphist integration |
|
||||
| Spotlight | `openSpotlight()` | `closeSpotlight()` | Command launcher |
|
||||
| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |
|
||||
| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Fullscreen version, has `toggleProcessListModal()` |
|
||||
| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |
|
||||
| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |
|
||||
| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network authentication |
|
||||
| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |
|
||||
| Modal | Show | Hide | Notes |
|
||||
| ------------------ | ------------------------- | ------------------------- | -------------------------------------------------- |
|
||||
| Settings | `openSettings()` | `closeSettings()` | Full settings interface |
|
||||
| Clipboard History | `openClipboardHistory()` | `closeClipboardHistory()` | Clipboard integration |
|
||||
| Spotlight | `openSpotlight()` | `closeSpotlight()` | Command launcher |
|
||||
| Power Menu | `openPowerMenu()` | `closePowerMenu()` | Also has `togglePowerMenu()` |
|
||||
| Process List Modal | `showProcessListModal()` | `hideProcessListModal()` | Fullscreen version, has `toggleProcessListModal()` |
|
||||
| Color Picker | `showColorPicker()` | `hideColorPicker()` | Theme color selection |
|
||||
| Notification | `showNotificationModal()` | `hideNotificationModal()` | Notification details |
|
||||
| WiFi Password | `showWifiPasswordModal()` | `hideWifiPasswordModal()` | Network authentication |
|
||||
| Network Info | `showNetworkInfoModal()` | `hideNetworkInfoModal()` | Network details |
|
||||
|
||||
### Slideouts
|
||||
|
||||
| Component | Open | Close | Toggle |
|
||||
|-----------|------|-------|--------|
|
||||
| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |
|
||||
| Component | Open | Close | Toggle |
|
||||
| --------- | --------------- | ---------------- | ----------------- |
|
||||
| Notepad | `openNotepad()` | `closeNotepad()` | `toggleNotepad()` |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
@@ -205,6 +206,7 @@ LazyLoader {
|
||||
The service is injected in three locations:
|
||||
|
||||
1. **DMSShell.qml** (daemon plugins):
|
||||
|
||||
```qml
|
||||
Instantiator {
|
||||
delegate: Loader {
|
||||
@@ -218,6 +220,7 @@ Instantiator {
|
||||
```
|
||||
|
||||
2. **WidgetHost.qml** (widget plugins):
|
||||
|
||||
```qml
|
||||
onLoaded: {
|
||||
if (item.popoutService !== undefined) {
|
||||
@@ -227,6 +230,7 @@ onLoaded: {
|
||||
```
|
||||
|
||||
3. **CenterSection.qml** (center widgets):
|
||||
|
||||
```qml
|
||||
onLoaded: {
|
||||
if (item.popoutService !== undefined) {
|
||||
@@ -236,6 +240,7 @@ onLoaded: {
|
||||
```
|
||||
|
||||
4. **PluginsTab.qml** (settings):
|
||||
|
||||
```qml
|
||||
onLoaded: {
|
||||
if (item && typeof PopoutService !== "undefined") {
|
||||
@@ -247,11 +252,13 @@ onLoaded: {
|
||||
## Best Practices
|
||||
|
||||
1. **Use Optional Chaining**: Always use `?.` to handle null cases
|
||||
|
||||
```qml
|
||||
popoutService?.toggleControlCenter()
|
||||
```
|
||||
|
||||
2. **Check Availability**: Some popouts may not be available
|
||||
|
||||
```qml
|
||||
if (popoutService && popoutService.controlCenterPopout) {
|
||||
popoutService.toggleControlCenter()
|
||||
@@ -261,6 +268,7 @@ onLoaded: {
|
||||
3. **Lazy Loading**: First access may activate lazy loaders - this is normal
|
||||
|
||||
4. **Feature Detection**: Some popouts require specific features
|
||||
|
||||
```qml
|
||||
if (BatteryService.batteryAvailable) {
|
||||
popoutService?.openBattery()
|
||||
@@ -272,6 +280,7 @@ onLoaded: {
|
||||
## Example Plugin
|
||||
|
||||
See `PLUGINS/PopoutControlExample/` for a complete working example that demonstrates:
|
||||
|
||||
- Widget creation with popout controls
|
||||
- Menu-based popout selection
|
||||
- Proper service usage
|
||||
|
||||
@@ -24,6 +24,7 @@ Singleton {
|
||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||
|
||||
property var pendingRequests: ({})
|
||||
property var clipboardRequestIds: ({})
|
||||
property int requestIdCounter: 0
|
||||
property bool shownOutdatedError: false
|
||||
property string updateCommand: "dms update"
|
||||
@@ -179,17 +180,19 @@ Singleton {
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => {
|
||||
if (!line || line.length === 0) {
|
||||
if (!line || line.length === 0)
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("DMSService: Request socket <<", line);
|
||||
|
||||
try {
|
||||
const response = JSON.parse(line);
|
||||
const isClipboard = clipboardRequestIds[response.id];
|
||||
if (isClipboard)
|
||||
delete clipboardRequestIds[response.id];
|
||||
else
|
||||
console.log("DMSService: Request socket <<", line);
|
||||
handleResponse(response);
|
||||
} catch (e) {
|
||||
console.warn("DMSService: Failed to parse request response:", line, e);
|
||||
console.warn("DMSService: Failed to parse request response");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,17 +212,16 @@ Singleton {
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => {
|
||||
if (!line || line.length === 0) {
|
||||
if (!line || line.length === 0)
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("DMSService: Subscribe socket <<", line);
|
||||
|
||||
try {
|
||||
const response = JSON.parse(line);
|
||||
if (!line.includes("clipboard"))
|
||||
console.log("DMSService: Subscribe socket <<", line);
|
||||
handleSubscriptionEvent(response);
|
||||
} catch (e) {
|
||||
console.warn("DMSService: Failed to parse subscription event:", line, e);
|
||||
console.warn("DMSService: Failed to parse subscription event");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,11 +396,14 @@ Singleton {
|
||||
request.params = params;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
if (callback)
|
||||
pendingRequests[id] = callback;
|
||||
}
|
||||
|
||||
console.log("DMSService.sendRequest: Sending request id=" + id + " method=" + method);
|
||||
if (method.startsWith("clipboard")) {
|
||||
clipboardRequestIds[id] = true;
|
||||
} else {
|
||||
console.log("DMSService.sendRequest: Sending request id=" + id + " method=" + method);
|
||||
}
|
||||
requestSocket.send(request);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user