diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 16:45:36 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 16:45:36 +0200 |
| commit | 193cd8c2e1cc7e16b1389b95036bf7f4f99f97e0 (patch) | |
| tree | ee6e88c3069baf51462f8f036ae8e69da3bd2902 /desktop/assistant | |
| parent | 607934e74ffabc1dcce72b29fc28e92a669e54ce (diff) | |
| download | quickshell-193cd8c2e1cc7e16b1389b95036bf7f4f99f97e0.tar.gz quickshell-193cd8c2e1cc7e16b1389b95036bf7f4f99f97e0.zip | |
feat(assistant): Ask and Do as captioned tile grids with icons
The actions sat as buttons bunched at the left of the card. Each group is
now a grid of equal tiles across the card's width, icon over label,
about 150 px a column, with a small Ask or Do caption. Icons are Font
Awesome and Octicons codepoints checked in Inconsolata Nerd Font's cmap;
a saved preset without one gets a fallback. The lists referenced from
QML are var, since a library's top-level const is not reliably exposed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/assistant')
| -rw-r--r-- | desktop/assistant/Card.qml | 136 | ||||
| -rw-r--r-- | desktop/assistant/Ops.js | 41 | ||||
| -rw-r--r-- | desktop/assistant/Presets.js | 24 |
3 files changed, 135 insertions, 66 deletions
diff --git a/desktop/assistant/Card.qml b/desktop/assistant/Card.qml index 02ee476..069d3a0 100644 --- a/desktop/assistant/Card.qml +++ b/desktop/assistant/Card.qml @@ -205,7 +205,87 @@ Rectangle { // Ask: canned questions matching the staged kinds (Presets.js). A click // sends one; Shift+click puts it in the input to edit first. - Flow { + // One action as a tile: its icon over its label, the tiles of a group + // sharing the card's width equally. + component Tile: Rectangle { + id: tile + property string icon + property string label + property bool on: true + signal picked(var mouse) + implicitHeight: 62 + radius: 8 + opacity: on ? 1 : 0.4 + color: area.pressed && on ? Qt.alpha(Theme.accent, 0.35) + : area.containsMouse && on ? Qt.alpha(Theme.accent, 0.18) + : Qt.alpha(Theme.surface, 0.8) + border.width: 1 + border.color: Qt.alpha(Theme.text, 0.12) + Glow { anchors.fill: parent; z: -1; radius: parent.radius; on: area.containsMouse && tile.on } + Column { + anchors.centerIn: parent + width: parent.width - 12 + spacing: 4 + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: tile.icon + color: Theme.accent + font { family: Theme.iconFamily; pixelSize: Theme.fontSize + 2 } + } + Text { + width: parent.width + horizontalAlignment: Text.AlignHCenter + text: tile.label + elide: Text.ElideRight + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + } + MouseArea { + id: area + anchors.fill: parent + hoverEnabled: true + cursorShape: tile.on ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: mouse => { if (tile.on) tile.picked(mouse); } + } + } + + // A captioned grid of tiles; columns follow the width, about 150 px each. + component Group: Column { + id: group + property string caption + property var items: [] + property bool on: true + signal picked(var item, var mouse) + spacing: 6 + Text { + text: group.caption + color: Theme.subtext + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5; capitalization: Font.AllUppercase } + } + Grid { + id: grid + width: group.width + spacing: 8 + columns: Math.max(1, Math.floor((width + spacing) / (150 + spacing))) + Repeater { + model: group.items + Tile { + required property var modelData + width: (grid.width - (grid.columns - 1) * grid.spacing) / grid.columns + icon: modelData.icon ?? Presets.FALLBACK + label: modelData.label + on: group.on && (modelData.op !== "md" || card.canvas.connected) + onPicked: mouse => group.picked(modelData, mouse) + } + } + } + } + + // Ask: canned questions matching the staged kinds (Presets.js). A click + // sends one; Shift+click puts it in the input to edit first. + Group { id: asks readonly property var offered: Presets.offered(Presets.list(Prefs.presets), card.canvas.staged) visible: card.mode === "file" && offered.length > 0 && !card.canvas.opRunning @@ -213,57 +293,41 @@ Rectangle { bottom: dos.visible ? dos.top : status.visible ? status.top : actions.top; bottomMargin: 10 left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14 } - spacing: 8 - - Repeater { - model: asks.offered - Button { - required property var modelData - text: modelData.label - enabled: card.canvas.connected && !card.canvas.blocked - onClicked: mouse => { - if (mouse && (mouse.modifiers & Qt.ShiftModifier)) { - input.text = Presets.prompt(modelData); - input.cursorPosition = input.text.length; - input.forceActiveFocus(); - } else { - card.canvas.ask(Presets.prompt(modelData)); - } - } + caption: "Ask" + items: offered + on: card.canvas.connected && !card.canvas.blocked + onPicked: (p, mouse) => { + if (mouse.modifiers & Qt.ShiftModifier) { + input.text = Presets.prompt(p); + input.cursorPosition = input.text.length; + input.forceActiveFocus(); + } else { + card.canvas.ask(Presets.prompt(p)); } } } // Do: local operations (Ops.js, run by fileops or the daemon's write). - // Compress… swaps the row for its formats. Hidden while one runs. + // Compress… swaps the group for its formats. Hidden while one runs. property bool compressing: false Connections { target: card.canvas; function onStagedChanged() { card.compressing = false; } } - Flow { + Group { id: dos readonly property var offered: card.compressing - ? Ops.COMPRESS.concat([{ label: "Back", op: "back" }]) + ? Ops.COMPRESS.concat([{ label: "Back", op: "back", icon: "" }]) : Ops.offered(card.canvas.staged) visible: card.mode === "file" && offered.length > 0 && !card.canvas.opRunning anchors { bottom: status.visible ? status.top : actions.top; bottomMargin: 10 left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14 } - spacing: 8 - - Repeater { - model: dos.offered - Button { - required property var modelData - text: modelData.label - // Local operations need no daemon; To .md does. - enabled: modelData.op !== "md" || card.canvas.connected - onClicked: { - if (modelData.op === "compress") card.compressing = true; - else if (modelData.op === "back") card.compressing = false; - else { card.compressing = false; card.canvas.runOp(modelData); } - } - } + caption: card.compressing ? "Compress as" : "Do" + items: offered + onPicked: o => { + if (o.op === "compress") card.compressing = true; + else if (o.op === "back") card.compressing = false; + else { card.compressing = false; card.canvas.runOp(o); } } } diff --git a/desktop/assistant/Ops.js b/desktop/assistant/Ops.js index e59560d..2761a2d 100644 --- a/desktop/assistant/Ops.js +++ b/desktop/assistant/Ops.js @@ -15,29 +15,30 @@ // daemon's write, or "compress", which opens the COMPRESS formats. kinds // lists the staged kinds it fits ("any" fits every mix); min is the number // of files it needs; format is the suffix a file already has when the -// operation would not change it. +// operation would not change it. icon is a Nerd Font codepoint, checked in +// Inconsolata Nerd Font's cmap. const OPS = [ - { label: "To .md", op: "md", kinds: ["pdf"] }, - { label: "To images", op: "pdf-images", kinds: ["pdf"] }, - { label: "Merge", op: "pdf-merge", kinds: ["pdf"], min: 2 }, - { label: "Extract audio", op: "audio", kinds: ["video"] }, - { label: "To MP4", op: "mp4", kinds: ["video"], format: [".mp4"] }, - { label: "To WebM", op: "webm", kinds: ["video"], format: [".webm"] }, - { label: "To GIF", op: "gif", kinds: ["video"] }, - { label: "Trackcrop", op: "trackcrop", kinds: ["video"] }, - { label: "To PNG", op: "png", kinds: ["image"], format: [".png"] }, - { label: "To JPEG", op: "jpeg", kinds: ["image"], format: [".jpg", ".jpeg"] }, - { label: "To WebP", op: "webp", kinds: ["image"], format: [".webp"] }, - { label: "Resize 50%", op: "half", kinds: ["image"] }, - { label: "Extract", op: "extract", kinds: ["archive"] }, - { label: "Compress…", op: "compress", kinds: ["any"] }, + { label: "To .md", icon: "\uf48a", op: "md", kinds: ["pdf"] }, + { label: "To images", icon: "\uf1c5", op: "pdf-images", kinds: ["pdf"] }, + { label: "Merge", icon: "\uf419", op: "pdf-merge", kinds: ["pdf"], min: 2 }, + { label: "Extract audio", icon: "\uf001", op: "audio", kinds: ["video"] }, + { label: "To MP4", icon: "\uf008", op: "mp4", kinds: ["video"], format: [".mp4"] }, + { label: "To WebM", icon: "\uf008", op: "webm", kinds: ["video"], format: [".webm"] }, + { label: "To GIF", icon: "\uf1c8", op: "gif", kinds: ["video"] }, + { label: "Trackcrop", icon: "\uf05b", op: "trackcrop", kinds: ["video"] }, + { label: "To PNG", icon: "\uf03e", op: "png", kinds: ["image"], format: [".png"] }, + { label: "To JPEG", icon: "\uf03e", op: "jpeg", kinds: ["image"], format: [".jpg", ".jpeg"] }, + { label: "To WebP", icon: "\uf03e", op: "webp", kinds: ["image"], format: [".webp"] }, + { label: "Resize 50%", icon: "\uf065", op: "half", kinds: ["image"] }, + { label: "Extract", icon: "\uf07c", op: "extract", kinds: ["archive"] }, + { label: "Compress…", icon: "\uf1c6", op: "compress", kinds: ["any"] }, ]; -const COMPRESS = [ - { label: ".zip", op: "zip" }, - { label: ".tar.gz", op: "tgz" }, - { label: ".tar.xz", op: "txz" }, - { label: ".tar.bz2", op: "tbz2" }, +var COMPRESS = [ + { label: ".zip", icon: "\uf1c6", op: "zip" }, + { label: ".tar.gz", icon: "\uf1c6", op: "tgz" }, + { label: ".tar.xz", icon: "\uf1c6", op: "txz" }, + { label: ".tar.bz2", icon: "\uf1c6", op: "tbz2" }, ]; function ext(name) { diff --git a/desktop/assistant/Presets.js b/desktop/assistant/Presets.js index 116d7d0..057f993 100644 --- a/desktop/assistant/Presets.js +++ b/desktop/assistant/Presets.js @@ -11,22 +11,26 @@ .pragma library -// Canned questions for the file state's Ask row. kinds lists the staged +// Canned questions for the file state's Ask row. icon is a Nerd Font +// codepoint, checked in Inconsolata Nerd Font's cmap; a saved preset +// without one gets FALLBACK. kinds lists the staged // kinds a preset applies to (Files.js); ["many"] offers it whenever two or // more files are staged. The list in use is Prefs.presets when one is // saved (a settings page will edit it), these otherwise. const DEFAULTS = [ - { label: "Summarize", kinds: ["pdf", "text"], prompt: "Summarize this in a few short paragraphs." }, - { label: "Key points", kinds: ["pdf"], prompt: "List the key points as bullets." }, - { label: "Explain simply", kinds: ["pdf", "text"], prompt: "Explain this simply, as to a non-expert." }, - { label: "Describe", kinds: ["image"], prompt: "Describe this image." }, - { label: "Extract text", kinds: ["image", "pdf"], prompt: "Transcribe all the text, keeping the layout where it helps." }, - { label: "What is this?", kinds: ["image"], prompt: "What is this, and what is it for?" }, - { label: "Explain", kinds: ["code"], prompt: "Explain what this code does." }, - { label: "Review", kinds: ["code"], prompt: "Review this code: bugs, risks, and simplifications." }, - { label: "Compare", kinds: ["many"], prompt: "Compare these files: what they share and how they differ." }, + { label: "Summarize", icon: "\uf036", kinds: ["pdf", "text"], prompt: "Summarize this in a few short paragraphs." }, + { label: "Key points", icon: "\uf0ca", kinds: ["pdf"], prompt: "List the key points as bullets." }, + { label: "Explain simply", icon: "\uf0eb", kinds: ["pdf", "text"], prompt: "Explain this simply, as to a non-expert." }, + { label: "Describe", icon: "\uf06e", kinds: ["image"], prompt: "Describe this image." }, + { label: "Extract text", icon: "\uf031", kinds: ["image", "pdf"], prompt: "Transcribe all the text, keeping the layout where it helps." }, + { label: "What is this?", icon: "\uf059", kinds: ["image"], prompt: "What is this, and what is it for?" }, + { label: "Explain", icon: "\uf121", kinds: ["code"], prompt: "Explain what this code does." }, + { label: "Review", icon: "\uf188", kinds: ["code"], prompt: "Review this code: bugs, risks, and simplifications." }, + { label: "Compare", icon: "\uf0db", kinds: ["many"], prompt: "Compare these files: what they share and how they differ." }, ]; +var FALLBACK = "\uf086"; + // The saved list if it is one, else the defaults: a hand edit with a typo // must not leave the row empty. function list(saved) { |
