forked from Shinonome/dots-hyprland
sidebar: translation widget
This commit is contained in:
@@ -90,6 +90,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property QtObject sidebar: QtObject {
|
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 QtObject booru: QtObject {
|
||||||
property bool allowNsfw: false
|
property bool allowNsfw: false
|
||||||
property string defaultProvider: "yandere"
|
property string defaultProvider: "yandere"
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import Quickshell.Hyprland
|
|||||||
Scope { // Scope
|
Scope { // Scope
|
||||||
id: root
|
id: root
|
||||||
property int sidebarPadding: 15
|
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 bool detach: false
|
||||||
property Component contentComponent: SidebarLeftContent {}
|
property Component contentComponent: SidebarLeftContent {}
|
||||||
property Item sidebarContent
|
property Item sidebarContent
|
||||||
|
|||||||
@@ -14,9 +14,15 @@ import Quickshell.Wayland
|
|||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: sidebarLeftBackground
|
id: root
|
||||||
required property var scopeRoot
|
required property var scopeRoot
|
||||||
anchors.fill: parent
|
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() {
|
function focusActiveItem() {
|
||||||
swipeView.currentItem.forceActiveFocus()
|
swipeView.currentItem.forceActiveFocus()
|
||||||
@@ -25,19 +31,19 @@ Item {
|
|||||||
Keys.onPressed: (event) => {
|
Keys.onPressed: (event) => {
|
||||||
if (event.modifiers === Qt.ControlModifier) {
|
if (event.modifiers === Qt.ControlModifier) {
|
||||||
if (event.key === Qt.Key_PageDown) {
|
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;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
else if (event.key === Qt.Key_PageUp) {
|
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;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
else if (event.key === Qt.Key_Tab) {
|
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;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
else if (event.key === Qt.Key_Backtab) {
|
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;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,10 +57,10 @@ Item {
|
|||||||
|
|
||||||
PrimaryTabBar { // Tab strip
|
PrimaryTabBar { // Tab strip
|
||||||
id: tabBar
|
id: tabBar
|
||||||
tabButtonList: scopeRoot.tabButtonList
|
tabButtonList: root.tabButtonList
|
||||||
externalTrackedTab: scopeRoot.selectedTab
|
externalTrackedTab: root.selectedTab
|
||||||
function onCurrentIndexChanged(currentIndex) {
|
function onCurrentIndexChanged(currentIndex) {
|
||||||
scopeRoot.selectedTab = currentIndex
|
root.selectedTab = currentIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +74,7 @@ Item {
|
|||||||
currentIndex: tabBar.externalTrackedTab
|
currentIndex: tabBar.externalTrackedTab
|
||||||
onCurrentIndexChanged: {
|
onCurrentIndexChanged: {
|
||||||
tabBar.enableIndicatorAnimation = true
|
tabBar.enableIndicatorAnimation = true
|
||||||
scopeRoot.selectedTab = currentIndex
|
root.selectedTab = currentIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
@@ -82,6 +88,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AiChat {}
|
AiChat {}
|
||||||
|
Translator {}
|
||||||
Anime {}
|
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
|
nm-connection-editor
|
||||||
quickshell
|
quickshell
|
||||||
swww
|
swww
|
||||||
|
translate-shell
|
||||||
wlogout
|
wlogout
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user