258 lines
11 KiB
QML
258 lines
11 KiB
QML
// 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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|