mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
627 lines
24 KiB
QML
627 lines
24 KiB
QML
import QtQuick
|
||
import QtQuick.Controls
|
||
import Quickshell
|
||
import Quickshell.Widgets
|
||
import Quickshell.Wayland
|
||
import Quickshell.Io
|
||
|
||
PanelWindow {
|
||
id: clipboardHistory
|
||
|
||
property var theme
|
||
property bool isVisible: false
|
||
|
||
// Default theme fallback
|
||
property var defaultTheme: QtObject {
|
||
property color primary: "#D0BCFF"
|
||
property color background: "#10121E"
|
||
property color surfaceContainer: "#1D1B20"
|
||
property color surfaceText: "#E6E0E9"
|
||
property color surfaceVariant: "#49454F"
|
||
property color surfaceVariantText: "#CAC4D0"
|
||
property color outline: "#938F99"
|
||
property color error: "#F2B8B5"
|
||
property real cornerRadius: 12
|
||
property real cornerRadiusLarge: 16
|
||
property real cornerRadiusXLarge: 24
|
||
property real cornerRadiusSmall: 8
|
||
property real spacingXS: 4
|
||
property real spacingS: 8
|
||
property real spacingM: 12
|
||
property real spacingL: 16
|
||
property real spacingXL: 24
|
||
property real fontSizeLarge: 16
|
||
property real fontSizeMedium: 14
|
||
property real fontSizeSmall: 12
|
||
property real iconSize: 24
|
||
property real iconSizeLarge: 32
|
||
property string iconFont: "Material Symbols Rounded"
|
||
property int iconFontWeight: Font.Normal
|
||
property int shortDuration: 150
|
||
property int mediumDuration: 300
|
||
property int standardEasing: Easing.OutCubic
|
||
property int emphasizedEasing: Easing.OutQuart
|
||
}
|
||
|
||
property var activeTheme: theme || defaultTheme
|
||
|
||
// Window properties
|
||
color: "transparent"
|
||
visible: isVisible
|
||
|
||
anchors {
|
||
top: true
|
||
left: true
|
||
right: true
|
||
bottom: true
|
||
}
|
||
|
||
WlrLayershell.layer: WlrLayershell.Overlay
|
||
WlrLayershell.exclusiveZone: -1
|
||
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||
|
||
// Clipboard entries model
|
||
property var clipboardEntries: []
|
||
|
||
ListModel {
|
||
id: clipboardModel
|
||
}
|
||
|
||
ListModel {
|
||
id: filteredClipboardModel
|
||
}
|
||
|
||
function updateFilteredModel() {
|
||
filteredClipboardModel.clear()
|
||
for (let i = 0; i < clipboardModel.count; i++) {
|
||
const entry = clipboardModel.get(i).entry
|
||
if (searchField.text.trim().length === 0) {
|
||
filteredClipboardModel.append({"entry": entry})
|
||
} else {
|
||
const content = getEntryPreview(entry).toLowerCase()
|
||
if (content.includes(searchField.text.toLowerCase())) {
|
||
filteredClipboardModel.append({"entry": entry})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function toggle() {
|
||
if (isVisible) {
|
||
hide()
|
||
} else {
|
||
show()
|
||
}
|
||
}
|
||
|
||
function show() {
|
||
clipboardHistory.isVisible = true
|
||
searchField.focus = true
|
||
refreshClipboard()
|
||
console.log("ClipboardHistory: Opening and refreshing")
|
||
}
|
||
|
||
function hide() {
|
||
clipboardHistory.isVisible = false
|
||
searchField.focus = false
|
||
searchField.text = ""
|
||
}
|
||
|
||
function refreshClipboard() {
|
||
clipboardProcess.running = true
|
||
}
|
||
|
||
function copyEntry(entry) {
|
||
const entryId = entry.split('\t')[0]
|
||
copyProcess.command = ["sh", "-c", `cliphist decode ${entryId} | wl-copy`]
|
||
copyProcess.running = true
|
||
hide()
|
||
}
|
||
|
||
function deleteEntry(entry) {
|
||
const entryId = entry.split('\t')[0]
|
||
deleteProcess.command = ["cliphist", "delete-query", entryId]
|
||
deleteProcess.running = true
|
||
}
|
||
|
||
function clearAll() {
|
||
clearProcess.running = true
|
||
}
|
||
|
||
function getEntryPreview(entry) {
|
||
// Remove cliphist ID prefix and clean up content
|
||
let content = entry.replace(/^\s*\d+\s+/, "")
|
||
|
||
// Handle different content types
|
||
if (content.includes("image/")) {
|
||
const match = content.match(/(\d+)x(\d+)/)
|
||
return match ? `Image ${match[1]}×${match[2]}` : "Image"
|
||
}
|
||
|
||
// Truncate long text
|
||
if (content.length > 100) {
|
||
return content.substring(0, 100) + "..."
|
||
}
|
||
|
||
return content
|
||
}
|
||
|
||
function getEntryType(entry) {
|
||
if (entry.includes("image/")) return "image"
|
||
if (entry.length > 200) return "long_text"
|
||
return "text"
|
||
}
|
||
|
||
// Background overlay
|
||
Rectangle {
|
||
anchors.fill: parent
|
||
color: Qt.rgba(0, 0, 0, 0.5)
|
||
opacity: clipboardHistory.isVisible ? 1.0 : 0.0
|
||
visible: clipboardHistory.isVisible
|
||
|
||
Behavior on opacity {
|
||
NumberAnimation {
|
||
duration: activeTheme.mediumDuration
|
||
easing.type: activeTheme.emphasizedEasing
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
enabled: clipboardHistory.isVisible
|
||
onClicked: clipboardHistory.hide()
|
||
}
|
||
}
|
||
|
||
// Main clipboard container
|
||
Rectangle {
|
||
id: clipboardContainer
|
||
width: Math.min(600, parent.width - 200)
|
||
height: Math.min(500, parent.height - 100)
|
||
anchors.centerIn: parent
|
||
|
||
color: activeTheme.surfaceContainer
|
||
radius: activeTheme.cornerRadiusXLarge
|
||
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
|
||
border.width: 1
|
||
|
||
opacity: clipboardHistory.isVisible ? 1.0 : 0.0
|
||
scale: clipboardHistory.isVisible ? 1.0 : 0.9
|
||
|
||
Behavior on opacity {
|
||
NumberAnimation {
|
||
duration: activeTheme.mediumDuration
|
||
easing.type: activeTheme.emphasizedEasing
|
||
}
|
||
}
|
||
|
||
Behavior on scale {
|
||
NumberAnimation {
|
||
duration: activeTheme.mediumDuration
|
||
easing.type: activeTheme.emphasizedEasing
|
||
}
|
||
}
|
||
|
||
// Header section
|
||
Column {
|
||
id: headerSection
|
||
anchors.top: parent.top
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.margins: activeTheme.spacingXL
|
||
spacing: activeTheme.spacingL
|
||
|
||
// Title and actions
|
||
Row {
|
||
width: parent.width
|
||
height: 40
|
||
|
||
Text {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
text: "Clipboard History"
|
||
font.pixelSize: activeTheme.fontSizeLarge + 4
|
||
font.weight: Font.Bold
|
||
color: activeTheme.surfaceText
|
||
}
|
||
|
||
Item {
|
||
width: parent.width - 180 - (clearAllButton.visible ? 48 : 0)
|
||
height: 1
|
||
}
|
||
|
||
// Clear all button
|
||
Rectangle {
|
||
id: clearAllButton
|
||
width: 40
|
||
height: 32
|
||
radius: activeTheme.cornerRadius
|
||
color: clearArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
visible: clipboardModel.count > 0
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "delete_sweep"
|
||
font.family: activeTheme.iconFont
|
||
font.pixelSize: activeTheme.iconSize
|
||
color: clearArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
|
||
}
|
||
|
||
MouseArea {
|
||
id: clearArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: clearAll()
|
||
}
|
||
|
||
Behavior on color {
|
||
ColorAnimation { duration: activeTheme.shortDuration }
|
||
}
|
||
}
|
||
|
||
// Close button
|
||
Rectangle {
|
||
width: 40
|
||
height: 32
|
||
radius: activeTheme.cornerRadius
|
||
color: closeArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
anchors.rightMargin: 4
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "close"
|
||
font.family: activeTheme.iconFont
|
||
font.pixelSize: activeTheme.iconSize
|
||
color: closeArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
|
||
}
|
||
|
||
MouseArea {
|
||
id: closeArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: clipboardHistory.hide()
|
||
}
|
||
|
||
Behavior on color {
|
||
ColorAnimation { duration: activeTheme.shortDuration }
|
||
}
|
||
}
|
||
}
|
||
|
||
// Search field
|
||
Rectangle {
|
||
width: parent.width
|
||
height: 48
|
||
radius: activeTheme.cornerRadiusLarge
|
||
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
||
border.color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
|
||
border.width: searchField.focus ? 2 : 1
|
||
|
||
Row {
|
||
anchors.left: parent.left
|
||
anchors.leftMargin: activeTheme.spacingL
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
spacing: activeTheme.spacingM
|
||
|
||
Text {
|
||
text: "search"
|
||
font.family: activeTheme.iconFont
|
||
font.pixelSize: activeTheme.iconSize
|
||
color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
}
|
||
|
||
TextInput {
|
||
id: searchField
|
||
width: parent.parent.width - 80
|
||
height: parent.parent.height
|
||
font.pixelSize: activeTheme.fontSizeLarge
|
||
color: activeTheme.surfaceText
|
||
verticalAlignment: TextInput.AlignVCenter
|
||
|
||
onTextChanged: updateFilteredModel()
|
||
|
||
Keys.onPressed: (event) => {
|
||
if (event.key === Qt.Key_Escape) {
|
||
clipboardHistory.hide()
|
||
}
|
||
}
|
||
|
||
// Placeholder text
|
||
Text {
|
||
text: "Search clipboard entries..."
|
||
font: searchField.font
|
||
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
visible: searchField.text.length === 0 && !searchField.focus
|
||
}
|
||
}
|
||
}
|
||
|
||
Behavior on border.color {
|
||
ColorAnimation { duration: activeTheme.shortDuration }
|
||
}
|
||
}
|
||
}
|
||
|
||
// Clipboard entries
|
||
Rectangle {
|
||
anchors.top: headerSection.bottom
|
||
anchors.bottom: parent.bottom
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.margins: activeTheme.spacingXL
|
||
anchors.topMargin: activeTheme.spacingL
|
||
|
||
color: "transparent"
|
||
|
||
ScrollView {
|
||
anchors.fill: parent
|
||
clip: true
|
||
|
||
ListView {
|
||
id: clipboardList
|
||
model: filteredClipboardModel
|
||
spacing: activeTheme.spacingS
|
||
|
||
delegate: Rectangle {
|
||
width: clipboardList.width
|
||
height: Math.max(60, contentColumn.implicitHeight + activeTheme.spacingM * 2)
|
||
radius: activeTheme.cornerRadius
|
||
color: entryArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) :
|
||
Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.05)
|
||
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.1)
|
||
border.width: 1
|
||
|
||
property string entryType: getEntryType(model.entry)
|
||
property string entryPreview: getEntryPreview(model.entry)
|
||
|
||
Row {
|
||
anchors.fill: parent
|
||
anchors.margins: activeTheme.spacingM
|
||
spacing: activeTheme.spacingL
|
||
|
||
// Entry type icon
|
||
Rectangle {
|
||
width: 36
|
||
height: 36
|
||
radius: activeTheme.cornerRadius
|
||
color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12)
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: {
|
||
switch (entryType) {
|
||
case "image": return "image"
|
||
case "long_text": return "subject"
|
||
default: return "content_paste"
|
||
}
|
||
}
|
||
font.family: activeTheme.iconFont
|
||
font.pixelSize: activeTheme.iconSize - 4
|
||
color: activeTheme.primary
|
||
}
|
||
}
|
||
|
||
// Entry content
|
||
Column {
|
||
id: contentColumn
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
width: parent.width - 100
|
||
spacing: activeTheme.spacingXS
|
||
|
||
Text {
|
||
text: {
|
||
switch (entryType) {
|
||
case "image": return "Image • " + entryPreview
|
||
case "long_text": return "Long Text"
|
||
default: return "Text"
|
||
}
|
||
}
|
||
font.pixelSize: activeTheme.fontSizeSmall
|
||
color: activeTheme.primary
|
||
font.weight: Font.Medium
|
||
width: parent.width
|
||
elide: Text.ElideRight
|
||
}
|
||
|
||
Text {
|
||
text: entryPreview
|
||
font.pixelSize: activeTheme.fontSizeMedium
|
||
color: activeTheme.surfaceText
|
||
width: parent.width
|
||
wrapMode: Text.WordWrap
|
||
maximumLineCount: entryType === "long_text" ? 3 : 2
|
||
elide: Text.ElideRight
|
||
visible: entryType !== "image"
|
||
}
|
||
}
|
||
|
||
// Actions
|
||
Column {
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
spacing: activeTheme.spacingXS
|
||
|
||
// Copy button
|
||
Rectangle {
|
||
width: 28
|
||
height: 28
|
||
radius: activeTheme.cornerRadiusSmall
|
||
color: copyArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "content_copy"
|
||
font.family: activeTheme.iconFont
|
||
font.pixelSize: activeTheme.iconSize - 8
|
||
color: copyArea.containsMouse ? activeTheme.primary : activeTheme.surfaceText
|
||
}
|
||
|
||
MouseArea {
|
||
id: copyArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: copyEntry(model.entry)
|
||
}
|
||
|
||
Behavior on color {
|
||
ColorAnimation { duration: activeTheme.shortDuration }
|
||
}
|
||
}
|
||
|
||
// Delete button
|
||
Rectangle {
|
||
width: 28
|
||
height: 28
|
||
radius: activeTheme.cornerRadiusSmall
|
||
color: deleteArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
|
||
|
||
Text {
|
||
anchors.centerIn: parent
|
||
text: "delete"
|
||
font.family: activeTheme.iconFont
|
||
font.pixelSize: activeTheme.iconSize - 8
|
||
color: deleteArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
|
||
}
|
||
|
||
MouseArea {
|
||
id: deleteArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
onClicked: deleteEntry(model.entry)
|
||
}
|
||
|
||
Behavior on color {
|
||
ColorAnimation { duration: activeTheme.shortDuration }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
id: entryArea
|
||
anchors.fill: parent
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
|
||
onClicked: copyEntry(model.entry)
|
||
}
|
||
|
||
Behavior on color {
|
||
ColorAnimation { duration: activeTheme.shortDuration }
|
||
}
|
||
}
|
||
}
|
||
|
||
// Empty state
|
||
Column {
|
||
anchors.centerIn: parent
|
||
spacing: activeTheme.spacingL
|
||
visible: filteredClipboardModel.count === 0
|
||
|
||
Text {
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
text: "content_paste_off"
|
||
font.family: activeTheme.iconFont
|
||
font.pixelSize: activeTheme.iconSizeLarge + 16
|
||
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.3)
|
||
}
|
||
|
||
Text {
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
text: "No clipboard history"
|
||
font.pixelSize: activeTheme.fontSizeLarge
|
||
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
|
||
}
|
||
|
||
Text {
|
||
anchors.horizontalCenter: parent.horizontalCenter
|
||
text: "Copy something to see it here"
|
||
font.pixelSize: activeTheme.fontSizeMedium
|
||
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.4)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Clipboard processes
|
||
Process {
|
||
id: clipboardProcess
|
||
command: ["cliphist", "list"]
|
||
running: false
|
||
|
||
stdout: SplitParser {
|
||
splitMarker: "\n"
|
||
onRead: (line) => {
|
||
if (line.trim()) {
|
||
clipboardHistory.clipboardEntries.push(line)
|
||
clipboardModel.append({"entry": line})
|
||
console.log("ClipboardHistory: Adding entry:", line.substring(0, 50) + "...")
|
||
}
|
||
}
|
||
}
|
||
|
||
onStarted: {
|
||
clipboardHistory.clipboardEntries = []
|
||
clipboardModel.clear()
|
||
console.log("ClipboardHistory: Starting cliphist process...")
|
||
}
|
||
|
||
onExited: (exitCode) => {
|
||
if (exitCode === 0) {
|
||
console.log("ClipboardHistory: Loaded", clipboardModel.count, "entries")
|
||
updateFilteredModel()
|
||
} else {
|
||
console.warn("ClipboardHistory: Failed to load clipboard history")
|
||
}
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: copyProcess
|
||
running: false
|
||
|
||
onExited: (exitCode) => {
|
||
if (exitCode !== 0) {
|
||
console.warn("ClipboardHistory: Failed to copy entry")
|
||
}
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: deleteProcess
|
||
running: false
|
||
|
||
onExited: (exitCode) => {
|
||
if (exitCode === 0) {
|
||
refreshClipboard()
|
||
}
|
||
}
|
||
}
|
||
|
||
Process {
|
||
id: clearProcess
|
||
command: ["cliphist", "wipe"]
|
||
running: false
|
||
|
||
onExited: (exitCode) => {
|
||
if (exitCode === 0) {
|
||
clipboardHistory.clipboardEntries = []
|
||
clipboardModel.clear()
|
||
}
|
||
}
|
||
}
|
||
|
||
Keys.onPressed: (event) => {
|
||
if (event.key === Qt.Key_Escape) {
|
||
hide()
|
||
}
|
||
}
|
||
} |