From 8c42ee5a72d83c07cd4d7067f5350d16f389c1e9 Mon Sep 17 00:00:00 2001 From: Tri Vo Khac Date: Sat, 7 Nov 2026 18:48:04 +0700 Subject: [PATCH] fix(settings): align settings pages with KDE HIG --- src/qml/GeneralSettings.qml | 50 +++++------ src/qml/PlaybackSettings.qml | 31 ++----- src/qml/PlaylistSettings.qml | 40 ++++----- src/qml/SettingsBasePage.qml | 1 + src/qml/SettingsHeader.qml | 32 +------ src/qml/SettingsWindow.qml | 9 +- src/qml/SubtitlesSettings.qml | 19 ++-- src/qml/TranslationSettings.qml | 148 +++++++++++++++++++------------- src/translate/apikeystore.cpp | 47 +++++++++- src/translate/apikeystore.h | 9 ++ 10 files changed, 200 insertions(+), 186 deletions(-) diff --git a/src/qml/GeneralSettings.qml b/src/qml/GeneralSettings.qml index fcda8cb..c694909 100644 --- a/src/qml/GeneralSettings.qml +++ b/src/qml/GeneralSettings.qml @@ -13,23 +13,19 @@ SettingsBasePage { required property MpvItem mpv - GridLayout { - columns: 2 + Kirigami.FormLayout { SettingsHeader { text: KI18n.i18nc("@title", "Interface") - topMargin: 0 - Layout.columnSpan: 2 Layout.fillWidth: true } - Label { - text: KI18n.i18nc("@label:listbox", "Footer style") - Layout.alignment: Qt.AlignRight - } - RowLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Footer style") + Kirigami.FormData.buddyFor: footerStyleCombo ComboBox { + id: footerStyleCombo textRole: "text" valueRole: "value" model: [ @@ -48,9 +44,7 @@ SettingsBasePage { } } - Item { Layout.preferredWidth: 1 } - - CheckBox { + Switch { text: KI18n.i18nc("@option:check", "Show menu bar") checked: QtPotatoSettings.showMenuBar onClicked: { @@ -59,9 +53,7 @@ SettingsBasePage { } } - Item { Layout.preferredWidth: 1 } - - CheckBox { + Switch { text: KI18n.i18nc("@option:check", "Show toolbar") checked: QtPotatoSettings.showHeader onClicked: { @@ -70,13 +62,12 @@ SettingsBasePage { } } - Label { - text: KI18n.i18nc("@label:spinbox", "Maximum recent files") - Layout.alignment: Qt.AlignRight - } - RowLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Maximum recent files") + Kirigami.FormData.buddyFor: maxRecentSpin SpinBox { + id: maxRecentSpin from: 0 to: 50 value: QtPotatoSettings.maxRecentFiles @@ -93,14 +84,12 @@ SettingsBasePage { SettingsHeader { text: KI18n.i18nc("@title", "mpv") - Layout.columnSpan: 2 Layout.fillWidth: true } - Item { Layout.preferredWidth: 1 } - RowLayout { - CheckBox { + spacing: Kirigami.Units.smallSpacing + Switch { text: KI18n.i18nc("@option:check", "Use mpv.conf") checked: QtPotatoSettings.loadMpvConfig 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 { text: root.mpv.loadedUserConfigFiles.length > 0 @@ -127,11 +122,6 @@ SettingsBasePage { Layout.fillWidth: true } - Item { - Layout.preferredWidth: 1 - visible: root.mpv.skippedUserOptions.length > 0 - } - Label { visible: root.mpv.skippedUserOptions.length > 0 text: KI18n.i18nc("@info", "Ignored because they would break the built-in player: %1", root.mpv.skippedUserOptions.join(", ")) diff --git a/src/qml/PlaybackSettings.qml b/src/qml/PlaybackSettings.qml index a027842..84883c6 100644 --- a/src/qml/PlaybackSettings.qml +++ b/src/qml/PlaybackSettings.qml @@ -5,27 +5,21 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import org.kde.kirigami as Kirigami import org.kde.ki18n SettingsBasePage { id: root - GridLayout { - columns: 2 + Kirigami.FormLayout { SettingsHeader { text: KI18n.i18nc("@title", "Seeking") - topMargin: 0 - Layout.columnSpan: 2 Layout.fillWidth: true } - Label { - text: KI18n.i18nc("@label:spinbox", "Small step") - Layout.alignment: Qt.AlignRight - } - SpinBox { + Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Small step") from: 1 to: 600 value: QtPotatoSettings.seekSmallStep @@ -37,12 +31,8 @@ SettingsBasePage { } } - Label { - text: KI18n.i18nc("@label:spinbox", "Medium step") - Layout.alignment: Qt.AlignRight - } - SpinBox { + Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Medium step") from: 1 to: 600 value: QtPotatoSettings.seekMediumStep @@ -54,12 +44,8 @@ SettingsBasePage { } } - Label { - text: KI18n.i18nc("@label:spinbox", "Big step") - Layout.alignment: Qt.AlignRight - } - SpinBox { + Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Big step") from: 1 to: 600 value: QtPotatoSettings.seekBigStep @@ -73,16 +59,11 @@ SettingsBasePage { SettingsHeader { text: KI18n.i18nc("@title", "Audio") - Layout.columnSpan: 2 Layout.fillWidth: true } - Label { - text: KI18n.i18nc("@label:spinbox", "Volume step") - Layout.alignment: Qt.AlignRight - } - SpinBox { + Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Volume step") from: 1 to: 100 value: QtPotatoSettings.volumeStep diff --git a/src/qml/PlaylistSettings.qml b/src/qml/PlaylistSettings.qml index f6d24f8..dbc8b84 100644 --- a/src/qml/PlaylistSettings.qml +++ b/src/qml/PlaylistSettings.qml @@ -5,20 +5,18 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import org.kde.kirigami as Kirigami import org.kde.ki18n SettingsBasePage { id: root - GridLayout { - columns: 2 - - Label { - text: KI18n.i18nc("@label:listbox", "Position") - Layout.alignment: Qt.AlignRight - } + Kirigami.FormLayout { ComboBox { + Layout.fillWidth: true + Layout.minimumWidth: 0 + Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "Position") textRole: "text" valueRole: "value" model: [ @@ -32,12 +30,10 @@ SettingsBasePage { } } - Label { - text: KI18n.i18nc("@label:listbox", "When a file ends") - Layout.alignment: Qt.AlignRight - } - ComboBox { + Layout.fillWidth: true + Layout.minimumWidth: 0 + Kirigami.FormData.label: KI18n.i18nc("@label:listbox", "When a file ends") textRole: "text" valueRole: "value" model: [ @@ -53,11 +49,10 @@ SettingsBasePage { } } - Item { Layout.preferredWidth: 1 } - RowLayout { - CheckBox { - text: KI18n.i18nc("@option:check", "Add the other files of the folder") + spacing: Kirigami.Units.smallSpacing + Switch { + text: KI18n.i18nc("@option:check", "Add other files from the folder") checked: QtPotatoSettings.playlistLoadSiblings onClicked: { QtPotatoSettings.playlistLoadSiblings = checked @@ -70,9 +65,7 @@ SettingsBasePage { } } - Item { Layout.preferredWidth: 1 } - - CheckBox { + Switch { text: KI18n.i18nc("@option:check", "Overlay video") checked: QtPotatoSettings.playlistOverlayVideo onClicked: { @@ -81,10 +74,9 @@ SettingsBasePage { } } - Item { Layout.preferredWidth: 1 } - RowLayout { - CheckBox { + spacing: Kirigami.Units.smallSpacing + Switch { text: KI18n.i18nc("@option:check", "Open with the mouse") checked: QtPotatoSettings.playlistToggleWithMouse onClicked: { @@ -98,9 +90,7 @@ SettingsBasePage { } } - Item { Layout.preferredWidth: 1 } - - CheckBox { + Switch { text: KI18n.i18nc("@option:check", "Show row numbers") checked: QtPotatoSettings.playlistShowRowNumber onClicked: { diff --git a/src/qml/SettingsBasePage.qml b/src/qml/SettingsBasePage.qml index 9ee70b6..835e53a 100644 --- a/src/qml/SettingsBasePage.qml +++ b/src/qml/SettingsBasePage.qml @@ -6,6 +6,7 @@ import org.kde.kirigami as Kirigami // Page of the settings window. Changes are saved as they are made. Kirigami.ScrollablePage { + padding: Kirigami.Units.largeSpacing globalToolBarStyle: Kirigami.ApplicationHeaderStyle.None Component.onDestruction: QtPotatoSettings.save() } diff --git a/src/qml/SettingsHeader.qml b/src/qml/SettingsHeader.qml index d7d06f4..db78fcb 100644 --- a/src/qml/SettingsHeader.qml +++ b/src/qml/SettingsHeader.qml @@ -2,36 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later import QtQuick -import QtQuick.Layouts import org.kde.kirigami as Kirigami -// Section title of a settings page, between two lines. -ColumnLayout { - id: root - +// FormLayout supplies the standard section heading and group spacing. +Item { property string text: "" - property int topMargin: Kirigami.Units.gridUnit - spacing: 0 - - 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 - } - } + Kirigami.FormData.label: text + Kirigami.FormData.isSection: true } diff --git a/src/qml/SettingsWindow.qml b/src/qml/SettingsWindow.qml index 3e86d41..9fd748b 100644 --- a/src/qml/SettingsWindow.qml +++ b/src/qml/SettingsWindow.qml @@ -109,10 +109,10 @@ Loader { property Item currentPage: null - width: 800 - height: 600 - minimumWidth: 750 - minimumHeight: 400 + width: Kirigami.Units.gridUnit * 45 + height: Kirigami.Units.gridUnit * 34 + minimumWidth: Kirigami.Units.gridUnit * 41 + minimumHeight: Kirigami.Units.gridUnit * 22 title: KI18n.i18nc("@title:window", "Settings") visible: true @@ -132,6 +132,7 @@ Loader { text: modelData.text icon.name: modelData.iconName highlighted: index === ListView.view.currentIndex + font.bold: highlighted onClicked: settingsWindow.showPage(modelData.name) } } diff --git a/src/qml/SubtitlesSettings.qml b/src/qml/SubtitlesSettings.qml index f526c73..43c7fee 100644 --- a/src/qml/SubtitlesSettings.qml +++ b/src/qml/SubtitlesSettings.qml @@ -5,21 +5,20 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import org.kde.kirigami as Kirigami import org.kde.ki18n SettingsBasePage { id: root - GridLayout { - columns: 2 - - Label { - text: KI18n.i18nc("@label:spinbox", "Font size") - Layout.alignment: Qt.AlignRight - } + Kirigami.FormLayout { RowLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Font size") + Kirigami.FormData.buddyFor: fontSizeSpin SpinBox { + id: fontSizeSpin from: 20 to: 150 stepSize: 2 @@ -35,12 +34,8 @@ SettingsBasePage { } } - Label { - text: KI18n.i18nc("@label:spinbox", "Outline size") - Layout.alignment: Qt.AlignRight - } - SpinBox { + Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Outline size") from: 0 to: 10 value: QtPotatoSettings.subtitleOutlineSize diff --git a/src/qml/TranslationSettings.qml b/src/qml/TranslationSettings.qml index 5eb8581..ca08316 100644 --- a/src/qml/TranslationSettings.qml +++ b/src/qml/TranslationSettings.qml @@ -13,44 +13,73 @@ SettingsBasePage { 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.fillWidth: true + Layout.preferredWidth: form.implicitWidth + Layout.alignment: Qt.AlignHCenter type: Kirigami.MessageType.Error - text: root.translator.errorText || ApiKeyStore.error + text: root.translator.errorText 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 - } + 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 - onTextEdited: QtPotatoSettings.apiBaseUrl = text.trim() - onEditingFinished: QtPotatoSettings.save() + Layout.preferredWidth: Kirigami.Units.gridUnit * 19 + onTextEdited: edited = true + onEditingFinished: saveUrl() } - Label { - text: KI18n.i18nc("@label:textbox", "API key") - Layout.alignment: Qt.AlignRight + 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) @@ -61,6 +90,7 @@ SettingsBasePage { Button { visible: ApiKeyStore.hasKey + enabled: !ApiKeyStore.busy icon.name: "edit-delete-remove" text: KI18n.i18nc("@action:button", "Remove") onClicked: ApiKeyStore.setKey("") @@ -71,12 +101,26 @@ SettingsBasePage { } } - Label { - text: KI18n.i18nc("@label:listbox", "Model") - Layout.alignment: Qt.AlignRight + 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 @@ -95,6 +139,7 @@ SettingsBasePage { editable: true Layout.fillWidth: true + Layout.preferredWidth: Kirigami.Units.gridUnit * 19 onEditTextChanged: if (!updatingList) QtPotatoSettings.model = editText.trim() onActivated: QtPotatoSettings.save() Component.onCompleted: showModels() @@ -112,8 +157,11 @@ SettingsBasePage { text: root.translator.fetchingModels ? KI18n.i18nc("@action:button", "Loading…") : KI18n.i18nc("@action:button", "Load Models") - enabled: !root.translator.fetchingModels - onClicked: root.translator.fetchModels() + 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 @@ -121,11 +169,6 @@ SettingsBasePage { } } - Item { - Layout.preferredWidth: 1 - visible: modelCountLabel.visible - } - Label { id: modelCountLabel visible: root.translator.availableModels.length > 0 @@ -133,26 +176,20 @@ SettingsBasePage { opacity: 0.7 } - Label { - text: KI18n.i18nc("@label:textbox", "Translate into") - Layout.alignment: Qt.AlignRight - } - 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() } - Label { - text: KI18n.i18nc("@label:listbox", "Preset") - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - } - 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" @@ -163,46 +200,49 @@ SettingsBasePage { } } - 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 + 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.alignment: Qt.AlignBaseline - Layout.preferredHeight: Kirigami.Units.gridUnit * 4 + 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() } - CheckBox { - text: KI18n.i18nc("@option:check", "Build a glossary automatically before translating") + Switch { + text: KI18n.i18nc("@option:check", "Build glossary automatically") checked: QtPotatoSettings.autoBuildGlossary - Layout.columnSpan: 2 - onToggled: { QtPotatoSettings.autoBuildGlossary = checked; QtPotatoSettings.save() } + onClicked: { + QtPotatoSettings.autoBuildGlossary = checked + QtPotatoSettings.save() + } } Label { - text: KI18n.i18nc("@label:spinbox", "Lines per request") - Layout.alignment: Qt.AlignRight + 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 @@ -212,16 +252,6 @@ SettingsBasePage { 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 - } } } } diff --git a/src/translate/apikeystore.cpp b/src/translate/apikeystore.cpp index e08bf53..54979b9 100644 --- a/src/translate/apikeystore.cpp +++ b/src/translate/apikeystore.cpp @@ -2,6 +2,8 @@ #include +#include + #include #include @@ -48,9 +50,31 @@ QString ApiKeyStore::error() const return m_error; } +bool ApiKeyStore::busy() const +{ + return m_busy; +} + 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::readKey() @@ -63,13 +87,14 @@ QCoro::Task ApiKeyStore::readKey() co_return ReadResult{}; } 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(), {}}; } QCoro::Task<> ApiKeyStore::refresh() { + setBusy(true); QPointer self(this); const ReadResult result = co_await readKey(); if (!self) { @@ -77,10 +102,12 @@ QCoro::Task<> ApiKeyStore::refresh() } setHasKey(!result.key.isEmpty()); setError(result.error); + setBusy(false); } QCoro::Task<> ApiKeyStore::store(QString key) { + setBusy(true); QPointer self(this); std::unique_ptr job; if (key.isEmpty()) { @@ -97,11 +124,17 @@ QCoro::Task<> ApiKeyStore::store(QString key) } 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; } setError({}); setHasKey(!key.isEmpty()); + m_retryStore = false; + m_pendingKey.clear(); + setBusy(false); } void ApiKeyStore::setHasKey(bool hasKey) @@ -119,3 +152,11 @@ void ApiKeyStore::setError(const QString &error) Q_EMIT errorChanged(); } } + +void ApiKeyStore::setBusy(bool busy) +{ + if (busy != m_busy) { + m_busy = busy; + Q_EMIT busyChanged(); + } +} diff --git a/src/translate/apikeystore.h b/src/translate/apikeystore.h index 56040b3..fd2fcd1 100644 --- a/src/translate/apikeystore.h +++ b/src/translate/apikeystore.h @@ -15,6 +15,7 @@ class ApiKeyStore : public QObject Q_PROPERTY(bool hasKey READ hasKey NOTIFY hasKeyChanged) Q_PROPERTY(QString error READ error NOTIFY errorChanged) + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) public: struct ReadResult { @@ -26,9 +27,12 @@ public: bool hasKey() const; QString error() const; + bool busy() const; // Stores \a key, or removes the stored key when it is empty. 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. static QCoro::Task readKey(); @@ -36,13 +40,18 @@ public: Q_SIGNALS: void hasKeyChanged(); void errorChanged(); + void busyChanged(); private: QCoro::Task<> refresh(); QCoro::Task<> store(QString key); void setHasKey(bool hasKey); void setError(const QString &error); + void setBusy(bool busy); bool m_hasKey = false; QString m_error; + bool m_busy = false; + bool m_retryStore = false; + QString m_pendingKey; };