- Impostazioni > Sessions: selettore "Profile" (elenco da /api/profiles; "(server active)" come prima voce = nessun cookie di profilo) - ApiClient: fetchProfiles() + switchProfile(); lo switch usa /api/profile/switch (process_wide=False lato server: il cookie firmato hermes_profile scopa le richieste dell'app per-client; il profilo attivo globale del server non viene toccato); refresh automatico delle sessioni - CookieJar::removeCookiesByName per l'opzione "(server active)" - Il profilo scelto viene riapplicato automaticamente dopo il login - i18n: nuove stringhe tradotte it/fr/de
290 lines
10 KiB
QML
290 lines
10 KiB
QML
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()
|
|
}
|
|
}
|