Clipboard history picker: Omarchy shell plugin (tank.clipboard) with fuzzy search, rich previews, capture daemon
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
import "Classify.js" as Classify
|
||||
|
||||
// Right-hand preview pane for the clipboard picker.
|
||||
// Renders a full preview plus metadata chips, per derived type.
|
||||
// `result` is a Fuzzy search result: { row: {entry, content, app, type, ...}, positions }
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var result: null
|
||||
property var entry: result ? result.row.entry : null
|
||||
property string derived: result ? result.row.type : ""
|
||||
|
||||
readonly property string font_: Style.font.menuFamily
|
||||
readonly property color fg: Color.menu.text
|
||||
readonly property color mutedFg: Util.alpha(fg, 0.55)
|
||||
readonly property color chipBg: Util.alpha(fg, 0.07)
|
||||
readonly property color lineColor: Util.alpha(fg, 0.16)
|
||||
|
||||
property string bodyText: ""
|
||||
|
||||
onResultChanged: prepare()
|
||||
|
||||
function prepare() {
|
||||
bodyText = ""
|
||||
if (!entry) return
|
||||
var t = derived
|
||||
if (t === "json") {
|
||||
var pretty = Classify.prettyJson(String(entry.text || ""), 200000)
|
||||
bodyText = pretty || String(entry.text || "")
|
||||
} else if (t === "html") {
|
||||
bodyText = Classify.stripHtml(String(entry.text || "")) || String(entry.text || "")
|
||||
} else if (t === "text" || t === "code" || t === "email" || t === "number") {
|
||||
bodyText = String(entry.text || "")
|
||||
}
|
||||
}
|
||||
|
||||
function rawSafe() {
|
||||
return entry ? String(entry.text || "").trim() : ""
|
||||
}
|
||||
|
||||
function rgbLine() {
|
||||
var rgb = Classify.colorToRgb(rawSafe())
|
||||
return rgb ? "rgb(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ")" : ""
|
||||
}
|
||||
|
||||
function hslLine() {
|
||||
var hsl = Classify.colorToHsl(rawSafe())
|
||||
return hsl ? "hsl(" + hsl[0] + ", " + hsl[1] + "%, " + hsl[2] + "%)" : ""
|
||||
}
|
||||
|
||||
function metaChips() {
|
||||
if (!result) return []
|
||||
var r = result.row
|
||||
var e = r.entry
|
||||
var chips = []
|
||||
if (r.app) chips.push(Classify.prettyApp(r.app))
|
||||
if (e.type === "text" && e.text) {
|
||||
var st = Classify.textStats(e.text)
|
||||
chips.push(Classify.plural(st.words, "word"))
|
||||
chips.push(Classify.plural(st.lines, "line"))
|
||||
}
|
||||
if (r.bytes > 0) chips.push(Classify.formatBytes(r.bytes))
|
||||
if (e.type === "image" && e.w && e.h) chips.push(e.w + "×" + e.h)
|
||||
if (e.type === "image") chips.push(e.mime || "image")
|
||||
if (e.pinned) chips.push("★ pinned")
|
||||
if (r.uses > 0) chips.push("pasted " + r.uses + "×")
|
||||
return chips
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- header
|
||||
|
||||
Row {
|
||||
id: headerRow
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Style.space(10)
|
||||
|
||||
Rectangle {
|
||||
width: Style.space(38)
|
||||
height: width
|
||||
radius: Style.cornerRadius
|
||||
color: root.chipBg
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: Classify.typeIcon(root.derived)
|
||||
color: root.fg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.heading
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Style.space(48) - badge.width - Style.space(10)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(2)
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: {
|
||||
if (!root.entry) return ""
|
||||
var e = root.entry
|
||||
if (e.type === "image") return Classify.fileBase(e.path)
|
||||
if (e.type === "files") {
|
||||
var base = Classify.fileBase(e.paths[0])
|
||||
return e.paths.length > 1 ? base + " +" + (e.paths.length - 1) + " more" : base
|
||||
}
|
||||
return Classify.firstLine(e.text, 200)
|
||||
}
|
||||
color: root.fg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.title
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: {
|
||||
if (!root.result) return ""
|
||||
var r = root.result.row
|
||||
var parts = []
|
||||
if (r.ts) parts.push(Classify.formatFullDate(r.ts))
|
||||
if (r.bytes) parts.push(Classify.formatBytes(r.bytes))
|
||||
return parts.join(" · ")
|
||||
}
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: badge
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: height / 2
|
||||
color: root.chipBg
|
||||
width: badgeLabel.implicitWidth + Style.space(14)
|
||||
height: Style.space(20)
|
||||
|
||||
Text {
|
||||
id: badgeLabel
|
||||
anchors.centerIn: parent
|
||||
text: Classify.typeIcon(root.derived) + " " + Classify.typeLabel(root.derived)
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: divider
|
||||
anchors.top: headerRow.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Style.space(10)
|
||||
height: 1
|
||||
color: root.lineColor
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- body
|
||||
|
||||
// text-ish body (text / code / json / html / email / number)
|
||||
Flickable {
|
||||
id: textBody
|
||||
anchors.top: divider.bottom
|
||||
anchors.bottom: metaRow.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Style.space(10)
|
||||
anchors.bottomMargin: Style.space(10)
|
||||
visible: root.bodyText !== ""
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: bodyEdit.implicitHeight
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
WheelHandler {
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: function(ev) {
|
||||
if (ev.angleDelta.y < 0) textBody.flick(0, -240)
|
||||
else textBody.flick(0, 240)
|
||||
ev.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
TextEdit {
|
||||
id: bodyEdit
|
||||
width: parent.width
|
||||
readOnly: true
|
||||
activeFocusOnPress: false
|
||||
text: root.bodyText
|
||||
textFormat: TextEdit.PlainText
|
||||
color: root.fg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.body
|
||||
wrapMode: TextEdit.Wrap
|
||||
selectionColor: Util.alpha(Color.accent, 0.4)
|
||||
}
|
||||
}
|
||||
|
||||
// color body
|
||||
Column {
|
||||
anchors.top: divider.bottom
|
||||
anchors.bottom: metaRow.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Style.space(10)
|
||||
anchors.bottomMargin: Style.space(10)
|
||||
visible: root.derived === "color"
|
||||
spacing: Style.space(14)
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(Style.space(240), parent.width)
|
||||
height: Style.space(120)
|
||||
radius: Style.cornerRadius
|
||||
color: root.hexRe.test(root.rawSafe()) ? root.rawSafe() : "transparent"
|
||||
border.width: 1
|
||||
border.color: root.lineColor
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.rawSafe()
|
||||
color: root.fg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.heading
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.rgbLine().length > 0
|
||||
text: root.rgbLine()
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.hslLine().length > 0
|
||||
text: root.hslLine()
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
}
|
||||
|
||||
// link body
|
||||
Column {
|
||||
anchors.top: divider.bottom
|
||||
anchors.bottom: metaRow.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Style.space(10)
|
||||
anchors.bottomMargin: Style.space(10)
|
||||
visible: root.derived === "link"
|
||||
spacing: Style.space(10)
|
||||
|
||||
Text {
|
||||
text: Classify.urlDomain(root.entry ? String(root.entry.text || "") : "")
|
||||
color: Color.accent
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.display
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.entry ? Classify.firstLine(String(root.entry.text || ""), 400) : ""
|
||||
color: root.fg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.body
|
||||
wrapMode: Text.WrapAnywhere
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Ctrl+O opens this link in your browser"
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
}
|
||||
}
|
||||
|
||||
// image body
|
||||
Column {
|
||||
anchors.top: divider.bottom
|
||||
anchors.bottom: metaRow.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Style.space(10)
|
||||
anchors.bottomMargin: Style.space(10)
|
||||
visible: root.derived === "image"
|
||||
spacing: Style.space(8)
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - infoLabel.height - Style.space(12)
|
||||
|
||||
Image {
|
||||
id: img
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
source: root.entry && root.entry.path ? Util.fileUrl(root.entry.path) : ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
cache: false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: img
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: root.lineColor
|
||||
radius: Style.space(4)
|
||||
visible: img.status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: img.status === Image.Error || img.status === Image.Null
|
||||
text: "Preview unavailable"
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: infoLabel
|
||||
text: {
|
||||
if (!root.entry) return ""
|
||||
var e = root.entry
|
||||
var dims = e.w && e.h ? e.w + " × " + e.h + " px · " : ""
|
||||
return dims + (e.mime || "image") + (e.bytes ? " · " + Classify.formatBytes(e.bytes) : "")
|
||||
}
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
}
|
||||
}
|
||||
|
||||
// files body
|
||||
Column {
|
||||
anchors.top: divider.bottom
|
||||
anchors.bottom: metaRow.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Style.space(10)
|
||||
anchors.bottomMargin: Style.space(10)
|
||||
visible: root.derived === "files"
|
||||
spacing: Style.space(6)
|
||||
clip: true
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!root.entry || root.entry.type !== "files") return []
|
||||
return (root.entry.paths || []).slice(0, 10)
|
||||
}
|
||||
|
||||
delegate: Row {
|
||||
required property var modelData
|
||||
width: parent ? parent.width : 0
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
text: ""
|
||||
color: Color.accent
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.body
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Classify.fileBase(modelData)
|
||||
color: root.fg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.body
|
||||
elide: Text.ElideMiddle
|
||||
width: parent.width - Style.space(24)
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.entry && root.entry.paths && root.entry.paths.length > 10
|
||||
text: root.entry && root.entry.paths ? "… and " + (root.entry.paths.length - 10) + " more" : ""
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.entry && root.entry.paths && root.entry.paths.length > 0
|
||||
? "in " + Classify.fileDir(root.entry.paths[0])
|
||||
: ""
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
elide: Text.ElideMiddle
|
||||
width: parent.width
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- meta chips
|
||||
|
||||
Row {
|
||||
id: metaRow
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Style.space(6)
|
||||
|
||||
Rectangle {
|
||||
width: Style.space(20)
|
||||
height: Style.space(20)
|
||||
radius: height / 2
|
||||
color: root.chipBg
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: Classify.typeIcon(root.derived)
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
radius: height / 2
|
||||
color: root.chipBg
|
||||
width: typeChipText.implicitWidth + Style.space(14)
|
||||
height: Style.space(20)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
id: typeChipText
|
||||
anchors.centerIn: parent
|
||||
text: Classify.typeLabel(root.derived)
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.metaChips()
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
property string chipText: String(modelData)
|
||||
|
||||
radius: height / 2
|
||||
color: root.chipBg
|
||||
width: chipLabel.implicitWidth + Style.space(14)
|
||||
height: Style.space(20)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
id: chipLabel
|
||||
anchors.centerIn: parent
|
||||
text: parent.chipText
|
||||
color: root.mutedFg
|
||||
font.family: root.font_
|
||||
font.pixelSize: Style.font.caption
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var hexRe: /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i
|
||||
}
|
||||
Reference in New Issue
Block a user