diff --git a/.config/quickshell/modules/sidebarLeft/Anime.qml b/.config/quickshell/modules/sidebarLeft/Anime.qml index 3d6100948..e11372600 100644 --- a/.config/quickshell/modules/sidebarLeft/Anime.qml +++ b/.config/quickshell/modules/sidebarLeft/Anime.qml @@ -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() } } } } } + } } } diff --git a/.config/quickshell/modules/sidebarLeft/anime/BooruImage.qml b/.config/quickshell/modules/sidebarLeft/anime/BooruImage.qml index de41d96c2..d91bb95a9 100644 --- a/.config/quickshell/modules/sidebarLeft/anime/BooruImage.qml +++ b/.config/quickshell/modules/sidebarLeft/anime/BooruImage.qml @@ -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}'`) } } } diff --git a/.config/quickshell/services/Booru.qml b/.config/quickshell/services/Booru.qml index 5523fcae6..2d8518abd 100644 --- a/.config/quickshell/services/Booru.qml +++ b/.config/quickshell/services/Booru.qml @@ -9,6 +9,7 @@ import QtQuick; Singleton { id: root + signal tagSuggestion(string query, var suggestions) Connections { target: ConfigOptions.sidebar.booru @@ -34,7 +35,6 @@ Singleton { "name": "yande.re", "url": "https://yande.re", "api": "https://yande.re/post.json", - "listAccess": [], "mapFunc": (response) => { return response.map(item => { return { @@ -53,13 +53,21 @@ Singleton { "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": { "name": "Konachan", "url": "https://konachan.com", "api": "https://konachan.com/post.json", - "listAccess": [], "mapFunc": (response) => { return response.map(item => { return { @@ -78,14 +86,23 @@ Singleton { "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": { "name": "Zerochan", "url": "https://www.zerochan.net", "api": "https://www.zerochan.net/?json", - "listAccess": ["items"], "mapFunc": (response) => { + response = response.items return response.map(item => { return { "id": item.id, @@ -110,7 +127,6 @@ Singleton { "name": "Danbooru", "url": "https://danbooru.donmai.us", "api": "https://danbooru.donmai.us/posts.json", - "listAccess": [], "mapFunc": (response) => { return response.map(item => { return { @@ -129,14 +145,24 @@ Singleton { "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": { "name": "Gelbooru", "url": "https://gelbooru.com", "api": "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1", - "listAccess": ["post"], "mapFunc": (response) => { + response = response.post return response.map(item => { return { "id": item.id, @@ -154,14 +180,23 @@ Singleton { "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": { "name": "waifu.im", "url": "https://waifu.im", "api": "https://api.waifu.im/search", - "listAccess": ["images"], "mapFunc": (response) => { + response = response.images return response.map(item => { return { "id": item.image_id, @@ -179,6 +214,11 @@ Singleton { "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 + (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 { - console.log("[Booru] Invalid provider: " + provider) - root.addSystemMessage(qsTr("Invalid provider. Supported providers: \n- ") + providerList.join("\n- ")) + root.addSystemMessage(qsTr("Invalid API provider. Supported: \n- ") + providerList.join("\n- ")) } } @@ -255,30 +294,17 @@ Singleton { function makeRequest(tags, nsfw=false, limit=20, page=1) { 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() xhr.open("GET", url) xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { try { - // console.log("[Booru] Raw response length: " + xhr.responseText.length) // console.log("[Booru] Raw response: " + 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) - // console.log("[Booru] Scoped & mapped response: " + JSON.stringify(response)) + // console.log("[Booru] Mapped response: " + JSON.stringify(response)) root.responses = [...root.responses, { "provider": currentProvider, "tags": tags, @@ -310,7 +336,6 @@ Singleton { } else if (currentProvider == "zerochan") { 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.send() @@ -318,5 +343,49 @@ Singleton { 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) + } + } }