ai: gemini: files

This commit is contained in:
end-4
2025-08-21 22:53:11 +07:00
parent be1974a89e
commit 690e934a46
11 changed files with 353 additions and 47 deletions
@@ -40,7 +40,6 @@ Singleton {
* @returns { string }
*/
function shellSingleQuoteEscape(str) {
// escape single quotes
return String(str)
// .replace(/\\/g, '\\\\')
.replace(/'/g, "'\\''");
@@ -38,6 +38,13 @@ Item {
}
property var allCommands: [
{
name: "attach",
description: Translation.tr("Attach a file. Only works with Gemini."),
execute: (args) => {
Ai.attachFile(args.join(" ").trim());
}
},
{
name: "model",
description: Translation.tr("Choose model"),
@@ -421,13 +428,13 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
Rectangle { // Input area
id: inputWrapper
property real columnSpacing: 5
property real spacing: 5
Layout.fillWidth: true
radius: Appearance.rounding.small
color: Appearance.colors.colLayer1
implicitWidth: messageInputField.implicitWidth
implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin
+ commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + columnSpacing, 45)
implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin
+ commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + spacing, 45)
+ (attachedFileIndicator.implicitHeight + spacing + attachedFileIndicator.anchors.topMargin)
clip: true
border.color: Appearance.colors.colOutlineVariant
border.width: 1
@@ -436,12 +443,26 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
AttachedFileIndicator {
id: attachedFileIndicator
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: visible ? 5 : 0
}
filePath: Ai.pendingFilePath
onRemove: Ai.attachFile("")
}
RowLayout { // Input field and send button
id: inputFieldRowLayout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 5
anchors {
top: attachedFileIndicator.bottom
left: parent.left
right: parent.right
topMargin: 5
}
spacing: 0
StyledTextArea { // The actual TextArea
@@ -233,6 +233,15 @@ Rectangle {
}
}
Loader {
Layout.fillWidth: true
active: root.messageData?.localFilePath && root.messageData?.localFilePath.length > 0
sourceComponent: AttachedFileIndicator {
filePath: root.messageData?.localFilePath
canRemove: false
}
}
ColumnLayout { // Message content
id: messageContentColumnLayout
@@ -1,7 +1,7 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.modules.common.functions
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
@@ -0,0 +1,152 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
Rectangle {
id: root
signal remove()
property bool canRemove: true
property string filePath: ""
property string mimeType: ""
property real maxHeight: 200
property real imageWidth: -1
property real imageHeight: -1
property real scale: Math.min(root.maxHeight / imageHeight, root.width / imageWidth)
onFilePathChanged: refresh()
visible: filePath !== ""
function refresh() {
root.mimeType = "";
root.imageWidth = -1;
root.imageHeight = -1;
fileTypeProc.exec(["file", "-b", "--mime-type", filePath]);
}
Process {
id: fileTypeProc
command: ["file", "-b", "--mime-type", filePath]
stdout: StdioCollector {
onStreamFinished: {
root.mimeType = this.text;
if (root.mimeType.startsWith("image/"))
imageSizeProc.exec(["identify", "-format", "%wx%h", filePath]);
}
}
}
Process {
id: imageSizeProc
command: ["identify", "-format", "%wx%h", filePath]
stdout: StdioCollector {
onStreamFinished: {
const dimensions = this.text.split("x");
root.imageWidth = parseInt(dimensions[0]);
root.imageHeight = parseInt(dimensions[1]);
}
}
}
// Styles/widgets
property real horizontalPadding: 10
property real verticalPadding: 10
radius: Appearance.rounding.small - anchors.margins
color: Appearance.colors.colLayer2
implicitHeight: visible ? (contentItem.implicitHeight + verticalPadding * 2) : 0
ColumnLayout {
id: contentItem
anchors {
fill: parent
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
topMargin: root.verticalPadding
bottomMargin: root.verticalPadding
}
RowLayout {
MaterialSymbol {
Layout.alignment: Qt.AlignTop
text: {
if (root.mimeType.startsWith("image/"))
return "image";
if (root.mimeType.startsWith("audio/"))
return "music_note";
if (root.mimeType.startsWith("video/"))
return "movie";
if (root.mimeType === "application/pdf")
return "picture_as_pdf";
if (root.mimeType.startsWith("text/"))
return "description";
return "file_present";
}
iconSize: Appearance.font.pixelSize.hugeass
}
StyledText {
Layout.fillWidth: true
Layout.topMargin: 4
text: root.filePath
font.pixelSize: Appearance.font.pixelSize.smaller
font.family: Appearance.font.family.monospace
wrapMode: Text.Wrap
}
RippleButton {
visible: root.canRemove
Layout.alignment: Qt.AlignTop
buttonRadius: Appearance.rounding.full
colBackground: Appearance.colors.colLayer2
implicitHeight: 28
implicitWidth: 28
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: "close"
horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnSurfaceVariant
}
onClicked: root.remove()
}
}
Loader {
id: imagePreviewLoader
visible: (root.imageWidth != -1) && (root.imageHeight != -1)
Layout.alignment: Qt.AlignHCenter
sourceComponent: Item {
implicitHeight: root.imageHeight * root.scale
implicitWidth: imagePreview.implicitWidth
Image {
id: imagePreview
anchors.fill: parent
source: Qt.resolvedUrl(root.filePath)
fillMode: Image.PreserveAspectFit
antialiasing: true
asynchronous: true
width: root.imageWidth * root.scale
height: root.imageHeight * root.scale
sourceSize.width: root.imageWidth * root.scale
sourceSize.height: root.imageHeight * root.scale
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: imagePreview.width
height: imagePreview.height
radius: Appearance.rounding.normal
}
}
}
}
}
}
}