From 277748bb830dd78017087daf7e37ebc164884b5a Mon Sep 17 00:00:00 2001 From: Anmol Garg Date: Thu, 10 Sep 2026 17:07:00 +0530 Subject: [PATCH 1/2] Decouple sync and download snackbars with dynamic stacking and static download icon - Stack download snackbar above sync snackbar when both are active - Isolate sync and download backend event handling between widgets - Add customizable icon source and rotation property, keeping download icon static - Provide dedicated titles and subtitles for voice model downloads - Add smooth transition on snackbar margin changes --- qml/app/GlobalWidgets.qml | 6 ++- qml/components/system/GlobalTimerWidget.qml | 48 ++++++++++++------- .../system/ModelDownloadTimerWidget.qml | 10 +++- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/qml/app/GlobalWidgets.qml b/qml/app/GlobalWidgets.qml index 7b9fdc62..522da04a 100644 --- a/qml/app/GlobalWidgets.qml +++ b/qml/app/GlobalWidgets.qml @@ -33,11 +33,15 @@ Item { id: modelDownloadTimerWidget z: 9999 anchors.bottom: parent.bottom - anchors.bottomMargin: (Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0) + units.gu(1) + anchors.bottomMargin: ((Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0) + units.gu(1)) + (globalTimerWidget.visible ? (globalTimerWidget.height + units.gu(1)) : 0) visible: false showNotification: function (title, message, type) { notifPopup.open(title, message, type); } + + Behavior on anchors.bottomMargin { + NumberAnimation { duration: 200; easing.type: Easing.OutQuad } + } } BackendBridge { diff --git a/qml/components/system/GlobalTimerWidget.qml b/qml/components/system/GlobalTimerWidget.qml index b18f30bc..1d8f8ea9 100644 --- a/qml/components/system/GlobalTimerWidget.qml +++ b/qml/components/system/GlobalTimerWidget.qml @@ -44,6 +44,16 @@ Rectangle { property bool syncFailed: false property string syncStatusMessage: "" + // Customization properties for derived widgets (e.g. ModelDownloadTimerWidget) + property bool isDownloadWidget: false + property string syncTitlePrefix: i18n.dtr("ubtms", "Syncing ") + property string defaultSyncTitle: i18n.dtr("ubtms", "Cloud Sync") + property string defaultSyncingSubtitle: i18n.dtr("ubtms", "Synchronizing...") + property string syncSuccessSubtitle: i18n.dtr("ubtms", "All items up to date") + property string defaultSyncFailedText: i18n.dtr("ubtms", "Sync failed") + property string syncIconSource: "../../images/refresh.svg" + property bool rotateIcon: true + // BackendBridge for real-time sync communication (connect to global bridge) property var backendBridge: null @@ -73,7 +83,9 @@ Rectangle { if (root.backend_bridge) { backendBridge = root.backend_bridge; Logger.debug("GlobalTimerWidget", "GlobalTimer: Connected to backend bridge") - backendBridge.messageReceived.connect(handleSyncEvent); + if (!isDownloadWidget) { + backendBridge.messageReceived.connect(handleSyncEvent); + } break; } } @@ -97,10 +109,10 @@ Rectangle { if (data.payload === true) completeSyncSuccessfully(); else - failSync("Sync Failed "); + failSync(defaultSyncFailedText); break; case "sync_error": - failSync("Failed " + data.payload); + failSync(data.payload ? (defaultSyncFailedText + ": " + data.payload) : defaultSyncFailedText); break; } } @@ -113,15 +125,15 @@ Rectangle { var progressPercent = Math.round(syncProgress * 100); if (progressPercent < 25) { - syncStatusMessage = "Initializing sync..."; + syncStatusMessage = i18n.dtr("ubtms", "Initializing sync..."); } else if (progressPercent < 50) { - syncStatusMessage = "Downloading from server..."; + syncStatusMessage = i18n.dtr("ubtms", "Downloading from server..."); } else if (progressPercent < 90) { - syncStatusMessage = "Uploading to server..."; + syncStatusMessage = i18n.dtr("ubtms", "Uploading to server..."); } else if (progressPercent < 100) { - syncStatusMessage = "Finalizing sync..."; + syncStatusMessage = i18n.dtr("ubtms", "Finalizing sync..."); } else { - syncStatusMessage = "Sync complete!"; + syncStatusMessage = syncSuccessSubtitle; } } @@ -130,7 +142,7 @@ Rectangle { syncSuccessful = true; syncFailed = false; syncProgress = 1.0; - syncStatusMessage = "Sync Complete!"; + syncStatusMessage = syncSuccessSubtitle; // Auto-hide after 3 seconds autoHideTimer.interval = 3000; @@ -141,7 +153,7 @@ Rectangle { function failSync(errorMessage) { syncSuccessful = false; syncFailed = true; - syncStatusMessage = errorMessage || "Sync Failed"; + syncStatusMessage = errorMessage || defaultSyncFailedText; // Auto-hide after 5 seconds autoHideTimer.interval = 5000; @@ -163,12 +175,12 @@ Rectangle { // Function to start sync indication with BackendBridge integration function startSync(accountId, accountName) { syncAccountId = accountId; - syncAccountName = accountName || "Account " + accountId; + syncAccountName = accountName || (isDownloadWidget ? "" : ("Account " + accountId)); isSyncing = true; syncSuccessful = false; syncFailed = false; syncProgress = 0.0; - syncStatusMessage = "Starting sync..."; + syncStatusMessage = isDownloadWidget ? i18n.dtr("ubtms", "Starting download...") : i18n.dtr("ubtms", "Starting sync..."); globalTimer.visible = true; } @@ -282,12 +294,12 @@ Rectangle { id: syncIcon visible: globalTimer.isSyncing && !globalTimer.isTimerRunning && !globalTimer.syncSuccessful && !globalTimer.syncFailed anchors.fill: parent - source: "../../images/refresh.svg" + source: globalTimer.syncIconSource fillMode: Image.PreserveAspectFit RotationAnimation on rotation { loops: Animation.Infinite - running: globalTimer.visible && globalTimer.isSyncing && !globalTimer.isTimerRunning && !globalTimer.syncSuccessful && !globalTimer.syncFailed + running: globalTimer.rotateIcon && globalTimer.visible && globalTimer.isSyncing && !globalTimer.isTimerRunning && !globalTimer.syncSuccessful && !globalTimer.syncFailed from: 0 to: 360 duration: 1200 @@ -335,7 +347,7 @@ Rectangle { if (globalTimer.isTimerRunning) { return globalTimer.activeTitle; } else if (globalTimer.isSyncing) { - return globalTimer.syncAccountName ? ("Syncing " + globalTimer.syncAccountName) : "Cloud Sync"; + return globalTimer.syncAccountName ? (globalTimer.syncTitlePrefix + globalTimer.syncAccountName) : globalTimer.defaultSyncTitle; } return ""; } @@ -381,9 +393,9 @@ Rectangle { visible: globalTimer.isSyncing && !globalTimer.isTimerRunning width: parent.width text: { - if (globalTimer.syncFailed) return globalTimer.syncStatusMessage || "Sync failed"; - if (globalTimer.syncSuccessful) return "All items up to date"; - return globalTimer.syncStatusMessage || "Synchronizing..."; + if (globalTimer.syncFailed) return globalTimer.syncStatusMessage || globalTimer.defaultSyncFailedText; + if (globalTimer.syncSuccessful) return globalTimer.syncSuccessSubtitle; + return globalTimer.syncStatusMessage || globalTimer.defaultSyncingSubtitle; } color: globalTimer.syncFailed ? "#DF382C" : (globalTimer.syncSuccessful ? "#38B44A" : "#D0CBC5") font.pixelSize: units.gu(1.3) diff --git a/qml/components/system/ModelDownloadTimerWidget.qml b/qml/components/system/ModelDownloadTimerWidget.qml index 279ccd31..59f1df1a 100644 --- a/qml/components/system/ModelDownloadTimerWidget.qml +++ b/qml/components/system/ModelDownloadTimerWidget.qml @@ -4,7 +4,15 @@ import Lomiri.Components 1.3 GlobalTimerWidget { id: downloadWidget enableTimesheetTimer: false - + isDownloadWidget: true + syncTitlePrefix: i18n.dtr("ubtms", "Downloading ") + defaultSyncTitle: i18n.dtr("ubtms", "Voice Model Download") + defaultSyncingSubtitle: i18n.dtr("ubtms", "Downloading...") + syncSuccessSubtitle: i18n.dtr("ubtms", "Installation complete!") + defaultSyncFailedText: i18n.dtr("ubtms", "Download failed") + syncIconSource: "../../images/download.svg" + rotateIcon: false + // Override completion logic to connect to our specific download events Component.onCompleted: { var root = downloadWidget; From e4dc01ed334efe5c0d0a95826878b90e6fc6e1fc Mon Sep 17 00:00:00 2001 From: Anmol Garg Date: Fri, 11 Sep 2026 12:12:37 +0530 Subject: [PATCH 2/2] Decouple base widget from derived flags and add white download icon - Remove isDownloadWidget from GlobalTimerWidget to keep base class generic - Restore original startSync and onCompleted behavior in GlobalTimerWidget - Override handleSyncEvent in ModelDownloadTimerWidget to isolate sync events - Add crisp white download vector icon for high contrast on dark snackbars --- qml/components/system/GlobalTimerWidget.qml | 11 ++++------- qml/components/system/ModelDownloadTimerWidget.qml | 5 +++-- qml/images/downloadWhite.svg | 5 +++++ 3 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 qml/images/downloadWhite.svg diff --git a/qml/components/system/GlobalTimerWidget.qml b/qml/components/system/GlobalTimerWidget.qml index 1d8f8ea9..80021eb0 100644 --- a/qml/components/system/GlobalTimerWidget.qml +++ b/qml/components/system/GlobalTimerWidget.qml @@ -44,8 +44,7 @@ Rectangle { property bool syncFailed: false property string syncStatusMessage: "" - // Customization properties for derived widgets (e.g. ModelDownloadTimerWidget) - property bool isDownloadWidget: false + // Customization properties for derived widgets property string syncTitlePrefix: i18n.dtr("ubtms", "Syncing ") property string defaultSyncTitle: i18n.dtr("ubtms", "Cloud Sync") property string defaultSyncingSubtitle: i18n.dtr("ubtms", "Synchronizing...") @@ -83,9 +82,7 @@ Rectangle { if (root.backend_bridge) { backendBridge = root.backend_bridge; Logger.debug("GlobalTimerWidget", "GlobalTimer: Connected to backend bridge") - if (!isDownloadWidget) { - backendBridge.messageReceived.connect(handleSyncEvent); - } + backendBridge.messageReceived.connect(handleSyncEvent); break; } } @@ -175,12 +172,12 @@ Rectangle { // Function to start sync indication with BackendBridge integration function startSync(accountId, accountName) { syncAccountId = accountId; - syncAccountName = accountName || (isDownloadWidget ? "" : ("Account " + accountId)); + syncAccountName = accountName || "Account " + accountId; isSyncing = true; syncSuccessful = false; syncFailed = false; syncProgress = 0.0; - syncStatusMessage = isDownloadWidget ? i18n.dtr("ubtms", "Starting download...") : i18n.dtr("ubtms", "Starting sync..."); + syncStatusMessage = "Starting sync..."; globalTimer.visible = true; } diff --git a/qml/components/system/ModelDownloadTimerWidget.qml b/qml/components/system/ModelDownloadTimerWidget.qml index 59f1df1a..7f5ac264 100644 --- a/qml/components/system/ModelDownloadTimerWidget.qml +++ b/qml/components/system/ModelDownloadTimerWidget.qml @@ -4,13 +4,14 @@ import Lomiri.Components 1.3 GlobalTimerWidget { id: downloadWidget enableTimesheetTimer: false - isDownloadWidget: true + function handleSyncEvent(data) { + } syncTitlePrefix: i18n.dtr("ubtms", "Downloading ") defaultSyncTitle: i18n.dtr("ubtms", "Voice Model Download") defaultSyncingSubtitle: i18n.dtr("ubtms", "Downloading...") syncSuccessSubtitle: i18n.dtr("ubtms", "Installation complete!") defaultSyncFailedText: i18n.dtr("ubtms", "Download failed") - syncIconSource: "../../images/download.svg" + syncIconSource: "../../images/downloadWhite.svg" rotateIcon: false // Override completion logic to connect to our specific download events diff --git a/qml/images/downloadWhite.svg b/qml/images/downloadWhite.svg new file mode 100644 index 00000000..391ff369 --- /dev/null +++ b/qml/images/downloadWhite.svg @@ -0,0 +1,5 @@ + + + + +