// SPDX-FileCopyrightText: 2026 Tri Vo Khac // SPDX-License-Identifier: GPL-3.0-or-later import QtQuick import QtQuick.Controls import QtQuick.Layouts import org.kde.ki18n import org.kde.kirigami as Kirigami SettingsBasePage { id: root required property TranslationService translator ColumnLayout { Kirigami.InlineMessage { Layout.fillWidth: true type: Kirigami.MessageType.Error text: root.translator.errorText || ApiKeyStore.error visible: text.length > 0 showCloseButton: true onVisibleChanged: if (!visible) root.translator.clearError() } GridLayout { columns: 2 Label { text: KI18n.i18nc("@label:textbox", "API base URL") Layout.alignment: Qt.AlignRight } TextField { placeholderText: "https://example.com/v1" text: QtPotatoSettings.apiBaseUrl inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase Layout.fillWidth: true onTextEdited: QtPotatoSettings.apiBaseUrl = text.trim() onEditingFinished: QtPotatoSettings.save() } Label { text: KI18n.i18nc("@label:textbox", "API key") Layout.alignment: Qt.AlignRight } RowLayout { Kirigami.PasswordField { id: apiKeyField placeholderText: ApiKeyStore.hasKey ? KI18n.i18nc("@info:placeholder", "Saved in the wallet. Type a new key to replace it.") : KI18n.i18nc("@info:placeholder", "Not set") Layout.fillWidth: true onEditingFinished: { if (text.trim().length > 0) { ApiKeyStore.setKey(text) text = "" } } } Button { visible: ApiKeyStore.hasKey icon.name: "edit-delete-remove" text: KI18n.i18nc("@action:button", "Remove") onClicked: ApiKeyStore.setKey("") ToolTip.text: KI18n.i18nc("@info:tooltip", "Remove the saved API key from the wallet") ToolTip.visible: hovered ToolTip.delay: Kirigami.Units.toolTipDelay } } Label { text: KI18n.i18nc("@label:listbox", "Model") Layout.alignment: Qt.AlignRight } RowLayout { ComboBox { id: modelCombo // Swapping the list makes the combo box jump to its first entry; that // must not overwrite the model the user chose. property bool updatingList: false function showModels() { const chosen = QtPotatoSettings.model updatingList = true model = root.translator.availableModels currentIndex = root.translator.availableModels.indexOf(chosen) editText = chosen updatingList = false } editable: true Layout.fillWidth: true onEditTextChanged: if (!updatingList) QtPotatoSettings.model = editText.trim() onActivated: QtPotatoSettings.save() Component.onCompleted: showModels() Connections { target: root.translator function onAvailableModelsChanged() { modelCombo.showModels() } } } Button { icon.name: "view-refresh" text: root.translator.fetchingModels ? KI18n.i18nc("@action:button", "Loading…") : KI18n.i18nc("@action:button", "Load Models") enabled: !root.translator.fetchingModels onClicked: root.translator.fetchModels() ToolTip.text: KI18n.i18nc("@info:tooltip", "Ask the API which models it offers") ToolTip.visible: hovered ToolTip.delay: Kirigami.Units.toolTipDelay } } Item { Layout.preferredWidth: 1 visible: modelCountLabel.visible } Label { id: modelCountLabel visible: root.translator.availableModels.length > 0 text: KI18n.i18ncp("@info", "%1 model available", "%1 models available", root.translator.availableModels.length) opacity: 0.7 } Label { text: KI18n.i18nc("@label:textbox", "Translate into") Layout.alignment: Qt.AlignRight } TextField { text: QtPotatoSettings.targetLanguage Layout.fillWidth: true onTextEdited: QtPotatoSettings.targetLanguage = text.trim() onEditingFinished: QtPotatoSettings.save() } Label { text: KI18n.i18nc("@label:listbox", "Preset") Layout.alignment: Qt.AlignRight | Qt.AlignVCenter } ComboBox { id: presetCombo Layout.fillWidth: true model: root.translator.translationPresets textRole: "name" valueRole: "id" Component.onCompleted: currentIndex = Math.max(0, indexOfValue(QtPotatoSettings.translationPreset)) onActivated: { QtPotatoSettings.translationPreset = currentValue QtPotatoSettings.save() } } Item { Layout.preferredWidth: 1 } Label { text: presetCombo.currentIndex >= 0 ? presetCombo.model[presetCombo.currentIndex].description : "" wrapMode: Text.WordWrap opacity: 0.7 font: Kirigami.Theme.smallFont Layout.fillWidth: true } Label { text: KI18n.i18nc("@label:textbox", "Additional instructions") Layout.alignment: Qt.AlignRight | Qt.AlignBaseline } TextArea { text: QtPotatoSettings.customInstructions placeholderText: KI18n.i18nc("@info:placeholder", "e.g. Translate 'Young Master' as 'Thiếu gia'. Use 'ta/ngươi' between enemies.") wrapMode: TextArea.Wrap Layout.fillWidth: true Layout.alignment: Qt.AlignBaseline Layout.preferredHeight: Kirigami.Units.gridUnit * 4 // TextArea has no textEdited before Qt 6.9. onTextChanged: QtPotatoSettings.customInstructions = text onEditingFinished: QtPotatoSettings.save() } CheckBox { text: KI18n.i18nc("@option:check", "Build a glossary automatically before translating") checked: QtPotatoSettings.autoBuildGlossary Layout.columnSpan: 2 onToggled: { QtPotatoSettings.autoBuildGlossary = checked; QtPotatoSettings.save() } } Label { text: KI18n.i18nc("@label:spinbox", "Lines per request") Layout.alignment: Qt.AlignRight } SpinBox { from: 5 to: 200 stepSize: 5 value: QtPotatoSettings.batchSize onValueModified: { QtPotatoSettings.batchSize = value QtPotatoSettings.save() } } Item { Layout.preferredWidth: 1 } Label { text: KI18n.i18nc("@info", "Works with any OpenAI-compatible API. The whole subtitle track is translated in batches before it is shown, and the result is kept so the same track is never translated twice.") wrapMode: Text.WordWrap opacity: 0.7 Layout.fillWidth: true Layout.topMargin: Kirigami.Units.largeSpacing } } } }