diff --git a/.config/quickshell/modules/common/ConfigOptions.qml b/.config/quickshell/modules/common/ConfigOptions.qml index bc4af762e..3c093fff1 100644 --- a/.config/quickshell/modules/common/ConfigOptions.qml +++ b/.config/quickshell/modules/common/ConfigOptions.qml @@ -90,6 +90,9 @@ Singleton { } property QtObject sidebar: QtObject { + property QtObject translator: QtObject { + property int delay: 100 // Delay before sending request. Reduces (potential) rate limits and lag. + } property QtObject booru: QtObject { property bool allowNsfw: false property string defaultProvider: "yandere" diff --git a/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml b/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml index 6d92952e4..5cfd79472 100644 --- a/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml +++ b/.config/quickshell/modules/sidebarLeft/SidebarLeft.qml @@ -16,8 +16,6 @@ import Quickshell.Hyprland Scope { // Scope id: root property int sidebarPadding: 15 - property var tabButtonList: [{"icon": "neurology", "name": qsTr("Intelligence")}, {"icon": "bookmark_heart", "name": qsTr("Anime")}] - property int selectedTab: 0 property bool detach: false property Component contentComponent: SidebarLeftContent {} property Item sidebarContent diff --git a/.config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml b/.config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml index 6cf7f556f..b0358e7b4 100644 --- a/.config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml +++ b/.config/quickshell/modules/sidebarLeft/SidebarLeftContent.qml @@ -14,9 +14,15 @@ import Quickshell.Wayland import Quickshell.Hyprland Item { - id: sidebarLeftBackground + id: root required property var scopeRoot anchors.fill: parent + property var tabButtonList: [ + {"icon": "neurology", "name": qsTr("Intelligence")}, + {"icon": "translate", "name": qsTr("Translator")}, + {"icon": "bookmark_heart", "name": qsTr("Anime")}, + ] + property int selectedTab: 0 function focusActiveItem() { swipeView.currentItem.forceActiveFocus() @@ -25,19 +31,19 @@ Item { Keys.onPressed: (event) => { if (event.modifiers === Qt.ControlModifier) { if (event.key === Qt.Key_PageDown) { - scopeRoot.selectedTab = Math.min(scopeRoot.selectedTab + 1, scopeRoot.tabButtonList.length - 1) + root.selectedTab = Math.min(root.selectedTab + 1, root.tabButtonList.length - 1) event.accepted = true; } else if (event.key === Qt.Key_PageUp) { - scopeRoot.selectedTab = Math.max(scopeRoot.selectedTab - 1, 0) + root.selectedTab = Math.max(root.selectedTab - 1, 0) event.accepted = true; } else if (event.key === Qt.Key_Tab) { - scopeRoot.selectedTab = (scopeRoot.selectedTab + 1) % scopeRoot.tabButtonList.length; + root.selectedTab = (root.selectedTab + 1) % root.tabButtonList.length; event.accepted = true; } else if (event.key === Qt.Key_Backtab) { - scopeRoot.selectedTab = (scopeRoot.selectedTab - 1 + scopeRoot.tabButtonList.length) % scopeRoot.tabButtonList.length; + root.selectedTab = (root.selectedTab - 1 + root.tabButtonList.length) % root.tabButtonList.length; event.accepted = true; } } @@ -51,10 +57,10 @@ Item { PrimaryTabBar { // Tab strip id: tabBar - tabButtonList: scopeRoot.tabButtonList - externalTrackedTab: scopeRoot.selectedTab + tabButtonList: root.tabButtonList + externalTrackedTab: root.selectedTab function onCurrentIndexChanged(currentIndex) { - scopeRoot.selectedTab = currentIndex + root.selectedTab = currentIndex } } @@ -68,7 +74,7 @@ Item { currentIndex: tabBar.externalTrackedTab onCurrentIndexChanged: { tabBar.enableIndicatorAnimation = true - scopeRoot.selectedTab = currentIndex + root.selectedTab = currentIndex } clip: true @@ -82,6 +88,7 @@ Item { } AiChat {} + Translator {} Anime {} } diff --git a/.config/quickshell/modules/sidebarLeft/Translator.qml b/.config/quickshell/modules/sidebarLeft/Translator.qml new file mode 100644 index 000000000..9c8900843 --- /dev/null +++ b/.config/quickshell/modules/sidebarLeft/Translator.qml @@ -0,0 +1,229 @@ +import "root:/" +import "root:/services" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +/** + * Translator widget with the `trans` commandline tool. + */ +Item { + id: root + property var inputField: inputTextArea + property var outputField: outputTextArea + + property bool translationFor: false // Indicates if the translation is for an autocorrected text + property string translatedText: "" + + onFocusChanged: (focus) => { + if (focus) { + root.inputField.forceActiveFocus() + } + } + + Timer { + id: translateTimer + interval: ConfigOptions.sidebar.translator.delay + repeat: false + onTriggered: () => { + if (inputTextArea.text.trim().length > 0) { + translateProc.running = false; + translateProc.buffer = ""; // Clear the buffer + translateProc.running = true; // Restart the process + } else { + outputTextArea.text = ""; + } + } + } + + Process { + id: translateProc + command: ["bash", "-c", `trans -no-theme -no-ansi '${StringUtils.shellSingleQuoteEscape(inputTextArea.text.trim())}'`] + property string buffer: "" + stdout: SplitParser { + onRead: data => { + translateProc.buffer += data + "\n"; + } + } + onExited: (exitCode, exitStatus) => { + // 1. Split into sections by double newlines + const sections = translateProc.buffer.trim().split(/\n\s*\n/); + // console.log("BUFFER:", translateProc.buffer); + // console.log("SECTIONS:", sections); + + // 2. Extract relevant data + root.translatedText = sections.length > 1 ? sections[1].trim() : ""; + root.outputField.text = root.translatedText; +} + } + + Flickable { + anchors.fill: parent + contentHeight: contentColumn.implicitHeight + + ColumnLayout { + id: contentColumn + anchors.fill: parent + + Rectangle { // INPUT + id: inputCanvas + Layout.fillWidth: true + implicitHeight: Math.max(150, inputColumn.implicitHeight) + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.normal + border.color: Appearance.m3colors.m3outlineVariant + border.width: 1 + + ColumnLayout { + id: inputColumn + anchors.fill: parent + spacing: 0 + + StyledTextArea { // Input area + id: inputTextArea + Layout.fillWidth: true + placeholderText: qsTr("Enter text to translate...") + wrapMode: TextEdit.Wrap + textFormat: TextEdit.PlainText + font.pixelSize: Appearance.font.pixelSize.small + color: Appearance.colors.colOnLayer1 + padding: 15 + background: null + onTextChanged: { + if (inputTextArea.text.trim().length > 0) { + translateTimer.restart(); + } else { + outputTextArea.text = ""; + } + } + } + + Item { Layout.fillHeight: true } + + RowLayout { // Status row + Layout.fillWidth: true + Layout.margins: 10 + spacing: 10 + + Text { + Layout.leftMargin: 10 + text: qsTr("%1 characters").arg(inputTextArea.text.length) + color: Appearance.colors.colOnLayer1 + font.pixelSize: Appearance.font.pixelSize.smaller + } + Item { Layout.fillWidth: true } + ButtonGroup { + GroupButton { + id: pasteButton + baseWidth: height + buttonRadius: Appearance.rounding.small + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + iconSize: Appearance.font.pixelSize.larger + text: "content_paste" + color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext + } + onClicked: { + root.inputField.text = Quickshell.clipboardText + } + } + GroupButton { + id: deleteButton + baseWidth: height + buttonRadius: Appearance.rounding.small + enabled: inputTextArea.text.length > 0 + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + iconSize: Appearance.font.pixelSize.larger + text: "close" + color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext + } + onClicked: { + root.inputField.text = "" + } + } + } + } + } + } + + Rectangle { // OUTPUT + id: outputCanvas + Layout.fillWidth: true + implicitHeight: Math.max(150, outputColumn.implicitHeight) + color: Appearance.m3colors.m3surfaceContainer + radius: Appearance.rounding.normal + + ColumnLayout { // Output column + id: outputColumn + anchors.fill: parent + spacing: 0 + + StyledText { // Output area + id: outputTextArea + Layout.fillWidth: true + property bool hasTranslation: (root.translatedText.trim().length > 0) + wrapMode: TextEdit.Wrap + font.pixelSize: Appearance.font.pixelSize.small + color: hasTranslation ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext + padding: 15 + text: hasTranslation ? root.translatedText : "" + } + Item { Layout.fillHeight: true } + RowLayout { // Status row + Layout.fillWidth: true + Layout.margins: 10 + spacing: 10 + Item { Layout.fillWidth: true } + ButtonGroup { + GroupButton { + id: copyButton + baseWidth: height + buttonRadius: Appearance.rounding.small + enabled: root.outputField.text.trim().length > 0 + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + iconSize: Appearance.font.pixelSize.larger + text: "content_copy" + color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext + } + onClicked: { + Quickshell.clipboardText = root.outputField.text + } + } + GroupButton { + id: searchButton + baseWidth: height + buttonRadius: Appearance.rounding.small + enabled: root.outputField.text.trim().length > 0 + contentItem: MaterialSymbol { + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + iconSize: Appearance.font.pixelSize.larger + text: "travel_explore" + color: deleteButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext + } + onClicked: { + let url = ConfigOptions.search.engineBaseUrl + root.outputField.text; + for (let site of ConfigOptions.search.excludedSites) { + url += ` -site:${site}`; + } + Qt.openUrlExternally(url); + } + } + } + } + } + } + } + } +} diff --git a/arch-packages/illogical-impulse-widgets/PKGBUILD b/arch-packages/illogical-impulse-widgets/PKGBUILD index 880116ed2..868bea9e6 100644 --- a/arch-packages/illogical-impulse-widgets/PKGBUILD +++ b/arch-packages/illogical-impulse-widgets/PKGBUILD @@ -14,5 +14,6 @@ depends=( nm-connection-editor quickshell swww + translate-shell wlogout )