mirror of
https://github.com/end-4/dots-hyprland.git
synced 2026-06-05 14:59:27 -05:00
booru: tag suggestions
This commit is contained in:
@@ -19,7 +19,19 @@ Item {
|
|||||||
property string previewDownloadPath: `${StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]}/media/waifus`.replace("file://", "")
|
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 downloadPath: (StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] + "/homework").replace("file://", "")
|
||||||
property string nsfwPath: (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 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: {
|
Component.onCompleted: {
|
||||||
Hyprland.dispatch(`exec rm -rf ${previewDownloadPath}`)
|
Hyprland.dispatch(`exec rm -rf ${previewDownloadPath}`)
|
||||||
@@ -27,7 +39,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleInput(inputText) {
|
function handleInput(inputText) {
|
||||||
if (inputText.startsWith("/")) {
|
if (inputText.startsWith(root.commandPrefix)) {
|
||||||
// Handle special commands
|
// Handle special commands
|
||||||
const command = inputText.split(" ")[0].substring(1);
|
const command = inputText.split(" ")[0].substring(1);
|
||||||
const args = inputText.split(" ").slice(1);
|
const args = inputText.split(" ").slice(1);
|
||||||
@@ -168,23 +180,98 @@ Item {
|
|||||||
text: "bookmark_heart"
|
text: "bookmark_heart"
|
||||||
}
|
}
|
||||||
StyledText {
|
StyledText {
|
||||||
|
id: widgetNameText
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
font.pixelSize: Appearance.font.pixelSize.normal
|
font.pixelSize: Appearance.font.pixelSize.normal
|
||||||
color: Appearance.m3colors.m3outline
|
color: Appearance.m3colors.m3outline
|
||||||
horizontalAlignment: Text.AlignHCenter
|
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
|
id: tagInputContainer
|
||||||
|
property real columnSpacing: 5
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
radius: Appearance.rounding.small
|
radius: Appearance.rounding.small
|
||||||
color: Appearance.colors.colLayer1
|
color: Appearance.colors.colLayer1
|
||||||
implicitWidth: tagInputColumnLayout.implicitWidth
|
implicitWidth: tagInputField.implicitWidth
|
||||||
implicitHeight: Math.max(tagInputColumnLayout.implicitHeight, 45)
|
implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin
|
||||||
|
+ commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + columnSpacing, 45)
|
||||||
clip: true
|
clip: true
|
||||||
border.color: Appearance.m3colors.m3outlineVariant
|
border.color: Appearance.m3colors.m3outlineVariant
|
||||||
border.width: 1
|
border.width: 1
|
||||||
@@ -196,72 +283,225 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
RowLayout { // Input field and send button
|
||||||
id: tagInputColumnLayout
|
id: inputFieldRowLayout
|
||||||
|
anchors.top: parent.top
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.topMargin: 5
|
||||||
|
spacing: 0
|
||||||
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
|
|
||||||
|
|
||||||
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() {
|
background: Item {}
|
||||||
root.handleInput(text)
|
|
||||||
text = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onPressed: (event) => {
|
property Timer searchTimer: Timer {
|
||||||
if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
|
interval: root.tagSuggestionDelay
|
||||||
if (event.modifiers & Qt.ShiftModifier) {
|
repeat: false
|
||||||
// Insert newline
|
onTriggered: {
|
||||||
tagInputField.insert(tagInputField.cursorPosition, "\n")
|
const inputText = tagInputField.text
|
||||||
event.accepted = true
|
if (inputText.length === 0 || inputText.startsWith(root.commandPrefix)) return;
|
||||||
} else { // Accept text
|
const words = inputText.trim().split(/\s+/);
|
||||||
const inputText = tagInputField.text
|
if (words.length > 0) {
|
||||||
root.handleInput(inputText)
|
Booru.triggerTagSearch(words[words.length - 1]);
|
||||||
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 {
|
onTextChanged: {
|
||||||
anchors.fill: parent
|
if(tagInputField.text.length === 0) {
|
||||||
cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
root.suggestionQuery = ""
|
||||||
onClicked: {
|
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
|
const inputText = tagInputField.text
|
||||||
root.handleInput(inputText)
|
root.handleInput(inputText)
|
||||||
tagInputField.clear()
|
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 {
|
background: Rectangle {
|
||||||
radius: Appearance.rounding.small
|
radius: Appearance.rounding.small
|
||||||
color: sendButton.enabled ? (sendButton.down ? Appearance.colors.colPrimaryActive :
|
color: tagButton.down ? Appearance.colors.colLayer2Active :
|
||||||
sendButton.hovered ? Appearance.colors.colPrimaryHover :
|
tagButton.hovered ? Appearance.colors.colLayer2Hover :
|
||||||
Appearance.m3colors.m3primary) : Appearance.colors.colLayer2Disabled
|
Appearance.colors.colLayer2
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
ColorAnimation {
|
ColorAnimation {
|
||||||
@@ -270,138 +510,19 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onClicked: {
|
||||||
contentItem: MaterialSymbol {
|
if(modelData.sendDirectly) {
|
||||||
anchors.centerIn: parent
|
root.handleInput(modelData.name)
|
||||||
text: "send"
|
} else {
|
||||||
horizontalAlignment: Text.AlignHCenter
|
tagInputField.text = modelData.name + " "
|
||||||
font.pixelSize: Appearance.font.pixelSize.larger
|
tagInputField.cursorPosition = tagInputField.text.length
|
||||||
color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled
|
tagInputField.forceActiveFocus()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ Button {
|
|||||||
onClicked: {
|
onClicked: {
|
||||||
root.showActions = false
|
root.showActions = false
|
||||||
// Hyprland.dispatch("global quickshell:sidebarLeftClose")
|
// 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}'`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import QtQuick;
|
|||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
signal tagSuggestion(string query, var suggestions)
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: ConfigOptions.sidebar.booru
|
target: ConfigOptions.sidebar.booru
|
||||||
@@ -34,7 +35,6 @@ Singleton {
|
|||||||
"name": "yande.re",
|
"name": "yande.re",
|
||||||
"url": "https://yande.re",
|
"url": "https://yande.re",
|
||||||
"api": "https://yande.re/post.json",
|
"api": "https://yande.re/post.json",
|
||||||
"listAccess": [],
|
|
||||||
"mapFunc": (response) => {
|
"mapFunc": (response) => {
|
||||||
return response.map(item => {
|
return response.map(item => {
|
||||||
return {
|
return {
|
||||||
@@ -53,13 +53,21 @@ Singleton {
|
|||||||
"source": getWorkingImageSource(item.source) ?? item.file_url,
|
"source": getWorkingImageSource(item.source) ?? item.file_url,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
"tagSearchTemplate": "https://yande.re/tag.json?order=count&name={{query}}*",
|
||||||
|
"tagMapFunc": (response) => {
|
||||||
|
return response.map(item => {
|
||||||
|
return {
|
||||||
|
"name": item.name,
|
||||||
|
"count": item.count
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"konachan": {
|
"konachan": {
|
||||||
"name": "Konachan",
|
"name": "Konachan",
|
||||||
"url": "https://konachan.com",
|
"url": "https://konachan.com",
|
||||||
"api": "https://konachan.com/post.json",
|
"api": "https://konachan.com/post.json",
|
||||||
"listAccess": [],
|
|
||||||
"mapFunc": (response) => {
|
"mapFunc": (response) => {
|
||||||
return response.map(item => {
|
return response.map(item => {
|
||||||
return {
|
return {
|
||||||
@@ -78,14 +86,23 @@ Singleton {
|
|||||||
"source": getWorkingImageSource(item.source) ?? item.file_url,
|
"source": getWorkingImageSource(item.source) ?? item.file_url,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
"tagSearchTemplate": "https://konachan.com/tag.json?order=count&name={{query}}*",
|
||||||
|
"tagMapFunc": (response) => {
|
||||||
|
return response.map(item => {
|
||||||
|
return {
|
||||||
|
"name": item.name,
|
||||||
|
"count": item.count
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zerochan": {
|
"zerochan": {
|
||||||
"name": "Zerochan",
|
"name": "Zerochan",
|
||||||
"url": "https://www.zerochan.net",
|
"url": "https://www.zerochan.net",
|
||||||
"api": "https://www.zerochan.net/?json",
|
"api": "https://www.zerochan.net/?json",
|
||||||
"listAccess": ["items"],
|
|
||||||
"mapFunc": (response) => {
|
"mapFunc": (response) => {
|
||||||
|
response = response.items
|
||||||
return response.map(item => {
|
return response.map(item => {
|
||||||
return {
|
return {
|
||||||
"id": item.id,
|
"id": item.id,
|
||||||
@@ -110,7 +127,6 @@ Singleton {
|
|||||||
"name": "Danbooru",
|
"name": "Danbooru",
|
||||||
"url": "https://danbooru.donmai.us",
|
"url": "https://danbooru.donmai.us",
|
||||||
"api": "https://danbooru.donmai.us/posts.json",
|
"api": "https://danbooru.donmai.us/posts.json",
|
||||||
"listAccess": [],
|
|
||||||
"mapFunc": (response) => {
|
"mapFunc": (response) => {
|
||||||
return response.map(item => {
|
return response.map(item => {
|
||||||
return {
|
return {
|
||||||
@@ -129,14 +145,24 @@ Singleton {
|
|||||||
"source": getWorkingImageSource(item.source) ?? item.file_url,
|
"source": getWorkingImageSource(item.source) ?? item.file_url,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
"tagSearchTemplate": "https://danbooru.donmai.us/tags.json?search[name_matches]={{query}}*",
|
||||||
|
"tagMapFunc": (response) => {
|
||||||
|
return response.map(item => {
|
||||||
|
return {
|
||||||
|
"name": item.name,
|
||||||
|
"count": item.post_count
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
"gelbooru": {
|
"gelbooru": {
|
||||||
"name": "Gelbooru",
|
"name": "Gelbooru",
|
||||||
"url": "https://gelbooru.com",
|
"url": "https://gelbooru.com",
|
||||||
"api": "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1",
|
"api": "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1",
|
||||||
"listAccess": ["post"],
|
|
||||||
"mapFunc": (response) => {
|
"mapFunc": (response) => {
|
||||||
|
response = response.post
|
||||||
return response.map(item => {
|
return response.map(item => {
|
||||||
return {
|
return {
|
||||||
"id": item.id,
|
"id": item.id,
|
||||||
@@ -154,14 +180,23 @@ Singleton {
|
|||||||
"source": getWorkingImageSource(item.source) ?? item.file_url,
|
"source": getWorkingImageSource(item.source) ?? item.file_url,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
"tagSearchTemplate": "https://gelbooru.com/index.php?page=dapi&s=tag&q=index&json=1&orderby=count&name_pattern={{query}}%",
|
||||||
|
"tagMapFunc": (response) => {
|
||||||
|
return response.tag.map(item => {
|
||||||
|
return {
|
||||||
|
"name": item.name,
|
||||||
|
"count": item.count
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"waifu.im": {
|
"waifu.im": {
|
||||||
"name": "waifu.im",
|
"name": "waifu.im",
|
||||||
"url": "https://waifu.im",
|
"url": "https://waifu.im",
|
||||||
"api": "https://api.waifu.im/search",
|
"api": "https://api.waifu.im/search",
|
||||||
"listAccess": ["images"],
|
|
||||||
"mapFunc": (response) => {
|
"mapFunc": (response) => {
|
||||||
|
response = response.images
|
||||||
return response.map(item => {
|
return response.map(item => {
|
||||||
return {
|
return {
|
||||||
"id": item.image_id,
|
"id": item.image_id,
|
||||||
@@ -179,6 +214,11 @@ Singleton {
|
|||||||
"source": getWorkingImageSource(item.source) ?? item.url,
|
"source": getWorkingImageSource(item.source) ?? item.url,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
"tagSearchTemplate": "https://api.waifu.im/tags",
|
||||||
|
"tagMapFunc": (response) => {
|
||||||
|
return [...response.versatile.map(item => {return {"name": item}}),
|
||||||
|
...response.nsfw.map(item => {return {"name": item}})]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -191,8 +231,7 @@ Singleton {
|
|||||||
root.addSystemMessage(qsTr("Provider set to ") + providers[provider].name
|
root.addSystemMessage(qsTr("Provider set to ") + providers[provider].name
|
||||||
+ (provider == "zerochan" ? qsTr(". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!") : ""))
|
+ (provider == "zerochan" ? qsTr(". Notes for Zerochan:\n- You must enter a color\n- Set your zerochan username in `sidebar.booru.zerochan.username` config option. You [might be banned for not doing so](https://www.zerochan.net/api#:~:text=The%20request%20may%20still%20be%20completed%20successfully%20without%20this%20custom%20header%2C%20but%20your%20project%20may%20be%20banned%20for%20being%20anonymous.)!") : ""))
|
||||||
} else {
|
} else {
|
||||||
console.log("[Booru] Invalid provider: " + provider)
|
root.addSystemMessage(qsTr("Invalid API provider. Supported: \n- ") + providerList.join("\n- "))
|
||||||
root.addSystemMessage(qsTr("Invalid provider. Supported providers: \n- ") + providerList.join("\n- "))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,30 +294,17 @@ Singleton {
|
|||||||
|
|
||||||
function makeRequest(tags, nsfw=false, limit=20, page=1) {
|
function makeRequest(tags, nsfw=false, limit=20, page=1) {
|
||||||
var url = constructRequestUrl(tags, nsfw, limit, page)
|
var url = constructRequestUrl(tags, nsfw, limit, page)
|
||||||
console.log("[Booru] Making request to " + url)
|
// console.log("[Booru] Making request to " + url)
|
||||||
|
|
||||||
var xhr = new XMLHttpRequest()
|
var xhr = new XMLHttpRequest()
|
||||||
xhr.open("GET", url)
|
xhr.open("GET", url)
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
|
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
|
||||||
try {
|
try {
|
||||||
// console.log("[Booru] Raw response length: " + xhr.responseText.length)
|
|
||||||
// console.log("[Booru] Raw response: " + xhr.responseText)
|
// console.log("[Booru] Raw response: " + xhr.responseText)
|
||||||
var response = JSON.parse(xhr.responseText)
|
var response = JSON.parse(xhr.responseText)
|
||||||
|
|
||||||
// Access nested properties based on listAccess
|
|
||||||
var accessList = providers[currentProvider].listAccess
|
|
||||||
for (var i = 0; i < accessList.length; ++i) {
|
|
||||||
// console.log("[Booru] Accessing property: " + accessList[i])
|
|
||||||
// console.log("[Booru] Current response: " + JSON.stringify(response))
|
|
||||||
if (response && response.hasOwnProperty(accessList[i])) {
|
|
||||||
response = response[accessList[i]]
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response = providers[currentProvider].mapFunc(response)
|
response = providers[currentProvider].mapFunc(response)
|
||||||
// console.log("[Booru] Scoped & mapped response: " + JSON.stringify(response))
|
// console.log("[Booru] Mapped response: " + JSON.stringify(response))
|
||||||
root.responses = [...root.responses, {
|
root.responses = [...root.responses, {
|
||||||
"provider": currentProvider,
|
"provider": currentProvider,
|
||||||
"tags": tags,
|
"tags": tags,
|
||||||
@@ -310,7 +336,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
else if (currentProvider == "zerochan") {
|
else if (currentProvider == "zerochan") {
|
||||||
const userAgent = ConfigOptions.sidebar.booru.zerochan.username ? `Desktop sidebar booru viewer - ${ConfigOptions.sidebar.booru.zerochan.username}` : defaultUserAgent
|
const userAgent = ConfigOptions.sidebar.booru.zerochan.username ? `Desktop sidebar booru viewer - ${ConfigOptions.sidebar.booru.zerochan.username}` : defaultUserAgent
|
||||||
console.log("Setting User-Agent for zerochan: " + userAgent)
|
|
||||||
xhr.setRequestHeader("User-Agent", userAgent)
|
xhr.setRequestHeader("User-Agent", userAgent)
|
||||||
}
|
}
|
||||||
xhr.send()
|
xhr.send()
|
||||||
@@ -318,5 +343,49 @@ Singleton {
|
|||||||
console.log("Could not set User-Agent:", error)
|
console.log("Could not set User-Agent:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property var currentTagRequest: null
|
||||||
|
function triggerTagSearch(query) {
|
||||||
|
if (currentTagRequest) {
|
||||||
|
currentTagRequest.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider = providers[currentProvider]
|
||||||
|
if (!provider.tagSearchTemplate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var url = provider.tagSearchTemplate.replace("{{query}}", encodeURIComponent(query))
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest()
|
||||||
|
currentTagRequest = xhr
|
||||||
|
xhr.open("GET", url)
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
|
||||||
|
currentTagRequest = null
|
||||||
|
try {
|
||||||
|
// console.log("[Booru] Raw response: " + xhr.responseText)
|
||||||
|
var response = JSON.parse(xhr.responseText)
|
||||||
|
response = provider.tagMapFunc(response)
|
||||||
|
// console.log("[Booru] Mapped response: " + JSON.stringify(response))
|
||||||
|
root.tagSuggestion(query, response)
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Booru] Failed to parse response: " + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||||
|
console.log("[Booru] Request failed with status: " + xhr.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Required for danbooru
|
||||||
|
if (currentProvider == "danbooru") {
|
||||||
|
xhr.setRequestHeader("User-Agent", defaultUserAgent)
|
||||||
|
}
|
||||||
|
xhr.send()
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Could not set User-Agent:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user