From ff7f323f074f55e8ce0e4e5b67529102242a93cc Mon Sep 17 00:00:00 2001 From: MuhammadAbdullahIqbal23 <112970908+MuhammadAbdullahIqbal23@users.noreply.github.com> Date: Thu, 17 Jul 2025 15:09:58 +0500 Subject: [PATCH 1/8] fix: update main page to use TranslationApp and set default languages to English fix: enhance response handler to manage original text and translated text fix: add storage methods for language preferences and app settings fix: update pubspec.lock for dependency version changes --- lib/main.dart | 3 +- lib/pages/main_page.dart | 508 +++++++++++++++++++++++++---- lib/services/response_handler.dart | 16 +- lib/services/storage_service.dart | 42 +++ pubspec.lock | 16 +- 5 files changed, 507 insertions(+), 78 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index a85331a..65acb37 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:audio_recorder/models/api_response.dart'; import 'package:audio_recorder/models/websocket_config.dart'; import 'package:audio_recorder/pages/login_page.dart'; +import 'package:audio_recorder/pages/main_page.dart'; import 'package:flutter/material.dart'; void main() async { @@ -39,7 +40,7 @@ class MyApp extends StatelessWidget { dynamicSchemeVariant: DynamicSchemeVariant.monochrome, ), ), - home: LoginScreen(), + home: TranslationApp(), ); } } diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 6253c65..622a86d 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -24,20 +24,27 @@ class TranslationAppState extends State { bool isExpandedTop = false; bool isExpandedBottom = false; bool isWebSocketConnected = false; - String topLanguage = 'fr'; - String bottomLanguage = 'it'; + // Default languages set to English for both (will be overridden by stored preferences) + String topLanguage = 'en'; + String bottomLanguage = 'en'; double heightTop = 100; double heightBottom = 100; + // Add fullSentence feature + bool fullSentence = true; + List translatedSentences = []; + String translatedText = ''; + + // Add original transcript tracking + String originalText = ''; + bool showOriginalText = true; // Toggle to show/hide original text + String serverUrl = WebSocketConfig.serverUrl; String? userID = 'ronaldo'; // Changed to nullable String? tokenJWT = ''; // Changed to nullable bool isRecording = false; - String translatedText = ''; - List translatedSentences = []; - RealtimeAudio? audioEngine; List>? _subscriptions; // ignore: unused_field @@ -63,6 +70,13 @@ class TranslationAppState extends State { static const _printTimeDifferences = false; + // Add constants for text animation + static const Duration textAnimationDuration = Duration(milliseconds: 300); + static const Curve textAnimationCurve = Curves.easeInOut; + + // For handling timing of player updates + DateTime? _lastPlayerChunk; + @override void initState() { super.initState(); @@ -87,14 +101,73 @@ class TranslationAppState extends State { Future _initializeUser() async { final (token, _, username, language) = await StorageService.getStoredData(); - print(username); + if (kDebugMode) { + print("INIT USER - Username: $username, Saved Language: $language"); + } if (username != null && mounted) { setState(() { userID = username; tokenJWT = token; - bottomLanguage = language ?? 'it'; }); } + + // Load top language preference + final topLangPreference = await StorageService.getTopLanguage(); + if (mounted) { + setState(() { + // Use the saved top language preference, or default to English if not set + topLanguage = topLangPreference ?? 'en'; + if (kDebugMode) { + print("TOP LANGUAGE SET TO: $topLanguage"); + } + }); + } + + // For consistency, also load bottom language preference directly using the new method + final bottomLangPreference = await StorageService.getBottomLanguage(); + if (mounted) { + setState(() { + // Use saved bottom language or default to English if not set + bottomLanguage = bottomLangPreference ?? 'en'; + if (kDebugMode) { + print("BOTTOM LANGUAGE LOADED DIRECTLY: $bottomLanguage"); + } + }); + } + + // Load full sentence mode preference + final savedFullSentenceMode = await StorageService.getFullSentenceMode(); + if (mounted) { + setState(() { + fullSentence = savedFullSentenceMode; + if (kDebugMode) { + print("FULL SENTENCE MODE: $fullSentence"); + } + }); + } + + // Load show original text preference - Default to true if not set + final savedShowOriginalText = await StorageService.getShowOriginalText(); + if (mounted) { + setState(() { + showOriginalText = savedShowOriginalText; + if (kDebugMode) { + print("SHOW ORIGINAL TEXT: $showOriginalText"); + } + }); + } + + // Ensure the language preferences are saved + await StorageService.saveBottomLanguagePreference(bottomLanguage); + await StorageService.saveTopLanguagePreference(topLanguage); + + // Ensure the original text display is enabled by default + if (showOriginalText == false) { + setState(() { + showOriginalText = true; + }); + await StorageService.saveShowOriginalText(true); + } } @override @@ -204,7 +277,7 @@ class TranslationAppState extends State { void _toggleSectionExpansion(isTop) { setState(() { - print("Toofle"); + print("Toggling expansion - isTop: $isTop"); if (isExpandedTop) { heightTop = MediaQuery.of(context).size.height * 1; heightBottom = MediaQuery.of(context).size.height * 0; @@ -214,8 +287,26 @@ class TranslationAppState extends State { } if (isExpandedTop && isTop) return; if (isExpandedBottom && !isTop) return; + isExpandedTop = isTop; isExpandedBottom = !isTop; + + // Update the currentLanguage based on which section is active + if (isExpandedTop && Languages.languages.containsKey(topLanguage)) { + currentLanguage = Languages.languages[topLanguage]!; + if (kDebugMode) { + print( + "ACTIVE LANGUAGE SWITCHED TO TOP: ${currentLanguage.name} (${currentLanguage.code})"); + } + } else if (isExpandedBottom && + Languages.languages.containsKey(bottomLanguage)) { + currentLanguage = Languages.languages[bottomLanguage]!; + if (kDebugMode) { + print( + "ACTIVE LANGUAGE SWITCHED TO BOTTOM: ${currentLanguage.name} (${currentLanguage.code})"); + } + } + if (isRecording) { _toggleRecording(); } @@ -223,13 +314,48 @@ class TranslationAppState extends State { }); } + void _processText(String text, String? original) { + translatedSentences.add(text); + + // Only log in debug mode + if (kDebugMode && original != null) { + print('Original text: $original'); + print('Translated text: $text'); + } + + setState(() { + if (fullSentence) { + translatedText = text; // Replace with full sentence + + // Set original text when available + if (original != null && original.isNotEmpty) { + originalText = original; + } + } else { + translatedText += '$text '; // Append text as before + + // Also append original text if available + if (original != null && original.isNotEmpty) { + originalText += '$original '; + } + } + }); + } + + void _resetTexts() { + setState(() { + translatedText = ''; + originalText = ''; + translatedSentences = []; + }); + } + void _stopRecording() { heightBottom = MediaQuery.of(context).size.height * 0.5; heightTop = MediaQuery.of(context).size.height * 0.5; isExpandedTop = false; isExpandedBottom = false; - translatedSentences = []; - translatedText = ''; + _resetTexts(); if (isRecording) { _toggleRecording(); } @@ -290,30 +416,78 @@ class TranslationAppState extends State { Widget _textDisplayTop() { return Column( children: [ - Spacer(), - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - translatedText, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black), - textAlign: TextAlign.center, + const SizedBox(height: 80), // Add space at the top for better centering + // Translated text with bold styling centered in the top half + Expanded( + flex: 3, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Main translated text + AnimatedSwitcher( + duration: textAnimationDuration, + child: Text( + translatedText, + key: ValueKey(translatedText), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black), + textAlign: TextAlign.center, + ), + ), + // Original text underneath with less opacity + if (showOriginalText && originalText.isNotEmpty) ...[ + const SizedBox(height: 16), + AnimatedSwitcher( + duration: textAnimationDuration, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.02), + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + "You said: \"$originalText\"", + key: ValueKey(originalText), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w300, // Lighter weight + fontStyle: FontStyle.italic, + color: Colors.black + .withValues(alpha: 0.4), // Lower opacity + letterSpacing: 0.2, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ], ), ), ), - Spacer(), - Text( - Languages.languages[bottomLanguage]!.upText, - style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w300, color: Colors.black), - ), - const Icon( - SFSymbols.chevron_compact_up, - size: 40, - color: Colors.black, + Expanded( + flex: 1, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + Languages.languages[bottomLanguage]!.upText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w300, + color: Colors.black), + ), + const Icon( + SFSymbols.chevron_compact_up, + size: 40, + color: Colors.black, + ), + ], + ), ), ], ); @@ -334,21 +508,62 @@ class TranslationAppState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w300, color: Colors.black), ), - Spacer(), - Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - translatedText, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black), - textAlign: TextAlign.center, + // Main content area with both translated and original text + Expanded( + flex: 3, + child: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Main translated text + AnimatedSwitcher( + duration: textAnimationDuration, + child: Text( + translatedText, + key: ValueKey(translatedText), + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black), + textAlign: TextAlign.center, + ), + ), + // Original text underneath with less opacity + if (showOriginalText && originalText.isNotEmpty) ...[ + const SizedBox(height: 16), + AnimatedSwitcher( + duration: textAnimationDuration, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.02), + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + "You said: \"$originalText\"", + key: ValueKey(originalText), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w300, // Lighter weight + fontStyle: FontStyle.italic, + color: Colors.black + .withValues(alpha: 0.4), // Lower opacity + letterSpacing: 0.2, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ], + ), ), ), ), - Spacer(), + const Spacer(), ], ); } @@ -359,6 +574,7 @@ class TranslationAppState extends State { ? 1.0 : heightTop / (MediaQuery.of(context).size.height * 0.5), child: Column( + mainAxisSize: MainAxisSize.min, children: [ Spacer(), Text( @@ -372,9 +588,10 @@ class TranslationAppState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w300, color: Colors.black), ), + SizedBox(height: 4), const Icon( SFSymbols.chevron_compact_down, - size: 40, + size: 36, color: Colors.black, ), ], @@ -389,12 +606,14 @@ class TranslationAppState extends State { : heightBottom / (MediaQuery.of(context).size.height * 0.5), child: Column( mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ const Icon( SFSymbols.chevron_compact_up, - size: 40, + size: 36, color: Colors.black, ), + SizedBox(height: 4), Text( Languages.languages[bottomLanguage]!.upText, style: TextStyle( @@ -412,7 +631,7 @@ class TranslationAppState extends State { ); } - // Add this method to the TranslationAppState class + // Fixed version of _showLanguageSelector method void _showLanguageSelector(bool isTop) { showModalBottomSheet( context: context, @@ -477,14 +696,70 @@ class TranslationAppState extends State { language.name, style: TextStyle(color: Colors.white), ), - onTap: () { - setState(() { - if (isTop) { + onTap: () async { + // Store old language for logging + final oldLanguage = + isTop ? topLanguage : bottomLanguage; + + if (isTop) { + setState(() { topLanguage = langCode; - } else { + }); + + // Save the chosen top language preference with debug logging + if (kDebugMode) { + print( + "SAVING TOP LANGUAGE PREFERENCE: $langCode (was: $oldLanguage)"); + } + await StorageService.saveTopLanguagePreference( + langCode); + + // Verify the save worked + String? savedTopLang = + await StorageService.getTopLanguage(); + if (kDebugMode) { + print("VERIFIED SAVED TOP LANGUAGE: $savedTopLang"); + } + + // Update current language if top section is active + if (isExpandedTop && + Languages.languages.containsKey(langCode)) { + currentLanguage = Languages.languages[langCode]!; + if (kDebugMode) { + print( + "UPDATED CURRENT LANGUAGE TO: ${currentLanguage.name} (TOP ACTIVE)"); + } + } + } else { + setState(() { bottomLanguage = langCode; + }); + + // Save the chosen bottom language preference with debug logging + if (kDebugMode) { + print( + "SAVING BOTTOM LANGUAGE PREFERENCE: $langCode (was: $oldLanguage)"); } - }); + await StorageService.saveBottomLanguagePreference( + langCode); + + // Verify the save worked + String? savedLang = + await StorageService.getBottomLanguage(); + if (kDebugMode) { + print("VERIFIED SAVED LANGUAGE: $savedLang"); + } + + // Update current language if bottom section is active + if (isExpandedBottom && + Languages.languages.containsKey(langCode)) { + currentLanguage = Languages.languages[langCode]!; + if (kDebugMode) { + print( + "UPDATED CURRENT LANGUAGE TO: ${currentLanguage.name} (BOTTOM ACTIVE)"); + } + } + } Navigator.pop(context); }, ); @@ -498,16 +773,6 @@ class TranslationAppState extends State { ); } - void _processText(String text) { - translatedSentences.add(text); - setState(() { - translatedText += '$text '; - }); - } - - //DateTime? _lastRecorderChunk; - DateTime? _lastPlayerChunk; - void _handleRecorderChunk(Uint8List chunk) { if (_previewData == null) return; if (kDebugMode) { @@ -515,8 +780,42 @@ class TranslationAppState extends State { } if (_websocketService != null && (_websocketService?.isWebSocketConnected ?? false)) { - _websocketService?.sendData(_sampleRate, userID ?? 'ronaldo', - isExpandedTop ? topLanguage : bottomLanguage, chunk); + String targetLanguage = isExpandedTop ? topLanguage : bottomLanguage; + + // Save language preference each time we record in that language + // and ensure currentLanguage is up to date + if (isExpandedTop) { + if (kDebugMode) { + print("RECORDING WITH TOP LANGUAGE: $topLanguage"); + } + StorageService.saveTopLanguagePreference(topLanguage); + + // Update current language if needed + if (currentLanguage.code != topLanguage && + Languages.languages.containsKey(topLanguage)) { + currentLanguage = Languages.languages[topLanguage]!; + if (kDebugMode) { + print("UPDATED CURRENT LANGUAGE TO: ${currentLanguage.name}"); + } + } + } else { + if (kDebugMode) { + print("RECORDING WITH BOTTOM LANGUAGE: $bottomLanguage"); + } + StorageService.saveBottomLanguagePreference(bottomLanguage); + + // Update current language if needed + if (currentLanguage.code != bottomLanguage && + Languages.languages.containsKey(bottomLanguage)) { + currentLanguage = Languages.languages[bottomLanguage]!; + if (kDebugMode) { + print("UPDATED CURRENT LANGUAGE TO: ${currentLanguage.name}"); + } + } + } + + _websocketService?.sendData( + _sampleRate, userID ?? 'ronaldo', targetLanguage, chunk); } } @@ -677,11 +976,11 @@ class TranslationAppState extends State { _websocketService?.sendMessage(userID ?? 'ronaldo'); _websocketService?.startListening((message) { - ResponseHandler.handleReponse(message, (message) { + ResponseHandler.handleReponse(message, (message, originalText) { if (kDebugMode) { print('Message from Server: $message'); } - _processText(message); + _processText(message, originalText); }, (audioData) { _previewData?.add(audioData); audioEngine?.queueChunk(audioData); @@ -707,6 +1006,21 @@ class TranslationAppState extends State { if (!isWebSocketConnected) { await _connectWebSocket(); } + + // Save language preferences at the start of recording + if (isExpandedTop) { + if (kDebugMode) { + print("SAVING TOP LANGUAGE AT START OF RECORDING: $topLanguage"); + } + await StorageService.saveTopLanguagePreference(topLanguage); + } else { + if (kDebugMode) { + print( + "SAVING BOTTOM LANGUAGE AT START OF RECORDING: $bottomLanguage"); + } + await StorageService.saveBottomLanguagePreference(bottomLanguage); + } + setState(() { isRecording = true; }); @@ -740,6 +1054,60 @@ class TranslationAppState extends State { labelText: "User ID", ), ), + const SizedBox(height: 15), + SwitchListTile( + title: Text("Full Sentence Mode"), + subtitle: Text("Show complete sentences instead of word-by-word"), + value: fullSentence, + onChanged: (value) async { + setState(() { + fullSentence = value; + }); + + // Save the full sentence mode preference + await StorageService.saveFullSentenceMode(value); + if (kDebugMode) { + print("SAVED FULL SENTENCE MODE: $value"); + } + + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(fullSentence + ? 'Full sentence mode enabled' + : 'Word-by-word mode enabled'), + duration: Duration(seconds: 2), + ), + ); + }, + ), + SwitchListTile( + title: Text("Show Original Text"), + subtitle: Text( + "Display your spoken language underneath the translation"), + value: showOriginalText, + onChanged: (value) async { + setState(() { + showOriginalText = value; + }); + + // Save the preference + await StorageService.saveShowOriginalText(value); + if (kDebugMode) { + print("SAVED SHOW ORIGINAL TEXT: $value"); + } + + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(value + ? 'Original text display enabled' + : 'Original text display disabled'), + duration: Duration(seconds: 2), + ), + ); + }, + ), const SizedBox(height: 20), ElevatedButton( onPressed: () async { @@ -782,6 +1150,16 @@ class TranslationAppState extends State { // Save WebSocket URL to storage await StorageService.saveWebsocketUrl(serverUrl); WebSocketConfig.serverUrl = serverUrl; + + // Save both language preferences when settings are updated + await StorageService.saveBottomLanguagePreference(bottomLanguage); + await StorageService.saveTopLanguagePreference(topLanguage); + + if (kDebugMode) { + print( + "SAVED LANGUAGES FROM SETTINGS DIALOG - Top: $topLanguage, Bottom: $bottomLanguage"); + } + Navigator.pop(context); }, child: Text("OK"), diff --git a/lib/services/response_handler.dart b/lib/services/response_handler.dart index b8d8a04..38d8a72 100644 --- a/lib/services/response_handler.dart +++ b/lib/services/response_handler.dart @@ -3,17 +3,25 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; class ResponseHandler { - static handleReponse(dynamic message, Function(String) onTextReceived, + static handleReponse( + dynamic message, + Function(String, String?) onTextReceived, Function(Uint8List) onAudioReceived) { if (message is String) { // Handle JSON messages Map response = jsonDecode(message); if (response['type'] == 'TS') { - // Handle real-time transcription + // Handle translated text if (kDebugMode) { - print("Translationxw: ${response['text']}"); + print("Translation: ${response['text']}"); } - onTextReceived(response['text']); + onTextReceived(response['text'], null); + } else if (response['type'] == 'fullSentence') { + // Handle original transcribed text + if (kDebugMode) { + print("Original text: ${response['text']}"); + } + onTextReceived(response['text'], response['text']); } else if (response['type'] == 'audio') { Float32List preprocessedAudioData = Float32List.fromList(response['audio_data']); diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 7a484f5..2d96c84 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -13,6 +13,12 @@ class StorageService { static const _apiUrlKey = 'api_base_url'; // Constants for WebSocket URL static const _websocketUrlKey = 'websocket_url'; + // Constants for language preferences + static const _topLanguageKey = 'top_language'; + static const _bottomLanguageKey = 'bottom_language'; + // Constants for app settings + static const _fullSentenceModeKey = 'full_sentence_mode'; + static const _showOriginalTextKey = 'show_original_text'; static Future saveToken( String username, @@ -86,4 +92,40 @@ class StorageService { static Future getWebsocketUrl() async { return await _storage.read(key: _websocketUrlKey); } + + // Language preference methods + static Future saveTopLanguagePreference(String language) async { + await _storage.write(key: _topLanguageKey, value: language); + } + + static Future getTopLanguage() async { + return await _storage.read(key: _topLanguageKey); + } + + static Future saveBottomLanguagePreference(String language) async { + await _storage.write(key: _bottomLanguageKey, value: language); + } + + static Future getBottomLanguage() async { + return await _storage.read(key: _bottomLanguageKey); + } + + // App settings methods + static Future saveFullSentenceMode(bool enabled) async { + await _storage.write(key: _fullSentenceModeKey, value: enabled.toString()); + } + + static Future getFullSentenceMode() async { + final value = await _storage.read(key: _fullSentenceModeKey); + return value == 'true'; + } + + static Future saveShowOriginalText(bool enabled) async { + await _storage.write(key: _showOriginalTextKey, value: enabled.toString()); + } + + static Future getShowOriginalText() async { + final value = await _storage.read(key: _showOriginalTextKey); + return value != 'false'; // Default to true if not set + } } diff --git a/pubspec.lock b/pubspec.lock index a170f4b..329f4f4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" audio_streamer: dependency: transitive description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -308,10 +308,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -617,10 +617,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: From 2d402f3693b80a826b9e6b28f6dd7d323d9bdd0e Mon Sep 17 00:00:00 2001 From: MuhammadAbdullahIqbal23 <112970908+MuhammadAbdullahIqbal23@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:54:41 +0500 Subject: [PATCH 2/8] fix: update WebSocket server URL to new endpoint --- lib/models/websocket_config.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/models/websocket_config.dart b/lib/models/websocket_config.dart index 8ee85a6..12e8fd0 100644 --- a/lib/models/websocket_config.dart +++ b/lib/models/websocket_config.dart @@ -1,8 +1,7 @@ import 'package:audio_recorder/services/storage_service.dart'; class WebSocketConfig { - static String serverUrl = - 'ws://ec2-51-21-138-103.eu-north-1.compute.amazonaws.com:8001/ws/client'; + static String serverUrl = 'ws://jenny.coldpeak.co/ws/client'; static Future initializeServerUrl() async { final savedUrl = await StorageService.getWebsocketUrl(); From ea963abb05311e0aebcc87239d246bd76ecc915f Mon Sep 17 00:00:00 2001 From: MuhammadAbdullahIqbal23 <112970908+MuhammadAbdullahIqbal23@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:51:04 +0500 Subject: [PATCH 3/8] feat: implement WebSocket error handling and display overlay in main page --- lib/pages/main_page.dart | 274 ++++++++++++++++++++-------- lib/services/websocket_service.dart | 79 +++++++- 2 files changed, 274 insertions(+), 79 deletions(-) diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 622a86d..6b940a7 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -7,12 +7,17 @@ import 'package:flutter/services.dart'; import 'package:flutter_sfsymbols/flutter_sfsymbols.dart'; import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:audio_recorder/services/response_handler.dart'; import 'package:audio_recorder/services/websocket_service.dart'; import 'package:realtime_audio/realtime_audio.dart'; +// Import WebSocketErrorType directly +import 'package:audio_recorder/services/websocket_service.dart' + show WebSocketErrorType, WebSocketError; + class TranslationApp extends StatefulWidget { const TranslationApp({super.key}); @@ -77,6 +82,11 @@ class TranslationAppState extends State { // For handling timing of player updates DateTime? _lastPlayerChunk; + // Add variables for error handling + String? _websocketErrorMessage; + Timer? _errorDisplayTimer; + bool _showErrorOverlay = false; + @override void initState() { super.initState(); @@ -173,6 +183,8 @@ class TranslationAppState extends State { @override void dispose() { destroyAudioEngine(); + _websocketService?.dispose(); // Dispose the WebSocket service properly + _errorDisplayTimer?.cancel(); super.dispose(); } @@ -183,84 +195,132 @@ class TranslationAppState extends State { heightBottom = MediaQuery.of(context).size.height * 0.5; _isLayoutInitialized = true; } + return Scaffold( backgroundColor: Colors.black, - body: GestureDetector( - onVerticalDragUpdate: (details) { - setState(() { - // Adjust the heights based on the drag delta - heightTop += details.primaryDelta!; - heightBottom -= details.primaryDelta!; - - // Ensure the heights stay within valid bounds - if (heightTop < 0) { - heightTop = 0; - heightBottom = MediaQuery.of(context).size.height; - } else if (heightBottom < 0) { - heightBottom = 0; - heightTop = MediaQuery.of(context).size.height; - } - }); - }, - onVerticalDragEnd: (details) { - final dragDistance = - heightTop - MediaQuery.of(context).size.height * 0.5; - final threshold = MediaQuery.of(context).size.height * 0.3; - setState(() { - if (dragDistance.abs() > threshold) { - _toggleSectionExpansion(dragDistance > 0); - } else { - _stopRecording(); - } - }); - }, - child: Stack( - children: [ - Column( - children: [ - _topSection(), - _bottomSection(), + body: Stack( + children: [ + // Main content with gesture detector + GestureDetector( + onVerticalDragUpdate: (details) { + setState(() { + // Adjust the heights based on the drag delta + heightTop += details.primaryDelta!; + heightBottom -= details.primaryDelta!; + + // Ensure the heights stay within valid bounds + if (heightTop < 0) { + heightTop = 0; + heightBottom = MediaQuery.of(context).size.height; + } else if (heightBottom < 0) { + heightBottom = 0; + heightTop = MediaQuery.of(context).size.height; + } + }); + }, + onVerticalDragEnd: (details) { + final dragDistance = + heightTop - MediaQuery.of(context).size.height * 0.5; + final threshold = MediaQuery.of(context).size.height * 0.3; + setState(() { + if (dragDistance.abs() > threshold) { + _toggleSectionExpansion(dragDistance > 0); + } else { + _stopRecording(); + } + }); + }, + child: Stack( + children: [ + Column( + children: [ + _topSection(), + _bottomSection(), + ], + ), + if (!isExpandedTop && !isExpandedBottom) + AnimatedPositioned( + duration: const Duration(milliseconds: 300), + top: heightTop - 4, + left: 0, + right: 0, + child: Opacity( + opacity: (1 - + (((heightTop / + MediaQuery.of(context) + .size + .height) - + 0.5) + .abs() * + 2)) + .clamp(0.0, 1.0), + child: Container( + height: 8, + decoration: BoxDecoration( + color: Colors.grey[200]?.withAlpha(150) ?? + Colors.grey.withAlpha(150), + boxShadow: [ + BoxShadow( + color: Colors.grey.withAlpha(200), + blurRadius: 4, + spreadRadius: 0, + ), + ], + ), + ), + ), + ), + Positioned( + top: 40, + right: 16, + child: IconButton( + onPressed: _showSettingsDialog, + icon: const Icon(Icons.settings), + ), + ), ], ), - if (!isExpandedTop && !isExpandedBottom) - AnimatedPositioned( - duration: const Duration( - milliseconds: - 300), // Match the sections' animation duration - top: heightTop - 4, // Center the 5px separator - left: 0, - right: 0, - child: Opacity( - opacity: (1 - - (((heightTop / MediaQuery.of(context).size.height) - - 0.5) - .abs() * - 2)) - .clamp(0.0, 1.0), - child: Container( - height: 8, - decoration: BoxDecoration( - color: Colors.grey[200]?.withAlpha(150) ?? - Colors.grey.withAlpha(150), - boxShadow: [ - BoxShadow( - color: Colors.grey.withAlpha(200), - blurRadius: 4, - spreadRadius: 0, + ), + + // Error overlay + if (_showErrorOverlay && _websocketErrorMessage != null) + Positioned( + top: 50, + left: 20, + right: 20, + child: Material( + elevation: 8, + borderRadius: BorderRadius.circular(8), + color: Colors.red.shade800, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + const Icon(Icons.error_outline, + color: Colors.white, size: 24), + const SizedBox(width: 12), + Expanded( + child: Text( + _websocketErrorMessage!, + style: const TextStyle( + color: Colors.white, fontSize: 14), ), - ], - ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () { + setState(() { + _showErrorOverlay = false; + }); + }, + ), + ], ), ), ), - Positioned( - top: 40, - right: 16, - child: IconButton( - onPressed: _showSettingsDialog, icon: Icon(Icons.settings)), ), - ], - ), + ], ), ); } @@ -934,13 +994,10 @@ class TranslationAppState extends State { } Future destroyAudioEngine() async { - // First stop the audio engine - await stopPlayer(); - - // Cancel all subscriptions - for (final subscription in _subscriptions ?? const []) { + // Cancel subscriptions + _subscriptions?.forEach((subscription) async { await subscription.cancel(); - } + }); _subscriptions?.clear(); // Dispose audio engine @@ -966,8 +1023,61 @@ class TranslationAppState extends State { } } + void _showError(String errorMessage, + {Duration duration = const Duration(seconds: 5)}) { + setState(() { + _websocketErrorMessage = errorMessage; + _showErrorOverlay = true; + }); + + // Auto-hide the error after duration + _errorDisplayTimer?.cancel(); + _errorDisplayTimer = Timer(duration, () { + if (mounted) { + setState(() { + _showErrorOverlay = false; + }); + } + }); + } + Future _connectWebSocket() async { _websocketService = WebsocketService(); + + // Listen for WebSocket errors + _websocketService!.errorStream.listen((error) { + String userFriendlyMessage; + + switch (error.type) { + case WebSocketErrorType.connectionFailed: + userFriendlyMessage = + 'Failed to connect to the server. Please check your internet connection and try again.'; + break; + case WebSocketErrorType.connectionTimeout: + userFriendlyMessage = + 'Connection timed out. The server is taking too long to respond.'; + break; + case WebSocketErrorType.connectionClosed: + userFriendlyMessage = + 'Connection closed unexpectedly. Please try reconnecting.'; + break; + case WebSocketErrorType.messageSendFailed: + userFriendlyMessage = + 'Failed to send message to the server. Please check your connection.'; + break; + case WebSocketErrorType.serverError: + userFriendlyMessage = + 'Server error occurred. Please try again later.'; + break; + default: + userFriendlyMessage = + 'An unexpected error occurred: ${error.message}'; + break; + } + + _showError(userFriendlyMessage); + }); + isWebSocketConnected = await _websocketService?.connect(serverUrl) ?? false; if (_websocketService?.isWebSocketConnected ?? false) { if (kDebugMode) { @@ -987,8 +1097,14 @@ class TranslationAppState extends State { }); }); return true; + } else { + // If connection failed and we don't have an error message yet (fallback) + if (!_showErrorOverlay) { + _showError( + 'Failed to connect to the server. Please check your internet connection and try again.'); + } + return false; } - return false; } void _toggleRecording() async { @@ -1004,7 +1120,11 @@ class TranslationAppState extends State { await createAudioEngine(recorderEnabled: true); } if (!isWebSocketConnected) { - await _connectWebSocket(); + final connected = await _connectWebSocket(); + if (!connected) { + // Don't proceed with recording if connection failed + return; + } } // Save language preferences at the start of recording diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart index 3f654ff..2931927 100644 --- a/lib/services/websocket_service.dart +++ b/lib/services/websocket_service.dart @@ -5,10 +5,37 @@ import 'package:flutter/foundation.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'dart:convert'; +enum WebSocketErrorType { + connectionFailed, + connectionTimeout, + connectionClosed, + messageSendFailed, + serverError, + unknown +} + +class WebSocketError { + final WebSocketErrorType type; + final String message; + + WebSocketError(this.type, this.message); + + @override + String toString() => message; +} + class WebsocketService { WebSocketChannel? _channel; bool isWebSocketConnected = false; StreamSubscription? _subscription; + WebSocketError? lastError; + + // Stream controller to broadcast error events + final StreamController _errorStreamController = + StreamController.broadcast(); + + // Expose the error stream + Stream get errorStream => _errorStreamController.stream; Future connect(String url) async { try { @@ -17,8 +44,11 @@ class WebsocketService { try { // Add timeout to prevent hanging await _channel!.ready.timeout( - const Duration(seconds: 3), + const Duration(seconds: 30), onTimeout: () { + lastError = WebSocketError(WebSocketErrorType.connectionTimeout, + 'Connection timed out after 30 seconds'); + _errorStreamController.add(lastError!); throw TimeoutException('WebSocket connection timed out'); }, ); @@ -33,21 +63,38 @@ class WebsocketService { .close(WebSocketStatus.normalClosure, 'Connection failed'); _channel = null; isWebSocketConnected = false; + + if (e is TimeoutException) { + // Already handled in the timeout callback + } else { + lastError = WebSocketError(WebSocketErrorType.connectionFailed, + 'Failed to establish connection: ${e.toString()}'); + _errorStreamController.add(lastError!); + } return false; } } + lastError = WebSocketError(WebSocketErrorType.connectionFailed, + 'Could not create WebSocket channel'); + _errorStreamController.add(lastError!); return false; } on WebSocketChannelException catch (e) { if (kDebugMode) { print("WebSocket connection failed: $e"); } isWebSocketConnected = false; + lastError = WebSocketError(WebSocketErrorType.connectionFailed, + 'WebSocket connection failed: ${e.toString()}'); + _errorStreamController.add(lastError!); return false; } catch (e) { if (kDebugMode) { print("Unexpected error during WebSocket connection: $e"); } isWebSocketConnected = false; + lastError = WebSocketError( + WebSocketErrorType.unknown, 'Unexpected error: ${e.toString()}'); + _errorStreamController.add(lastError!); return false; } } @@ -63,12 +110,18 @@ class WebsocketService { print("Channel closed"); } isWebSocketConnected = false; + lastError = WebSocketError(WebSocketErrorType.connectionClosed, + 'WebSocket connection closed'); + _errorStreamController.add(lastError!); }, onError: (error) { if (kDebugMode) { print("Error: $error"); } isWebSocketConnected = false; + lastError = WebSocketError(WebSocketErrorType.serverError, + 'Server error: ${error.toString()}'); + _errorStreamController.add(lastError!); }, ); } @@ -94,6 +147,9 @@ class WebsocketService { if (kDebugMode) { print("Error closing WebSocket: $e"); } + lastError = WebSocketError(WebSocketErrorType.unknown, + 'Error closing WebSocket: ${e.toString()}'); + _errorStreamController.add(lastError!); } } @@ -101,11 +157,18 @@ class WebsocketService { try { if (_channel != null) { _channel!.sink.add(message); + } else { + lastError = WebSocketError(WebSocketErrorType.messageSendFailed, + 'Cannot send message: WebSocket not connected'); + _errorStreamController.add(lastError!); } } catch (e) { if (kDebugMode) { - print("Failed to send chunk with metadata: $e"); + print("Failed to send message: $e"); } + lastError = WebSocketError(WebSocketErrorType.messageSendFailed, + 'Failed to send message: ${e.toString()}'); + _errorStreamController.add(lastError!); } } @@ -139,7 +202,19 @@ class WebsocketService { if (kDebugMode) { print("Failed to send chunk with metadata: $e"); } + lastError = WebSocketError(WebSocketErrorType.messageSendFailed, + 'Failed to send audio data: ${e.toString()}'); + _errorStreamController.add(lastError!); } + } else { + lastError = WebSocketError(WebSocketErrorType.messageSendFailed, + 'Cannot send audio data: WebSocket not connected'); + _errorStreamController.add(lastError!); } } + + // Call this when disposing the service + void dispose() { + _errorStreamController.close(); + } } From 7d5aedad8490f5b6de66181df5b8a51fbb9eca57 Mon Sep 17 00:00:00 2001 From: MuhammadAbdullahIqbal23 <112970908+MuhammadAbdullahIqbal23@users.noreply.github.com> Date: Fri, 18 Jul 2025 01:57:48 +0500 Subject: [PATCH 4/8] feat: add centralized error logging and handling across the application --- lib/pages/login_page.dart | 27 +++-- lib/pages/main_page.dart | 59 +++++----- lib/pages/voice_cloning.dart | 64 +++++------ lib/services/api_service.dart | 57 ++++++++-- lib/services/voice_cloning_service.dart | 12 +- lib/services/websocket_service.dart | 17 +++ lib/utils.dart | 142 ++++++++++++++++++++++++ 7 files changed, 289 insertions(+), 89 deletions(-) diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 2f6a113..91f03a7 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -4,6 +4,7 @@ import 'package:audio_recorder/pages/main_page.dart'; import 'package:audio_recorder/pages/voice_cloning.dart'; import 'package:audio_recorder/services/api_service.dart'; import 'package:audio_recorder/services/storage_service.dart'; +import 'package:audio_recorder/utils.dart'; import 'package:flutter/material.dart'; class LoginScreen extends StatefulWidget { @@ -34,6 +35,8 @@ class _LoginScreenState extends State { bool _showSignupForm = false; bool _isLoading = false; + final ErrorLogger _errorLogger = ErrorLogger(); + @override void initState() { super.initState(); @@ -99,11 +102,10 @@ class _LoginScreenState extends State { await StorageService.saveApiUrl(_baseUrlController.text); if (mounted) { Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('API URL updated successfully'), - duration: Duration(seconds: 2), - ), + ErrorLogger.showError( + context, + 'API URL updated successfully', + duration: const Duration(seconds: 2), ); } }, @@ -326,10 +328,19 @@ class _LoginScreenState extends State { } } } on APIError catch (e) { + _errorLogger.logError(e.message, + severity: ErrorSeverity.high, source: 'Authentication', error: e); + if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(e.message)), - ); + ErrorLogger.showError(context, 'Authentication error: ${e.message}'); + } + } catch (e) { + _errorLogger.logError('Unexpected authentication error', + severity: ErrorSeverity.critical, source: 'Authentication', error: e); + + if (mounted) { + ErrorLogger.showError(context, + 'Unexpected error during authentication. Please try again.'); } } finally { if (mounted) { diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 6b940a7..c3db45d 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -2,6 +2,7 @@ import 'package:audio_recorder/models/language_model.dart'; import 'package:audio_recorder/models/websocket_config.dart'; import 'package:audio_recorder/pages/login_page.dart'; import 'package:audio_recorder/services/storage_service.dart'; +import 'package:audio_recorder/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_sfsymbols/flutter_sfsymbols.dart'; @@ -86,6 +87,7 @@ class TranslationAppState extends State { String? _websocketErrorMessage; Timer? _errorDisplayTimer; bool _showErrorOverlay = false; + final ErrorLogger _errorLogger = ErrorLogger(); @override void initState() { @@ -288,36 +290,13 @@ class TranslationAppState extends State { top: 50, left: 20, right: 20, - child: Material( - elevation: 8, - borderRadius: BorderRadius.circular(8), - color: Colors.red.shade800, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - children: [ - const Icon(Icons.error_outline, - color: Colors.white, size: 24), - const SizedBox(width: 12), - Expanded( - child: Text( - _websocketErrorMessage!, - style: const TextStyle( - color: Colors.white, fontSize: 14), - ), - ), - IconButton( - icon: const Icon(Icons.close, color: Colors.white), - onPressed: () { - setState(() { - _showErrorOverlay = false; - }); - }, - ), - ], - ), - ), + child: ErrorLogger.errorOverlay( + errorMessage: _websocketErrorMessage!, + onDismiss: () { + setState(() { + _showErrorOverlay = false; + }); + }, ), ), ], @@ -1024,7 +1003,12 @@ class TranslationAppState extends State { } void _showError(String errorMessage, - {Duration duration = const Duration(seconds: 5)}) { + {Duration duration = const Duration(seconds: 5), + ErrorSeverity severity = ErrorSeverity.medium, + String source = 'WebSocket'}) { + // Log the error through our centralized error logger + _errorLogger.logError(errorMessage, severity: severity, source: source); + setState(() { _websocketErrorMessage = errorMessage; _showErrorOverlay = true; @@ -1047,35 +1031,46 @@ class TranslationAppState extends State { // Listen for WebSocket errors _websocketService!.errorStream.listen((error) { String userFriendlyMessage; + ErrorSeverity severity; switch (error.type) { case WebSocketErrorType.connectionFailed: userFriendlyMessage = 'Failed to connect to the server. Please check your internet connection and try again.'; + severity = ErrorSeverity.high; break; case WebSocketErrorType.connectionTimeout: userFriendlyMessage = 'Connection timed out. The server is taking too long to respond.'; + severity = ErrorSeverity.high; break; case WebSocketErrorType.connectionClosed: userFriendlyMessage = 'Connection closed unexpectedly. Please try reconnecting.'; + severity = ErrorSeverity.medium; break; case WebSocketErrorType.messageSendFailed: userFriendlyMessage = 'Failed to send message to the server. Please check your connection.'; + severity = ErrorSeverity.medium; break; case WebSocketErrorType.serverError: userFriendlyMessage = 'Server error occurred. Please try again later.'; + severity = ErrorSeverity.high; break; default: userFriendlyMessage = 'An unexpected error occurred: ${error.message}'; + severity = ErrorSeverity.medium; break; } - _showError(userFriendlyMessage); + _showError( + userFriendlyMessage, + severity: severity, + source: 'WebSocket', + ); }); isWebSocketConnected = await _websocketService?.connect(serverUrl) ?? false; diff --git a/lib/pages/voice_cloning.dart b/lib/pages/voice_cloning.dart index 900dc8c..24955cd 100644 --- a/lib/pages/voice_cloning.dart +++ b/lib/pages/voice_cloning.dart @@ -8,6 +8,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:noise_meter/noise_meter.dart'; import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:audio_recorder/models/cloning_sentences.dart'; +import 'package:audio_recorder/utils.dart'; import '../services/voice_cloning_service.dart'; import 'package:path_provider/path_provider.dart'; @@ -27,6 +28,7 @@ class VoiceCloningScreenState extends State { late String _audioFile; // Changed to late bool _isComplete = false; VoiceCloningState _voiceCloningState = VoiceCloningState.recording; + final ErrorLogger _errorLogger = ErrorLogger(); late NoiseMeter _noiseMeter; StreamSubscription? _noiseSubscription; @@ -193,14 +195,12 @@ class VoiceCloningScreenState extends State { } } - void _showErrorDialog(String message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(message), - backgroundColor: Colors.red, - duration: Duration(seconds: 3), - ), - ); + void _showErrorDialog(String message, + {ErrorSeverity severity = ErrorSeverity.medium, + String source = 'VoiceCloning'}) { + _errorLogger.logError(message, severity: severity, source: source); + + ErrorLogger.showError(context, message); } Future _submitVoiceCloning() async { @@ -228,6 +228,10 @@ class VoiceCloningScreenState extends State { setState(() { _voiceCloningState = VoiceCloningState.error; }); + + _errorLogger.logError('Voice cloning submission failed', + severity: ErrorSeverity.high, source: 'VoiceCloning', error: e); + _showRetryDialog('Voice cloning failed. Would you like to try again?'); } } @@ -249,34 +253,22 @@ class VoiceCloningScreenState extends State { } void _showRetryDialog(String message) { - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => AlertDialog( - title: const Text('Error'), - content: Text(message), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - _handleSkip(); - }, - child: const Text('Skip'), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(context); - setState(() { - _currentSentenceIndex = 0; - _isComplete = false; - _voiceCloningState = VoiceCloningState.recording; - _recordingStatus = 'Ready to start'; - }); - }, - child: const Text('Retry'), - ), - ], - ), + _errorLogger.logError(message, + severity: ErrorSeverity.high, source: 'VoiceCloning'); + + ErrorLogger.showErrorDialog( + context, + 'Voice Cloning Error', + message, + onRetry: () { + setState(() { + _currentSentenceIndex = 0; + _isComplete = false; + _voiceCloningState = VoiceCloningState.recording; + _recordingStatus = 'Ready to start'; + }); + }, + onDismiss: _handleSkip, ); } diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index 666fa42..d2077ab 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:audio_recorder/models/api_response.dart'; +import 'package:audio_recorder/utils.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as path; import 'package:http_parser/http_parser.dart'; import 'dart:convert'; +import 'package:flutter/foundation.dart'; Future<(Uint8List, http.Response)> postVoiceClone({ required String filePath, @@ -62,11 +64,28 @@ Future<(Uint8List, http.Response)> postVoiceClone({ } else { throw APIError.errorCode(response.statusCode); } - } on FileError { + } on FileError catch (error) { + // Log file error + if (kDebugMode) { + ErrorLogger().logError('File error during voice cloning', + severity: ErrorSeverity.high, source: 'API Service', error: error); + } rethrow; - } on APIError { + } on APIError catch (error) { + // Log API error + if (kDebugMode) { + ErrorLogger().logError('API error during voice cloning', + severity: ErrorSeverity.high, source: 'API Service', error: error); + } rethrow; - } catch (e) { + } catch (error) { + // Log unknown error + if (kDebugMode) { + ErrorLogger().logError('Unknown error during voice cloning', + severity: ErrorSeverity.critical, + source: 'API Service', + error: error); + } throw APIError.unknown(); } } @@ -94,11 +113,19 @@ Future signup({ throw APIError( errorBody['detail'] ?? 'Signup failed', response.statusCode); } - } on TimeoutException { + } on TimeoutException catch (error) { + if (kDebugMode) { + ErrorLogger().logError('Signup request timed out', + severity: ErrorSeverity.medium, source: 'API Service', error: error); + } throw APIError('Request timed out after ${timeout.inSeconds} seconds'); - } catch (e) { - if (e is APIError) rethrow; - throw APIError(e.toString()); + } catch (error) { + if (kDebugMode) { + ErrorLogger().logError('Signup error', + severity: ErrorSeverity.high, source: 'API Service', error: error); + } + if (error is APIError) rethrow; + throw APIError(error.toString()); } } @@ -125,10 +152,18 @@ Future login({ throw APIError( errorBody['detail'] ?? 'Login failed', response.statusCode); } - } on TimeoutException { + } on TimeoutException catch (error) { + if (kDebugMode) { + ErrorLogger().logError('Login request timed out', + severity: ErrorSeverity.medium, source: 'API Service', error: error); + } throw APIError('Request timed out after ${timeout.inSeconds} seconds'); - } catch (e) { - if (e is APIError) rethrow; - throw APIError(e.toString()); + } catch (error) { + if (kDebugMode) { + ErrorLogger().logError('Login error', + severity: ErrorSeverity.high, source: 'API Service', error: error); + } + if (error is APIError) rethrow; + throw APIError(error.toString()); } } diff --git a/lib/services/voice_cloning_service.dart b/lib/services/voice_cloning_service.dart index 8a67aff..a7c4b85 100644 --- a/lib/services/voice_cloning_service.dart +++ b/lib/services/voice_cloning_service.dart @@ -1,5 +1,7 @@ import 'package:audio_recorder/models/api_response.dart'; import 'package:audio_recorder/services/storage_service.dart'; +import 'package:audio_recorder/utils.dart'; +import 'package:flutter/foundation.dart'; import '../services/api_service.dart'; class VoiceCloningService { @@ -21,8 +23,14 @@ class VoiceCloningService { throw APIError( 'Failed to submit voice cloning: ${response.statusCode}'); } - } catch (e) { - throw ('$e'); + } catch (error) { + if (kDebugMode) { + ErrorLogger().logError('Voice cloning service error', + severity: ErrorSeverity.high, + source: 'Voice Cloning Service', + error: error); + } + throw ('$error'); } } } diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart index 2931927..f17acef 100644 --- a/lib/services/websocket_service.dart +++ b/lib/services/websocket_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:audio_recorder/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'dart:convert'; @@ -57,6 +58,10 @@ class WebsocketService { } catch (e) { if (kDebugMode) { print("Failed to establish WebSocket connection: $e"); + ErrorLogger().logError('Failed to establish WebSocket connection', + severity: ErrorSeverity.high, + source: 'WebSocket Service', + error: e); } // Clean up the channel on failure _channel?.sink @@ -81,6 +86,10 @@ class WebsocketService { } on WebSocketChannelException catch (e) { if (kDebugMode) { print("WebSocket connection failed: $e"); + ErrorLogger().logError('WebSocket channel exception', + severity: ErrorSeverity.high, + source: 'WebSocket Service', + error: e); } isWebSocketConnected = false; lastError = WebSocketError(WebSocketErrorType.connectionFailed, @@ -90,6 +99,10 @@ class WebsocketService { } catch (e) { if (kDebugMode) { print("Unexpected error during WebSocket connection: $e"); + ErrorLogger().logError('Unexpected WebSocket error', + severity: ErrorSeverity.critical, + source: 'WebSocket Service', + error: e); } isWebSocketConnected = false; lastError = WebSocketError( @@ -117,6 +130,10 @@ class WebsocketService { onError: (error) { if (kDebugMode) { print("Error: $error"); + ErrorLogger().logError('WebSocket stream error', + severity: ErrorSeverity.high, + source: 'WebSocket Service', + error: error); } isWebSocketConnected = false; lastError = WebSocketError(WebSocketErrorType.serverError, diff --git a/lib/utils.dart b/lib/utils.dart index 8b13789..d1c9647 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1 +1,143 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +class ErrorLogger { + static final ErrorLogger _instance = ErrorLogger._internal(); + factory ErrorLogger() => _instance; + ErrorLogger._internal(); + + // Stream controller to broadcast errors app-wide + final _errorController = StreamController.broadcast(); + Stream get errorStream => _errorController.stream; + + // Log an error and broadcast it + void logError(String message, + {ErrorSeverity severity = ErrorSeverity.medium, + String? source, + dynamic error}) { + final errorEvent = ErrorEvent( + message: message, + timestamp: DateTime.now(), + severity: severity, + source: source ?? 'Unknown', + error: error, + ); + + print( + 'ERROR [${errorEvent.severity}] ${errorEvent.source}: ${errorEvent.message}'); + if (error != null) { + print('Error details: $error'); + } + + _errorController.add(errorEvent); + } + + // Show a toast/snackbar error + static void showError(BuildContext context, String message, + {Duration duration = const Duration(seconds: 4)}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red[700], + behavior: SnackBarBehavior.floating, + duration: duration, + action: SnackBarAction( + label: 'Dismiss', + textColor: Colors.white, + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + }, + ), + ), + ); + } + + // Show an error dialog + static Future showErrorDialog( + BuildContext context, String title, String message, + {VoidCallback? onRetry, VoidCallback? onDismiss}) async { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + if (onDismiss != null) + TextButton( + onPressed: () { + Navigator.of(context).pop(); + onDismiss(); + }, + child: const Text('Dismiss'), + ), + if (onRetry != null) + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red[700], + ), + onPressed: () { + Navigator.of(context).pop(); + onRetry(); + }, + child: const Text('Retry', style: TextStyle(color: Colors.white)), + ), + ], + ), + ); + } + + // Display a persistent error overlay + static Widget errorOverlay({ + required String errorMessage, + VoidCallback? onDismiss, + Color backgroundColor = const Color(0xDDC62828), + }) { + return Material( + elevation: 8, + borderRadius: BorderRadius.circular(8), + color: backgroundColor, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + const Icon(Icons.error_outline, color: Colors.white, size: 24), + const SizedBox(width: 12), + Expanded( + child: Text( + errorMessage, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + if (onDismiss != null) + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: onDismiss, + ), + ], + ), + ), + ); + } + + void dispose() { + _errorController.close(); + } +} + +class ErrorEvent { + final String message; + final DateTime timestamp; + final ErrorSeverity severity; + final String source; + final dynamic error; + + ErrorEvent({ + required this.message, + required this.timestamp, + required this.severity, + required this.source, + this.error, + }); +} + +enum ErrorSeverity { low, medium, high, critical } From c704e498edef1d448be8c9aeba1011a7bcf5343d Mon Sep 17 00:00:00 2001 From: MuhammadAbdullahIqbal23 <112970908+MuhammadAbdullahIqbal23@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:05:11 +0500 Subject: [PATCH 5/8] feat: enhance error handling with technical details and improved UI interactions --- lib/pages/login_page.dart | 6 +- lib/pages/main_page.dart | 18 ++++- lib/pages/voice_cloning.dart | 16 ++-- lib/utils.dart | 150 +++++++++++++++++++++++++++++++---- 4 files changed, 163 insertions(+), 27 deletions(-) diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 91f03a7..52b10ef 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -332,7 +332,8 @@ class _LoginScreenState extends State { severity: ErrorSeverity.high, source: 'Authentication', error: e); if (mounted) { - ErrorLogger.showError(context, 'Authentication error: ${e.message}'); + ErrorLogger.showError(context, 'Authentication error: ${e.message}', + technicalError: e); } } catch (e) { _errorLogger.logError('Unexpected authentication error', @@ -340,7 +341,8 @@ class _LoginScreenState extends State { if (mounted) { ErrorLogger.showError(context, - 'Unexpected error during authentication. Please try again.'); + 'Unexpected error during authentication. Please try again.', + technicalError: e); } } finally { if (mounted) { diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index c3db45d..c9ae083 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -88,6 +88,7 @@ class TranslationAppState extends State { Timer? _errorDisplayTimer; bool _showErrorOverlay = false; final ErrorLogger _errorLogger = ErrorLogger(); + dynamic _technicalErrorDetails; @override void initState() { @@ -297,6 +298,13 @@ class TranslationAppState extends State { _showErrorOverlay = false; }); }, + technicalError: _technicalErrorDetails, + onShowDetails: _technicalErrorDetails != null + ? () { + ErrorLogger.showTechnicalErrorDialog( + context, _technicalErrorDetails); + } + : null, ), ), ], @@ -354,7 +362,7 @@ class TranslationAppState extends State { } void _processText(String text, String? original) { - translatedSentences.add(text); + // translatedSentences.add(text); // Only log in debug mode if (kDebugMode && original != null) { @@ -1005,13 +1013,16 @@ class TranslationAppState extends State { void _showError(String errorMessage, {Duration duration = const Duration(seconds: 5), ErrorSeverity severity = ErrorSeverity.medium, - String source = 'WebSocket'}) { + String source = 'WebSocket', + dynamic error}) { // Log the error through our centralized error logger - _errorLogger.logError(errorMessage, severity: severity, source: source); + _errorLogger.logError(errorMessage, + severity: severity, source: source, error: error); setState(() { _websocketErrorMessage = errorMessage; _showErrorOverlay = true; + _technicalErrorDetails = error; }); // Auto-hide the error after duration @@ -1070,6 +1081,7 @@ class TranslationAppState extends State { userFriendlyMessage, severity: severity, source: 'WebSocket', + error: error, ); }); diff --git a/lib/pages/voice_cloning.dart b/lib/pages/voice_cloning.dart index 24955cd..57c3e75 100644 --- a/lib/pages/voice_cloning.dart +++ b/lib/pages/voice_cloning.dart @@ -197,10 +197,12 @@ class VoiceCloningScreenState extends State { void _showErrorDialog(String message, {ErrorSeverity severity = ErrorSeverity.medium, - String source = 'VoiceCloning'}) { - _errorLogger.logError(message, severity: severity, source: source); + String source = 'VoiceCloning', + dynamic error}) { + _errorLogger.logError(message, + severity: severity, source: source, error: error); - ErrorLogger.showError(context, message); + ErrorLogger.showError(context, message, technicalError: error); } Future _submitVoiceCloning() async { @@ -232,7 +234,8 @@ class VoiceCloningScreenState extends State { _errorLogger.logError('Voice cloning submission failed', severity: ErrorSeverity.high, source: 'VoiceCloning', error: e); - _showRetryDialog('Voice cloning failed. Would you like to try again?'); + _showRetryDialog('Voice cloning failed. Would you like to try again?', + error: e); } } @@ -252,14 +255,15 @@ class VoiceCloningScreenState extends State { ); } - void _showRetryDialog(String message) { + void _showRetryDialog(String message, {dynamic error}) { _errorLogger.logError(message, - severity: ErrorSeverity.high, source: 'VoiceCloning'); + severity: ErrorSeverity.high, source: 'VoiceCloning', error: error); ErrorLogger.showErrorDialog( context, 'Voice Cloning Error', message, + technicalError: error, onRetry: () { setState(() { _currentSentenceIndex = 0; diff --git a/lib/utils.dart b/lib/utils.dart index d1c9647..2010709 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; class ErrorLogger { static final ErrorLogger _instance = ErrorLogger._internal(); @@ -34,7 +36,8 @@ class ErrorLogger { // Show a toast/snackbar error static void showError(BuildContext context, String message, - {Duration duration = const Duration(seconds: 4)}) { + {Duration duration = const Duration(seconds: 4), + dynamic technicalError}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), @@ -42,10 +45,13 @@ class ErrorLogger { behavior: SnackBarBehavior.floating, duration: duration, action: SnackBarAction( - label: 'Dismiss', + label: 'Details', textColor: Colors.white, onPressed: () { ScaffoldMessenger.of(context).hideCurrentSnackBar(); + if (kDebugMode && technicalError != null) { + showTechnicalErrorDialog(context, technicalError); + } }, ), ), @@ -55,13 +61,22 @@ class ErrorLogger { // Show an error dialog static Future showErrorDialog( BuildContext context, String title, String message, - {VoidCallback? onRetry, VoidCallback? onDismiss}) async { + {VoidCallback? onRetry, + VoidCallback? onDismiss, + dynamic technicalError}) async { return showDialog( context: context, builder: (context) => AlertDialog( title: Text(title), content: Text(message), actions: [ + if (kDebugMode && technicalError != null) + TextButton( + onPressed: () { + showTechnicalErrorDialog(context, technicalError); + }, + child: const Text('Technical Details'), + ), if (onDismiss != null) TextButton( onPressed: () { @@ -90,7 +105,9 @@ class ErrorLogger { static Widget errorOverlay({ required String errorMessage, VoidCallback? onDismiss, + VoidCallback? onShowDetails, Color backgroundColor = const Color(0xDDC62828), + dynamic technicalError, }) { return Material( elevation: 8, @@ -98,27 +115,128 @@ class ErrorLogger { color: backgroundColor, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.error_outline, color: Colors.white, size: 24), - const SizedBox(width: 12), - Expanded( - child: Text( - errorMessage, - style: const TextStyle(color: Colors.white, fontSize: 14), - ), + Row( + children: [ + const Icon(Icons.error_outline, color: Colors.white, size: 24), + const SizedBox(width: 12), + Expanded( + child: Text( + errorMessage, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + if (kDebugMode && technicalError != null) + IconButton( + icon: const Icon(Icons.code, color: Colors.white), + onPressed: onShowDetails, + tooltip: 'Show Technical Details', + ), + if (onDismiss != null) + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: onDismiss, + tooltip: 'Dismiss', + ), + ], ), - if (onDismiss != null) - IconButton( - icon: const Icon(Icons.close, color: Colors.white), - onPressed: onDismiss, - ), ], ), ), ); } + // Show technical error details for debugging + static Future showTechnicalErrorDialog( + BuildContext context, dynamic error) async { + String errorDetails = ''; + + if (error is Exception || error is Error) { + errorDetails = error.toString(); + } else if (error is Map) { + errorDetails = const JsonEncoder.withIndent(' ').convert(error); + } else { + errorDetails = error.toString(); + } + + return showDialog( + context: context, + builder: (context) => Dialog( + child: Container( + padding: const EdgeInsets.all(16), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.9, + maxHeight: MediaQuery.of(context).size.height * 0.8, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.code, size: 24), + const SizedBox(width: 8), + const Text( + 'Technical Error Details', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + const Divider(), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Error Information:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(8), + ), + width: double.infinity, + child: SelectableText( + errorDetails, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 12, + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ), + ], + ), + ), + ), + ); + } + void dispose() { _errorController.close(); } From a64b4702b5eae2529a1d01dfb572dfb0039f219e Mon Sep 17 00:00:00 2001 From: MuhammadAbdullahIqbal23 <112970908+MuhammadAbdullahIqbal23@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:14:51 +0500 Subject: [PATCH 6/8] refactor: comment out fullSentence assignment to allow only translated text to display --- lib/pages/main_page.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index c9ae083..9b2f356 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -149,15 +149,15 @@ class TranslationAppState extends State { } // Load full sentence mode preference - final savedFullSentenceMode = await StorageService.getFullSentenceMode(); - if (mounted) { - setState(() { - fullSentence = savedFullSentenceMode; - if (kDebugMode) { - print("FULL SENTENCE MODE: $fullSentence"); - } - }); - } + // final savedFullSentenceMode = await StorageService.getFullSentenceMode(); + // if (mounted) { + // setState(() { + // fullSentence = savedFullSentenceMode; + // if (kDebugMode) { + // print("FULL SENTENCE MODE: $fullSentence"); + // } + // }); + // } // Load show original text preference - Default to true if not set final savedShowOriginalText = await StorageService.getShowOriginalText(); From 6c7e88f934d01c974d269f6f350ea9c7f0963c14 Mon Sep 17 00:00:00 2001 From: MuhammadAbdullahIqbal23 <112970908+MuhammadAbdullahIqbal23@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:38:29 +0500 Subject: [PATCH 7/8] feat: integrate flutter_spinkit for connection status indicators and enhance text processing with spoken language support --- lib/pages/main_page.dart | 628 ++++++++++++++++++++++++++++- lib/services/response_handler.dart | 9 +- lib/services/storage_service.dart | 10 + pubspec.lock | 8 + pubspec.yaml | 1 + 5 files changed, 636 insertions(+), 20 deletions(-) diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 9b2f356..b1e7be4 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -6,6 +6,7 @@ import 'package:audio_recorder/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_sfsymbols/flutter_sfsymbols.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'dart:async'; import 'dart:typed_data'; @@ -36,6 +37,10 @@ class TranslationAppState extends State { double heightTop = 100; double heightBottom = 100; + // Connection state variables + bool isConnecting = false; + bool isListening = false; + // Add fullSentence feature bool fullSentence = true; List translatedSentences = []; @@ -43,7 +48,9 @@ class TranslationAppState extends State { // Add original transcript tracking String originalText = ''; + String spokenText = ''; // Add spoken language text variable bool showOriginalText = true; // Toggle to show/hide original text + bool showSpokenText = true; // Toggle to show/hide spoken language text String serverUrl = WebSocketConfig.serverUrl; String? userID = 'ronaldo'; // Changed to nullable @@ -170,6 +177,17 @@ class TranslationAppState extends State { }); } + // Load show spoken text preference - Default to true if not set + final savedShowSpokenText = await StorageService.getShowSpokenText(); + if (mounted) { + setState(() { + showSpokenText = savedShowSpokenText; + if (kDebugMode) { + print("SHOW SPOKEN TEXT: $showSpokenText"); + } + }); + } + // Ensure the language preferences are saved await StorageService.saveBottomLanguagePreference(bottomLanguage); await StorageService.saveTopLanguagePreference(topLanguage); @@ -181,6 +199,14 @@ class TranslationAppState extends State { }); await StorageService.saveShowOriginalText(true); } + + // Ensure the spoken text display is enabled by default + if (showSpokenText == false) { + setState(() { + showSpokenText = true; + }); + await StorageService.saveShowSpokenText(true); + } } @override @@ -285,13 +311,13 @@ class TranslationAppState extends State { ), ), - // Error overlay + // Enhanced Error overlay if (_showErrorOverlay && _websocketErrorMessage != null) Positioned( - top: 50, - left: 20, - right: 20, - child: ErrorLogger.errorOverlay( + top: 60, + left: 0, + right: 0, + child: enhancedErrorOverlay( errorMessage: _websocketErrorMessage!, onDismiss: () { setState(() { @@ -301,7 +327,7 @@ class TranslationAppState extends State { technicalError: _technicalErrorDetails, onShowDetails: _technicalErrorDetails != null ? () { - ErrorLogger.showTechnicalErrorDialog( + showEnhancedTechnicalErrorDialog( context, _technicalErrorDetails); } : null, @@ -354,19 +380,54 @@ class TranslationAppState extends State { } } - if (isRecording) { - _toggleRecording(); + // Start connecting when section is expanded + if (isExpandedTop || isExpandedBottom) { + _startConnectionProcess(); } - _toggleRecording(); }); } - void _processText(String text, String? original) { - // translatedSentences.add(text); + void _startConnectionProcess() async { + setState(() { + isConnecting = true; + isListening = false; + }); + // Initialize audio engine and connect WebSocket + if (!isInitialized) { + await createAudioEngine(recorderEnabled: true); + } + if (!isWebSocketConnected) { + final connected = await _connectWebSocket(); + if (!connected) { + setState(() { + isConnecting = false; + }); + return; + } + } + + // Connection successful, now start listening + setState(() { + isConnecting = false; + isListening = true; + }); + + // Start recording + if (!isRecording) { + _toggleRecording(); + } + } + + void _processText(String text, String? spoken, String? original) { // Only log in debug mode - if (kDebugMode && original != null) { - print('Original text: $original'); + if (kDebugMode) { + if (original != null) { + print('Original text: $original'); + } + if (spoken != null) { + print('Spoken language text: $spoken'); + } print('Translated text: $text'); } @@ -374,6 +435,11 @@ class TranslationAppState extends State { if (fullSentence) { translatedText = text; // Replace with full sentence + // Set spoken text when available + if (spoken != null && spoken.isNotEmpty) { + spokenText = spoken; + } + // Set original text when available if (original != null && original.isNotEmpty) { originalText = original; @@ -381,6 +447,11 @@ class TranslationAppState extends State { } else { translatedText += '$text '; // Append text as before + // Also append spoken text if available + if (spoken != null && spoken.isNotEmpty) { + spokenText += '$spoken '; + } + // Also append original text if available if (original != null && original.isNotEmpty) { originalText += '$original '; @@ -393,6 +464,7 @@ class TranslationAppState extends State { setState(() { translatedText = ''; originalText = ''; + spokenText = ''; translatedSentences = []; }); } @@ -402,6 +474,13 @@ class TranslationAppState extends State { heightTop = MediaQuery.of(context).size.height * 0.5; isExpandedTop = false; isExpandedBottom = false; + + // Reset connection states + setState(() { + isConnecting = false; + isListening = false; + }); + _resetTexts(); if (isRecording) { _toggleRecording(); @@ -463,7 +542,36 @@ class TranslationAppState extends State { Widget _textDisplayTop() { return Column( children: [ - const SizedBox(height: 80), // Add space at the top for better centering + const SizedBox(height: 80), + + // Connection/Listening Status + if (isConnecting || isListening) ...[ + SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SpinKitPulse( + color: Colors.black, + size: 20.0, + ), + const SizedBox(width: 12), + Text( + isConnecting ? 'Connecting...' : 'I am listening...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + ], + // Translated text with bold styling centered in the top half Expanded( flex: 3, @@ -484,6 +592,32 @@ class TranslationAppState extends State { textAlign: TextAlign.center, ), ), + // Spoken language text with medium opacity + if (showSpokenText && spokenText.isNotEmpty) ...[ + const SizedBox(height: 16), + AnimatedSwitcher( + duration: textAnimationDuration, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.02), + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + "Spoken: \"$spokenText\"", + key: ValueKey(spokenText), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.black.withValues(alpha: 0.6), + letterSpacing: 0.2, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], // Original text underneath with less opacity if (showOriginalText && originalText.isNotEmpty) ...[ const SizedBox(height: 16), @@ -555,6 +689,35 @@ class TranslationAppState extends State { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w300, color: Colors.black), ), + + // Connection/Listening Status + if (isConnecting || isListening) ...[ + const SizedBox(height: 20), + SingleChildScrollView( + child: Container( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SpinKitPulse( + color: Colors.black, + size: 20.0, + ), + const SizedBox(width: 12), + Text( + isConnecting ? 'Connecting...' : 'I am listening...', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + ], + ), + ), + ), + ], + // Main content area with both translated and original text Expanded( flex: 3, @@ -577,6 +740,32 @@ class TranslationAppState extends State { textAlign: TextAlign.center, ), ), + // Spoken language text with medium opacity + if (showSpokenText && spokenText.isNotEmpty) ...[ + const SizedBox(height: 16), + AnimatedSwitcher( + duration: textAnimationDuration, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, vertical: 8.0), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.02), + borderRadius: BorderRadius.circular(8.0), + ), + child: Text( + "Spoken: \"$spokenText\"", + key: ValueKey(spokenText), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.black.withValues(alpha: 0.6), + letterSpacing: 0.2, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], // Original text underneath with less opacity if (showOriginalText && originalText.isNotEmpty) ...[ const SizedBox(height: 16), @@ -678,7 +867,7 @@ class TranslationAppState extends State { ); } - // Fixed version of _showLanguageSelector method +// Fixed version of _showLanguageSelector method with debug logs shown on screen void _showLanguageSelector(bool isTop) { showModalBottomSheet( context: context, @@ -758,6 +947,21 @@ class TranslationAppState extends State { print( "SAVING TOP LANGUAGE PREFERENCE: $langCode (was: $oldLanguage)"); } + + // Show debug log on screen + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Saving top language preference: $langCode (was: $oldLanguage)', + style: TextStyle(fontSize: 12), + ), + duration: Duration(seconds: 2), + backgroundColor: Colors.blue.withOpacity(0.8), + ), + ); + } + await StorageService.saveTopLanguagePreference( langCode); @@ -768,6 +972,20 @@ class TranslationAppState extends State { print("VERIFIED SAVED TOP LANGUAGE: $savedTopLang"); } + // Show verification log on screen + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Verified saved top language: $savedTopLang', + style: TextStyle(fontSize: 12), + ), + duration: Duration(seconds: 2), + backgroundColor: Colors.green.withOpacity(0.8), + ), + ); + } + // Update current language if top section is active if (isExpandedTop && Languages.languages.containsKey(langCode)) { @@ -776,6 +994,21 @@ class TranslationAppState extends State { print( "UPDATED CURRENT LANGUAGE TO: ${currentLanguage.name} (TOP ACTIVE)"); } + + // Show current language update log on screen + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Updated current language to: ${currentLanguage.name} (TOP ACTIVE)', + style: TextStyle(fontSize: 12), + ), + duration: Duration(seconds: 2), + backgroundColor: + Colors.purple.withOpacity(0.8), + ), + ); + } } } else { setState(() { @@ -787,6 +1020,21 @@ class TranslationAppState extends State { print( "SAVING BOTTOM LANGUAGE PREFERENCE: $langCode (was: $oldLanguage)"); } + + // Show debug log on screen + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Saving bottom language preference: $langCode (was: $oldLanguage)', + style: TextStyle(fontSize: 12), + ), + duration: Duration(seconds: 2), + backgroundColor: Colors.blue.withOpacity(0.8), + ), + ); + } + await StorageService.saveBottomLanguagePreference( langCode); @@ -797,6 +1045,20 @@ class TranslationAppState extends State { print("VERIFIED SAVED LANGUAGE: $savedLang"); } + // Show verification log on screen + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Verified saved bottom language: $savedLang', + style: TextStyle(fontSize: 12), + ), + duration: Duration(seconds: 2), + backgroundColor: Colors.green.withOpacity(0.8), + ), + ); + } + // Update current language if bottom section is active if (isExpandedBottom && Languages.languages.containsKey(langCode)) { @@ -805,6 +1067,21 @@ class TranslationAppState extends State { print( "UPDATED CURRENT LANGUAGE TO: ${currentLanguage.name} (BOTTOM ACTIVE)"); } + + // Show current language update log on screen + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Updated current language to: ${currentLanguage.name} (BOTTOM ACTIVE)', + style: TextStyle(fontSize: 12), + ), + duration: Duration(seconds: 2), + backgroundColor: + Colors.purple.withOpacity(0.8), + ), + ); + } } } Navigator.pop(context); @@ -1010,6 +1287,296 @@ class TranslationAppState extends State { } } + // Enhanced error overlay widget with better styling + Widget enhancedErrorOverlay({ + required String errorMessage, + required VoidCallback onDismiss, + dynamic technicalError, + VoidCallback? onShowDetails, + }) { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.red.shade200, width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with error icon and title + Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.red.shade600, + size: 24, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Connection Error', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.red.shade800, + ), + ), + ), + IconButton( + icon: Icon(Icons.close, color: Colors.red.shade600), + onPressed: onDismiss, + splashRadius: 20, + ), + ], + ), + const SizedBox(height: 12), + + // Error message + Text( + errorMessage, + style: TextStyle( + fontSize: 14, + color: Colors.red.shade700, + height: 1.4, + ), + ), + + // Technical details button if available + if (technicalError != null && onShowDetails != null) ...[ + const SizedBox(height: 16), + Row( + children: [ + const Spacer(), + TextButton.icon( + onPressed: onShowDetails, + icon: Icon( + Icons.info_outline, + size: 16, + color: Colors.red.shade600, + ), + label: Text( + 'Technical Details', + style: TextStyle( + color: Colors.red.shade600, + fontWeight: FontWeight.w500, + ), + ), + style: TextButton.styleFrom( + backgroundColor: Colors.red.shade50, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: BorderSide(color: Colors.red.shade200), + ), + ), + ), + ], + ), + ], + ], + ), + ); + } + + // Enhanced technical error dialog + void showEnhancedTechnicalErrorDialog( + BuildContext context, dynamic technicalError) { + String errorDetails = ''; + String errorType = 'Unknown Error'; + + if (technicalError is WebSocketError) { + errorType = technicalError.type.toString().split('.').last; + errorDetails = ''' +Error Type: ${technicalError.type.toString().split('.').last} +Message: ${technicalError.message} +'''; + } else if (technicalError is Exception) { + errorType = technicalError.runtimeType.toString(); + errorDetails = technicalError.toString(); + } else { + errorDetails = technicalError.toString(); + } + + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + maxWidth: MediaQuery.of(context).size.width * 0.9, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( + children: [ + Icon( + Icons.bug_report, + color: Colors.red.shade600, + size: 28, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Technical Error Details', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.red.shade800, + ), + ), + const SizedBox(height: 4), + Text( + errorType, + style: TextStyle( + fontSize: 14, + color: Colors.red.shade600, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + ), + + // Content + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.code, + color: Colors.grey.shade600, + size: 16, + ), + const SizedBox(width: 8), + Text( + 'Error Information', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + ], + ), + const SizedBox(height: 12), + SelectableText( + errorDetails, + style: TextStyle( + fontSize: 13, + fontFamily: 'monospace', + color: Colors.grey.shade800, + height: 1.4, + ), + ), + ], + ), + ), + ), + ), + + // Actions + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.grey.shade50, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () { + Clipboard.setData(ClipboardData(text: errorDetails)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text( + 'Error details copied to clipboard'), + backgroundColor: Colors.green.shade600, + duration: const Duration(seconds: 2), + ), + ); + }, + icon: Icon( + Icons.copy, + size: 16, + color: Colors.grey.shade600, + ), + label: Text( + 'Copy', + style: TextStyle(color: Colors.grey.shade600), + ), + ), + const SizedBox(width: 12), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red.shade600, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text('Close'), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + void _showError(String errorMessage, {Duration duration = const Duration(seconds: 5), ErrorSeverity severity = ErrorSeverity.medium, @@ -1093,11 +1660,12 @@ class TranslationAppState extends State { _websocketService?.sendMessage(userID ?? 'ronaldo'); _websocketService?.startListening((message) { - ResponseHandler.handleReponse(message, (message, originalText) { + ResponseHandler.handleReponse(message, + (message, spokenText, originalText) { if (kDebugMode) { print('Message from Server: $message'); } - _processText(message, originalText); + _processText(message, spokenText, originalText); }, (audioData) { _previewData?.add(audioData); audioEngine?.queueChunk(audioData); @@ -1235,6 +1803,32 @@ class TranslationAppState extends State { ); }, ), + SwitchListTile( + title: Text("Show Spoken Language Text"), + subtitle: Text("Display text in the spoken language"), + value: showSpokenText, + onChanged: (value) async { + setState(() { + showSpokenText = value; + }); + + // Save the preference + await StorageService.saveShowSpokenText(value); + if (kDebugMode) { + print("SAVED SHOW SPOKEN TEXT: $value"); + } + + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(value + ? 'Spoken language text display enabled' + : 'Spoken language text display disabled'), + duration: Duration(seconds: 2), + ), + ); + }, + ), const SizedBox(height: 20), ElevatedButton( onPressed: () async { diff --git a/lib/services/response_handler.dart b/lib/services/response_handler.dart index 38d8a72..1f85864 100644 --- a/lib/services/response_handler.dart +++ b/lib/services/response_handler.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; class ResponseHandler { static handleReponse( dynamic message, - Function(String, String?) onTextReceived, + Function(String, String?, String?) onTextReceived, Function(Uint8List) onAudioReceived) { if (message is String) { // Handle JSON messages @@ -14,14 +14,17 @@ class ResponseHandler { // Handle translated text if (kDebugMode) { print("Translation: ${response['text']}"); + if (response['spoken_text'] != null) { + print("Spoken text: ${response['spoken_text']}"); + } } - onTextReceived(response['text'], null); + onTextReceived(response['text'], response['spoken_text'], null); } else if (response['type'] == 'fullSentence') { // Handle original transcribed text if (kDebugMode) { print("Original text: ${response['text']}"); } - onTextReceived(response['text'], response['text']); + onTextReceived(response['text'], null, response['text']); } else if (response['type'] == 'audio') { Float32List preprocessedAudioData = Float32List.fromList(response['audio_data']); diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 2d96c84..8129576 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -19,6 +19,7 @@ class StorageService { // Constants for app settings static const _fullSentenceModeKey = 'full_sentence_mode'; static const _showOriginalTextKey = 'show_original_text'; + static const _showSpokenTextKey = 'show_spoken_text'; static Future saveToken( String username, @@ -128,4 +129,13 @@ class StorageService { final value = await _storage.read(key: _showOriginalTextKey); return value != 'false'; // Default to true if not set } + + static Future saveShowSpokenText(bool enabled) async { + await _storage.write(key: _showSpokenTextKey, value: enabled.toString()); + } + + static Future getShowSpokenText() async { + final value = await _storage.read(key: _showSpokenTextKey); + return value != 'false'; // Default to true if not set + } } diff --git a/pubspec.lock b/pubspec.lock index 329f4f4..03a4d4f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -254,6 +254,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.28.0" + flutter_spinkit: + dependency: "direct main" + description: + name: flutter_spinkit + sha256: d2696eed13732831414595b98863260e33e8882fc069ee80ec35d4ac9ddb0472 + url: "https://pub.dev" + source: hosted + version: "5.2.1" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 1c5e65f..0838d66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: audioplayers_darwin: ^6.1.0 provider: ^6.1.2 flutter_sfsymbols: ^2.0.0 + flutter_spinkit: ^5.2.0 # The following adds the Cupertino Icons font to your application. From 3bff5a5e0c36ecebb4b67b51ea02b6732071e36b Mon Sep 17 00:00:00 2001 From: MuhammadAbdullahIqbal23 <112970908+MuhammadAbdullahIqbal23@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:09:24 +0500 Subject: [PATCH 8/8] fix: increase WebSocket connection timeout duration to prevent premature disconnections --- lib/services/websocket_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart index f17acef..26ce013 100644 --- a/lib/services/websocket_service.dart +++ b/lib/services/websocket_service.dart @@ -45,7 +45,7 @@ class WebsocketService { try { // Add timeout to prevent hanging await _channel!.ready.timeout( - const Duration(seconds: 30), + const Duration(seconds: 365000), onTimeout: () { lastError = WebSocketError(WebSocketErrorType.connectionTimeout, 'Connection timed out after 30 seconds');