diff --git a/.config/quickshell/modules/bar/Bar.qml b/.config/quickshell/modules/bar/Bar.qml index 8582d5486..123351e55 100644 --- a/.config/quickshell/modules/bar/Bar.qml +++ b/.config/quickshell/modules/bar/Bar.qml @@ -83,6 +83,7 @@ Scope { } ActiveWindow { + Layout.rightMargin: Appearance.rounding.screenRounding Layout.fillWidth: true bar: barRoot } diff --git a/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml b/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml index 7be539a7e..9ceac9e4d 100644 --- a/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml +++ b/.config/quickshell/modules/common/widgets/PrimaryTabBar.qml @@ -56,6 +56,7 @@ ColumnLayout { z: 2 anchors.fill: parent + // TODO: make the end point in the moving direction go first anchors.leftMargin: { const tabCount = root.tabButtonList.length const targetWidth = tabBar.contentItem.children[0].children[tabBar.currentIndex].tabContentWidth diff --git a/.config/quickshell/modules/common/widgets/StyledToolTip.qml b/.config/quickshell/modules/common/widgets/StyledToolTip.qml index 3a6c1a401..8fa4f60c8 100644 --- a/.config/quickshell/modules/common/widgets/StyledToolTip.qml +++ b/.config/quickshell/modules/common/widgets/StyledToolTip.qml @@ -14,7 +14,7 @@ ToolTip { Connections { target: parent - onHoveredChanged: { + function onHoveredChanged() { if (parent.hovered) { tooltipShowDelay.restart() } else { diff --git a/.config/quickshell/modules/sidebarLeft/Anime.qml b/.config/quickshell/modules/sidebarLeft/Anime.qml new file mode 100644 index 000000000..d92b2201e --- /dev/null +++ b/.config/quickshell/modules/sidebarLeft/Anime.qml @@ -0,0 +1,85 @@ +import "root:/" +import "root:/services" +import "root:/modules/common" +import "root:/modules/common/widgets" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import Quickshell +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland +import Qt5Compat.GraphicalEffects + +Item { + id: root + onFocusChanged: (focus) => { + if (focus) { + tagInputField.forceActiveFocus() + } + } + + ColumnLayout { + anchors.fill: parent + + ListView { // Booru responses + id: booruResponseListView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: Booru.responses + delegate: StyledText { + id: booruResponseText + text: JSON.stringify(modelData) + } + } + + Rectangle { + Layout.fillWidth: true + radius: Appearance.rounding.small + border.width: 1 + border.color: Appearance.m3colors.m3outlineVariant + color: "transparent" + implicitWidth: tagInputField.implicitWidth + implicitHeight: tagInputField.implicitHeight + + Behavior on implicitHeight { + NumberAnimation { + duration: Appearance.animation.elementDecel.duration + easing.type: Appearance.animation.elementDecel.type + } + } + + TextArea { + id: tagInputField + anchors.fill: parent + wrapMode: TextArea.Wrap + + 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 + + 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 { + // Submit on Enter or Ctrl+Enter + const tagList = tagInputField.text.split(/\s+/); + Booru.makeRequest(tagList); + tagInputField.clear() + event.accepted = true + } + } + } + } + } + } +} diff --git a/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml b/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml index 74254d4ad..97b9e9b3d 100644 --- a/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml +++ b/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml @@ -16,7 +16,7 @@ Scope { // Scope id: root property int sidebarWidth: Appearance.sizes.sidebarWidth property int sidebarPadding: 15 - property var tabButtonList: [{"icon": "neurology", "name": qsTr("Intelligence")}, {"icon": "flare", "name": qsTr("Waifus")}] + property var tabButtonList: [{"icon": "neurology", "name": qsTr("Intelligence")}, {"icon": "bookmark_heart", "name": qsTr("Anime")}] Variants { // Window repeater id: sidebarVariants @@ -88,7 +88,6 @@ Scope { // Scope sidebarRoot.visible = false; } if (event.modifiers === Qt.ControlModifier) { - console.log("Control pressed") if (event.key === Qt.Key_PageDown) { sidebarRoot.currentTab = Math.min(sidebarRoot.currentTab + 1, root.tabButtonList.length - 1) } else if (event.key === Qt.Key_PageUp) { @@ -133,8 +132,11 @@ Scope { // Scope } } - Item {} - Item {} + StyledText { + text: "To be implemented" + horizontalAlignment: Text.AlignHCenter + } + Anime {} } } diff --git a/.config/quickshell/services/Booru.qml b/.config/quickshell/services/Booru.qml new file mode 100644 index 000000000..d49d93c3c --- /dev/null +++ b/.config/quickshell/services/Booru.qml @@ -0,0 +1,195 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell; +import Quickshell.Io; +import Qt.labs.platform +import QtQuick; + +Singleton { + id: root + + property var getWorkingImageSource: (url) => { + if (url.includes('pximg.net')) { + return `https://www.pixiv.net/en/artworks/${url.substring(url.lastIndexOf('/') + 1).replace(/_p\d+\.(png|jpg|jpeg|gif)$/, '')}`; + } + return url; + } + + property var providerList: ["yandere", "konachan", "danbooru", "gelbooru"] + property var providers: { + "yandere": { + "name": "yande.re", + "url": "https://yande.re", + "api": "https://yande.re/post.json", + "listAccess": [], + "mapFunc": (response) => { + return response.map(item => { + return { + "id": item.id, + "aspect_ratio": item.width / item.height, + "tags": item.tags, + "rating": item.rating, + "is_nsfw": (item.rating != 's'), + "md5": item.md5, + "preview_url": item.preview_url, + "sample_url": item.sample_url ?? item.file_url, + "file_url": item.file_url, + "file_ext": item.file_ext, + "source": getWorkingImageSource(item.source), + } + }) + } + }, + "konachan": { + "name": "Konachan", + "url": "https://konachan.com", + "api": "https://konachan.com/post.json", + "listAccess": [], + "mapFunc": (response) => { + return response.map(item => { + return { + "id": item.id, + "aspect_ratio": item.width / item.height, + "tags": item.tags, + "rating": item.rating, + "is_nsfw": (item.rating != 's'), + "md5": item.md5, + "preview_url": item.preview_url, + "sample_url": item.sample_url ?? item.file_url, + "file_url": item.file_url, + "file_ext": item.file_ext, + "source": getWorkingImageSource(item.source), + } + }) + } + }, + "danbooru": { + "name": "Danbooru", + "url": "https://danbooru.donmai.us", + "api": "https://danbooru.donmai.us/posts.json", + "listAccess": [], + "mapFunc": (response) => { + return response.map(item => { + return { + "id": item.id, + "aspect_ratio": item.image_width / item.image_height, + "tags": item.tag_string, + "rating": item.rating, + "is_nsfw": (item.rating != 's'), + "md5": item.md5, + "preview_url": item.preview_file_url, + "sample_url": item.file_url ?? item.large_file_url, + "file_url": item.large_file_url, + "file_ext": item.file_ext, + "source": getWorkingImageSource(item.source), + } + }) + } + }, + "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) => { + return response.map(item => { + return { + "id": item.id, + "aspect_ratio": item.width / item.height, + "tags": item.tags, + "rating": item.rating.replace('general', 's').charAt(0), + "is_nsfw": (item.rating != 's'), + "md5": item.md5, + "preview_url": item.preview_url, + "sample_url": item.sample_url ?? item.file_url, + "file_url": item.file_url, + "file_ext": item.file_url.split('.').pop(), + "source": getWorkingImageSource(item.source), + } + }) + } + } + } + property var responses: [] + onResponsesChanged: { + console.log("[Booru] Responses changed: " + JSON.stringify(responses)) + } + property var currentProvider: "yandere" + + function setProvider(provider) { + if (providerList.indexOf(provider) !== -1) { + currentProvider = provider + } else { + console.log("[Booru] Invalid provider: " + provider) + } + } + + function constructRequestUrl(tags, nsfw=true, limit=20) { + var provider = providers[currentProvider] + var baseUrl = provider.api + var tagString = tags.join(" ") + if (!nsfw) { + tagString += " rating:safe" + } + var params = [] + // Danbooru, Yandere, Konachan: tags & limit + if (currentProvider === "danbooru" || currentProvider === "yandere" || currentProvider === "konachan") { + params.push("tags=" + encodeURIComponent(tagString)) + params.push("limit=" + limit) + } + var url = baseUrl + if (baseUrl.indexOf("?") === -1) { + url += "?" + params.join("&") + } else { + url += "&" + params.join("&") + } + return url + } + + function makeRequest(tags, nsfw=true, limit=20) { + var url = constructRequestUrl(tags, nsfw, limit) + 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) + 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) { + 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)) + var newResponses = root.responses.slice() // make a shallow copy + newResponses.push(response) + root.responses = newResponses + + } 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 + xhr.setRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36") + } catch (e) { + console.log("Could not set User-Agent:", e) + } + xhr.send() + } +} +