// 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 { spacing: Kirigami.Units.largeSpacing Label { text: KI18n.i18nc("@info", "Use an OpenAI-compatible API to translate subtitles. Tracks are translated in batches and saved for reuse.") wrapMode: Text.WordWrap Layout.preferredWidth: form.implicitWidth Layout.alignment: Qt.AlignHCenter } Kirigami.InlineMessage { Layout.preferredWidth: form.implicitWidth Layout.alignment: Qt.AlignHCenter type: Kirigami.MessageType.Error text: root.translator.errorText visible: text.length > 0 showCloseButton: true onVisibleChanged: if (!visible) root.translator.clearError() } Kirigami.FormLayout { id: form TextField { Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "API base URL") id: apiUrlField // Keep an invalid draft in the field; only valid URLs reach KConfig. property bool edited: false readonly property bool valid: /^https?:\/\/(?:\[[0-9a-f:.]+\]|[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.?)(?::[0-9]+)?(?:[/?#][^\s]*)?$/i.test(text.trim()) function saveUrl() { if (valid) { QtPotatoSettings.apiBaseUrl = text.trim() QtPotatoSettings.save() } } placeholderText: "https://example.com/v1" text: QtPotatoSettings.apiBaseUrl inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase Layout.fillWidth: true Layout.preferredWidth: Kirigami.Units.gridUnit * 19 onTextEdited: edited = true onEditingFinished: saveUrl() } Kirigami.InlineMessage { Layout.fillWidth: true Layout.preferredWidth: Kirigami.Units.gridUnit * 19 type: Kirigami.MessageType.Error text: KI18n.i18nc("@info", "Enter a valid API base URL with http:// or https:// and a host name.") visible: !apiUrlField.valid && (apiUrlField.edited || apiUrlField.text.length > 0) } RowLayout { spacing: Kirigami.Units.smallSpacing Layout.fillWidth: true Layout.preferredWidth: Kirigami.Units.gridUnit * 19 Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "API key") Kirigami.FormData.buddyFor: apiKeyField Kirigami.PasswordField { id: apiKeyField enabled: !ApiKeyStore.busy 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 Layout.preferredWidth: Kirigami.Units.gridUnit * 19 onEditingFinished: { if (text.trim().length > 0) { ApiKeyStore.setKey(text) text = "" } } } Button { visible: ApiKeyStore.hasKey enabled: !ApiKeyStore.busy 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 } } Kirigami.InlineMessage { Layout.fillWidth: true Layout.preferredWidth: Kirigami.Units.gridUnit * 19 type: Kirigami.MessageType.Error text: ApiKeyStore.error visible: text.length > 0 actions: Kirigami.Action { text: KI18n.i18nc("@action:button", "Retry") icon.name: "view-refresh" enabled: !ApiKeyStore.busy onTriggered: ApiKeyStore.retry() } } RowLayout { spacing: Kirigami.Units.smallSpacing Layout.fillWidth: true Layout.preferredWidth: Kirigami.Units.gridUnit * 19 Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Model") Kirigami.FormData.buddyFor: modelCombo 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 Layout.preferredWidth: Kirigami.Units.gridUnit * 19 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: apiUrlField.valid && !root.translator.fetchingModels onClicked: { apiUrlField.saveUrl() root.translator.fetchModels() } ToolTip.text: KI18n.i18nc("@info:tooltip", "Ask the API which models it offers") ToolTip.visible: hovered ToolTip.delay: Kirigami.Units.toolTipDelay } } 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 } TextField { Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "Translate into") text: QtPotatoSettings.targetLanguage Layout.fillWidth: true Layout.preferredWidth: Kirigami.Units.gridUnit * 19 onTextEdited: QtPotatoSettings.targetLanguage = text.trim() onEditingFinished: QtPotatoSettings.save() } ComboBox { Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Preset") id: presetCombo Layout.fillWidth: true Layout.preferredWidth: Kirigami.Units.gridUnit * 19 model: root.translator.translationPresets textRole: "name" valueRole: "id" Component.onCompleted: currentIndex = Math.max(0, indexOfValue(QtPotatoSettings.translationPreset)) onActivated: { QtPotatoSettings.translationPreset = currentValue QtPotatoSettings.save() } } Label { text: presetCombo.currentIndex >= 0 ? presetCombo.model[presetCombo.currentIndex].description : "" wrapMode: Text.WordWrap opacity: 0.7 font: Kirigami.Theme.smallFont Layout.fillWidth: true Layout.preferredWidth: Kirigami.Units.gridUnit * 19 } TextArea { Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "Additional instructions") 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.preferredWidth: Kirigami.Units.gridUnit * 19 Kirigami.FormData.labelAlignment: Qt.AlignTop implicitHeight: Math.max(contentHeight + topPadding + bottomPadding, Kirigami.Units.gridUnit * 4) // TextArea has no textEdited before Qt 6.9. onTextChanged: QtPotatoSettings.customInstructions = text onEditingFinished: QtPotatoSettings.save() } Switch { text: KI18n.i18nc("@option:check", "Build glossary automatically") checked: QtPotatoSettings.autoBuildGlossary onClicked: { QtPotatoSettings.autoBuildGlossary = checked QtPotatoSettings.save() } } Label { text: KI18n.i18nc("@info", "Collect names and terms before translating.") wrapMode: Text.WordWrap font: Kirigami.Theme.smallFont color: Kirigami.Theme.disabledTextColor Layout.fillWidth: true Layout.preferredWidth: Kirigami.Units.gridUnit * 19 } SpinBox { Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Lines per request") from: 5 to: 200 stepSize: 5 value: QtPotatoSettings.batchSize onValueModified: { QtPotatoSettings.batchSize = value QtPotatoSettings.save() } } } } }