sidebar: translator: language selector

This commit is contained in:
end-4
2025-06-11 10:59:52 +02:00
parent 244d3f6067
commit 65983ade46
8 changed files with 439 additions and 156 deletions
@@ -63,6 +63,14 @@ Singleton {
]
}
property QtObject language: QtObject {
property QtObject translator: QtObject {
property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google
property string sourceLanguage: "auto"
property string targetLanguage: "English" // Run `trans -list-all` for available languages
}
}
property QtObject networking: QtObject {
property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
}
@@ -11,7 +11,7 @@ import QtQuick.Layouts
*/
Rectangle {
id: root
default property alias content: rowLayout.data
default property alias data: rowLayout.data
property real spacing: 5
property real padding: 0
property int clickIndex: rowLayout.clickIndex
@@ -0,0 +1,127 @@
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/services"
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
Item {
id: root
property real dialogPadding: 15
property real dialogMargin: 30
property string titleText: "Selection Dialog"
property alias items: choiceModel.values
property int selectedId: -1 // -1 means no selection
signal canceled();
signal selected(var result);
Rectangle { // Scrim
id: scrimOverlay
anchors.fill: parent
radius: Appearance.rounding.small
color: Appearance.colors.colScrim
MouseArea {
hoverEnabled: true
anchors.fill: parent
preventStealing: true
propagateComposedEvents: false
}
}
Rectangle { // The dialog
id: dialog
color: Appearance.m3colors.m3surfaceContainerHigh
radius: Appearance.rounding.normal
anchors.fill: parent
anchors.margins: dialogMargin
implicitHeight: dialogColumnLayout.implicitHeight
ColumnLayout {
id: dialogColumnLayout
anchors.fill: parent
spacing: 16
StyledText {
id: dialogTitle
Layout.topMargin: dialogPadding
Layout.leftMargin: dialogPadding
Layout.rightMargin: dialogPadding
Layout.alignment: Qt.AlignLeft
color: Appearance.m3colors.m3onSurface
font.pixelSize: Appearance.font.pixelSize.larger
text: root.titleText
}
Rectangle {
color: Appearance.m3colors.m3outline
implicitHeight: 1
Layout.fillWidth: true
Layout.leftMargin: dialogPadding
Layout.rightMargin: dialogPadding
}
ListView {
id: choiceListView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: ScriptModel {
id: choiceModel
}
delegate: StyledRadioButton {
id: radioButton
required property var modelData
required property int index
anchors {
left: parent?.left
right: parent?.right
leftMargin: root.dialogPadding
rightMargin: root.dialogPadding
}
description: modelData.toString()
checked: index === root.selectedId
onCheckedChanged: {
if (checked) {
root.selectedId = index;
}
}
}
}
Rectangle {
color: Appearance.m3colors.m3outline
implicitHeight: 1
Layout.fillWidth: true
Layout.leftMargin: dialogPadding
Layout.rightMargin: dialogPadding
}
RowLayout {
id: dialogButtonsRowLayout
Layout.bottomMargin: dialogPadding
Layout.leftMargin: dialogPadding
Layout.rightMargin: dialogPadding
Layout.alignment: Qt.AlignRight
DialogButton {
buttonText: qsTr("Cancel")
onClicked: root.canceled()
}
DialogButton {
buttonText: qsTr("OK")
onClicked: root.selected(
root.selectedId === -1 ? null :
root.items[root.selectedId]
)
}
}
}
}
}
@@ -3,6 +3,7 @@ import "root:/services"
import "root:/modules/common"
import "root:/modules/common/widgets"
import "root:/modules/common/functions/string_utils.js" as StringUtils
import "./translator/"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -15,11 +16,26 @@ import Quickshell.Hyprland
*/
Item {
id: root
property var inputField: inputTextArea
property var outputField: outputTextArea
// Widgets
property var inputField: inputCanvas.inputTextArea
// Widget variables
property bool translationFor: false // Indicates if the translation is for an autocorrected text
property string translatedText: ""
property list<string> languages: []
// Options
property string targetLanguage: ConfigOptions.language.translator.targetLanguage
property string sourceLanguage: ConfigOptions.language.translator.sourceLanguage
property string hostLanguage: targetLanguage
property bool showLanguageSelector: false
property bool languageSelectorTarget: false // true for target language, false for source language
property string languageSelectorLanguage: ""
function showLanguageSelectorDialog(isTargetLang: bool) {
root.showLanguageSelector = true
root.languageSelectorTarget = isTargetLang;
root.languageSelectorLanguage = isTargetLang ? root.targetLanguage : root.sourceLanguage;
}
onFocusChanged: (focus) => {
if (focus) {
@@ -32,19 +48,23 @@ Item {
interval: ConfigOptions.sidebar.translator.delay
repeat: false
onTriggered: () => {
if (inputTextArea.text.trim().length > 0) {
if (root.inputField.text.trim().length > 0) {
console.log("Translating with command:", translateProc.command);
translateProc.running = false;
translateProc.buffer = ""; // Clear the buffer
translateProc.running = true; // Restart the process
} else {
outputTextArea.text = "";
root.translatedText = "";
}
}
}
Process {
id: translateProc
command: ["bash", "-c", `trans -no-theme -no-ansi '${StringUtils.shellSingleQuoteEscape(inputTextArea.text.trim())}'`]
command: ["bash", "-c", `trans -no-theme`
+ ` -source '${StringUtils.shellSingleQuoteEscape(root.sourceLanguage)}'`
+ ` -target '${StringUtils.shellSingleQuoteEscape(root.targetLanguage)}'`
+ ` -no-ansi '${StringUtils.shellSingleQuoteEscape(root.inputField.text.trim())}'`]
property string buffer: ""
stdout: SplitParser {
onRead: data => {
@@ -54,12 +74,29 @@ Item {
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);
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;
}
}
Process {
id: getLanguagesProc
command: ["trans", "-list-languages"]
property list<string> bufferList: ["auto"]
running: true
stdout: SplitParser {
onRead: data => {
getLanguagesProc.bufferList.push(data.trim());
}
}
onExited: (exitCode, exitStatus) => {
root.languages = getLanguagesProc.bufferList
.filter(lang => lang.trim().length > 0) // Filter out empty lines
.sort((a, b) => a.localeCompare(b)); // Sort alphabetically
getLanguagesProc.bufferList = []; // Clear the buffer
}
}
@@ -71,159 +108,133 @@ Item {
id: contentColumn
anchors.fill: parent
Rectangle { // INPUT
LanguageSelectorButton { // Source language button
id: sourceLanguageButton
displayText: root.sourceLanguage
onClicked: {
root.showLanguageSelectorDialog(false);
}
}
TextCanvas { // Content 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 = "";
}
}
isInput: true
placeholderText: qsTr("Enter text to translate...")
onInputTextChanged: {
translateTimer.restart();
}
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
}
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 = ""
}
}
}
onClicked: {
root.inputField.text = Quickshell.clipboardText
}
}
GroupButton {
id: deleteButton
baseWidth: height
buttonRadius: Appearance.rounding.small
enabled: inputCanvas.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
LanguageSelectorButton { // Target language button
id: targetLanguageButton
displayText: root.targetLanguage
onClicked: {
root.showLanguageSelectorDialog(true);
}
}
TextCanvas { // Content translation
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 : ""
isInput: false
placeholderText: qsTr("Translation goes here...")
property bool hasTranslation: (root.translatedText.trim().length > 0)
text: hasTranslation ? root.translatedText : ""
GroupButton {
id: copyButton
baseWidth: height
buttonRadius: Appearance.rounding.small
enabled: outputCanvas.displayedText.trim().length > 0
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger
text: "content_copy"
color: copyButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext
}
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);
}
}
onClicked: {
Quickshell.clipboardText = outputCanvas.displayedText
}
}
GroupButton {
id: searchButton
baseWidth: height
buttonRadius: Appearance.rounding.small
enabled: outputCanvas.displayedText.trim().length > 0
contentItem: MaterialSymbol {
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger
text: "travel_explore"
color: searchButton.enabled ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext
}
onClicked: {
let url = ConfigOptions.search.engineBaseUrl + outputCanvas.displayedText;
for (let site of ConfigOptions.search.excludedSites) {
url += ` -site:${site}`;
}
Qt.openUrlExternally(url);
}
}
}
}
}
Loader {
anchors.fill: parent
active: root.showLanguageSelector
visible: root.showLanguageSelector
z: 9999
sourceComponent: SelectionDialog {
id: languageSelectorDialog
titleText: qsTr("Select Language")
items: root.languages
onCanceled: () => {
root.showLanguageSelector = false;
}
onSelected: (result) => {
root.showLanguageSelector = false;
if (!result || result.length === 0) return; // No selection made
if (root.languageSelectorTarget) {
root.targetLanguage = result;
ConfigOptions.language.translator.targetLanguage = result; // Save to config
} else {
root.sourceLanguage = result;
ConfigOptions.language.translator.sourceLanguage = result; // Save to config
}
}
}
}
}
@@ -0,0 +1,42 @@
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
RippleButton {
id: root
property string displayText: ""
colBackground: Appearance.colors.colLayer2
contentItem: Item {
anchors.centerIn: parent
implicitWidth: languageRow.implicitWidth
implicitHeight: languageText.implicitHeight
RowLayout {
id: languageRow
anchors.centerIn: parent
spacing: 0
StyledText {
id: languageText
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 5
text: root.displayText
color: Appearance.colors.colOnLayer2
font.pixelSize: Appearance.font.pixelSize.small
}
MaterialSymbol {
Layout.alignment: Qt.AlignVCenter
iconSize: Appearance.font.pixelSize.hugeass
text: "arrow_drop_down"
color: Appearance.colors.colOnLayer2
}
}
}
}
@@ -0,0 +1,92 @@
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
Rectangle {
id: root
property bool isInput: true // true for input, false for output
property string placeholderText
property string text: ""
property var inputTextArea: isInput ? inputLoader.item : undefined
readonly property string displayedText: isInput ? inputLoader.item.text :
root.text.length > 0 ? outputLoader.item.text : ""
default property alias actionButtons: actions.data
Layout.fillWidth: true
implicitHeight: Math.max(150, inputColumn.implicitHeight)
color: isInput ? Appearance.colors.colLayer1 : Appearance.m3colors.m3surfaceContainer
radius: Appearance.rounding.normal
border.color: isInput ? Appearance.m3colors.m3outlineVariant : "transparent"
border.width: isInput ? 1 : 0
signal inputTextChanged(); // Signal emitted when text changes
ColumnLayout {
id: inputColumn
anchors.fill: parent
spacing: 0
Loader {
id: inputLoader
active: root.isInput
visible: root.isInput
Layout.fillWidth: true
sourceComponent: StyledTextArea { // Input area
id: inputTextArea
placeholderText: root.placeholderText
wrapMode: TextEdit.Wrap
textFormat: TextEdit.PlainText
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
padding: 15
background: null
onTextChanged: root.inputTextChanged()
}
}
Loader {
id: outputLoader
active: !root.isInput
visible: !root.isInput
Layout.fillWidth: true
sourceComponent: StyledText { // Output area
id: outputTextArea
padding: 15
wrapMode: Text.Wrap
font.pixelSize: Appearance.font.pixelSize.small
color: root.text.length > 0 ? Appearance.colors.colOnLayer1 : Appearance.colors.colSubtext
text: root.text.length > 0 ? root.text : root.placeholderText
}
}
Item { Layout.fillHeight: true }
RowLayout { // Status row
Layout.fillWidth: true
Layout.margins: 10
spacing: 10
Loader {
active: root.isInput
visible: root.isInput
Layout.leftMargin: 10
sourceComponent: Text {
text: qsTr("%1 characters").arg(inputLoader.item.text.length)
color: Appearance.colors.colOnLayer1
font.pixelSize: Appearance.font.pixelSize.smaller
}
}
Item { Layout.fillWidth: true }
ButtonGroup {
id: actions
}
}
}
}
@@ -5,6 +5,7 @@ import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Pipewire
@@ -16,7 +17,7 @@ Item {
property int dialogMargins: 16
property PwNode selectedDevice
function showDeviceSelectorDialog(input) {
function showDeviceSelectorDialog(input: bool) {
root.selectedDevice = null
root.showDeviceSelector = true
root.deviceSelectorInput = input
@@ -207,9 +208,11 @@ Item {
spacing: 0
Repeater {
model: Pipewire.nodes.values.filter(node => {
return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio
})
model: ScriptModel {
values: Pipewire.nodes.values.filter(node => {
return !node.isStream && node.isSink !== root.deviceSelectorInput && node.audio
})
}
// This could and should be refractored, but all data becomes null when passed wtf
delegate: StyledRadioButton {
+1 -1
View File
@@ -29,7 +29,7 @@ ShellRoot {
property bool enableBar: true
property bool enableBackgroundWidgets: true
property bool enableCheatsheet: true
property bool enableDock: true
property bool enableDock: false
property bool enableMediaControls: true
property bool enableNotificationPopup: true
property bool enableOnScreenDisplayBrightness: true