forked from Shinonome/dots-hyprland
Rearrange for tidier structure (#2212)
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property int messageIndex
|
||||
property var messageData
|
||||
property var messageInputField
|
||||
|
||||
property real messagePadding: 7
|
||||
property real contentSpacing: 3
|
||||
|
||||
property bool enableMouseSelection: false
|
||||
property bool renderMarkdown: true
|
||||
property bool editing: false
|
||||
|
||||
property list<var> messageBlocks: StringUtils.splitMarkdownBlocks(root.messageData?.content)
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
implicitHeight: columnLayout.implicitHeight + root.messagePadding * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
function saveMessage() {
|
||||
if (!root.editing) return;
|
||||
// Get all Loader children (each represents a segment)
|
||||
const segments = messageContentColumnLayout.children
|
||||
.map(child => child.segment)
|
||||
.filter(segment => (segment));
|
||||
|
||||
// Reconstruct markdown
|
||||
const newContent = segments.map(segment => {
|
||||
if (segment.type === "code") {
|
||||
const lang = segment.lang ? segment.lang : "";
|
||||
// Remove trailing newlines
|
||||
const code = segment.content.replace(/\n+$/, "");
|
||||
return "```" + lang + "\n" + code + "\n```";
|
||||
} else {
|
||||
return segment.content;
|
||||
}
|
||||
}).join("");
|
||||
|
||||
root.editing = false
|
||||
root.messageData.content = newContent;
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if ( // Prevent de-select
|
||||
event.key === Qt.Key_Control ||
|
||||
event.key == Qt.Key_Shift ||
|
||||
event.key == Qt.Key_Alt ||
|
||||
event.key == Qt.Key_Meta
|
||||
) {
|
||||
event.accepted = true
|
||||
}
|
||||
// Ctrl + S to save
|
||||
if ((event.key === Qt.Key_S) && event.modifiers == Qt.ControlModifier) {
|
||||
root.saveMessage();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout { // Main layout of the whole thing
|
||||
id: columnLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: messagePadding
|
||||
spacing: root.contentSpacing
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitWidth: headerRowLayout.implicitWidth + 4 * 2
|
||||
implicitHeight: headerRowLayout.implicitHeight + 4 * 2
|
||||
color: Appearance.colors.colSecondaryContainer
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
RowLayout { // Header
|
||||
id: headerRowLayout
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: 4
|
||||
}
|
||||
spacing: 18
|
||||
|
||||
Item { // Name
|
||||
id: nameWrapper
|
||||
implicitHeight: Math.max(nameRowLayout.implicitHeight + 5 * 2, 30)
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
RowLayout {
|
||||
id: nameRowLayout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 7
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: messageData?.role == 'assistant' ? modelIcon.width : roleIcon.implicitWidth
|
||||
implicitHeight: messageData?.role == 'assistant' ? modelIcon.height : roleIcon.implicitHeight
|
||||
|
||||
CustomIcon {
|
||||
id: modelIcon
|
||||
anchors.centerIn: parent
|
||||
visible: messageData?.role == 'assistant' && Ai.models[messageData?.model].icon
|
||||
width: Appearance.font.pixelSize.large
|
||||
height: Appearance.font.pixelSize.large
|
||||
source: messageData?.role == 'assistant' ? Ai.models[messageData?.model].icon :
|
||||
messageData?.role == 'user' ? 'linux-symbolic' : 'desktop-symbolic'
|
||||
|
||||
colorize: true
|
||||
color: Appearance.m3colors.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
id: roleIcon
|
||||
anchors.centerIn: parent
|
||||
visible: !modelIcon.visible
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: Appearance.m3colors.m3onSecondaryContainer
|
||||
text: messageData?.role == 'user' ? 'person' :
|
||||
messageData?.role == 'interface' ? 'settings' :
|
||||
messageData?.role == 'assistant' ? 'neurology' :
|
||||
'computer'
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: providerName
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.m3colors.m3onSecondaryContainer
|
||||
text: messageData?.role == 'assistant' ? Ai.models[messageData?.model].name :
|
||||
(messageData?.role == 'user' && SystemInfo.username) ? SystemInfo.username :
|
||||
Translation.tr("Interface")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button { // Not visible to model
|
||||
id: modelVisibilityIndicator
|
||||
visible: messageData?.role == 'interface'
|
||||
implicitWidth: 16
|
||||
implicitHeight: 30
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
background: Item
|
||||
|
||||
MaterialSymbol {
|
||||
id: notVisibleToModelText
|
||||
anchors.centerIn: parent
|
||||
iconSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colSubtext
|
||||
text: "visibility_off"
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Not visible to model")
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
spacing: 5
|
||||
|
||||
AiMessageControlButton {
|
||||
id: copyButton
|
||||
buttonIcon: activated ? "inventory" : "content_copy"
|
||||
|
||||
onClicked: {
|
||||
Quickshell.clipboardText = root.messageData?.content
|
||||
copyButton.activated = true
|
||||
copyIconTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyIconTimer
|
||||
interval: 1500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
copyButton.activated = false
|
||||
}
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Copy")
|
||||
}
|
||||
}
|
||||
AiMessageControlButton {
|
||||
id: editButton
|
||||
activated: root.editing
|
||||
enabled: root.messageData?.done ?? false
|
||||
buttonIcon: "edit"
|
||||
onClicked: {
|
||||
root.editing = !root.editing
|
||||
if (!root.editing) { // Save changes
|
||||
root.saveMessage()
|
||||
}
|
||||
}
|
||||
StyledToolTip {
|
||||
text: root.editing ? Translation.tr("Save") : Translation.tr("Edit")
|
||||
}
|
||||
}
|
||||
AiMessageControlButton {
|
||||
id: toggleMarkdownButton
|
||||
activated: !root.renderMarkdown
|
||||
buttonIcon: "code"
|
||||
onClicked: {
|
||||
root.renderMarkdown = !root.renderMarkdown
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("View Markdown source")
|
||||
}
|
||||
}
|
||||
AiMessageControlButton {
|
||||
id: deleteButton
|
||||
buttonIcon: "close"
|
||||
onClicked: {
|
||||
Ai.removeMessage(root.messageIndex)
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
spacing: 0
|
||||
Repeater {
|
||||
model: root.messageBlocks.length
|
||||
delegate: Loader {
|
||||
required property int index
|
||||
property var thisBlock: root.messageBlocks[index]
|
||||
Layout.fillWidth: true
|
||||
// property var segment: thisBlock
|
||||
property var segmentContent: thisBlock.content
|
||||
property var segmentLang: thisBlock.lang
|
||||
property var messageData: root.messageData
|
||||
property var editing: root.editing
|
||||
property var renderMarkdown: root.renderMarkdown
|
||||
property var enableMouseSelection: root.enableMouseSelection
|
||||
property bool thinking: root.messageData?.thinking ?? true
|
||||
property bool done: root.messageData?.done ?? false
|
||||
property bool completed: thisBlock.completed ?? false
|
||||
|
||||
property bool forceDisableChunkSplitting: root.messageData.content.includes("```")
|
||||
|
||||
source: thisBlock.type === "code" ? "MessageCodeBlock.qml" :
|
||||
thisBlock.type === "think" ? "MessageThinkBlock.qml" :
|
||||
"MessageTextBlock.qml"
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow { // Annotations
|
||||
visible: root.messageData?.annotationSources?.length > 0
|
||||
spacing: 5
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.messageData?.annotationSources || []
|
||||
}
|
||||
delegate: AnnotationSourceButton {
|
||||
required property var modelData
|
||||
displayText: modelData.text
|
||||
url: modelData.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow { // Search queries
|
||||
visible: root.messageData?.searchQueries?.length > 0
|
||||
spacing: 5
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.messageData?.searchQueries || []
|
||||
}
|
||||
delegate: SearchQueryButton {
|
||||
required property var modelData
|
||||
query: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
|
||||
GroupButton {
|
||||
id: button
|
||||
property string buttonIcon
|
||||
property bool activated: false
|
||||
toggled: activated
|
||||
baseWidth: height
|
||||
colBackgroundHover: Appearance.colors.colSecondaryContainerHover
|
||||
colBackgroundActive: Appearance.colors.colSecondaryContainerActive
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
text: buttonIcon
|
||||
color: button.activated ? Appearance.m3colors.m3onPrimary :
|
||||
button.enabled ? Appearance.m3colors.m3onSurface :
|
||||
Appearance.colors.colOnLayer1Inactive
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
property string displayText
|
||||
property string url
|
||||
|
||||
property real faviconSize: 20
|
||||
implicitHeight: 30
|
||||
leftPadding: (implicitHeight - faviconSize) / 2
|
||||
rightPadding: 10
|
||||
buttonRadius: Appearance.rounding.full
|
||||
colBackground: Appearance.colors.colSurfaceContainerHighest
|
||||
colBackgroundHover: Appearance.colors.colSurfaceContainerHighestHover
|
||||
colRipple: Appearance.colors.colSurfaceContainerHighestActive
|
||||
|
||||
PointingHandInteraction {}
|
||||
onClicked: {
|
||||
if (url) {
|
||||
Qt.openUrlExternally(url)
|
||||
GlobalStates.sidebarLeftOpen = false
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: rowLayout.implicitWidth
|
||||
implicitHeight: rowLayout.implicitHeight
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
Favicon {
|
||||
url: root.url
|
||||
size: root.faviconSize
|
||||
displayText: root.displayText
|
||||
}
|
||||
StyledText {
|
||||
id: text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: displayText
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Io
|
||||
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
|
||||
StyledImage {
|
||||
id: imagePreview
|
||||
anchors.fill: parent
|
||||
source: Qt.resolvedUrl(root.filePath)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
antialiasing: 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
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colOutlineVariant
|
||||
radius: Appearance.rounding.normal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import org.kde.syntaxhighlighting
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
// These are needed on the parent loader
|
||||
property bool editing: parent?.editing ?? false
|
||||
property bool renderMarkdown: parent?.renderMarkdown ?? true
|
||||
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
|
||||
property var segmentContent: parent?.segmentContent ?? ({})
|
||||
property var segmentLang: parent?.segmentLang ?? "txt"
|
||||
property bool isCommandRequest: segmentLang === "command"
|
||||
property var displayLang: (isCommandRequest ? "bash" : segmentLang)
|
||||
property var messageData: parent?.messageData ?? {}
|
||||
|
||||
property real codeBlockBackgroundRounding: Appearance.rounding.small
|
||||
property real codeBlockHeaderPadding: 3
|
||||
property real codeBlockComponentSpacing: 2
|
||||
|
||||
spacing: codeBlockComponentSpacing
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
Rectangle { // Code background
|
||||
Layout.fillWidth: true
|
||||
topLeftRadius: codeBlockBackgroundRounding
|
||||
topRightRadius: codeBlockBackgroundRounding
|
||||
bottomLeftRadius: Appearance.rounding.unsharpen
|
||||
bottomRightRadius: Appearance.rounding.unsharpen
|
||||
color: Appearance.colors.colSurfaceContainerHighest
|
||||
implicitHeight: codeBlockTitleBarRowLayout.implicitHeight + codeBlockHeaderPadding * 2
|
||||
|
||||
RowLayout { // Language and buttons
|
||||
id: codeBlockTitleBarRowLayout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: codeBlockHeaderPadding
|
||||
anchors.rightMargin: codeBlockHeaderPadding
|
||||
spacing: 5
|
||||
|
||||
StyledText {
|
||||
id: codeBlockLanguage
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.fillWidth: false
|
||||
Layout.topMargin: 7
|
||||
Layout.bottomMargin: 7
|
||||
Layout.leftMargin: 10
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
font.weight: Font.DemiBold
|
||||
color: Appearance.colors.colOnLayer2
|
||||
text: root.displayLang ? Repository.definitionForName(root.displayLang).name : "plain"
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
ButtonGroup {
|
||||
AiMessageControlButton {
|
||||
id: copyCodeButton
|
||||
buttonIcon: activated ? "inventory" : "content_copy"
|
||||
|
||||
onClicked: {
|
||||
Quickshell.clipboardText = segmentContent
|
||||
copyCodeButton.activated = true
|
||||
copyIconTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: copyIconTimer
|
||||
interval: 1500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
copyCodeButton.activated = false
|
||||
}
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Copy code")
|
||||
}
|
||||
}
|
||||
AiMessageControlButton {
|
||||
id: saveCodeButton
|
||||
buttonIcon: activated ? "check" : "save"
|
||||
|
||||
onClicked: {
|
||||
const downloadPath = FileUtils.trimFileProtocol(Directories.downloads)
|
||||
Quickshell.execDetached(["bash", "-c",
|
||||
`echo '${StringUtils.shellSingleQuoteEscape(segmentContent)}' > '${downloadPath}/code.${segmentLang || "txt"}'`
|
||||
])
|
||||
Quickshell.execDetached(["notify-send",
|
||||
Translation.tr("Code saved to file"),
|
||||
Translation.tr("Saved to %1").arg(`${downloadPath}/code.${segmentLang || "txt"}`),
|
||||
"-a", "Shell"
|
||||
])
|
||||
saveCodeButton.activated = true
|
||||
saveIconTimer.restart()
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveIconTimer
|
||||
interval: 1500
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
saveCodeButton.activated = false
|
||||
}
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Save to Downloads")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout { // Line numbers and code
|
||||
spacing: codeBlockComponentSpacing
|
||||
|
||||
Rectangle { // Line numbers
|
||||
implicitWidth: 40
|
||||
implicitHeight: lineNumberColumnLayout.implicitHeight
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: false
|
||||
topLeftRadius: Appearance.rounding.unsharpen
|
||||
bottomLeftRadius: codeBlockBackgroundRounding
|
||||
topRightRadius: Appearance.rounding.unsharpen
|
||||
bottomRightRadius: Appearance.rounding.unsharpen
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
ColumnLayout {
|
||||
id: lineNumberColumnLayout
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
top: parent.top
|
||||
topMargin: 6
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: codeTextArea.text.split("\n").length
|
||||
Text {
|
||||
required property int index
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignRight
|
||||
font.family: Appearance.font.family.monospace
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colSubtext
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // Code background
|
||||
Layout.fillWidth: true
|
||||
topLeftRadius: Appearance.rounding.unsharpen
|
||||
bottomLeftRadius: Appearance.rounding.unsharpen
|
||||
topRightRadius: Appearance.rounding.unsharpen
|
||||
bottomRightRadius: codeBlockBackgroundRounding
|
||||
color: Appearance.colors.colLayer2
|
||||
implicitHeight: codeColumnLayout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: codeColumnLayout
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
ScrollView {
|
||||
id: codeScrollView
|
||||
Layout.fillWidth: true
|
||||
// Layout.fillHeight: true
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: codeTextArea.implicitHeight + 1
|
||||
contentWidth: codeTextArea.width - 1
|
||||
// contentHeight: codeTextArea.contentHeight
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
padding: 5
|
||||
policy: ScrollBar.AsNeeded
|
||||
opacity: visualSize == 1 ? 0 : 1
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitHeight: 6
|
||||
radius: Appearance.rounding.small
|
||||
color: Appearance.colors.colLayer2Active
|
||||
}
|
||||
}
|
||||
|
||||
TextArea { // Code
|
||||
id: codeTextArea
|
||||
Layout.fillWidth: true
|
||||
readOnly: !editing
|
||||
selectByMouse: enableMouseSelection || editing
|
||||
renderType: Text.NativeRendering
|
||||
font.family: Appearance.font.family.monospace
|
||||
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
// wrapMode: TextEdit.Wrap
|
||||
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
||||
|
||||
text: segmentContent
|
||||
onTextChanged: {
|
||||
segmentContent = text
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Tab) {
|
||||
// Insert 4 spaces at cursor
|
||||
const cursor = codeTextArea.cursorPosition;
|
||||
codeTextArea.insert(cursor, " ");
|
||||
codeTextArea.cursorPosition = cursor + 4;
|
||||
event.accepted = true;
|
||||
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
|
||||
codeTextArea.copy();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
SyntaxHighlighter {
|
||||
id: highlighter
|
||||
textEdit: codeTextArea
|
||||
repository: Repository
|
||||
definition: Repository.definitionForName(root.displayLang || "plaintext")
|
||||
theme: Appearance.syntaxHighlightingTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
Loader {
|
||||
active: root.isCommandRequest && root.messageData.functionPending
|
||||
visible: active
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 6
|
||||
Layout.topMargin: 0
|
||||
sourceComponent: RowLayout {
|
||||
Item { Layout.fillWidth: true }
|
||||
ButtonGroup {
|
||||
GroupButton {
|
||||
contentItem: StyledText {
|
||||
text: Translation.tr("Reject")
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnLayer2
|
||||
}
|
||||
onClicked: Ai.rejectCommand(root.messageData)
|
||||
}
|
||||
GroupButton {
|
||||
toggled: true
|
||||
contentItem: StyledText {
|
||||
text: Translation.tr("Approve")
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
color: Appearance.colors.colOnPrimary
|
||||
}
|
||||
onClicked: Ai.approveCommand(root.messageData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseArea to block scrolling
|
||||
// MouseArea {
|
||||
// id: codeBlockMouseArea
|
||||
// anchors.fill: parent
|
||||
// acceptedButtons: editing ? Qt.NoButton : Qt.LeftButton
|
||||
// cursorShape: (enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
|
||||
// onWheel: (event) => {
|
||||
// event.accepted = false
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
// These are needed on the parent loader
|
||||
property bool editing: parent?.editing ?? false
|
||||
property bool renderMarkdown: parent?.renderMarkdown ?? true
|
||||
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
|
||||
property string segmentContent: parent?.segmentContent ?? ({})
|
||||
property var messageData: parent?.messageData ?? {}
|
||||
property bool done: parent?.done ?? true
|
||||
property list<string> renderedLatexHashes: []
|
||||
|
||||
property string renderedSegmentContent: ""
|
||||
property string shownText: ""
|
||||
property bool forceDisableChunkSplitting: parent?.forceDisableChunkSplitting ?? false
|
||||
property bool fadeChunkSplitting: !forceDisableChunkSplitting && !editing && !/\n\|/.test(shownText) && Config.options.sidebar.ai.textFadeIn
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
Timer {
|
||||
id: renderTimer
|
||||
interval: 1000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
renderLatex()
|
||||
for (const hash of renderedLatexHashes) {
|
||||
handleRenderedLatex(hash, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderLatex() {
|
||||
// Regex for $...$, $$...$$, \[...\]
|
||||
// Note: This is a simple approach and may need refinement for edge cases
|
||||
let regex = /(\$\$([\s\S]+?)\$\$)|(\$([^\$]+?)\$)|(\\\[((?:.|\n)+?)\\\])|(\\\(([\s\S]+?)\\\))/g;
|
||||
let match;
|
||||
while ((match = regex.exec(segmentContent)) !== null) {
|
||||
let expression = match[1] || match[2] || match[3] || match[4] || match[5] || match[6] || match[7] || match[8];
|
||||
if (expression) {
|
||||
Qt.callLater(() => {
|
||||
const [renderHash, isNew] = LatexRenderer.requestRender(expression.trim());
|
||||
if (!renderedLatexHashes.includes(renderHash)) {
|
||||
renderedLatexHashes.push(renderHash);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRenderedLatex(hash, force = false) {
|
||||
if (renderedLatexHashes.includes(hash) || force) {
|
||||
const imagePath = LatexRenderer.renderedImagePaths[hash];
|
||||
const markdownImage = ``;
|
||||
|
||||
const expression = LatexRenderer.processedExpressions[hash];
|
||||
renderedSegmentContent = renderedSegmentContent.replace(expression, markdownImage);
|
||||
}
|
||||
}
|
||||
|
||||
onDoneChanged: {
|
||||
renderTimer.restart();
|
||||
}
|
||||
onEditingChanged: {
|
||||
if (!editing) {
|
||||
renderLatex()
|
||||
} else {
|
||||
// console.log("Editing mode enabled", segmentContent)
|
||||
root.shownText = segmentContent
|
||||
}
|
||||
}
|
||||
|
||||
onSegmentContentChanged: {
|
||||
// console.log("Segment content changed: " + segmentContent);
|
||||
renderedSegmentContent = segmentContent;
|
||||
if (!root.editing && segmentContent) {
|
||||
root.renderLatex();
|
||||
}
|
||||
}
|
||||
|
||||
onRenderedSegmentContentChanged: {
|
||||
// console.log("Rendered segment content changed: " + renderedSegmentContent);
|
||||
if (renderedSegmentContent) {
|
||||
root.shownText = renderedSegmentContent;
|
||||
}
|
||||
}
|
||||
|
||||
// When something finishes rendering
|
||||
// 1. Check if the hash is in the list
|
||||
// 2. If it is, replace the expression with the image path
|
||||
Connections {
|
||||
target: LatexRenderer
|
||||
function onRenderFinished(hash, imagePath) {
|
||||
const expression = LatexRenderer.processedExpressions[hash];
|
||||
// console.log("Render finished: " + hash + " " + expression);
|
||||
handleRenderedLatex(hash);
|
||||
}
|
||||
}
|
||||
|
||||
spacing: 0
|
||||
Repeater {
|
||||
id: textLinesRepeater
|
||||
property list<real> textLineOpacities: []
|
||||
model: ScriptModel {
|
||||
// Split by either double newlines or single newlines in a list
|
||||
values: root.fadeChunkSplitting ? root.shownText.split(/\n\n(?= {0,2})|\n(?= {0,2}[-\*])/g).filter(line => line.trim() !== "") : [root.shownText]
|
||||
onValuesChanged: {
|
||||
while (textLinesRepeater.textLineOpacities.length < values.length) {
|
||||
textLinesRepeater.textLineOpacities.push(root.messageData.done ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
delegate: TextArea {
|
||||
id: textArea
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
// Fade in animation
|
||||
visible: opacity > 0
|
||||
opacity: fadeChunkSplitting ? (textLinesRepeater.textLineOpacities[index] ?? (root.messageData.done ? 1 : 0)) : 1
|
||||
Connections {
|
||||
target: root.messageData
|
||||
function onDoneChanged() {
|
||||
if (root.messageData.done) {
|
||||
textLinesRepeater.textLineOpacities[textArea.index] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: textLinesRepeater.model
|
||||
function onValuesChanged() {
|
||||
if (textLinesRepeater.model.values.length > textArea.index + 1) {
|
||||
textLinesRepeater.textLineOpacities[textArea.index] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
readOnly: !editing
|
||||
selectByMouse: enableMouseSelection || editing
|
||||
renderType: Text.NativeRendering
|
||||
font.family: Appearance.font.family.reading
|
||||
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
wrapMode: TextEdit.Wrap
|
||||
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
|
||||
textFormat: renderMarkdown ? TextEdit.MarkdownText : TextEdit.PlainText
|
||||
text: modelData
|
||||
|
||||
onTextChanged: {
|
||||
if (!root.editing) return
|
||||
segmentContent = text
|
||||
}
|
||||
|
||||
onLinkActivated: (link) => {
|
||||
Qt.openUrlExternally(link)
|
||||
GlobalStates.sidebarLeftOpen = false
|
||||
}
|
||||
|
||||
MouseArea { // Pointing hand for links
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton // Only for hover
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor :
|
||||
(enableMouseSelection || editing) ? Qt.IBeamCursor : Qt.ArrowCursor
|
||||
}
|
||||
|
||||
// Rectangle {
|
||||
// anchors.fill: parent
|
||||
// color: "#22786378"
|
||||
// border.width: 1
|
||||
// border.color: "#7E7E7E"
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
// These are needed on the parent loader
|
||||
property bool editing: parent?.editing ?? false
|
||||
property bool renderMarkdown: parent?.renderMarkdown ?? true
|
||||
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
|
||||
property string segmentContent: parent?.segmentContent ?? ({})
|
||||
property var messageData: parent?.messageData ?? {}
|
||||
property bool done: parent?.done ?? true
|
||||
property bool completed: parent?.completed ?? false
|
||||
|
||||
property real thinkBlockBackgroundRounding: Appearance.rounding.small
|
||||
property real thinkBlockHeaderPaddingVertical: 3
|
||||
property real thinkBlockHeaderPaddingHorizontal: 10
|
||||
property real thinkBlockComponentSpacing: 2
|
||||
|
||||
property var collapseAnimation: messageTextBlock.implicitHeight > 40 ? Appearance.animation.elementMoveEnter : Appearance.animation.elementMoveFast
|
||||
property bool collapsed: true /* should be root.completed but its kinda buggy rn so nope */
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: collapsed ? header.implicitHeight : columnLayout.implicitHeight
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: root.width
|
||||
height: root.height
|
||||
radius: thinkBlockBackgroundRounding
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: root.completed ?? false
|
||||
NumberAnimation {
|
||||
duration: collapseAnimation.duration
|
||||
easing.type: collapseAnimation.type
|
||||
easing.bezierCurve: collapseAnimation.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: 0
|
||||
|
||||
Rectangle { // Header background
|
||||
id: header
|
||||
color: Appearance.colors.colSurfaceContainerHighest
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: thinkBlockTitleBarRowLayout.implicitHeight + thinkBlockHeaderPaddingVertical * 2
|
||||
|
||||
MouseArea { // Click to reveal
|
||||
id: headerMouseArea
|
||||
enabled: root.completed
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
root.collapsed = !root.collapsed
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout { // Header content
|
||||
id: thinkBlockTitleBarRowLayout
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: thinkBlockHeaderPaddingHorizontal
|
||||
anchors.rightMargin: thinkBlockHeaderPaddingHorizontal
|
||||
spacing: 10
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.fillWidth: false
|
||||
Layout.topMargin: 7
|
||||
Layout.bottomMargin: 7
|
||||
Layout.leftMargin: 3
|
||||
text: "linked_services"
|
||||
}
|
||||
StyledText {
|
||||
id: thinkBlockLanguage
|
||||
Layout.fillWidth: false
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: root.completed ? Translation.tr("Thought") : (Translation.tr("Thinking") + ".".repeat(Math.random() * 4))
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
RippleButton { // Expand button
|
||||
id: expandButton
|
||||
visible: root.completed
|
||||
implicitWidth: 22
|
||||
implicitHeight: 22
|
||||
colBackground: headerMouseArea.containsMouse ? Appearance.colors.colLayer2Hover
|
||||
: ColorUtils.transparentize(Appearance.colors.colLayer2, 1)
|
||||
colBackgroundHover: Appearance.colors.colLayer2Hover
|
||||
colRipple: Appearance.colors.colLayer2Active
|
||||
|
||||
onClicked: { root.collapsed = !root.collapsed }
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
text: "keyboard_arrow_down"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer2
|
||||
rotation: root.collapsed ? 0 : 180
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: content
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: collapsed ? 0 : contentBackground.implicitHeight + thinkBlockComponentSpacing
|
||||
clip: true
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: root.completed ?? false
|
||||
NumberAnimation {
|
||||
duration: collapseAnimation.duration
|
||||
easing.type: collapseAnimation.type
|
||||
easing.bezierCurve: collapseAnimation.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contentBackground
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
implicitHeight: messageTextBlock.implicitHeight
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
// Load data for the message at the correct scope
|
||||
property bool editing: root.editing
|
||||
property bool renderMarkdown: root.renderMarkdown
|
||||
property bool enableMouseSelection: root.enableMouseSelection
|
||||
property string segmentContent: root.segmentContent
|
||||
property var messageData: root.messageData
|
||||
property bool done: root.done
|
||||
|
||||
MessageTextBlock {
|
||||
id: messageTextBlock
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
property string query
|
||||
|
||||
implicitHeight: 30
|
||||
leftPadding: 6
|
||||
rightPadding: 10
|
||||
buttonRadius: Appearance.rounding.verysmall
|
||||
colBackground: Appearance.colors.colSurfaceContainerHighest
|
||||
colBackgroundHover: Appearance.colors.colSurfaceContainerHighestHover
|
||||
colRipple: Appearance.colors.colSurfaceContainerHighestActive
|
||||
|
||||
PointingHandInteraction {}
|
||||
onClicked: {
|
||||
let url = Config.options.search.engineBaseUrl + root.query;
|
||||
for (let site of (Config?.options?.search.excludedSites ?? [])) {
|
||||
url += ` -site:${site}`;
|
||||
}
|
||||
Qt.openUrlExternally(url);
|
||||
GlobalStates.sidebarLeftOpen = false;
|
||||
}
|
||||
|
||||
contentItem: Item {
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: rowLayout.implicitWidth
|
||||
implicitHeight: rowLayout.implicitHeight
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
MaterialSymbol {
|
||||
text: "search"
|
||||
iconSize: 20
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
StyledText {
|
||||
id: text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.query
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user