Skip to content
Open
3 changes: 2 additions & 1 deletion qml/components/navigation/ListHeader.qml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ".."
Rectangle {
id: topFilterBar
width: parent ? parent.width : Screen.width
height: showSearchBox ? units.gu(11) : units.gu(6) // Restored height to give proper space
height: (showSearchBox ? units.gu(5) : 0) + (filterModel.length > 0 ? units.gu(6) : 0)
color: "transparent"

// Helper property to check if dark mode is active
Expand Down Expand Up @@ -207,6 +207,7 @@ Rectangle {

// Filter buttons row below search
Item {
visible: topFilterBar.filterModel.length > 0
width: parent.width
height: units.gu(6) // Adjusted back

Expand Down
1 change: 1 addition & 0 deletions qml/components/richtext/HtmlEditorToolbar.qml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Rectangle {
width: units.gu(5)
height: units.gu(4)
color: (editor && editor.listening) ? LomiriColors.red : (darkMode ? "#555555" : "#F0F0F0")
visible: editor ? editor.isVoiceInputEnabled : false

Icon {
anchors.centerIn: parent
Expand Down
197 changes: 187 additions & 10 deletions qml/components/richtext/ReadMorePage.qml

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ReadMorePage.qml (and RichText views)
If a user navigates to the Read More page before visiting settings on a fresh launch, the database query SELECT value FROM app_settings is executed before the table is ever created, throwing an SQL exception. While wrapped in try/catch, it leads to messy logs.

Solution : Always run CREATE TABLE IF NOT EXISTS app_settings (key TEXT PRIMARY KEY, value TEXT) inside the transaction block before querying.

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import QtQuick 2.7
import Lomiri.Components 1.3
import QtQuick.LocalStorage 2.7 as Sql
import "../../../models/global.js" as Global
import "../system"

Page {
id: readmepage
Expand All @@ -23,6 +25,109 @@ Page {
property string _lastKnownHolder: ""
property bool _parentSaveCommitted: false

property bool listening: false
property bool processing: false
property bool ignoreNextResult: false
property string textBeforeRecording: ""
property bool isVoiceInputEnabled: true
property string _partialRecognizedText: ""
property string _currentVoiceStatus: ""

function checkVoiceInputEnabled() {
try {
var db = Sql.LocalStorage.openDatabaseSync("UBTMS_SettingsDB", "1.0", "UBTMS Settings Database", 1000000);
var result = true;
db.transaction(function (tx) {
var rs = tx.executeSql('SELECT value FROM app_settings WHERE key = "voice_input_enabled"');
if (rs.rows.length > 0) {
result = rs.rows.item(0).value === "true";
}
});
isVoiceInputEnabled = result;
} catch (e) {
console.warn("Error reading voice_input_enabled:", e);
}
}

Connections {
target: mainView.backend_bridge
onMessageReceived: {
if (!readmepage.listening && !readmepage.processing) return;

if (data.event === "voice_recognition_partial") {
var partialText = data.payload
if (partialText) {
readmepage._currentVoiceStatus = i18n.dtr("ubtms", "Listening...");

var prefix = "";
if (readmepage.textBeforeRecording.length > 0) {
var lastChar = readmepage.textBeforeRecording.charAt(readmepage.textBeforeRecording.length - 1);
if (lastChar !== '\n' && lastChar !== '\r') {
prefix = "\n";
}
}
simpleEditor.text = readmepage.textBeforeRecording + prefix + partialText;
cursorTimer.start();
}
} else if (data.event === "voice_recognition_status") {
var statusText = data.payload;
if (statusText) {
readmepage._currentVoiceStatus = statusText;
}
} else if (data.event === "voice_recognition_result") {
if (readmepage.ignoreNextResult) {
readmepage.ignoreNextResult = false;
readmepage.listening = false;
readmepage.processing = false;
readmepage._currentVoiceStatus = "";
readmepage.textBeforeRecording = simpleEditor.text;
cursorTimer.start();
return;
}

readmepage.listening = false
readmepage.processing = false
var recognizedText = data.payload
readmepage._currentVoiceStatus = "";

if (recognizedText) {
var prefix = "";
if (readmepage.textBeforeRecording.length > 0) {
var lastChar = readmepage.textBeforeRecording.charAt(readmepage.textBeforeRecording.length - 1);
if (lastChar !== '\n' && lastChar !== '\r') {
prefix = "\n";
}
}
simpleEditor.text = readmepage.textBeforeRecording + prefix + recognizedText;
readmepage.textBeforeRecording = simpleEditor.text;
cursorTimer.start();
}
} else if (data.event === "voice_recognition_error") {
readmepage.listening = false
readmepage.processing = false
readmepage._currentVoiceStatus = "";
readmepage.textBeforeRecording = simpleEditor.text;
cursorTimer.start()
}
}
}

Timer {
id: cursorTimer
interval: 100
repeat: false
onTriggered: {
if (simpleEditor) {
simpleEditor.cursorPosition = simpleEditor.length;
if (simpleEditor.flickableItem) {
simpleEditor.flickableItem.contentY = Math.max(0, simpleEditor.flickableItem.contentHeight - simpleEditor.flickableItem.height);
} else if (simpleEditor.flickable) {
simpleEditor.flickable.contentY = Math.max(0, simpleEditor.flickable.contentHeight - simpleEditor.flickable.height);
}
}
}
}

header: PageHeader {
id: header
title: i18n.dtr("ubtms","Description")
Expand All @@ -35,20 +140,45 @@ Page {
dividerColor: LomiriColors.slate
}

trailingActionBar.numberOfSlots: 3
trailingActionBar.actions: [
Action {
visible: !isReadOnly && useRichText
iconName: editor.toolbarExpanded ? "view-collapse" : "view-expand"
text: editor.toolbarExpanded ? i18n.dtr("ubtms", "Hide Toolbar") : i18n.dtr("ubtms", "Show Toolbar")
visible: !isReadOnly
iconName: "tick"
onTriggered: {
editor.toolbarExpanded = !editor.toolbarExpanded
saveAndClose()
}
},
Action {
visible: !isReadOnly
iconName: "tick"
visible: !isReadOnly && readmepage.isVoiceInputEnabled && !useRichText
iconName: "microphone"
text: readmepage.listening ? i18n.dtr("ubtms", "Stop Recording") : i18n.dtr("ubtms", "Start Recording")
onTriggered: {
saveAndClose()
if (readmepage.listening) {
readmepage.listening = false
readmepage.processing = true
readmepage._currentVoiceStatus = i18n.dtr("ubtms", "Processing...");

backend_bridge.call("backend.stop_voice_recognition", [])
return;
}
if (readmepage.processing) return;

readmepage.textBeforeRecording = simpleEditor.text
readmepage._partialRecognizedText = "";
readmepage._currentVoiceStatus = i18n.dtr("ubtms", "Starting...");

readmepage.listening = true
readmepage.processing = false
backend_bridge.call("backend.run_voice_recognition", [])
}
},
Action {
visible: !isReadOnly && useRichText && (!readmepage.listening && !readmepage.processing)
iconName: editor.toolbarExpanded ? "view-collapse" : "view-expand"
text: editor.toolbarExpanded ? i18n.dtr("ubtms", "Hide Toolbar") : i18n.dtr("ubtms", "Show Toolbar")
onTriggered: {
editor.toolbarExpanded = !editor.toolbarExpanded
}
}
]
Expand Down Expand Up @@ -109,13 +239,28 @@ Page {
}

function saveAndClose() {
// Auto-stop voice recognition if it's still running
if (readmepage.listening || readmepage.processing) {
readmepage.ignoreNextResult = true;
readmepage.listening = false;
readmepage.processing = false;
readmepage._currentVoiceStatus = "";
backend_bridge.call("backend.stop_voice_recognition", []);
}

if (useRichText) {
// Finalize voice span in RichTextEditor if needed
if (editor.editor && editor.editor.listening || editor.editor && editor.editor.processing) {
editor.editor.stopAndFinalizeVoice();
}
editor.getText(function (content) {
if (commitContent(content)) {
closePage();
}
});
} else {
// For plain text, textBeforeRecording is already updated
readmepage.textBeforeRecording = simpleEditor.text;
if (commitContent(simpleEditor.text)) {
closePage();
}
Expand All @@ -133,8 +278,8 @@ Page {
id: editor
visible: useRichText
text: Global.description_temporary_holder
readOnly: isReadOnly
showToolbar: !isReadOnly
readOnly: isReadOnly || readmepage.listening || readmepage.processing
showToolbar: !isReadOnly && !readmepage.listening && !readmepage.processing
anchors.fill: parent

onContentChanged: {
Expand Down Expand Up @@ -165,7 +310,7 @@ Page {
id: simpleEditor
visible: !useRichText
text: Global.description_temporary_holder
readOnly: isReadOnly
readOnly: isReadOnly || readmepage.listening || readmepage.processing
textFormat: Text.PlainText
font.pixelSize: units.gu(2)
wrapMode: TextArea.Wrap
Expand Down Expand Up @@ -222,6 +367,27 @@ Page {
}
}

VoiceTimerWidget {
id: voiceTimerWidget
parent: readmepage

isListening: readmepage.listening
isProcessing: readmepage.processing
partialText: "" // Don't show partial text in the widget anymore
voiceStatus: readmepage._currentVoiceStatus

onStopClicked: {
readmepage.ignoreNextResult = true;
readmepage.listening = false
readmepage.processing = false
readmepage.textBeforeRecording = simpleEditor.text;

readmepage._currentVoiceStatus = "";

backend_bridge.call("backend.stop_voice_recognition", [])
}
}

// Handle page visibility changes to ensure content is saved
onVisibleChanged: {
if (!visible && !isReadOnly && !_parentSaveCommitted) {
Expand Down Expand Up @@ -249,9 +415,15 @@ Page {
}
}
}

if (!visible && (listening || processing)) {
console.log("[ReadMorePage] visibility changed: Stopping voice recognition...")
mainView.backend_bridge.call("backend.stop_voice_recognition", [])
}
}

Component.onCompleted: {
checkVoiceInputEnabled();
// Initialize tracking to avoid false external-change detection
_lastKnownHolder = Global.description_temporary_holder || "";

Expand Down Expand Up @@ -281,5 +453,10 @@ Page {
parentDraftHandler.saveDraft();
}
}

if (listening || processing) {
console.log("[ReadMorePage] destruction: Stopping voice recognition...")
mainView.backend_bridge.call("backend.stop_voice_recognition", [])
}
}
}
Loading