mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-07 19:59:14 -04:00
feat(Clipboard): Revive ClipboardEditor PR
- Original PR #1916 by @nabaco
This commit is contained in:
@@ -0,0 +1,508 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var modal
|
||||||
|
property var keyController: null
|
||||||
|
|
||||||
|
property var entry: null
|
||||||
|
property string editorText: ""
|
||||||
|
|
||||||
|
function decodeEntryData(data) {
|
||||||
|
if (!data) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (typeof data !== "string") {
|
||||||
|
return String(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = data.replace(/\s+/g, "");
|
||||||
|
if (sanitized.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chars = new Array(sanitized.length);
|
||||||
|
for (let i = 0; i < sanitized.length; i++) {
|
||||||
|
chars[i] = sanitized.charAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = null;
|
||||||
|
if (typeof Qt !== "undefined" && typeof Qt.atob === "function") {
|
||||||
|
buffer = Qt.atob(chars);
|
||||||
|
} else if (typeof atob === "function") {
|
||||||
|
const binary = atob(sanitized);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
buffer = bytes.buffer;
|
||||||
|
}
|
||||||
|
if (!buffer || buffer.byteLength === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(escape(binary));
|
||||||
|
} catch (e) {
|
||||||
|
return binary;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEntry(newEntry) {
|
||||||
|
entry = newEntry;
|
||||||
|
editorText = newEntry?.text ?? newEntry?.preview ?? "";
|
||||||
|
if (editField) {
|
||||||
|
editField.text = editorText;
|
||||||
|
}
|
||||||
|
Qt.callLater(function () {
|
||||||
|
if (editField) {
|
||||||
|
editField.forceActiveFocus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newEntry || newEntry.isImage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestedId = newEntry.id;
|
||||||
|
DMSService.sendRequest("clipboard.getEntry", {
|
||||||
|
"id": requestedId
|
||||||
|
}, function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!root.entry || root.entry.id !== requestedId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = response.result;
|
||||||
|
let fullText = "";
|
||||||
|
if (result?.data) {
|
||||||
|
fullText = root.decodeEntryData(result.data);
|
||||||
|
} else {
|
||||||
|
fullText = result?.preview ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fullText || fullText.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.editorText = fullText;
|
||||||
|
if (editField) {
|
||||||
|
editField.text = fullText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEntry(action) {
|
||||||
|
const saveAction = action ?? "history";
|
||||||
|
DMSService.sendRequest("clipboard.copy", {
|
||||||
|
"text": root.editorText
|
||||||
|
}, function (response) {
|
||||||
|
if (response.error) {
|
||||||
|
ToastService.showError(I18n.tr("Failed to update clipboard"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveAction === "history") {
|
||||||
|
modal.mode = "history";
|
||||||
|
Qt.callLater(function () {
|
||||||
|
ClipboardService.reset();
|
||||||
|
ClipboardService.refresh();
|
||||||
|
if (keyController) {
|
||||||
|
keyController.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveAction === "close") {
|
||||||
|
modal.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (saveAction === "paste") {
|
||||||
|
ClipboardService.pasteClipboard(modal.hide);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSaveMenu() {
|
||||||
|
if (saveMenu.visible) {
|
||||||
|
saveMenu.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveMenu.open();
|
||||||
|
const pos = saveButton.mapToItem(Overlay.overlay, 0, 0);
|
||||||
|
const popupW = saveMenu.width;
|
||||||
|
const popupH = saveMenu.height;
|
||||||
|
const overlayW = Overlay.overlay.width;
|
||||||
|
const overlayH = Overlay.overlay.height;
|
||||||
|
|
||||||
|
let x = pos.x + (saveButton.width - popupW) / 2;
|
||||||
|
let y = pos.y + saveButton.height + 4;
|
||||||
|
if (y + popupH > overlayH) {
|
||||||
|
y = pos.y - popupH - 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Math.max(8, Math.min(x, overlayW - popupW - 8));
|
||||||
|
y = Math.max(8, y);
|
||||||
|
|
||||||
|
saveMenu.x = x;
|
||||||
|
saveMenu.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequences: ["Escape"]
|
||||||
|
enabled: modal.mode === "editor"
|
||||||
|
onActivated: modal.mode = "history"
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: editorHeader
|
||||||
|
width: parent.width
|
||||||
|
height: ClipboardConstants.headerHeight
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "arrow_back"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: modal.mode = "history"
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Edit Clipboard")
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: Theme.iconSize - 4
|
||||||
|
iconColor: Theme.surfaceText
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onClicked: modal.mode = "history"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
id: editFieldContainer
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(Theme.fontSizeMedium * 8, parent.height - editorHeader.height - editorActions.height - Theme.spacingM * 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
border.color: editField.activeFocus ? Theme.primary : Theme.outlineMedium
|
||||||
|
border.width: editField.activeFocus ? 2 : 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: editIcon
|
||||||
|
name: "edit"
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: editField.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: editScroll
|
||||||
|
anchors.left: editIcon.right
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
anchors.bottomMargin: Theme.spacingS
|
||||||
|
clip: true
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: editField.height
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: editField
|
||||||
|
width: editScroll.width
|
||||||
|
height: Math.max(editScroll.height, contentHeight)
|
||||||
|
text: root.editorText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
wrapMode: TextEdit.Wrap
|
||||||
|
selectByMouse: true
|
||||||
|
onTextChanged: root.editorText = text
|
||||||
|
Keys.onPressed: function (event) {
|
||||||
|
const hasCtrl = (event.modifiers & Qt.ControlModifier) !== 0;
|
||||||
|
const hasShift = (event.modifiers & Qt.ShiftModifier) !== 0;
|
||||||
|
|
||||||
|
if (hasCtrl && event.key === Qt.Key_S) {
|
||||||
|
root.saveEntry(hasShift ? "close" : "history");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasCtrl && hasShift && event.key === Qt.Key_V) {
|
||||||
|
root.saveEntry("paste");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Edit clipboard text")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.outlineButton
|
||||||
|
anchors.left: editScroll.left
|
||||||
|
anchors.right: editScroll.right
|
||||||
|
anchors.top: editScroll.top
|
||||||
|
anchors.bottom: editScroll.bottom
|
||||||
|
visible: editField.text.length === 0 && !editField.activeFocus
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: editorActions
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: buttonSpacer
|
||||||
|
width: Math.max(0, parent.width - cancelButton.width - saveButton.width - Theme.spacingS)
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: cancelButton
|
||||||
|
text: I18n.tr("Cancel")
|
||||||
|
backgroundColor: Theme.surfaceContainerHigh
|
||||||
|
textColor: Theme.surfaceText
|
||||||
|
onClicked: modal.mode = "history"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: saveButton
|
||||||
|
property int arrowWidth: Theme.iconSize + Theme.spacingS
|
||||||
|
width: cancelButton.implicitWidth
|
||||||
|
height: cancelButton.implicitHeight
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: saveMainArea
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: saveArrowArea.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.onPrimary
|
||||||
|
anchors.centerIn: saveMainArea
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: saveArrowArea
|
||||||
|
width: saveButton.arrowWidth
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 1
|
||||||
|
height: parent.height - Theme.spacingM
|
||||||
|
color: Theme.withAlpha(Theme.onPrimary, 0.2)
|
||||||
|
anchors.right: saveArrowArea.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: saveMenu.visible ? "expand_less" : "expand_more"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.onPrimary
|
||||||
|
anchors.centerIn: saveArrowArea
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
anchors.fill: saveMainArea
|
||||||
|
stateColor: Theme.onPrimary
|
||||||
|
onClicked: root.saveEntry("history")
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
anchors.fill: saveArrowArea
|
||||||
|
stateColor: Theme.onPrimary
|
||||||
|
onClicked: root.toggleSaveMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: saveMenu
|
||||||
|
parent: Overlay.overlay
|
||||||
|
width: saveMenuColumn.implicitWidth + padding * 2
|
||||||
|
padding: Theme.spacingM
|
||||||
|
modal: false
|
||||||
|
focus: true
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
|
background: StyledRect {
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainer
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Column {
|
||||||
|
id: saveMenuColumn
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: saveMenuRow.implicitWidth + Theme.spacingS * 2
|
||||||
|
implicitHeight: saveMenuRow.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: saveMenuSaveArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveMenuRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "save"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: saveMenuSaveArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
saveMenu.close();
|
||||||
|
root.saveEntry("history");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: saveMenuCloseRow.implicitWidth + Theme.spacingS * 2
|
||||||
|
implicitHeight: saveMenuCloseRow.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: saveMenuCloseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveMenuCloseRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "close"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save and close")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: saveMenuCloseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
saveMenu.close();
|
||||||
|
root.saveEntry("close");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledRect {
|
||||||
|
implicitWidth: saveMenuPasteRow.implicitWidth + Theme.spacingS * 2
|
||||||
|
implicitHeight: saveMenuPasteRow.implicitHeight + Theme.spacingS * 2
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: saveMenuPasteArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
||||||
|
opacity: modal.wtypeAvailable ? 1 : 0.5
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: saveMenuPasteRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "content_paste"
|
||||||
|
size: Theme.iconSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Save and paste")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: saveMenuPasteArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: modal.wtypeAvailable
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: {
|
||||||
|
saveMenu.close();
|
||||||
|
root.saveEntry("paste");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Clipboard
|
import qs.Modals.Clipboard
|
||||||
import qs.Modals.Common
|
import qs.Modals.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
DankModal {
|
DankModal {
|
||||||
id: clipboardHistoryModal
|
id: clipboardHistoryModal
|
||||||
@@ -24,7 +22,6 @@ DankModal {
|
|||||||
ClipboardService.selectedIndex = 0;
|
ClipboardService.selectedIndex = 0;
|
||||||
ClipboardService.keyboardNavigationActive = false;
|
ClipboardService.keyboardNavigationActive = false;
|
||||||
}
|
}
|
||||||
property var editClipboardModal: null
|
|
||||||
property bool showKeyboardHints: false
|
property bool showKeyboardHints: false
|
||||||
property Component clipboardContent
|
property Component clipboardContent
|
||||||
property int activeImageLoads: 0
|
property int activeImageLoads: 0
|
||||||
@@ -172,34 +169,6 @@ DankModal {
|
|||||||
closeOnEscapeKey: mode !== "editor"
|
closeOnEscapeKey: mode !== "editor"
|
||||||
onBackgroundClicked: hide()
|
onBackgroundClicked: hide()
|
||||||
modalFocusScope.Keys.onPressed: function (event) {
|
modalFocusScope.Keys.onPressed: function (event) {
|
||||||
if (mode === "history" && (event.modifiers & Qt.ControlModifier) && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
|
|
||||||
activeTab = activeTab === "recents" ? "saved" : "recents";
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mode === "history" && (event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_S) {
|
|
||||||
const entries = activeTab === "saved" ? pinnedEntries : unpinnedEntries;
|
|
||||||
if (entries && entries.length > 0) {
|
|
||||||
const index = ClipboardService.selectedIndex >= 0 && ClipboardService.selectedIndex < entries.length ? ClipboardService.selectedIndex : 0;
|
|
||||||
const entry = entries[index];
|
|
||||||
if (activeTab === "saved") {
|
|
||||||
unpinEntry(entry);
|
|
||||||
} else {
|
|
||||||
pinEntry(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mode === "history" && (event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_E) {
|
|
||||||
const entries = activeTab === "saved" ? pinnedEntries : unpinnedEntries;
|
|
||||||
if (entries && entries.length > 0) {
|
|
||||||
const index = ClipboardService.selectedIndex >= 0 && ClipboardService.selectedIndex < entries.length ? ClipboardService.selectedIndex : 0;
|
|
||||||
editEntry(entries[index]);
|
|
||||||
}
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
keyboardController.handleKey(event);
|
keyboardController.handleKey(event);
|
||||||
}
|
}
|
||||||
content: clipboardContent
|
content: clipboardContent
|
||||||
@@ -259,7 +228,7 @@ DankModal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
ClipboardEditor {
|
||||||
id: editorView
|
id: editorView
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -268,460 +237,8 @@ DankModal {
|
|||||||
visible: opacity > 0.01
|
visible: opacity > 0.01
|
||||||
enabled: clipboardHistoryModal.mode === "editor"
|
enabled: clipboardHistoryModal.mode === "editor"
|
||||||
focus: clipboardHistoryModal.mode === "editor"
|
focus: clipboardHistoryModal.mode === "editor"
|
||||||
|
modal: clipboardHistoryModal
|
||||||
Shortcut {
|
keyController: keyboardController
|
||||||
sequences: ["Escape"]
|
|
||||||
enabled: clipboardHistoryModal.mode === "editor"
|
|
||||||
onActivated: clipboardHistoryModal.mode = "history"
|
|
||||||
}
|
|
||||||
|
|
||||||
property var entry: null
|
|
||||||
property string editorText: ""
|
|
||||||
|
|
||||||
function setEntry(newEntry) {
|
|
||||||
entry = newEntry;
|
|
||||||
editorText = newEntry?.text ?? newEntry?.preview ?? "";
|
|
||||||
if (editField) {
|
|
||||||
editField.text = editorText;
|
|
||||||
}
|
|
||||||
Qt.callLater(function () {
|
|
||||||
if (editField) {
|
|
||||||
editField.forceActiveFocus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!newEntry || newEntry.isImage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestedId = newEntry.id;
|
|
||||||
DMSService.sendRequest("clipboard.getEntry", {
|
|
||||||
"id": requestedId
|
|
||||||
}, function (response) {
|
|
||||||
if (response.error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!editorView.entry || editorView.entry.id !== requestedId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rawText = response.result?.text ?? response.result?.content ?? response.result?.data ?? "";
|
|
||||||
let fullText = rawText;
|
|
||||||
try {
|
|
||||||
const sanitized = rawText.replace(/\s+/g, "");
|
|
||||||
const decoder = (typeof Qt !== "undefined" && typeof Qt.atob === "function") ? Qt.atob : (typeof atob === "function" ? atob : null);
|
|
||||||
if (decoder) {
|
|
||||||
const decoded = decoder(sanitized);
|
|
||||||
fullText = decoded;
|
|
||||||
try {
|
|
||||||
fullText = decodeURIComponent(escape(decoded));
|
|
||||||
} catch (e) {
|
|
||||||
fullText = decoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
fullText = rawText;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fullText || fullText.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
editorView.editorText = fullText;
|
|
||||||
if (editField) {
|
|
||||||
editField.text = fullText;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveEntry(action) {
|
|
||||||
const saveAction = action ?? "history";
|
|
||||||
DMSService.sendRequest("clipboard.copy", {
|
|
||||||
"text": editorView.editorText
|
|
||||||
}, function (response) {
|
|
||||||
if (response.error) {
|
|
||||||
ToastService.showError(I18n.tr("Failed to update clipboard"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (saveAction === "history") {
|
|
||||||
clipboardHistoryModal.mode = "history";
|
|
||||||
Qt.callLater(function () {
|
|
||||||
ClipboardService.reset();
|
|
||||||
ClipboardService.refresh();
|
|
||||||
keyboardController.reset();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (saveAction === "close") {
|
|
||||||
clipboardHistoryModal.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (saveAction === "paste") {
|
|
||||||
ClipboardService.pasteClipboard(clipboardHistoryModal.hide);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSaveMenu() {
|
|
||||||
if (saveMenu.visible) {
|
|
||||||
saveMenu.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveMenu.open();
|
|
||||||
const pos = saveButton.mapToItem(Overlay.overlay, 0, 0);
|
|
||||||
const popupW = saveMenu.width;
|
|
||||||
const popupH = saveMenu.height;
|
|
||||||
const overlayW = Overlay.overlay.width;
|
|
||||||
const overlayH = Overlay.overlay.height;
|
|
||||||
|
|
||||||
let x = pos.x + (saveButton.width - popupW) / 2;
|
|
||||||
let y = pos.y + saveButton.height + 4;
|
|
||||||
if (y + popupH > overlayH) {
|
|
||||||
y = pos.y - popupH - 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
x = Math.max(8, Math.min(x, overlayW - popupW - 8));
|
|
||||||
y = Math.max(8, y);
|
|
||||||
|
|
||||||
saveMenu.x = x;
|
|
||||||
saveMenu.y = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: editorHeader
|
|
||||||
width: parent.width
|
|
||||||
height: ClipboardConstants.headerHeight
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "arrow_back"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onClicked: clipboardHistoryModal.mode = "history"
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Edit Clipboard")
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
DankActionButton {
|
|
||||||
iconName: "close"
|
|
||||||
iconSize: Theme.iconSize - 4
|
|
||||||
iconColor: Theme.surfaceText
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onClicked: clipboardHistoryModal.mode = "history"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
id: editFieldContainer
|
|
||||||
width: parent.width
|
|
||||||
height: Math.max(Theme.fontSizeMedium * 8, parent.height - editorHeader.height - editorActions.height - Theme.spacingM * 2)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
|
||||||
border.color: editField.activeFocus ? Theme.primary : Theme.outlineMedium
|
|
||||||
border.width: editField.activeFocus ? 2 : 1
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: editIcon
|
|
||||||
name: "edit"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: editField.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: Theme.spacingM
|
|
||||||
}
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
id: editScroll
|
|
||||||
anchors.left: editIcon.right
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingS
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
clip: true
|
|
||||||
contentWidth: width
|
|
||||||
contentHeight: editField.height
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: editField
|
|
||||||
width: editScroll.width
|
|
||||||
height: Math.max(editScroll.height, contentHeight)
|
|
||||||
text: editorView.editorText
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
wrapMode: TextEdit.Wrap
|
|
||||||
selectByMouse: true
|
|
||||||
onTextChanged: editorView.editorText = text
|
|
||||||
Keys.onPressed: function (event) {
|
|
||||||
const hasCtrl = (event.modifiers & Qt.ControlModifier) !== 0;
|
|
||||||
const hasShift = (event.modifiers & Qt.ShiftModifier) !== 0;
|
|
||||||
|
|
||||||
if (hasCtrl && event.key === Qt.Key_S) {
|
|
||||||
editorView.saveEntry(hasShift ? "close" : "history");
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (hasCtrl && hasShift && event.key === Qt.Key_V) {
|
|
||||||
editorView.saveEntry("paste");
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Edit clipboard text")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.outlineButton
|
|
||||||
anchors.left: editScroll.left
|
|
||||||
anchors.right: editScroll.right
|
|
||||||
anchors.top: editScroll.top
|
|
||||||
anchors.bottom: editScroll.bottom
|
|
||||||
visible: editField.text.length === 0 && !editField.activeFocus
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: editorActions
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: buttonSpacer
|
|
||||||
width: Math.max(0, parent.width - cancelButton.width - saveButton.width - Theme.spacingS)
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
DankButton {
|
|
||||||
id: cancelButton
|
|
||||||
text: I18n.tr("Cancel")
|
|
||||||
backgroundColor: Theme.surfaceContainerHigh
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
onClicked: clipboardHistoryModal.mode = "history"
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: saveButton
|
|
||||||
property int arrowWidth: 32
|
|
||||||
property int horizontalPadding: Theme.spacingL
|
|
||||||
width: cancelButton.width
|
|
||||||
height: 40
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: saveMainArea
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: saveArrowArea.left
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: saveLabel
|
|
||||||
text: I18n.tr("Save")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
font.weight: Font.Medium
|
|
||||||
color: Theme.onPrimary
|
|
||||||
anchors.centerIn: saveMainArea
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: saveArrowArea
|
|
||||||
width: saveButton.arrowWidth
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 1
|
|
||||||
height: parent.height - Theme.spacingM
|
|
||||||
color: Theme.withAlpha(Theme.onPrimary, 0.2)
|
|
||||||
anchors.right: saveArrowArea.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: saveMenu.visible ? "expand_less" : "expand_more"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.onPrimary
|
|
||||||
anchors.centerIn: saveArrowArea
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
anchors.fill: saveMainArea
|
|
||||||
stateColor: Theme.onPrimary
|
|
||||||
onClicked: editorView.saveEntry("history")
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
anchors.fill: saveArrowArea
|
|
||||||
stateColor: Theme.onPrimary
|
|
||||||
onClicked: editorView.toggleSaveMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Popup {
|
|
||||||
id: saveMenu
|
|
||||||
parent: Overlay.overlay
|
|
||||||
width: 220
|
|
||||||
padding: Theme.spacingM
|
|
||||||
modal: false
|
|
||||||
focus: true
|
|
||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
||||||
|
|
||||||
background: StyledRect {
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: Column {
|
|
||||||
id: saveMenuColumn
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: saveMenu.width - saveMenu.padding * 2
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: saveMenuSaveArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "save"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Save")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: saveMenuSaveArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
saveMenu.close();
|
|
||||||
editorView.saveEntry("history");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: saveMenu.width - saveMenu.padding * 2
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: saveMenuCloseArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "close"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Save and close")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: saveMenuCloseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
saveMenu.close();
|
|
||||||
editorView.saveEntry("close");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledRect {
|
|
||||||
width: saveMenu.width - saveMenu.padding * 2
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: saveMenuPasteArea.containsMouse ? Theme.surfaceVariant : "transparent"
|
|
||||||
opacity: clipboardHistoryModal.wtypeAvailable ? 1 : 0.5
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "content_paste"
|
|
||||||
size: Theme.iconSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: I18n.tr("Save and paste")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: saveMenuPasteArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
enabled: clipboardHistoryModal.wtypeAvailable
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
onClicked: {
|
|
||||||
saveMenu.close();
|
|
||||||
editorView.saveEntry("paste");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
states: [
|
states: [
|
||||||
|
|||||||
@@ -66,12 +66,27 @@ QtObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editSelected() {
|
||||||
|
const entries = modal.activeTab === "saved" ? ClipboardService.pinnedEntries : ClipboardService.unpinnedEntries;
|
||||||
|
if (!entries || entries.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = ClipboardService.selectedIndex >= 0 && ClipboardService.selectedIndex < entries.length ? ClipboardService.selectedIndex : 0;
|
||||||
|
modal.editEntry(entries[index]);
|
||||||
|
}
|
||||||
|
|
||||||
function handleKey(event) {
|
function handleKey(event) {
|
||||||
|
if (modal.mode === "editor") {
|
||||||
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
modal.mode = "history";
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case Qt.Key_Escape:
|
case Qt.Key_Escape:
|
||||||
if (modal.mode === "editor") {
|
if (ClipboardService.keyboardNavigationActive) {
|
||||||
modal.mode = "history";
|
|
||||||
} else if (ClipboardService.keyboardNavigationActive) {
|
|
||||||
ClipboardService.keyboardNavigationActive = false;
|
ClipboardService.keyboardNavigationActive = false;
|
||||||
} else {
|
} else {
|
||||||
modal.hide();
|
modal.hide();
|
||||||
@@ -154,6 +169,10 @@ QtObject {
|
|||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
case Qt.Key_E:
|
||||||
|
editSelected();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user