sidebar: translation widget

This commit is contained in:
end-4
2025-06-02 23:24:10 +02:00
parent c26685b1de
commit 81446091d3
5 changed files with 249 additions and 11 deletions
@@ -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"
@@ -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
@@ -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 {}
}
@@ -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);
}
}
}
}
}
}
}
}
}
@@ -14,5 +14,6 @@ depends=(
nm-connection-editor
quickshell
swww
translate-shell
wlogout
)