import QtQuick 2.0 import Sailfish.Silica 1.0 Page { id: page // Nessun elenco cablato: le voci/gli engine dei selettori vengono dalle // impostazioni (ttsVoices/ttsEngines) e il valore corrente è sempre // incluso. loading evita di salvare durante il caricamento iniziale. property bool loading: true // Feedback del selettore profilo (mostrato sotto il ComboBox). property string profileStatusText: "" property bool profileStatusError: false SilicaFlickable { anchors.fill: parent contentHeight: col.height + Theme.paddingLarge Column { id: col width: parent.width spacing: Theme.paddingMedium PageHeader { title: qsTr("Settings") } SectionHeader { text: qsTr("Server") } TextField { id: urlField width: parent.width label: qsTr("Web UI address") placeholderText: "https://hermes.example.com" inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase onTextChanged: { if (text !== appSettings.baseUrl) appSettings.baseUrl = text } } SectionHeader { text: qsTr("Sessions") } ComboBox { label: qsTr("Profile") currentIndex: page.profileIndex() // Stesso pattern dei selettori voce (Repeater, provato in tide). menu: ContextMenu { Repeater { model: page.profileChoices().length delegate: MenuItem { text: page.profileChoices()[index] onClicked: api.switchProfile(page.profileValue(index)) } } } } Label { x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin font.pixelSize: Theme.fontSizeExtraSmall color: page.profileStatusError ? Theme.highlightColor : Theme.secondaryHighlightColor wrapMode: Text.WordWrap visible: text.length > 0 text: page.profileStatusText } SectionHeader { text: qsTr("Voice") } ComboBox { label: qsTr("Read-aloud voice") currentIndex: page.voiceIndex() // Repeater dentro ContextMenu = pattern provato sul device (tide) menu: ContextMenu { MenuItem { text: qsTr("(server default)") onClicked: appSettings.ttsVoice = "" } Repeater { model: page.voiceChoices().length delegate: MenuItem { text: page.voiceChoices()[index] onClicked: appSettings.ttsVoice = page.voiceChoices()[index] } } } } TextField { id: voicesField width: parent.width label: qsTr("Voice presets (comma-separated)") onTextChanged: { if (!page.loading) appSettings.ttsVoices = text.split(",") } } ComboBox { label: qsTr("TTS engine") currentIndex: page.engineIndex() menu: ContextMenu { MenuItem { text: qsTr("(server default)") onClicked: appSettings.ttsEngine = "" } Repeater { model: page.engineChoices().length delegate: MenuItem { text: page.engineChoices()[index] onClicked: appSettings.ttsEngine = page.engineChoices()[index] } } } } TextField { id: enginesField width: parent.width label: qsTr("Engine presets (comma-separated)") onTextChanged: { if (!page.loading) appSettings.ttsEngines = text.split(",") } } TextSwitch { text: qsTr("Read replies aloud") description: qsTr("Speak every completed reply automatically") checked: appSettings.ttsAutoRead onCheckedChanged: appSettings.ttsAutoRead = checked } SectionHeader { text: qsTr("Voice dialog") } TextSwitch { text: qsTr("Send right after dictation") description: qsTr("Send the transcribed text automatically, without tapping Send") checked: appSettings.dictationAutoSend onCheckedChanged: appSettings.dictationAutoSend = checked } ComboBox { label: qsTr("Auto-stop pause") currentIndex: { var v = appSettings.vadSilenceMs return v <= 1000 ? 0 : (v <= 1500 ? 1 : (v <= 2000 ? 2 : 3)) } menu: ContextMenu { MenuItem { text: qsTr("1.0 second"); onClicked: appSettings.vadSilenceMs = 1000 } MenuItem { text: qsTr("1.5 seconds"); onClicked: appSettings.vadSilenceMs = 1500 } MenuItem { text: qsTr("2.0 seconds"); onClicked: appSettings.vadSilenceMs = 2000 } MenuItem { text: qsTr("3.0 seconds"); onClicked: appSettings.vadSilenceMs = 3000 } } } ComboBox { label: qsTr("Mic sensitivity") currentIndex: appSettings.vadThreshold <= 500 ? 0 : (appSettings.vadThreshold <= 1000 ? 1 : 2) menu: ContextMenu { MenuItem { text: qsTr("High (even soft voices)"); onClicked: appSettings.vadThreshold = 400 } MenuItem { text: qsTr("Medium"); onClicked: appSettings.vadThreshold = 800 } MenuItem { text: qsTr("Low (noisy rooms)"); onClicked: appSettings.vadThreshold = 1600 } } } SectionHeader { text: qsTr("Account") } Button { anchors.horizontalCenter: parent.horizontalCenter text: qsTr("Sign out") onClicked: page.signOut() } SectionHeader { text: qsTr("About") } Label { x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin wrapMode: Text.WordWrap font.pixelSize: Theme.fontSizeSmall color: Theme.secondaryColor text: "harbour-hermes " + appVersion + "\n\n" + qsTr("Native client for the Hermes Web UI (hermes-webui): " + "sessions, live streaming chat, server-side voice " + "dictation and read-aloud.") } } VerticalScrollDecorator {} } Connections { target: api onProfileSwitched: { page.profileStatusError = false page.profileStatusText = name.length > 0 ? qsTr("Profile: %1").arg(name) : qsTr("Server active profile") statusClear.restart() } onProfileSwitchFailed: { page.profileStatusError = true page.profileStatusText = error statusClear.restart() } } Timer { id: statusClear interval: 4000 onTriggered: page.profileStatusText = "" } function signOut() { api.logout() pageStack.replace(Qt.resolvedUrl("LoginPage.qml")) } // Valori del selettore profilo: "" (attivo sul server) + profili del // server (da /api/profiles); il valore corrente e' sempre incluso. function profileChoices() { var out = [qsTr("(server active)")] var list = api.profiles for (var i = 0; i < list.length; i++) out.push(list[i]) var cur = appSettings.profile if (cur.length > 0 && out.indexOf(cur) < 0) out.push(cur) return out } function profileValue(i) { if (i <= 0) return "" return page.profileChoices()[i] } function profileIndex() { var cur = appSettings.profile if (cur.length === 0) return 0 var i = page.profileChoices().indexOf(cur) return i < 0 ? 0 : i } // Elenchi dei selettori: preset configurati + valore corrente (così resta // selezionabile anche se non è tra i preset). Indice 0 = "(server default)". function voiceChoices() { var out = appSettings.ttsVoices.slice() var cur = appSettings.ttsVoice if (cur.length > 0 && out.indexOf(cur) < 0) out.push(cur) return out } function voiceIndex() { var cur = appSettings.ttsVoice if (cur.length === 0) return 0 var i = page.voiceChoices().indexOf(cur) return i < 0 ? 0 : i + 1 } function engineChoices() { var out = appSettings.ttsEngines.slice() var cur = appSettings.ttsEngine if (cur.length > 0 && out.indexOf(cur) < 0) out.push(cur) return out } function engineIndex() { var cur = appSettings.ttsEngine if (cur.length === 0) return 0 var i = page.engineChoices().indexOf(cur) return i < 0 ? 0 : i + 1 } Component.onCompleted: { urlField.text = appSettings.baseUrl voicesField.text = appSettings.ttsVoices.join(", ") enginesField.text = appSettings.ttsEngines.join(", ") page.loading = false api.fetchProfiles() } }