booru: tag suggestions

This commit is contained in:
end-4
2025-05-01 19:57:10 +02:00
parent 730488f3c5
commit 1beb723cf3
3 changed files with 396 additions and 206 deletions
+302 -181
View File
@@ -19,7 +19,19 @@ Item {
property string previewDownloadPath: `${StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]}/media/waifus`.replace("file://", "")
property string downloadPath: (StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] + "/homework").replace("file://", "")
property string nsfwPath: (StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] + "/homework/🌶️").replace("file://", "")
property string commandPrefix: "/"
property real scrollOnNewResponse: 100
property int tagSuggestionDelay: 210
property var suggestionQuery: ""
property var suggestionList: []
Connections {
target: Booru
function onTagSuggestion(query, suggestions) {
root.suggestionQuery = query;
root.suggestionList = suggestions;
}
}
Component.onCompleted: {
Hyprland.dispatch(`exec rm -rf ${previewDownloadPath}`)
@@ -27,7 +39,7 @@ Item {
}
function handleInput(inputText) {
if (inputText.startsWith("/")) {
if (inputText.startsWith(root.commandPrefix)) {
// Handle special commands
const command = inputText.split(" ")[0].substring(1);
const args = inputText.split(" ").slice(1);
@@ -168,23 +180,98 @@ Item {
text: "bookmark_heart"
}
StyledText {
id: widgetNameText
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
text: "Anime boorus"
text: qsTr("Anime boorus")
}
}
}
}
Rectangle { // Tag input field
Flow { // Tag suggestions
id: tagSuggestions
visible: root.suggestionList.length > 0 &&
tagInputField.text.length > 0
property int selectedIndex: 0
Layout.fillWidth: true
spacing: 5
Repeater {
id: tagSuggestionRepeater
model: {
tagSuggestions.selectedIndex = 0
return root.suggestionList.slice(0, 10)
}
delegate: BooruTagButton {
id: tagButton
// buttonText: `${modelData.name}_{${modelData.count}}`
background: Rectangle {
radius: Appearance.rounding.small
color: tagSuggestions.selectedIndex === index ? Appearance.colors.colLayer2Hover :
tagButton.down ? Appearance.colors.colLayer2Active :
tagButton.hovered ? Appearance.colors.colLayer2Hover :
Appearance.colors.colLayer2
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
}
contentItem: RowLayout {
spacing: 5
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.m3colors.m3onSurface
text: modelData.name
}
StyledText {
visible: modelData.count !== undefined
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.m3colors.m3outline
text: modelData.count ?? ""
}
}
onClicked: {
tagSuggestions.acceptTag(modelData.name)
}
}
}
function acceptTag(tag) {
const words = tagInputField.text.trim().split(/\s+/);
if (words.length > 0) {
words[words.length - 1] = tag;
} else {
words.push(tag);
}
const updatedText = words.join(" ") + " ";
tagInputField.text = updatedText;
tagInputField.cursorPosition = tagInputField.text.length;
tagInputField.forceActiveFocus();
}
function acceptSelectedTag() {
if (tagSuggestions.selectedIndex >= 0 && tagSuggestions.selectedIndex < tagSuggestionRepeater.count) {
const tag = root.suggestionList[tagSuggestions.selectedIndex].name;
tagSuggestions.acceptTag(tag);
}
}
}
Rectangle { // Tag input area
id: tagInputContainer
property real columnSpacing: 5
Layout.fillWidth: true
radius: Appearance.rounding.small
color: Appearance.colors.colLayer1
implicitWidth: tagInputColumnLayout.implicitWidth
implicitHeight: Math.max(tagInputColumnLayout.implicitHeight, 45)
implicitWidth: tagInputField.implicitWidth
implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin
+ commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + columnSpacing, 45)
clip: true
border.color: Appearance.m3colors.m3outlineVariant
border.width: 1
@@ -196,72 +283,225 @@ Item {
}
}
ColumnLayout {
id: tagInputColumnLayout
RowLayout { // Input field and send button
id: inputFieldRowLayout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
RowLayout {
Layout.topMargin: 5
spacing: 0
TextArea { // The actual input field widget
id: tagInputField
wrapMode: TextArea.Wrap
Layout.fillWidth: true
padding: 10
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
renderType: Text.NativeRendering
selectedTextColor: Appearance.m3colors.m3onPrimary
selectionColor: Appearance.m3colors.m3primary
placeholderText: qsTr("Enter tags")
placeholderTextColor: Appearance.m3colors.m3outline
anchors.topMargin: 5
spacing: 0
background: Item {}
TextArea { // The actual TextArea
id: tagInputField
wrapMode: TextArea.Wrap
Layout.fillWidth: true
padding: 10
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
renderType: Text.NativeRendering
selectedTextColor: Appearance.m3colors.m3onPrimary
selectionColor: Appearance.m3colors.m3primary
placeholderText: qsTr("Enter tags")
placeholderTextColor: Appearance.m3colors.m3outline
function accept() {
root.handleInput(text)
text = ""
}
background: Item {}
Keys.onPressed: (event) => {
if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
if (event.modifiers & Qt.ShiftModifier) {
// Insert newline
tagInputField.insert(tagInputField.cursorPosition, "\n")
event.accepted = true
} else { // Accept text
const inputText = tagInputField.text
root.handleInput(inputText)
tagInputField.clear()
event.accepted = true
}
property Timer searchTimer: Timer {
interval: root.tagSuggestionDelay
repeat: false
onTriggered: {
const inputText = tagInputField.text
if (inputText.length === 0 || inputText.startsWith(root.commandPrefix)) return;
const words = inputText.trim().split(/\s+/);
if (words.length > 0) {
Booru.triggerTagSearch(words[words.length - 1]);
}
}
}
Button { // Send button
id: sendButton
Layout.alignment: Qt.AlignTop
Layout.rightMargin: 5
implicitWidth: 40
implicitHeight: 40
enabled: tagInputField.text.length > 0
MouseArea {
anchors.fill: parent
cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
onTextChanged: {
if(tagInputField.text.length === 0) {
root.suggestionQuery = ""
root.suggestionList = []
return
}
searchTimer.restart();
}
function accept() {
root.handleInput(text)
text = ""
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Tab) {
tagSuggestions.acceptSelectedTag();
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
tagSuggestions.selectedIndex = Math.max(0, tagSuggestions.selectedIndex - 1);
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
tagSuggestions.selectedIndex = Math.min(root.suggestionList.length - 1, tagSuggestions.selectedIndex + 1);
event.accepted = true;
} else if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
if (event.modifiers & Qt.ShiftModifier) {
// Insert newline
tagInputField.insert(tagInputField.cursorPosition, "\n")
event.accepted = true
} else { // Accept text
const inputText = tagInputField.text
root.handleInput(inputText)
tagInputField.clear()
event.accepted = true
}
}
}
}
Button { // Send button
id: sendButton
Layout.alignment: Qt.AlignTop
Layout.rightMargin: 5
implicitWidth: 40
implicitHeight: 40
enabled: tagInputField.text.length > 0
MouseArea {
anchors.fill: parent
cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
const inputText = tagInputField.text
root.handleInput(inputText)
tagInputField.clear()
}
}
background: Rectangle {
radius: Appearance.rounding.small
color: sendButton.enabled ? (sendButton.down ? Appearance.colors.colPrimaryActive :
sendButton.hovered ? Appearance.colors.colPrimaryHover :
Appearance.m3colors.m3primary) : Appearance.colors.colLayer2Disabled
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: "send"
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled
}
}
}
RowLayout { // Controls
id: commandButtonsRow
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
anchors.leftMargin: 5
anchors.rightMargin: 5
spacing: 5
property var commands: [
{
name: "/mode",
sendDirectly: false,
},
{
name: "/clear",
sendDirectly: true,
},
]
Item {
implicitHeight: providerRowLayout.implicitHeight + 5 * 2
implicitWidth: providerRowLayout.implicitWidth + 10 * 2
RowLayout {
id: providerRowLayout
anchors.centerIn: parent
MaterialSymbol {
text: "api"
font.pixelSize: Appearance.font.pixelSize.large
}
StyledText {
id: providerName
font.pixelSize: Appearance.font.pixelSize.small
font.weight: Font.DemiBold
color: Appearance.m3colors.m3onSurface
text: Booru.providers[Booru.currentProvider].name
}
}
StyledToolTip {
id: toolTip
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
content: qsTr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + qsTr("\nSet with /mode PROVIDER")
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
text: "•"
}
Rectangle {
implicitWidth: switchesRow.implicitWidth
RowLayout {
id: switchesRow
spacing: 5
anchors.centerIn: parent
StyledText {
Layout.fillHeight: true
Layout.leftMargin: 10
Layout.alignment: Qt.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOnLayer1
text: qsTr("NSFW")
}
StyledSwitch {
id: nsfwSwitch
enabled: Booru.currentProvider !== "zerochan"
scale: 0.6
Layout.alignment: Qt.AlignVCenter
checked: (ConfigOptions.sidebar.booru.allowNsfw && Booru.currentProvider !== "zerochan")
onCheckedChanged: {
if (!nsfwSwitch.enabled) return;
ConfigOptions.sidebar.booru.allowNsfw = checked
}
}
}
}
Item { Layout.fillWidth: true }
Repeater { // Command buttons
id: commandRepeater
model: commandButtonsRow.commands
delegate: BooruTagButton {
id: tagButton
buttonText: modelData.name
background: Rectangle {
radius: Appearance.rounding.small
color: sendButton.enabled ? (sendButton.down ? Appearance.colors.colPrimaryActive :
sendButton.hovered ? Appearance.colors.colPrimaryHover :
Appearance.m3colors.m3primary) : Appearance.colors.colLayer2Disabled
color: tagButton.down ? Appearance.colors.colLayer2Active :
tagButton.hovered ? Appearance.colors.colLayer2Hover :
Appearance.colors.colLayer2
Behavior on color {
ColorAnimation {
@@ -270,138 +510,19 @@ Item {
}
}
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
text: "send"
horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled
}
}
}
RowLayout { // Controls
id: commandButtonsRow
spacing: 5
Layout.bottomMargin: 5
Layout.leftMargin: 5
Layout.rightMargin: 5
property var commands: [
{
name: "/mode",
sendDirectly: false,
},
{
name: "/clear",
sendDirectly: true,
},
]
Item {
implicitHeight: providerRowLayout.implicitHeight + 5 * 2
implicitWidth: providerRowLayout.implicitWidth + 10 * 2
RowLayout {
id: providerRowLayout
anchors.centerIn: parent
MaterialSymbol {
text: "api"
font.pixelSize: Appearance.font.pixelSize.large
}
StyledText {
id: providerName
font.pixelSize: Appearance.font.pixelSize.small
font.weight: Font.DemiBold
color: Appearance.m3colors.m3onSurface
text: Booru.providers[Booru.currentProvider].name
}
}
StyledToolTip {
id: toolTip
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
content: qsTr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + qsTr("\nSet with /mode PROVIDER")
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
text: "•"
}
Rectangle {
implicitWidth: switchesRow.implicitWidth
RowLayout {
id: switchesRow
spacing: 5
anchors.centerIn: parent
StyledText {
Layout.fillHeight: true
Layout.leftMargin: 10
Layout.alignment: Qt.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOnLayer1
text: qsTr("NSFW")
}
StyledSwitch {
id: nsfwSwitch
enabled: Booru.currentProvider !== "zerochan"
scale: 0.6
Layout.alignment: Qt.AlignVCenter
checked: (ConfigOptions.sidebar.booru.allowNsfw && Booru.currentProvider !== "zerochan")
onCheckedChanged: {
if (!nsfwSwitch.enabled) return;
ConfigOptions.sidebar.booru.allowNsfw = checked
}
}
}
}
Item { Layout.fillWidth: true }
Repeater { // Command buttons
id: commandRepeater
model: commandButtonsRow.commands
delegate: BooruTagButton {
id: tagButton
buttonText: modelData.name
background: Rectangle {
radius: Appearance.rounding.small
color: tagButton.down ? Appearance.colors.colLayer2Active :
tagButton.hovered ? Appearance.colors.colLayer2Hover :
Appearance.colors.colLayer2
Behavior on color {
ColorAnimation {
duration: Appearance.animation.elementDecel.duration
easing.type: Appearance.animation.elementDecel.type
}
}
}
onClicked: {
if(modelData.sendDirectly) {
root.handleInput(modelData.name)
} else {
tagInputField.text = modelData.name + " "
tagInputField.cursorPosition = tagInputField.text.length
tagInputField.forceActiveFocus()
}
onClicked: {
if(modelData.sendDirectly) {
root.handleInput(modelData.name)
} else {
tagInputField.text = modelData.name + " "
tagInputField.cursorPosition = tagInputField.text.length
tagInputField.forceActiveFocus()
}
}
}
}
}
}
}
}
@@ -153,7 +153,7 @@ Button {
onClicked: {
root.showActions = false
// Hyprland.dispatch("global quickshell:sidebarLeftClose")
Hyprland.dispatch(`exec curl '${root.imageData.file_url}' -o '${root.imageData.is_nsfw ? root.nsfwPath : root.downloadPath}/${root.fileName}' && notify-send 'Download complete' '${root.downloadPath}/${root.fileName}'`)
Hyprland.dispatch(`exec curl '${root.imageData.file_url}' -o '${root.imageData.is_nsfw ? root.nsfwPath : root.downloadPath}/${root.fileName}' && notify-send '${qsTr("Download complete")}' '${root.downloadPath}/${root.fileName}'`)
}
}
}