dots-hyprland/.config/quickshell/modules/sidebarLeft/Anime.qml
2025-05-29 22:13:18 +02:00

646 lines
27 KiB
QML

import "root:/"
import "root:/services"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
import "root:/modules/common/functions/string_utils.js" as StringUtils
import "root:/modules/common/functions/file_utils.js" as FileUtils
import "./anime/"
import Qt.labs.platform
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell
import Quickshell.Hyprland
Item {
id: root
property var panelWindow
property var inputField: tagInputField
readonly property var responses: Booru.responses
property string previewDownloadPath: Directories.booruPreviews
property string downloadPath: Directories.booruDownloads
property string nsfwPath: Directories.booruDownloadsNsfw
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;
}
}
property var allCommands: [
{
name: "mode",
description: qsTr("Set the current API provider"),
execute: (args) => {
Booru.setProvider(args[0]);
}
},
{
name: "clear",
description: qsTr("Clear the current list of images"),
execute: () => {
Booru.clearResponses();
}
},
{
name: "next",
description: qsTr("Get the next page of results"),
execute: () => {
if (root.responses.length > 0) {
const lastResponse = root.responses[root.responses.length - 1];
root.handleInput(`${lastResponse.tags.join(" ")} ${parseInt(lastResponse.page) + 1}`);
}
}
},
{
name: "safe",
description: qsTr("Disable NSFW content"),
execute: () => {
PersistentStateManager.setState("booru.allowNsfw", false);
}
},
{
name: "lewd",
description: qsTr("Allow NSFW content"),
execute: () => {
PersistentStateManager.setState("booru.allowNsfw", true);
}
},
]
function handleInput(inputText) {
if (inputText.startsWith(root.commandPrefix)) {
// Handle special commands
const command = inputText.split(" ")[0].substring(1);
const args = inputText.split(" ").slice(1);
const commandObj = root.allCommands.find(cmd => cmd.name === `${command}`);
if (commandObj) {
commandObj.execute(args);
} else {
Booru.addSystemMessage(qsTr("Unknown command: ") + command);
}
}
else if (inputText.trim() == "+") {
if (root.responses.length > 0) {
const lastResponse = root.responses[root.responses.length - 1]
root.handleInput(lastResponse.tags.join(" ") + ` ${parseInt(lastResponse.page) + 1}`);
}
}
else {
// Create tag list
const tagList = inputText.split(/\s+/).filter(tag => tag.length > 0);
let pageIndex = 1;
for (let i = 0; i < tagList.length; ++i) { // Detect page number
if (/^\d+$/.test(tagList[i])) {
pageIndex = parseInt(tagList[i], 10);
tagList.splice(i, 1);
break;
}
}
Booru.makeRequest(tagList, PersistentStates.booru.allowNsfw, ConfigOptions.sidebar.booru.limit, pageIndex);
}
}
onFocusChanged: (focus) => {
if (focus) {
tagInputField.forceActiveFocus()
}
}
Keys.onPressed: (event) => {
tagInputField.forceActiveFocus()
if (event.modifiers === Qt.NoModifier) {
if (event.key === Qt.Key_PageUp) {
booruResponseListView.contentY = Math.max(0, booruResponseListView.contentY - booruResponseListView.height / 2)
event.accepted = true
} else if (event.key === Qt.Key_PageDown) {
booruResponseListView.contentY = Math.min(booruResponseListView.contentHeight - booruResponseListView.height / 2, booruResponseListView.contentY + booruResponseListView.height / 2)
event.accepted = true
}
}
}
ColumnLayout {
id: columnLayout
anchors.fill: parent
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ListView { // Booru responses
id: booruResponseListView
anchors.fill: parent
spacing: 10
property int lastResponseLength: 0
clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: swipeView.width
height: swipeView.height
radius: Appearance.rounding.small
}
}
Behavior on contentY {
NumberAnimation {
id: scrollAnim
duration: Appearance.animation.scroll.duration
easing.type: Appearance.animation.scroll.type
easing.bezierCurve: Appearance.animation.scroll.bezierCurve
}
}
add: Transition {
animations: [Appearance.animation.elementMoveEnter.numberAnimation.createObject(this, {
property: "opacity",
from: 0,
to: 1
})]
}
model: ScriptModel {
values: {
if(root.responses.length > booruResponseListView.lastResponseLength) {
if (booruResponseListView.lastResponseLength > 0 && root.responses[booruResponseListView.lastResponseLength].provider != "system")
booruResponseListView.contentY = booruResponseListView.contentY + root.scrollOnNewResponse
booruResponseListView.lastResponseLength = root.responses.length
}
return root.responses
}
}
delegate: BooruResponse {
responseData: modelData
tagInputField: root.inputField
previewDownloadPath: root.previewDownloadPath
downloadPath: root.downloadPath
nsfwPath: root.nsfwPath
}
}
Item { // Placeholder when list is empty
opacity: root.responses.length === 0 ? 1 : 0
visible: opacity > 0
anchors.fill: parent
Behavior on opacity {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
ColumnLayout {
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
Layout.alignment: Qt.AlignHCenter
iconSize: 55
color: Appearance.m3colors.m3outline
text: "bookmark_heart"
}
StyledText {
id: widgetNameText
Layout.alignment: Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignHCenter
text: qsTr("Anime boorus")
}
}
}
Item { // Queries awaiting response
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 10
implicitHeight: pendingBackground.implicitHeight
opacity: Booru.runningRequests > 0 ? 1 : 0
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Rectangle {
id: pendingBackground
color: Appearance.m3colors.m3inverseSurface
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: pendingText.implicitHeight + 12 * 2
radius: Appearance.rounding.verysmall
StyledText {
id: pendingText
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 12
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.m3colors.m3inverseOnSurface
wrapMode: Text.Wrap
text: StringUtils.format(qsTr("{0} queries pending"), Booru.runningRequests)
}
}
}
}
Item { // Tag suggestion description
visible: tagDescriptionText.text.length > 0
Layout.fillWidth: true
implicitHeight: tagDescriptionBackground.implicitHeight
Rectangle {
id: tagDescriptionBackground
color: Appearance.colors.colTooltip
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
implicitHeight: tagDescriptionText.implicitHeight + 5 * 2
radius: Appearance.rounding.verysmall
StyledText {
id: tagDescriptionText
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colOnTooltip
wrapMode: Text.Wrap
text: root.suggestionList[tagSuggestions.selectedIndex]?.description ?? ""
}
}
}
FlowButtonGroup { // 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: ApiCommandButton {
id: tagButton
colBackground: tagSuggestions.selectedIndex === index ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2
bounce: false
contentItem: RowLayout {
anchors.centerIn: parent
spacing: 5
StyledText {
Layout.fillWidth: false
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.m3colors.m3onSurface
horizontalAlignment: Text.AlignRight
text: modelData.displayName ?? modelData.name
}
StyledText {
Layout.fillWidth: false
visible: modelData.count !== undefined
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.m3colors.m3outline
horizontalAlignment: Text.AlignLeft
text: modelData.count ?? ""
}
}
onHoveredChanged: {
if (tagButton.hovered) {
tagSuggestions.selectedIndex = index;
}
}
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: 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
Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
RowLayout { // Input field and send button
id: inputFieldRowLayout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 5
spacing: 0
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.m3onSecondaryContainer
selectionColor: Appearance.m3colors.m3secondaryContainer
placeholderText: StringUtils.format(qsTr('Enter tags, or "{0}" for commands'), root.commandPrefix)
placeholderTextColor: Appearance.m3colors.m3outline
background: null
property Timer searchTimer: Timer { // Timer for tag suggestions
interval: root.tagSuggestionDelay
repeat: false
onTriggered: {
const inputText = tagInputField.text
const words = inputText.trim().split(/\s+/);
if (words.length > 0) {
Booru.triggerTagSearch(words[words.length - 1]);
}
}
}
onTextChanged: { // Handle tag suggestions
if(tagInputField.text.length === 0) {
root.suggestionQuery = ""
root.suggestionList = []
searchTimer.stop();
return
}
if(tagInputField.text.startsWith(`${root.commandPrefix}mode`)) {
root.suggestionQuery = tagInputField.text.split(" ")[1] ?? ""
const providerResults = Fuzzy.go(root.suggestionQuery, Booru.providerList.map(provider => {
return {
name: Fuzzy.prepare(provider),
obj: provider,
}
}), {
all: true,
key: "name"
})
root.suggestionList = providerResults.map(provider => {
return {
name: `${tagInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "mode ") : ""}${provider.target}`,
displayName: `${Booru.providers[provider.target].name}`,
description: `${Booru.providers[provider.target].description}`,
}
})
searchTimer.stop();
return
}
if(tagInputField.text.startsWith(root.commandPrefix)) {
root.suggestionQuery = tagInputField.text
root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(tagInputField.text.substring(1))).map(cmd => {
return {
name: `${root.commandPrefix}${cmd.name}`,
description: `${cmd.description}`,
}
})
searchTimer.stop();
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
}
}
}
}
RippleButton { // Send button
id: sendButton
Layout.alignment: Qt.AlignTop
Layout.rightMargin: 5
implicitWidth: 40
implicitHeight: 40
buttonRadius: Appearance.rounding.small
enabled: tagInputField.text.length > 0
toggled: enabled
MouseArea {
anchors.fill: parent
cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
const inputText = tagInputField.text
root.handleInput(inputText)
tagInputField.clear()
}
}
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger
// fill: sendButton.enabled ? 1 : 0
color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled
text: "send"
}
}
}
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 commandsShown: [
{
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"
iconSize: Appearance.font.pixelSize.large
}
StyledText {
id: providerName
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.m3colors.m3onSurface
text: Booru.providers[Booru.currentProvider].name
}
}
StyledToolTip {
id: toolTip
extraVisibleCondition: false
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
// content: qsTr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + qsTr("\nSet with /mode PROVIDER")
content: StringUtils.format(qsTr("Current API endpoint: {0}\nSet it with {1}mode PROVIDER"),
Booru.providers[Booru.currentProvider].url, root.commandPrefix)
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
text: "•"
}
Rectangle { // NSFW toggle
implicitWidth: switchesRow.implicitWidth
RowLayout {
id: switchesRow
spacing: 5
anchors.centerIn: parent
MouseArea {
hoverEnabled: true
PointingHandInteraction {}
onClicked: {
nsfwSwitch.checked = !nsfwSwitch.checked
}
}
StyledText {
Layout.fillHeight: true
Layout.leftMargin: 10
Layout.alignment: Qt.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.smaller
color: nsfwSwitch.enabled ? Appearance.colors.colOnLayer1 : Appearance.m3colors.m3outline
text: qsTr("Allow NSFW")
}
StyledSwitch {
id: nsfwSwitch
enabled: Booru.currentProvider !== "zerochan"
scale: 0.6
Layout.alignment: Qt.AlignVCenter
checked: (PersistentStates.booru.allowNsfw && Booru.currentProvider !== "zerochan")
onCheckedChanged: {
if (!nsfwSwitch.enabled) return;
PersistentStateManager.setState("booru.allowNsfw", checked)
}
}
}
}
Item { Layout.fillWidth: true }
ButtonGroup {
padding: 0
Repeater { // Command buttons
id: commandRepeater
model: commandButtonsRow.commandsShown
delegate: ApiCommandButton {
id: tagButton
property string commandRepresentation: `${root.commandPrefix}${modelData.name}`
buttonText: commandRepresentation
colBackground: Appearance.colors.colLayer2
onClicked: {
if(modelData.sendDirectly) {
root.handleInput(commandRepresentation)
} else {
tagInputField.text = commandRepresentation + " "
tagInputField.cursorPosition = tagInputField.text.length
tagInputField.forceActiveFocus()
}
if (modelData.name === "clear") {
tagInputField.text = ""
}
}
}
}
}
}
}
}
}