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/Podfile.lock b/ios/Podfile.lock
index fbeca2d..d8f4363 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -5,11 +5,23 @@ PODS:
- 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:
- 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`)
+
+SPEC REPOS:
+ trunk:
+ - flutter_sound_core
EXTERNAL SOURCES:
device_calendar:
@@ -18,12 +30,19 @@ EXTERNAL SOURCES:
: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:
device_calendar: b55b2c5406cfba45c95a59f9059156daee1f74ed
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
+ flutter_sound: d95194f6476c9ad211d22b3a414d852c12c7ca44
+ flutter_sound_core: 7f2626d249d3a57bfa6da892ef7e22d234482c1a
+ permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
-PODFILE CHECKSUM: 8c4d30c2258325612f2b7276ac7aac1f617fcf63
+PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
COCOAPODS: 1.16.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 6413778..665f40d 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 */; };
@@ -14,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 */
@@ -46,13 +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 = ""; };
- 61CA3FFAF8421699CD486D19 /* 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 = ""; };
+ 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 = ""; };
- 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 = ""; };
+ 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; };
@@ -60,19 +62,17 @@
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 = ""; };
- 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; };
- E805B90C1540E73EE0890817 /* 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 = ""; };
- E9C6A6BD5AE93A3E540561BF /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.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 = ""; };
+ 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 */
- 7A60E1A3672AB68E0E2A42D1 /* Frameworks */ = {
+ 1C1F3D585EBC7A9D5F841A4B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- F8A02A1E8E6684C1D8450DF1 /* Pods_RunnerTests.framework in Frameworks */,
+ 13B07DAAD2277EC02485444C /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -80,7 +80,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- 99F5015DBD6F6F67074A1B9D /* Pods_Runner.framework in Frameworks */,
+ 11042EE6E6B7D9E6B3203B5F /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -95,6 +95,28 @@
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 */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -113,8 +135,8 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
- F0CFB2D7AEEBFE525929A1A9 /* Pods */,
- CAAD90FF3B44AD28E7C94A7C /* Frameworks */,
+ 820C0A71E31DDDCBE4781028 /* Pods */,
+ 3A67259495ADFC50EA787FF3 /* Frameworks */,
);
sourceTree = "";
};
@@ -142,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 */
@@ -172,10 +171,10 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
- FEF6B413DA9FC937656B660D /* [CP] Check Pods Manifest.lock */,
+ 30194C4A061055FA00E6E695 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
- 7A60E1A3672AB68E0E2A42D1 /* Frameworks */,
+ 1C1F3D585EBC7A9D5F841A4B /* Frameworks */,
);
buildRules = (
);
@@ -191,14 +190,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
- BEC52B047FD349E6B05F8651 /* [CP] Check Pods Manifest.lock */,
+ 12062EAA91003871FC9F02AB /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- 62220B443F82F6667CD7939F /* [CP] Embed Pods Frameworks */,
+ E7F491FBE75B21AAC1960BCF /* [CP] Embed Pods Frameworks */,
+ 5930E5E9A4A35EF8EAF80E24 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -270,96 +270,113 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
- 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ 12062EAA91003871FC9F02AB /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
+ inputFileListPaths = (
+ );
inputPaths = (
- "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
);
- name = "Thin Binary";
outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ 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;
};
- 62220B443F82F6667CD7939F /* [CP] Embed Pods Frameworks */ = {
+ 30194C4A061055FA00E6E695 /* [CP] Check Pods Manifest.lock */ = {
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";
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
- "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ 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;
};
- 9740EEB61CF901F6004384FC /* Run Script */ = {
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
- name = "Run Script";
+ name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
- BEC52B047FD349E6B05F8651 /* [CP] Check Pods Manifest.lock */ = {
+ 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",
);
- inputPaths = (
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
- "${PODS_ROOT}/Manifest.lock",
- );
- name = "[CP] Check Pods Manifest.lock";
+ 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;
+ buildActionMask = 2147483647;
+ files = (
);
+ inputPaths = (
+ );
+ name = "Run Script";
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;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
- FEF6B413DA9FC937656B660D /* [CP] Check Pods Manifest.lock */ = {
+ 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",
);
- inputPaths = (
- "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
- "${PODS_ROOT}/Manifest.lock",
- );
- name = "[CP] Check Pods Manifest.lock";
+ name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
- );
- outputPaths = (
- "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
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";
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@@ -471,14 +488,13 @@
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 = (
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = "com.example.front-ai-smarties";
+ PRODUCT_BUNDLE_IDENTIFIER = "com.ai-smarties.front";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -488,7 +504,7 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 807E81FEBDA1B3C207D2F02D /* Pods-RunnerTests.debug.xcconfig */;
+ baseConfigurationReference = E3CB0A10B61A6DA72D29435D /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -506,7 +522,7 @@
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = E805B90C1540E73EE0890817 /* Pods-RunnerTests.release.xcconfig */;
+ baseConfigurationReference = F85674E7919C9C6910F5C52A /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -522,7 +538,7 @@
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
- baseConfigurationReference = 96D8DD3C040343A039F310FE /* Pods-RunnerTests.profile.xcconfig */;
+ baseConfigurationReference = 6F703DB2DF8D00352F539BEF /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
@@ -654,14 +670,13 @@
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 = (
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = "com.example.front-ai-smarties";
+ PRODUCT_BUNDLE_IDENTIFIER = "com.ai-smarties.front";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -677,14 +692,13 @@
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 = (
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = "com.example.front-ai-smarties";
+ 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 010caf8..2885430 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -26,6 +26,27 @@
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneClassName
+ UIWindowScene
+ UISceneConfigurationName
+ flutter
+ UISceneDelegateClassName
+ FlutterSceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+
NSCalendarsFullAccessUsageDescription
Access most functions for calendar viewing.
UIApplicationSupportsIndirectInputEvents
@@ -47,5 +68,9 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ 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/lib/screens/landing_screen.dart b/lib/screens/landing_screen.dart
index 86b3ded..db042dd 100644
--- a/lib/screens/landing_screen.dart
+++ b/lib/screens/landing_screen.dart
@@ -37,7 +37,9 @@ class _LandingScreenState extends State {
late final PhoneAudioService _phoneAudio;
bool _usePhoneMic = false;
+ bool _isMuted = false;
final ValueNotifier _isRecording = ValueNotifier(false);
+ final ValueNotifier _isRecordingBusy = ValueNotifier(false);
final List _displayedSentences = [];
static const int _maxDisplayedSentences = 4;
@@ -74,6 +76,7 @@ class _LandingScreenState extends State {
void dispose() {
_ws.aiResponse.removeListener(_onAiResponse);
_isRecording.dispose();
+ _isRecordingBusy.dispose();
_audioPipeline.dispose();
_phoneAudio.dispose();
_ws.dispose();
@@ -86,9 +89,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'");
}
}
@@ -96,9 +103,10 @@ 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) {
_displayedSentences.removeAt(0);
}
@@ -109,6 +117,7 @@ class _LandingScreenState extends State {
);
Future.delayed(const Duration(seconds: 10), () {
+ if (!mounted || _isMuted) return;
_displayedSentences.remove(sentence);
_manager.transcription.displayLines(
List.unmodifiable(_displayedSentences),
@@ -122,63 +131,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;
}
}
@@ -304,11 +326,217 @@ class _LandingScreenState extends State {
const SizedBox(width: 14),
// Mic toggle
+ Expanded(
+ 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);
+ 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(height: 14),
+
+ Row(
+ children: [
+ // Start / Stop recording
+ Expanded(
+ 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);
+ 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,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ },
+ ),
+ ),
+
+ const SizedBox(width: 14),
+
+ // Mute button
Expanded(
child: InkWell(
onTap: () {
setState(() {
- _usePhoneMic = !_usePhoneMic;
+ _isMuted = !_isMuted;
});
},
child: Container(
@@ -316,46 +544,43 @@ class _LandingScreenState extends State {
padding:
const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
- color: _usePhoneMic
- ? Colors.lightGreen
+ color: _isMuted
+ ? Colors.orange
.withAlpha((0.15 * 255).round())
: Colors.transparent,
border: Border.all(
- color: _usePhoneMic
- ? Colors.lightGreen
- : Colors.black12,
+ color:
+ _isMuted ? Colors.orange : Colors.black12,
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- _usePhoneMic
- ? const Icon(
- Icons.mic,
- size: 22,
- color: Colors.lightGreen,
- )
- : Image.asset(
- 'assets/images/g1-smart-glasses.webp',
- height: 22,
- fit: BoxFit.contain,
- ),
+ 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(
- _usePhoneMic
- ? 'Phone mic\n(Active)'
- : 'Glasses mic\n(Active)',
+ _isMuted
+ ? 'Unmute display'
+ : 'Mute display',
textAlign: TextAlign.center,
style: TextStyle(
- fontSize: 13,
- fontWeight: _usePhoneMic
+ fontSize: 14,
+ fontWeight: _isMuted
? FontWeight.bold
: FontWeight.normal,
- color: _usePhoneMic
- ? Colors.lightGreen
- : Colors.black,
+ color: _isMuted
+ ? Colors.orange
+ : Colors.grey[800],
),
),
),
@@ -367,88 +592,6 @@ class _LandingScreenState extends State {
],
),
- const SizedBox(height: 14),
-
- Row(
- 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
- ? 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],
- ),
- ),
- ),
- ],
- ),
- ),
- );
- },
- ),
- ),
-
- const SizedBox(width: 14),
-
- // Recordings placeholder
- Expanded(
- child: LandingTile(
- icon: Icons.play_circle_outline,
- label: 'Recordings',
- onTap: () {},
- ),
- ),
- ],
- ),
-
const SizedBox(height: 22),
ValueListenableBuilder(
diff --git a/lib/services/websocket_service.dart b/lib/services/websocket_service.dart
index aa61d00..f4555d2 100644
--- a/lib/services/websocket_service.dart
+++ b/lib/services/websocket_service.dart
@@ -155,11 +155,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/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;
}
}
diff --git a/pubspec.lock b/pubspec.lock
index aa8ce67..c164041 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:
@@ -315,18 +315,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:
@@ -560,10 +560,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"
timezone:
dependency: transitive
description:
diff --git a/test/ble_mock/g1_manager_mock.dart b/test/ble_mock/g1_manager_mock.dart
index afa2579..7640d1b 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,20 @@ 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 5003a35..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;
@@ -31,15 +41,81 @@ 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);
+ });
+
+ 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',