From 94129cf274aab1e2e770792d0e877779298868a7 Mon Sep 17 00:00:00 2001 From: vainiovesa Date: Tue, 10 Mar 2026 14:00:46 +0200 Subject: [PATCH 01/13] feat: add mute button --- lib/screens/landing_screen.dart | 60 ++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/lib/screens/landing_screen.dart b/lib/screens/landing_screen.dart index c788fa8..3339593 100644 --- a/lib/screens/landing_screen.dart +++ b/lib/screens/landing_screen.dart @@ -35,6 +35,7 @@ class _LandingScreenState extends State { late final PhoneAudioService _phoneAudio; bool _usePhoneMic = false; + bool _isMuted = false; final ValueNotifier _isRecording = ValueNotifier(false); final List _displayedSentences = []; @@ -415,12 +416,61 @@ class _LandingScreenState extends State { const SizedBox(width: 14), - // Recordings placeholder + // Mute button Expanded( - child: LandingTile( - icon: Icons.play_circle_outline, - label: 'Recordings', - onTap: () {}, + child: InkWell( + onTap: () { + setState(() { + _isMuted = !_isMuted; + }); + }, + child: Container( + height: 72, + padding: + const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: _isMuted + ? Colors.orange + .withAlpha((0.15 * 255).round()) + : Colors.transparent, + border: Border.all( + color: _isMuted + ? Colors.orange + : Colors.black12, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _isMuted + ? Icons.comments_disabled_outlined + : Icons.comment_outlined, + size: 22, + color: _isMuted + ? Colors.orange + : Colors.grey[700], + ), + const SizedBox(width: 10), + Expanded( + child: Text( + _isMuted ? 'Unmute display' : 'Mute display', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: _isMuted + ? FontWeight.bold + : FontWeight.normal, + color: _isMuted + ? Colors.orange + : Colors.grey[800], + ), + ), + ), + ], + ), + ), ), ), ], From 83396265fa5aa78aae825593b6389c8324fcef9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:03:22 +0200 Subject: [PATCH 02/13] fix: update mic icon and text for clarity --- lib/screens/landing_screen.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/screens/landing_screen.dart b/lib/screens/landing_screen.dart index 3339593..9e7e26f 100644 --- a/lib/screens/landing_screen.dart +++ b/lib/screens/landing_screen.dart @@ -310,7 +310,7 @@ class _LandingScreenState extends State { children: [ _usePhoneMic ? const Icon( - Icons.mic, + Icons.phone_android, size: 22, color: Colors.lightGreen, ) @@ -323,8 +323,8 @@ class _LandingScreenState extends State { Expanded( child: Text( _usePhoneMic - ? 'Phone mic\n(Active)' - : 'Glasses mic\n(Active)', + ? 'Switch to glasses mic' + : 'Switch to phone mic', textAlign: TextAlign.center, style: TextStyle( fontSize: 13, From 267ea3f8882469752782078708c623beefa59aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:40:39 +0200 Subject: [PATCH 03/13] feat: add functionality for display mute Co-authored-by: vainiovesa --- lib/screens/landing_screen.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/screens/landing_screen.dart b/lib/screens/landing_screen.dart index 9e7e26f..e87446b 100644 --- a/lib/screens/landing_screen.dart +++ b/lib/screens/landing_screen.dart @@ -84,9 +84,13 @@ class _LandingScreenState extends State { debugPrint(aiResponse); - debugPrint("→ Adding to display: '$aiResponse'"); - if (_manager.isConnected && _manager.transcription.isActive.value) { - _addSentenceToDisplay(aiResponse); + if (!_isMuted) { + debugPrint("→ Adding to display: '$aiResponse'"); + if (_manager.isConnected && _manager.transcription.isActive.value) { + _addSentenceToDisplay(aiResponse); + } + } else { + debugPrint("→ Display is muted, skipping display update: '$aiResponse'"); } } From e15e043202096b10f6acff26e93c13cba5ecf2ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:42:22 +0200 Subject: [PATCH 04/13] fix: edit ios files --- ios/Podfile.lock | 42 ++++++ ios/Runner.xcodeproj/project.pbxproj | 130 ++++++++++++++++++ .../contents.xcworkspacedata | 3 + 3 files changed, 175 insertions(+) create mode 100644 ios/Podfile.lock diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..ce5377c --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,42 @@ +PODS: + - Flutter (1.0.0) + - flutter_blue_plus_darwin (0.0.2): + - Flutter + - FlutterMacOS + - flutter_sound (9.30.0): + - Flutter + - flutter_sound_core (= 9.30.0) + - flutter_sound_core (9.30.0) + - permission_handler_apple (9.3.0): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - flutter_sound (from `.symlinks/plugins/flutter_sound/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + +SPEC REPOS: + trunk: + - flutter_sound_core + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_blue_plus_darwin: + :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin" + flutter_sound: + :path: ".symlinks/plugins/flutter_sound/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + +SPEC CHECKSUMS: + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 + flutter_sound: d95194f6476c9ad211d22b3a414d852c12c7ca44 + flutter_sound_core: 7f2626d249d3a57bfa6da892ef7e22d234482c1a + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1d1b77b..00afd12 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 11042EE6E6B7D9E6B3203B5F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 384BAE8F142FA9F916740E80 /* Pods_Runner.framework */; }; + 13B07DAAD2277EC02485444C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C7762E6CD773CA6A6329EA96 /* Pods_RunnerTests.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -44,10 +46,15 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 384BAE8F142FA9F916740E80 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4AF0703E326EC888C79A6934 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 6F703DB2DF8D00352F539BEF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 75606A58A2724B2236273247 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 80B8CDD639846BDA7DEBA175 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,13 +62,25 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C7762E6CD773CA6A6329EA96 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E3CB0A10B61A6DA72D29435D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + F85674E7919C9C6910F5C52A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 1C1F3D585EBC7A9D5F841A4B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07DAAD2277EC02485444C /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 11042EE6E6B7D9E6B3203B5F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,29 @@ path = RunnerTests; sourceTree = ""; }; + 3A67259495ADFC50EA787FF3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 384BAE8F142FA9F916740E80 /* Pods_Runner.framework */, + C7762E6CD773CA6A6329EA96 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 820C0A71E31DDDCBE4781028 /* Pods */ = { + isa = PBXGroup; + children = ( + 80B8CDD639846BDA7DEBA175 /* Pods-Runner.debug.xcconfig */, + 4AF0703E326EC888C79A6934 /* Pods-Runner.release.xcconfig */, + 75606A58A2724B2236273247 /* Pods-Runner.profile.xcconfig */, + E3CB0A10B61A6DA72D29435D /* Pods-RunnerTests.debug.xcconfig */, + F85674E7919C9C6910F5C52A /* Pods-RunnerTests.release.xcconfig */, + 6F703DB2DF8D00352F539BEF /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +136,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 820C0A71E31DDDCBE4781028 /* Pods */, + 3A67259495ADFC50EA787FF3 /* Frameworks */, ); sourceTree = ""; }; @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 30194C4A061055FA00E6E695 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 1C1F3D585EBC7A9D5F841A4B /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 12062EAA91003871FC9F02AB /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E7F491FBE75B21AAC1960BCF /* [CP] Embed Pods Frameworks */, + 5930E5E9A4A35EF8EAF80E24 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -222,6 +271,50 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 12062EAA91003871FC9F02AB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 30194C4A061055FA00E6E695 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +331,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 5930E5E9A4A35EF8EAF80E24 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +363,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + E7F491FBE75B21AAC1960BCF /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -378,6 +505,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E3CB0A10B61A6DA72D29435D /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -395,6 +523,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = F85674E7919C9C6910F5C52A /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -410,6 +539,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 6F703DB2DF8D00352F539BEF /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + From 528658013b2100afdf07385deb0ec6a2d07bc6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:57:43 +0200 Subject: [PATCH 05/13] fix: tests --- test/widget_test.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/widget_test.dart b/test/widget_test.dart index 5003a35..d023755 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -31,13 +31,16 @@ void main() { await tester.pump(const Duration(milliseconds: 600)); } - testWidgets('App shows text input and send button', - (WidgetTester tester) async { + testWidgets('Landing screen shows key elements', (WidgetTester tester) async { await pumpLanding(tester); expect(find.text('Even realities G1 smart glasses'), findsOneWidget); - expect(find.text('Recordings'), findsOneWidget); - expect(find.text('Even realities G1 smart glasses'), findsOneWidget); + expect(find.text('Connect to glasses'), findsOneWidget); + expect(find.text('Switch to phone mic'), findsOneWidget); + expect(find.text('Start\nRecording'), findsOneWidget); + expect(find.text('Mute display'), findsOneWidget); + expect(find.text('Sign in'), findsOneWidget); + expect(find.text('Register'), findsOneWidget); await disposeLanding(tester); }); From 2f3746325d202337d1e831dfcfc8d00f5794b84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:10:32 +0200 Subject: [PATCH 06/13] feat: add functionality for blocking pressing buttons when not supposed to be pressed --- lib/screens/landing_screen.dart | 405 +++++++++++++++++++------------- 1 file changed, 246 insertions(+), 159 deletions(-) diff --git a/lib/screens/landing_screen.dart b/lib/screens/landing_screen.dart index e87446b..ab7db22 100644 --- a/lib/screens/landing_screen.dart +++ b/lib/screens/landing_screen.dart @@ -37,6 +37,7 @@ class _LandingScreenState extends State { bool _usePhoneMic = false; bool _isMuted = false; final ValueNotifier _isRecording = ValueNotifier(false); + final ValueNotifier _isRecordingBusy = ValueNotifier(false); final List _displayedSentences = []; static const int _maxDisplayedSentences = 4; @@ -72,6 +73,7 @@ class _LandingScreenState extends State { void dispose() { _ws.aiResponse.removeListener(_onAiResponse); _isRecording.dispose(); + _isRecordingBusy.dispose(); _audioPipeline.dispose(); _phoneAudio.dispose(); _ws.dispose(); @@ -124,63 +126,76 @@ class _LandingScreenState extends State { /// Begin a transcription session Future _startTranscription() async { - if (_manager.isConnected) { - //glasses implementation - await _manager.transcription.stop(); // pakota clean stop ensin - await Future.delayed(const Duration(milliseconds: 300)); - _ws.clearCommittedText(); // reset accumulated text — backend starts fresh too - _clearDisplayQueue(); - - await _ws.startAudioStream(); - await _manager.transcription.start(); - - if (_usePhoneMic) { - await _phoneAudio.start((pcm) { - if (_ws.connected.value) { - _ws.sendAudio(pcm); - } - }); + if (_isRecordingBusy.value) return; + if (!_usePhoneMic && !_manager.isConnected) return; + _isRecordingBusy.value = true; + try { + if (_manager.isConnected) { + //glasses implementation + await _manager.transcription.stop(); // pakota clean stop ensin + await Future.delayed(const Duration(milliseconds: 300)); + _ws.clearCommittedText(); // reset accumulated text — backend starts fresh too + _clearDisplayQueue(); + + await _ws.startAudioStream(); + await _manager.transcription.start(); + + if (_usePhoneMic) { + await _phoneAudio.start((pcm) { + if (_ws.connected.value) { + _ws.sendAudio(pcm); + } + }); + } else { + await _manager.microphone.enable(); + _audioPipeline.addListenerToMicrophone(); + } + + await _manager.transcription.displayText('Recording started.'); + debugPrint("Transcription (re)started"); } else { - await _manager.microphone.enable(); - _audioPipeline.addListenerToMicrophone(); + //wo glasses + _ws.clearCommittedText(); // reset accumulated text — backend starts fresh too + _clearDisplayQueue(); + await _ws.startAudioStream(); + await _phoneAudio.start( + (pcm) { + if (_ws.connected.value) _ws.sendAudio(pcm); + }, + ); } - - await _manager.transcription.displayText('Recording started.'); - debugPrint("Transcription (re)started"); - } else { - //wo glasses - _ws.clearCommittedText(); // reset accumulated text — backend starts fresh too - _clearDisplayQueue(); - await _ws.startAudioStream(); - await _phoneAudio.start( - (pcm) { - if (_ws.connected.value) _ws.sendAudio(pcm); - }, - ); + _isRecording.value = true; + } finally { + _isRecordingBusy.value = false; } - _isRecording.value = true; } /// End a transcription session Future _stopTranscription() async { + if (_isRecordingBusy.value) return; + _isRecordingBusy.value = true; _isRecording.value = false; - if (_manager.isConnected) { - _clearDisplayQueue(); - await _manager.transcription.displayText('Recording stopped.'); - await Future.delayed(const Duration(seconds: 2)); - if (_usePhoneMic) { - await _phoneAudio.stop(); + try { + if (_manager.isConnected) { + _clearDisplayQueue(); + await _manager.transcription.displayText('Recording stopped.'); + await Future.delayed(const Duration(seconds: 2)); + if (_usePhoneMic) { + await _phoneAudio.stop(); + } else { + await _manager.microphone.disable(); + await _audioPipeline.stop(); + } + // lisätty jotta paketit kerkiävät lähteä ennen sulkemista + await Future.delayed(const Duration(milliseconds: 200)); + await _ws.stopAudioStream(); + await _manager.transcription.stop(); } else { - await _manager.microphone.disable(); - await _audioPipeline.stop(); + await _phoneAudio.stop(); + await _ws.stopAudioStream(); } - // lisätty jotta paketit kerkiävät lähteä ennen sulkemista - await Future.delayed(const Duration(milliseconds: 200)); - await _ws.stopAudioStream(); - await _manager.transcription.stop(); - } else { - await _phoneAudio.stop(); - await _ws.stopAudioStream(); + } finally { + _isRecordingBusy.value = false; } } @@ -287,63 +302,95 @@ class _LandingScreenState extends State { // Mic toggle Expanded( - child: InkWell( - onTap: () { - setState(() { - _usePhoneMic = !_usePhoneMic; - }); - }, - child: Container( - height: 72, - padding: - const EdgeInsets.symmetric(horizontal: 14), - decoration: BoxDecoration( - color: _usePhoneMic - ? Colors.lightGreen - .withAlpha((0.15 * 255).round()) - : Colors.transparent, - border: Border.all( - color: _usePhoneMic + child: ListenableBuilder( + listenable: Listenable.merge( + [_isRecording, _isRecordingBusy]), + builder: (context, _) { + final isLocked = + _isRecording.value || _isRecordingBusy.value; + + final borderColor = isLocked + ? Colors.black26 + : (_usePhoneMic ? Colors.lightGreen - : Colors.black12, - ), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _usePhoneMic - ? const Icon( - Icons.phone_android, - size: 22, - color: Colors.lightGreen, - ) - : Image.asset( - 'assets/images/g1-smart-glasses.webp', - height: 22, - fit: BoxFit.contain, + : Colors.black12); + final backgroundColor = isLocked + ? Colors.black.withAlpha((0.04 * 255).round()) + : (_usePhoneMic + ? Colors.lightGreen + .withAlpha((0.15 * 255).round()) + : Colors.transparent); + + final textColor = isLocked + ? Colors.black38 + : (_usePhoneMic + ? Colors.lightGreen + : Colors.black); + + return Opacity( + opacity: isLocked ? 0.55 : 1, + child: InkWell( + onTap: isLocked + ? null + : () { + setState(() { + _usePhoneMic = !_usePhoneMic; + }); + }, + child: Container( + height: 72, + padding: const EdgeInsets.symmetric( + horizontal: 14), + decoration: BoxDecoration( + color: backgroundColor, + border: Border.all(color: borderColor), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + _usePhoneMic + ? Icon( + Icons.phone_android, + size: 22, + color: isLocked + ? Colors.black38 + : Colors.lightGreen, + ) + : Image.asset( + 'assets/images/g1-smart-glasses.webp', + height: 22, + fit: BoxFit.contain, + color: isLocked + ? Colors.black38 + : null, + colorBlendMode: isLocked + ? BlendMode.srcIn + : null, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + _usePhoneMic + ? 'Switch to glasses mic' + : 'Switch to phone mic', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: _usePhoneMic + ? FontWeight.bold + : FontWeight.normal, + color: textColor, + ), + ), ), - const SizedBox(width: 10), - Expanded( - child: Text( - _usePhoneMic - ? 'Switch to glasses mic' - : 'Switch to phone mic', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13, - fontWeight: _usePhoneMic - ? FontWeight.bold - : FontWeight.normal, - color: _usePhoneMic - ? Colors.lightGreen - : Colors.black, - ), + ], ), ), - ], - ), - ), + ), + ); + }, ), ), ], @@ -355,64 +402,103 @@ class _LandingScreenState extends State { children: [ // Start / Stop recording Expanded( - child: ValueListenableBuilder( - valueListenable: _isRecording, - builder: (context, isRecording, _) { - return InkWell( - onTap: () async { - if (!isRecording) { - await _startTranscription(); - } else { - await _stopTranscription(); - } - }, - child: Container( - height: 72, - padding: const EdgeInsets.symmetric( - horizontal: 14), - decoration: BoxDecoration( - color: isRecording - ? Colors.red - .withAlpha((0.15 * 255).round()) - : Colors.transparent, - border: Border.all( - color: isRecording + child: StreamBuilder( + stream: _manager.connectionState, + initialData: G1ConnectionEvent( + state: _manager.isConnected + ? G1ConnectionState.connected + : G1ConnectionState.disconnected, + ), + builder: (context, snapshot) { + final isGlassesConnected = snapshot.data?.state == + G1ConnectionState.connected; + + return ListenableBuilder( + listenable: Listenable.merge( + [_isRecording, _isRecordingBusy]), + builder: (context, _) { + final isRecording = _isRecording.value; + final isBusy = _isRecordingBusy.value; + + final canStart = _usePhoneMic || + isGlassesConnected == true; + + final isDisabled = + isBusy || (!isRecording && !canStart); + + final borderColor = isDisabled + ? Colors.black26 + : (isRecording ? Colors.red - : Colors.black12, - ), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - isRecording - ? Icons.stop_circle_outlined - : Icons.fiber_manual_record, - size: 22, - color: isRecording - ? Colors.red - : Colors.grey[800], - ), - const SizedBox(width: 10), - Expanded( - child: Text( - isRecording - ? 'Stop\nRecording' - : 'Start\nRecording', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - color: isRecording - ? Colors.red - : Colors.grey[800], - ), + : Colors.black12); + final backgroundColor = isDisabled + ? Colors.black + .withAlpha((0.04 * 255).round()) + : (isRecording + ? Colors.red + .withAlpha((0.15 * 255).round()) + : Colors.transparent); + + final foregroundColor = isDisabled + ? Colors.black38 + : (isRecording + ? Colors.red + : Colors.grey[800]); + + return Opacity( + opacity: isDisabled ? 0.55 : 1, + child: InkWell( + onTap: isDisabled + ? null + : () async { + if (!isRecording) { + await _startTranscription(); + } else { + await _stopTranscription(); + } + }, + child: Container( + height: 72, + padding: const EdgeInsets.symmetric( + horizontal: 14), + decoration: BoxDecoration( + color: backgroundColor, + border: + Border.all(color: borderColor), + borderRadius: + BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + isRecording + ? Icons.stop_circle_outlined + : Icons.fiber_manual_record, + size: 22, + color: foregroundColor, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + isRecording + ? 'Stop\nRecording' + : 'Start\nRecording', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: foregroundColor, + ), + ), + ), + ], ), ), - ], - ), - ), + ), + ); + }, ); }, ), @@ -438,9 +524,8 @@ class _LandingScreenState extends State { .withAlpha((0.15 * 255).round()) : Colors.transparent, border: Border.all( - color: _isMuted - ? Colors.orange - : Colors.black12, + color: + _isMuted ? Colors.orange : Colors.black12, ), borderRadius: BorderRadius.circular(8), ), @@ -459,7 +544,9 @@ class _LandingScreenState extends State { const SizedBox(width: 10), Expanded( child: Text( - _isMuted ? 'Unmute display' : 'Mute display', + _isMuted + ? 'Unmute display' + : 'Mute display', textAlign: TextAlign.center, style: TextStyle( fontSize: 14, From a8a638f83831e23f5258147825ce79f08e4cd17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:02:34 +0200 Subject: [PATCH 07/13] fix: update package versions in pubspec.lock --- pubspec.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 1ecbb85..1ce19e3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -307,18 +307,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -544,10 +544,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" typed_data: dependency: transitive description: From 5b8fb07feee8fe0f27b9cf9e98300a7a768e18d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:19:37 +0200 Subject: [PATCH 08/13] feat: working IOS build --- ios/Flutter/AppFrameworkInfo.plist | 2 - ios/Runner.xcodeproj/project.pbxproj | 10 ++-- ios/Runner/AppDelegate.swift | 7 ++- ios/Runner/Info.plist | 33 ++++++++-- .../lib/src/bluetooth/g1_glass.dart | 11 ++-- .../lib/src/bluetooth/g1_manager.dart | 60 +++++++++++++++++-- 6 files changed, 102 insertions(+), 21 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7..391a902 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 00afd12..8593100 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -114,7 +114,6 @@ F85674E7919C9C6910F5C52A /* Pods-RunnerTests.release.xcconfig */, 6F703DB2DF8D00352F539BEF /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -489,13 +488,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = H5536DZLJW; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.front; + PRODUCT_BUNDLE_IDENTIFIER = "com.ai-smarties.front"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -671,13 +671,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = H5536DZLJW; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.front; + PRODUCT_BUNDLE_IDENTIFIER = "com.ai-smarties.front"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -693,13 +694,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = H5536DZLJW; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.front; + PRODUCT_BUNDLE_IDENTIFIER = "com.ai-smarties.front"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 6266644..c30b367 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7788352..7f439fd 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,29 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +66,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - + NSBluetoothAlwaysUsageDescription + This app uses Bluetooth to discover and connect to Even Realities G1 glasses. + NSMicrophoneUsageDescription + This app uses the microphone for audio capture and transcription. diff --git a/packages/even_realities_g1/lib/src/bluetooth/g1_glass.dart b/packages/even_realities_g1/lib/src/bluetooth/g1_glass.dart index 2c0a16b..dd501c3 100644 --- a/packages/even_realities_g1/lib/src/bluetooth/g1_glass.dart +++ b/packages/even_realities_g1/lib/src/bluetooth/g1_glass.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -51,10 +52,12 @@ class G1Glass { try { await device.connect(); await _discoverServices(); - await device.requestMtu(BluetoothConstants.defaultMtu); - await device.requestConnectionPriority( - connectionPriorityRequest: ConnectionPriority.high, - ); + if (!kIsWeb && Platform.isAndroid) { + await device.requestMtu(BluetoothConstants.defaultMtu); + await device.requestConnectionPriority( + connectionPriorityRequest: ConnectionPriority.high, + ); + } _startHeartbeat(); debugPrint('[$side Glass] Connected successfully'); } catch (e) { diff --git a/packages/even_realities_g1/lib/src/bluetooth/g1_manager.dart b/packages/even_realities_g1/lib/src/bluetooth/g1_manager.dart index bdfa491..b32fca1 100644 --- a/packages/even_realities_g1/lib/src/bluetooth/g1_manager.dart +++ b/packages/even_realities_g1/lib/src/bluetooth/g1_manager.dart @@ -66,6 +66,7 @@ class G1Manager { Timer? _scanTimer; StreamSubscription? _scanSubscription; bool _isScanning = false; + bool _isConnecting = false; int _retryCount = 0; bool _connectionCallbackFired = false; @@ -166,7 +167,7 @@ class G1Manager { throw Exception(msg); } - final adapterState = await FlutterBluePlus.adapterState.first; + final adapterState = FlutterBluePlus.adapterStateNow; if (adapterState != BluetoothAdapterState.on) { const msg = 'Bluetooth is turned off'; onUpdate?.call(msg); @@ -175,6 +176,7 @@ class G1Manager { // Reset state _isScanning = true; + _isConnecting = false; _retryCount = 0; _connectionCallbackFired = false; _leftGlass = null; @@ -204,7 +206,9 @@ class G1Manager { ) async { try { // Check system connected devices - final connectedDevices = await FlutterBluePlus.systemDevices([]); + final connectedDevices = await FlutterBluePlus.systemDevices([ + Guid(BluetoothConstants.uartServiceUuid), + ]); debugPrint('Found ${connectedDevices.length} system connected devices'); for (final device in connectedDevices) { @@ -381,11 +385,57 @@ class G1Manager { } if (glass != null) { - await glass.connect(); - _setupReconnect(glass); + if (_leftGlass != null && _rightGlass != null && !_isConnecting) { + await _connectDiscoveredGlasses( + onUpdate, + onGlassesFound, + onConnected, + ); + } + } + } + + Future _connectDiscoveredGlasses( + OnStatusUpdate? onUpdate, + OnGlassesFound? onGlassesFound, + OnConnected? onConnected, + ) async { + if (_leftGlass == null || _rightGlass == null || _isConnecting) return; + + _isConnecting = true; + _connectionStateController.add(const G1ConnectionEvent( + state: G1ConnectionState.connecting, + )); + onConnectionChanged?.call(G1ConnectionState.connecting, null); + onUpdate?.call('Connecting to glasses...'); + + try { + if (_isScanning) { + await stopScan(); + } + + if (!_leftGlass!.isConnected) { + await _leftGlass!.connect(); + _setupReconnect(_leftGlass!); + } + + if (!_rightGlass!.isConnected) { + await _rightGlass!.connect(); + _setupReconnect(_rightGlass!); + } - // Check if both glasses are now connected after this connection completes _checkBothConnected(onUpdate, onGlassesFound, onConnected); + } catch (e, st) { + debugPrint('Error connecting discovered glasses: $e'); + debugPrintStack(stackTrace: st); + onUpdate?.call('Failed to connect to glasses: $e'); + _connectionStateController.add(G1ConnectionEvent( + state: G1ConnectionState.error, + errorMessage: e.toString(), + )); + onConnectionChanged?.call(G1ConnectionState.error, null); + } finally { + _isConnecting = false; } } From 176a0371ee1fac8d6fe1b3e27390882f268bedd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:12:32 +0200 Subject: [PATCH 09/13] feat: tests --- lib/screens/landing_screen.dart | 2 + lib/services/websocket_service.dart | 29 +++++++++--- test/ble_mock/g1_manager_mock.dart | 17 ++++++- test/widget_test.dart | 73 +++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 7 deletions(-) diff --git a/lib/screens/landing_screen.dart b/lib/screens/landing_screen.dart index ab7db22..89b0d84 100644 --- a/lib/screens/landing_screen.dart +++ b/lib/screens/landing_screen.dart @@ -103,6 +103,7 @@ class _LandingScreenState extends State { /// Sentences never disappear on a timer — they scroll off only when /// pushed out by new ones. void _addSentenceToDisplay(String sentence) { + if (_isMuted) return; if (_displayedSentences.length >= _maxDisplayedSentences) { _displayedSentences.removeAt(0); } @@ -113,6 +114,7 @@ class _LandingScreenState extends State { ); Future.delayed(const Duration(seconds: 10), () { + if (!mounted || _isMuted) return; _displayedSentences.remove(sentence); _manager.transcription.displayLines( List.unmodifiable(_displayedSentences), diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart index 52adcfb..3bb5d1d 100644 --- a/lib/services/websocket_service.dart +++ b/lib/services/websocket_service.dart @@ -149,11 +149,28 @@ class WebsocketService { } void dispose() { - disconnect(); - connected.dispose(); - committedText.dispose(); - interimText.dispose(); - asrActive.dispose(); - aiResponse.dispose(); + // `disconnect()` is async and may touch ValueNotifiers after awaiting. + // During dispose we must not schedule work that can run after notifiers + // are disposed. + final channel = _audioChannel; + _audioChannel = null; + connected.value = false; + try { + channel?.sink.add(jsonEncode({'type': 'control', 'cmd': 'stop'})); + channel?.sink.close(); + } catch (_) { + // ignore: connection may already be closed + } finally { + committedText.value = ''; + interimText.value = ''; + asrActive.value = false; + aiResponse.value = ''; + + connected.dispose(); + committedText.dispose(); + interimText.dispose(); + asrActive.dispose(); + aiResponse.dispose(); + } } } diff --git a/test/ble_mock/g1_manager_mock.dart b/test/ble_mock/g1_manager_mock.dart index afa2579..339506d 100644 --- a/test/ble_mock/g1_manager_mock.dart +++ b/test/ble_mock/g1_manager_mock.dart @@ -145,6 +145,9 @@ class MockG1Microphone implements G1Microphone { } class MockG1Transcription implements G1Transcription { + final List displayTextCalls = []; + final List> displayLinesCalls = []; + @override final isActive = ValueNotifier(false); @@ -159,7 +162,19 @@ class MockG1Transcription implements G1Transcription { } @override - Future displayText(String text, {bool isInterim = false}) async {} + Future displayText(String text, {bool isInterim = false}) async { + displayTextCalls.add(text); + } + + @override + Future displayLines(List lines, {bool isInterim = false}) async { + displayLinesCalls.add(List.from(lines)); + } + + void clearDisplayCalls() { + displayTextCalls.clear(); + displayLinesCalls.clear(); + } @override Future pause() async {} diff --git a/test/widget_test.dart b/test/widget_test.dart index d023755..d9ff236 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -3,6 +3,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'ble_mock/g1_manager_mock.dart'; import 'package:front/screens/landing_screen.dart'; +import 'package:front/services/websocket_service.dart'; + +class TestWebsocketService extends WebsocketService { + TestWebsocketService() : super(baseUrl: 'test'); + + @override + Future connect() async { + connected.value = true; + } +} void main() { late MockG1Manager mockManager; @@ -45,6 +55,69 @@ void main() { await disposeLanding(tester); }); + testWidgets('No text is sent to glasses when display is muted', + (WidgetTester tester) async { + final ws = TestWebsocketService(); + mockManager.setConnected(true); + await mockManager.transcription.start(); + + await tester.pumpWidget( + MaterialApp( + home: LandingScreen( + manager: mockManager, + ws: ws, + ), + ), + ); + await tester.pump(); + + await tester.tap(find.text('Mute display')); + await tester.pump(); + expect(find.text('Unmute display'), findsOneWidget); + + (mockManager.transcription as MockG1Transcription).clearDisplayCalls(); + + ws.aiResponse.value = 'Hello from backend'; + await tester.pump(); + + final tx = mockManager.transcription as MockG1Transcription; + expect(tx.displayTextCalls, isEmpty); + expect(tx.displayLinesCalls, isEmpty); + + await disposeLanding(tester); + }); + + testWidgets('Text is sent to glasses when display is not muted', + (WidgetTester tester) async { + final ws = TestWebsocketService(); + mockManager.setConnected(true); + await mockManager.transcription.start(); + + await tester.pumpWidget( + MaterialApp( + home: LandingScreen( + manager: mockManager, + ws: ws, + ), + ), + ); + await tester.pump(); + + final tx = mockManager.transcription as MockG1Transcription; + tx.clearDisplayCalls(); + + ws.aiResponse.value = 'Hello from backend'; + await tester.pump(); + + expect(tx.displayLinesCalls, isNotEmpty); + expect(tx.displayLinesCalls.last, contains('Hello from backend')); + + await disposeLanding(tester); + // LandingScreen schedules a 10s cleanup timer per sentence. + // Advance time so the timer fires and doesn't remain pending. + await tester.pump(const Duration(seconds: 11)); + }); + testWidgets('Connecting to glasses text is shown when bluetooth is scanning', (tester) async { await pumpLanding(tester); From 3099ba716649a6490f0f353788f5f7840b16ea59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:43:59 +0200 Subject: [PATCH 10/13] fix: remove hardcoded DEVELOPMENT_TEAM from project configuration --- ios/Runner.xcodeproj/project.pbxproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8593100..665f40d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -488,7 +488,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = H5536DZLJW; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -671,7 +670,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = H5536DZLJW; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -694,7 +692,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = H5536DZLJW; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( From aa37067034538ada872754315975992b3fc94e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Mon, 16 Mar 2026 14:46:24 +0200 Subject: [PATCH 11/13] fix: outdated comment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/screens/landing_screen.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/screens/landing_screen.dart b/lib/screens/landing_screen.dart index 89b0d84..6fcbd05 100644 --- a/lib/screens/landing_screen.dart +++ b/lib/screens/landing_screen.dart @@ -100,8 +100,8 @@ class _LandingScreenState extends State { /// /// Each sentence is a separate BLE packet (lineNumber 1..N). /// When the list is full, the oldest sentence is evicted to make room. - /// Sentences never disappear on a timer — they scroll off only when - /// pushed out by new ones. + /// Sentences are also automatically removed after a fixed timeout + /// (currently 10 seconds), so older lines can clear even without new ones. void _addSentenceToDisplay(String sentence) { if (_isMuted) return; if (_displayedSentences.length >= _maxDisplayedSentences) { From 2ae3c368c9dba429a62e31f114fe624512429438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:22:36 +0200 Subject: [PATCH 12/13] fix: formatting --- test/ble_mock/g1_manager_mock.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/ble_mock/g1_manager_mock.dart b/test/ble_mock/g1_manager_mock.dart index 339506d..7640d1b 100644 --- a/test/ble_mock/g1_manager_mock.dart +++ b/test/ble_mock/g1_manager_mock.dart @@ -167,7 +167,8 @@ class MockG1Transcription implements G1Transcription { } @override - Future displayLines(List lines, {bool isInterim = false}) async { + Future displayLines(List lines, + {bool isInterim = false}) async { displayLinesCalls.add(List.from(lines)); } From 3f3953902e78022fefed38ae5b868ea001f7f75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veeti=20Martinm=C3=A4ki?= <181813689+veetimar@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:34:48 +0200 Subject: [PATCH 13/13] fix: ios files --- ios/Podfile.lock | 4 ++-- ios/Runner.xcodeproj/project.pbxproj | 33 ---------------------------- 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ad3788e..d8f4363 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -13,11 +13,11 @@ PODS: - Flutter DEPENDENCIES: + - device_calendar (from `.symlinks/plugins/device_calendar/ios`) - Flutter (from `Flutter`) - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_sound (from `.symlinks/plugins/flutter_sound/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - - device_calendar (from `.symlinks/plugins/device_calendar/ios`) SPEC REPOS: trunk: @@ -36,12 +36,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" SPEC CHECKSUMS: + device_calendar: b55b2c5406cfba45c95a59f9059156daee1f74ed Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 flutter_sound: d95194f6476c9ad211d22b3a414d852c12c7ca44 flutter_sound_core: 7f2626d249d3a57bfa6da892ef7e22d234482c1a permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d - device_calendar: b55b2c5406cfba45c95a59f9059156daee1f74ed PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 9875db3..665f40d 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -16,8 +16,6 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 99F5015DBD6F6F67074A1B9D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9B72C4D29009EF8D1D1EEA7 /* Pods_Runner.framework */; }; - F8A02A1E8E6684C1D8450DF1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9C6A6BD5AE93A3E540561BF /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,8 +55,6 @@ 75606A58A2724B2236273247 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 80B8CDD639846BDA7DEBA175 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 807E81FEBDA1B3C207D2F02D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 96D8DD3C040343A039F310FE /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -69,9 +65,6 @@ C7762E6CD773CA6A6329EA96 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E3CB0A10B61A6DA72D29435D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; F85674E7919C9C6910F5C52A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - A66E4728E7B9DA7D3BF9212D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - A9B72C4D29009EF8D1D1EEA7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F06CDC837EF05FDD4CA99461 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -171,29 +164,6 @@ path = Runner; sourceTree = ""; }; - CAAD90FF3B44AD28E7C94A7C /* Frameworks */ = { - isa = PBXGroup; - children = ( - A9B72C4D29009EF8D1D1EEA7 /* Pods_Runner.framework */, - E9C6A6BD5AE93A3E540561BF /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - F0CFB2D7AEEBFE525929A1A9 /* Pods */ = { - isa = PBXGroup; - children = ( - F06CDC837EF05FDD4CA99461 /* Pods-Runner.debug.xcconfig */, - A66E4728E7B9DA7D3BF9212D /* Pods-Runner.release.xcconfig */, - 61CA3FFAF8421699CD486D19 /* Pods-Runner.profile.xcconfig */, - 807E81FEBDA1B3C207D2F02D /* Pods-RunnerTests.debug.xcconfig */, - E805B90C1540E73EE0890817 /* Pods-RunnerTests.release.xcconfig */, - 96D8DD3C040343A039F310FE /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -518,7 +488,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = F2VMBPUNG6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -701,7 +670,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = F2VMBPUNG6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -724,7 +692,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = F2VMBPUNG6; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = (