fix(settings): align settings pages with KDE HIG

This commit is contained in:
tri committed 2026-11-07 18:48:04 +07:00
1 parent de9e6484ee
commit 8c42ee5a72
10 files changed
+200 -186

No files matched your search

+20 -30
View File
@@ -13,23 +13,19 @@ SettingsBasePage {
required property MpvItem mpv required property MpvItem mpv
GridLayout { Kirigami.FormLayout {
columns: 2
SettingsHeader { SettingsHeader {
text: KI18n.i18nc("@title", "Interface") text: KI18n.i18nc("@title", "Interface")
topMargin: 0
Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
} }
Label {
text: KI18n.i18nc("@label:listbox", "Footer style")
Layout.alignment: Qt.AlignRight
}
RowLayout { RowLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Footer style")
Kirigami.FormData.buddyFor: footerStyleCombo
ComboBox { ComboBox {
id: footerStyleCombo
textRole: "text" textRole: "text"
valueRole: "value" valueRole: "value"
model: [ model: [
@@ -48,9 +44,7 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Switch {
CheckBox {
text: KI18n.i18nc("@option:check", "Show menu bar") text: KI18n.i18nc("@option:check", "Show menu bar")
checked: QtPotatoSettings.showMenuBar checked: QtPotatoSettings.showMenuBar
onClicked: { onClicked: {
@@ -59,9 +53,7 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Switch {
CheckBox {
text: KI18n.i18nc("@option:check", "Show toolbar") text: KI18n.i18nc("@option:check", "Show toolbar")
checked: QtPotatoSettings.showHeader checked: QtPotatoSettings.showHeader
onClicked: { onClicked: {
@@ -70,13 +62,12 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Maximum recent files")
Layout.alignment: Qt.AlignRight
}
RowLayout { RowLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Maximum recent files")
Kirigami.FormData.buddyFor: maxRecentSpin
SpinBox { SpinBox {
id: maxRecentSpin
from: 0 from: 0
to: 50 to: 50
value: QtPotatoSettings.maxRecentFiles value: QtPotatoSettings.maxRecentFiles
@@ -93,14 +84,12 @@ SettingsBasePage {
SettingsHeader { SettingsHeader {
text: KI18n.i18nc("@title", "mpv") text: KI18n.i18nc("@title", "mpv")
Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
} }
Item { Layout.preferredWidth: 1 }
RowLayout { RowLayout {
CheckBox { spacing: Kirigami.Units.smallSpacing
Switch {
text: KI18n.i18nc("@option:check", "Use mpv.conf") text: KI18n.i18nc("@option:check", "Use mpv.conf")
checked: QtPotatoSettings.loadMpvConfig checked: QtPotatoSettings.loadMpvConfig
onClicked: { onClicked: {
@@ -114,7 +103,13 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Label {
text: KI18n.i18nc("@info", "Restart QtPotato to apply changes to this setting or to mpv.conf.")
wrapMode: Text.WordWrap
font: Kirigami.Theme.smallFont
color: Kirigami.Theme.disabledTextColor
Layout.fillWidth: true
}
Label { Label {
text: root.mpv.loadedUserConfigFiles.length > 0 text: root.mpv.loadedUserConfigFiles.length > 0
@@ -127,11 +122,6 @@ SettingsBasePage {
Layout.fillWidth: true Layout.fillWidth: true
} }
Item {
Layout.preferredWidth: 1
visible: root.mpv.skippedUserOptions.length > 0
}
Label { Label {
visible: root.mpv.skippedUserOptions.length > 0 visible: root.mpv.skippedUserOptions.length > 0
text: KI18n.i18nc("@info", "Ignored because they would break the built-in player: %1", root.mpv.skippedUserOptions.join(", ")) text: KI18n.i18nc("@info", "Ignored because they would break the built-in player: %1", root.mpv.skippedUserOptions.join(", "))
+6 -25
View File
@@ -5,27 +5,21 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.ki18n import org.kde.ki18n
SettingsBasePage { SettingsBasePage {
id: root id: root
GridLayout { Kirigami.FormLayout {
columns: 2
SettingsHeader { SettingsHeader {
text: KI18n.i18nc("@title", "Seeking") text: KI18n.i18nc("@title", "Seeking")
topMargin: 0
Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Small step")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Small step")
from: 1 from: 1
to: 600 to: 600
value: QtPotatoSettings.seekSmallStep value: QtPotatoSettings.seekSmallStep
@@ -37,12 +31,8 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Medium step")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Medium step")
from: 1 from: 1
to: 600 to: 600
value: QtPotatoSettings.seekMediumStep value: QtPotatoSettings.seekMediumStep
@@ -54,12 +44,8 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Big step")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Big step")
from: 1 from: 1
to: 600 to: 600
value: QtPotatoSettings.seekBigStep value: QtPotatoSettings.seekBigStep
@@ -73,16 +59,11 @@ SettingsBasePage {
SettingsHeader { SettingsHeader {
text: KI18n.i18nc("@title", "Audio") text: KI18n.i18nc("@title", "Audio")
Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Volume step")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Volume step")
from: 1 from: 1
to: 100 to: 100
value: QtPotatoSettings.volumeStep value: QtPotatoSettings.volumeStep
+15 -25
View File
@@ -5,20 +5,18 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.ki18n import org.kde.ki18n
SettingsBasePage { SettingsBasePage {
id: root id: root
GridLayout { Kirigami.FormLayout {
columns: 2
Label {
text: KI18n.i18nc("@label:listbox", "Position")
Layout.alignment: Qt.AlignRight
}
ComboBox { ComboBox {
Layout.fillWidth: true
Layout.minimumWidth: 0
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Position")
textRole: "text" textRole: "text"
valueRole: "value" valueRole: "value"
model: [ model: [
@@ -32,12 +30,10 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:listbox", "When a file ends")
Layout.alignment: Qt.AlignRight
}
ComboBox { ComboBox {
Layout.fillWidth: true
Layout.minimumWidth: 0
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "When a file ends")
textRole: "text" textRole: "text"
valueRole: "value" valueRole: "value"
model: [ model: [
@@ -53,11 +49,10 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 }
RowLayout { RowLayout {
CheckBox { spacing: Kirigami.Units.smallSpacing
text: KI18n.i18nc("@option:check", "Add the other files of the folder") Switch {
text: KI18n.i18nc("@option:check", "Add other files from the folder")
checked: QtPotatoSettings.playlistLoadSiblings checked: QtPotatoSettings.playlistLoadSiblings
onClicked: { onClicked: {
QtPotatoSettings.playlistLoadSiblings = checked QtPotatoSettings.playlistLoadSiblings = checked
@@ -70,9 +65,7 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Switch {
CheckBox {
text: KI18n.i18nc("@option:check", "Overlay video") text: KI18n.i18nc("@option:check", "Overlay video")
checked: QtPotatoSettings.playlistOverlayVideo checked: QtPotatoSettings.playlistOverlayVideo
onClicked: { onClicked: {
@@ -81,10 +74,9 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 }
RowLayout { RowLayout {
CheckBox { spacing: Kirigami.Units.smallSpacing
Switch {
text: KI18n.i18nc("@option:check", "Open with the mouse") text: KI18n.i18nc("@option:check", "Open with the mouse")
checked: QtPotatoSettings.playlistToggleWithMouse checked: QtPotatoSettings.playlistToggleWithMouse
onClicked: { onClicked: {
@@ -98,9 +90,7 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 } Switch {
CheckBox {
text: KI18n.i18nc("@option:check", "Show row numbers") text: KI18n.i18nc("@option:check", "Show row numbers")
checked: QtPotatoSettings.playlistShowRowNumber checked: QtPotatoSettings.playlistShowRowNumber
onClicked: { onClicked: {
+1
View File
@@ -6,6 +6,7 @@ import org.kde.kirigami as Kirigami
// Page of the settings window. Changes are saved as they are made. // Page of the settings window. Changes are saved as they are made.
Kirigami.ScrollablePage { Kirigami.ScrollablePage {
padding: Kirigami.Units.largeSpacing
globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None
Component.onDestruction: QtPotatoSettings.save() Component.onDestruction: QtPotatoSettings.save()
} }
+4 -28
View File
@@ -2,36 +2,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
// Section title of a settings page, between two lines. // FormLayout supplies the standard section heading and group spacing.
ColumnLayout { Item {
id: root
property string text: "" property string text: ""
property int topMargin: Kirigami.Units.gridUnit
spacing: 0 Kirigami.FormData.label: text
Kirigami.FormData.isSection: true
RowLayout {
Layout.topMargin: root.topMargin
Rectangle {
color: Kirigami.Theme.alternateBackgroundColor
Layout.preferredWidth: Kirigami.Units.gridUnit
Layout.preferredHeight: 1
}
Kirigami.Heading {
text: root.text
level: 1
}
Rectangle {
color: Kirigami.Theme.alternateBackgroundColor
Layout.preferredHeight: 1
Layout.fillWidth: true
}
}
} }
+5 -4
View File
@@ -109,10 +109,10 @@ Loader {
property Item currentPage: null property Item currentPage: null
width: 800 width: Kirigami.Units.gridUnit * 45
height: 600 height: Kirigami.Units.gridUnit * 34
minimumWidth: 750 minimumWidth: Kirigami.Units.gridUnit * 41
minimumHeight: 400 minimumHeight: Kirigami.Units.gridUnit * 22
title: KI18n.i18nc("@title:window", "Settings") title: KI18n.i18nc("@title:window", "Settings")
visible: true visible: true
@@ -132,6 +132,7 @@ Loader {
text: modelData.text text: modelData.text
icon.name: modelData.iconName icon.name: modelData.iconName
highlighted: index === ListView.view.currentIndex highlighted: index === ListView.view.currentIndex
font.bold: highlighted
onClicked: settingsWindow.showPage(modelData.name) onClicked: settingsWindow.showPage(modelData.name)
} }
} }
+7 -12
View File
@@ -5,21 +5,20 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.ki18n import org.kde.ki18n
SettingsBasePage { SettingsBasePage {
id: root id: root
GridLayout { Kirigami.FormLayout {
columns: 2
Label {
text: KI18n.i18nc("@label:spinbox", "Font size")
Layout.alignment: Qt.AlignRight
}
RowLayout { RowLayout {
spacing: Kirigami.Units.smallSpacing
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Font size")
Kirigami.FormData.buddyFor: fontSizeSpin
SpinBox { SpinBox {
id: fontSizeSpin
from: 20 from: 20
to: 150 to: 150
stepSize: 2 stepSize: 2
@@ -35,12 +34,8 @@ SettingsBasePage {
} }
} }
Label {
text: KI18n.i18nc("@label:spinbox", "Outline size")
Layout.alignment: Qt.AlignRight
}
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Outline size")
from: 0 from: 0
to: 10 to: 10
value: QtPotatoSettings.subtitleOutlineSize value: QtPotatoSettings.subtitleOutlineSize
+89 -59
View File
@@ -13,44 +13,73 @@ SettingsBasePage {
required property TranslationService translator required property TranslationService translator
ColumnLayout { 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 { Kirigami.InlineMessage {
Layout.fillWidth: true Layout.preferredWidth: form.implicitWidth
Layout.alignment: Qt.AlignHCenter
type: Kirigami.MessageType.Error type: Kirigami.MessageType.Error
text: root.translator.errorText || ApiKeyStore.error text: root.translator.errorText
visible: text.length > 0 visible: text.length > 0
showCloseButton: true showCloseButton: true
onVisibleChanged: if (!visible) root.translator.clearError() onVisibleChanged: if (!visible) root.translator.clearError()
} }
GridLayout { Kirigami.FormLayout {
columns: 2 id: form
Label {
text: KI18n.i18nc("@label:textbox", "API base URL")
Layout.alignment: Qt.AlignRight
}
TextField { 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" placeholderText: "https://example.com/v1"
text: QtPotatoSettings.apiBaseUrl text: QtPotatoSettings.apiBaseUrl
inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase
Layout.fillWidth: true Layout.fillWidth: true
onTextEdited: QtPotatoSettings.apiBaseUrl = text.trim() Layout.preferredWidth: Kirigami.Units.gridUnit * 19
onEditingFinished: QtPotatoSettings.save() onTextEdited: edited = true
onEditingFinished: saveUrl()
} }
Label { Kirigami.InlineMessage {
text: KI18n.i18nc("@label:textbox", "API key") Layout.fillWidth: true
Layout.alignment: Qt.AlignRight 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 { 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 { Kirigami.PasswordField {
id: apiKeyField id: apiKeyField
enabled: !ApiKeyStore.busy
placeholderText: ApiKeyStore.hasKey placeholderText: ApiKeyStore.hasKey
? KI18n.i18nc("@info:placeholder", "Saved in the wallet. Type a new key to replace it.") ? KI18n.i18nc("@info:placeholder", "Saved in the wallet. Type a new key to replace it.")
: KI18n.i18nc("@info:placeholder", "Not set") : KI18n.i18nc("@info:placeholder", "Not set")
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
onEditingFinished: { onEditingFinished: {
if (text.trim().length > 0) { if (text.trim().length > 0) {
ApiKeyStore.setKey(text) ApiKeyStore.setKey(text)
@@ -61,6 +90,7 @@ SettingsBasePage {
Button { Button {
visible: ApiKeyStore.hasKey visible: ApiKeyStore.hasKey
enabled: !ApiKeyStore.busy
icon.name: "edit-delete-remove" icon.name: "edit-delete-remove"
text: KI18n.i18nc("@action:button", "Remove") text: KI18n.i18nc("@action:button", "Remove")
onClicked: ApiKeyStore.setKey("") onClicked: ApiKeyStore.setKey("")
@@ -71,12 +101,26 @@ SettingsBasePage {
} }
} }
Label { Kirigami.InlineMessage {
text: KI18n.i18nc("@label:listbox", "Model") Layout.fillWidth: true
Layout.alignment: Qt.AlignRight 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 { 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 { ComboBox {
id: modelCombo id: modelCombo
@@ -95,6 +139,7 @@ SettingsBasePage {
editable: true editable: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
onEditTextChanged: if (!updatingList) QtPotatoSettings.model = editText.trim() onEditTextChanged: if (!updatingList) QtPotatoSettings.model = editText.trim()
onActivated: QtPotatoSettings.save() onActivated: QtPotatoSettings.save()
Component.onCompleted: showModels() Component.onCompleted: showModels()
@@ -112,8 +157,11 @@ SettingsBasePage {
text: root.translator.fetchingModels text: root.translator.fetchingModels
? KI18n.i18nc("@action:button", "Loading…") ? KI18n.i18nc("@action:button", "Loading…")
: KI18n.i18nc("@action:button", "Load Models") : KI18n.i18nc("@action:button", "Load Models")
enabled: !root.translator.fetchingModels enabled: apiUrlField.valid && !root.translator.fetchingModels
onClicked: root.translator.fetchModels() onClicked: {
apiUrlField.saveUrl()
root.translator.fetchModels()
}
ToolTip.text: KI18n.i18nc("@info:tooltip", "Ask the API which models it offers") ToolTip.text: KI18n.i18nc("@info:tooltip", "Ask the API which models it offers")
ToolTip.visible: hovered ToolTip.visible: hovered
@@ -121,11 +169,6 @@ SettingsBasePage {
} }
} }
Item {
Layout.preferredWidth: 1
visible: modelCountLabel.visible
}
Label { Label {
id: modelCountLabel id: modelCountLabel
visible: root.translator.availableModels.length > 0 visible: root.translator.availableModels.length > 0
@@ -133,26 +176,20 @@ SettingsBasePage {
opacity: 0.7 opacity: 0.7
} }
Label {
text: KI18n.i18nc("@label:textbox", "Translate into")
Layout.alignment: Qt.AlignRight
}
TextField { TextField {
Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "Translate into")
text: QtPotatoSettings.targetLanguage text: QtPotatoSettings.targetLanguage
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
onTextEdited: QtPotatoSettings.targetLanguage = text.trim() onTextEdited: QtPotatoSettings.targetLanguage = text.trim()
onEditingFinished: QtPotatoSettings.save() onEditingFinished: QtPotatoSettings.save()
} }
Label {
text: KI18n.i18nc("@label:listbox", "Preset")
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
}
ComboBox { ComboBox {
Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Preset")
id: presetCombo id: presetCombo
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
model: root.translator.translationPresets model: root.translator.translationPresets
textRole: "name" textRole: "name"
valueRole: "id" valueRole: "id"
@@ -163,46 +200,49 @@ SettingsBasePage {
} }
} }
Item { Layout.preferredWidth: 1 }
Label { Label {
text: presetCombo.currentIndex >= 0 ? presetCombo.model[presetCombo.currentIndex].description : "" text: presetCombo.currentIndex >= 0 ? presetCombo.model[presetCombo.currentIndex].description : ""
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
opacity: 0.7 opacity: 0.7
font: Kirigami.Theme.smallFont font: Kirigami.Theme.smallFont
Layout.fillWidth: true Layout.fillWidth: true
} Layout.preferredWidth: Kirigami.Units.gridUnit * 19
Label {
text: KI18n.i18nc("@label:textbox", "Additional instructions")
Layout.alignment: Qt.AlignRight | Qt.AlignBaseline
} }
TextArea { TextArea {
Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "Additional instructions")
text: QtPotatoSettings.customInstructions text: QtPotatoSettings.customInstructions
placeholderText: KI18n.i18nc("@info:placeholder", "e.g. Translate 'Young Master' as 'Thiếu gia'. Use 'ta/ngươi' between enemies.") placeholderText: KI18n.i18nc("@info:placeholder", "e.g. Translate 'Young Master' as 'Thiếu gia'. Use 'ta/ngươi' between enemies.")
wrapMode: TextArea.Wrap wrapMode: TextArea.Wrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.alignment: Qt.AlignBaseline Layout.preferredWidth: Kirigami.Units.gridUnit * 19
Layout.preferredHeight: Kirigami.Units.gridUnit * 4 Kirigami.FormData.labelAlignment: Qt.AlignTop
implicitHeight: Math.max(contentHeight + topPadding + bottomPadding, Kirigami.Units.gridUnit * 4)
// TextArea has no textEdited before Qt 6.9. // TextArea has no textEdited before Qt 6.9.
onTextChanged: QtPotatoSettings.customInstructions = text onTextChanged: QtPotatoSettings.customInstructions = text
onEditingFinished: QtPotatoSettings.save() onEditingFinished: QtPotatoSettings.save()
} }
CheckBox { Switch {
text: KI18n.i18nc("@option:check", "Build a glossary automatically before translating") text: KI18n.i18nc("@option:check", "Build glossary automatically")
checked: QtPotatoSettings.autoBuildGlossary checked: QtPotatoSettings.autoBuildGlossary
Layout.columnSpan: 2 onClicked: {
onToggled: { QtPotatoSettings.autoBuildGlossary = checked; QtPotatoSettings.save() } QtPotatoSettings.autoBuildGlossary = checked
QtPotatoSettings.save()
}
} }
Label { Label {
text: KI18n.i18nc("@label:spinbox", "Lines per request") text: KI18n.i18nc("@info", "Collect names and terms before translating.")
Layout.alignment: Qt.AlignRight wrapMode: Text.WordWrap
font: Kirigami.Theme.smallFont
color: Kirigami.Theme.disabledTextColor
Layout.fillWidth: true
Layout.preferredWidth: Kirigami.Units.gridUnit * 19
} }
SpinBox { SpinBox {
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Lines per request")
from: 5 from: 5
to: 200 to: 200
stepSize: 5 stepSize: 5
@@ -212,16 +252,6 @@ SettingsBasePage {
QtPotatoSettings.save() 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
}
} }
} }
} }
+44 -3
View File
@@ -2,6 +2,8 @@
#include <QCoroSignal> #include <QCoroSignal>
#include <KLocalizedString>
#include <QPointer> #include <QPointer>
#include <qt6keychain/keychain.h> #include <qt6keychain/keychain.h>
@@ -48,9 +50,31 @@ QString ApiKeyStore::error() const
return m_error; return m_error;
} }
bool ApiKeyStore::busy() const
{
return m_busy;
}
void ApiKeyStore::setKey(const QString &key) void ApiKeyStore::setKey(const QString &key)
{ {
store(key.trimmed()); if (m_busy) {
return;
}
m_pendingKey = key.trimmed();
m_retryStore = true;
store(m_pendingKey);
}
void ApiKeyStore::retry()
{
if (m_busy) {
return;
}
if (m_retryStore) {
store(m_pendingKey);
} else {
refresh();
}
} }
QCoro::Task<ApiKeyStore::ReadResult> ApiKeyStore::readKey() QCoro::Task<ApiKeyStore::ReadResult> ApiKeyStore::readKey()
@@ -63,13 +87,14 @@ QCoro::Task<ApiKeyStore::ReadResult> ApiKeyStore::readKey()
co_return ReadResult{}; co_return ReadResult{};
} }
if (job->error() != QKeychain::NoError) { if (job->error() != QKeychain::NoError) {
co_return ReadResult{{}, job->errorString()}; co_return ReadResult{{}, i18nc("@info", "Could not read the API key from the system wallet.\nMake sure KWallet or Secret Service is running, then retry.")};
} }
co_return ReadResult{job->textData(), {}}; co_return ReadResult{job->textData(), {}};
} }
QCoro::Task<> ApiKeyStore::refresh() QCoro::Task<> ApiKeyStore::refresh()
{ {
setBusy(true);
QPointer<ApiKeyStore> self(this); QPointer<ApiKeyStore> self(this);
const ReadResult result = co_await readKey(); const ReadResult result = co_await readKey();
if (!self) { if (!self) {
@@ -77,10 +102,12 @@ QCoro::Task<> ApiKeyStore::refresh()
} }
setHasKey(!result.key.isEmpty()); setHasKey(!result.key.isEmpty());
setError(result.error); setError(result.error);
setBusy(false);
} }
QCoro::Task<> ApiKeyStore::store(QString key) QCoro::Task<> ApiKeyStore::store(QString key)
{ {
setBusy(true);
QPointer<ApiKeyStore> self(this); QPointer<ApiKeyStore> self(this);
std::unique_ptr<QKeychain::Job, JobDeleter> job; std::unique_ptr<QKeychain::Job, JobDeleter> job;
if (key.isEmpty()) { if (key.isEmpty()) {
@@ -97,11 +124,17 @@ QCoro::Task<> ApiKeyStore::store(QString key)
} }
if (job->error() != QKeychain::NoError && job->error() != QKeychain::EntryNotFound) { if (job->error() != QKeychain::NoError && job->error() != QKeychain::EntryNotFound) {
setError(job->errorString()); setError(key.isEmpty()
? i18nc("@info", "Could not remove the API key from the system wallet.\nMake sure KWallet or Secret Service is running, then retry.")
: i18nc("@info", "Could not store the API key in the system wallet.\nMake sure KWallet or Secret Service is running, then retry."));
setBusy(false);
co_return; co_return;
} }
setError({}); setError({});
setHasKey(!key.isEmpty()); setHasKey(!key.isEmpty());
m_retryStore = false;
m_pendingKey.clear();
setBusy(false);
} }
void ApiKeyStore::setHasKey(bool hasKey) void ApiKeyStore::setHasKey(bool hasKey)
@@ -119,3 +152,11 @@ void ApiKeyStore::setError(const QString &error)
Q_EMIT errorChanged(); Q_EMIT errorChanged();
} }
} }
void ApiKeyStore::setBusy(bool busy)
{
if (busy != m_busy) {
m_busy = busy;
Q_EMIT busyChanged();
}
}
+9
View File
@@ -15,6 +15,7 @@ class ApiKeyStore : public QObject
Q_PROPERTY(bool hasKey READ hasKey NOTIFY hasKeyChanged) Q_PROPERTY(bool hasKey READ hasKey NOTIFY hasKeyChanged)
Q_PROPERTY(QString error READ error NOTIFY errorChanged) Q_PROPERTY(QString error READ error NOTIFY errorChanged)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public: public:
struct ReadResult { struct ReadResult {
@@ -26,9 +27,12 @@ public:
bool hasKey() const; bool hasKey() const;
QString error() const; QString error() const;
bool busy() const;
// Stores \a key, or removes the stored key when it is empty. // Stores \a key, or removes the stored key when it is empty.
Q_INVOKABLE void setKey(const QString &key); Q_INVOKABLE void setKey(const QString &key);
// Repeats the failed read, write, or removal without asking for the key again.
Q_INVOKABLE void retry();
// An empty key without error means none is stored. // An empty key without error means none is stored.
static QCoro::Task<ReadResult> readKey(); static QCoro::Task<ReadResult> readKey();
@@ -36,13 +40,18 @@ public:
Q_SIGNALS: Q_SIGNALS:
void hasKeyChanged(); void hasKeyChanged();
void errorChanged(); void errorChanged();
void busyChanged();
private: private:
QCoro::Task<> refresh(); QCoro::Task<> refresh();
QCoro::Task<> store(QString key); QCoro::Task<> store(QString key);
void setHasKey(bool hasKey); void setHasKey(bool hasKey);
void setError(const QString &error); void setError(const QString &error);
void setBusy(bool busy);
bool m_hasKey = false; bool m_hasKey = false;
QString m_error; QString m_error;
bool m_busy = false;
bool m_retryStore = false;
QString m_pendingKey;
}; };