diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ac856b9..92c6ecd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -38,6 +38,8 @@ PODS: - Flutter - flutter_secure_storage (6.0.0): - Flutter + - flutter_tts (0.0.1): + - Flutter - image_picker_ios (0.0.1): - Flutter - package_info_plus (0.4.5): @@ -79,6 +81,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -108,6 +111,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + flutter_tts: + :path: ".symlinks/plugins/flutter_tts/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" package_info_plus: @@ -140,6 +145,7 @@ SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b51e57a..caf99ec 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -9,15 +9,14 @@ /* Begin PBXBuildFile section */ 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 */; }; - 4A7698E95402502ED3A27D07 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E21C32C282E5A003D094A6E9 /* Pods_RunnerTests.framework */; }; - 5318C41BD6012F73B0A81F8D /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15F5EAC162214F437B9D5305 /* Pods_ShareExtension.framework */; }; + 55DE1EF6874060F683F7BE5A /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D444AAE6AC78C530C74E51F /* Pods_ShareExtension.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 925EEBF822EC5FC307568548 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */; }; 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 */; }; - B26D75F4FE6FFABCFAC07E43 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BA04FBA3F37D0A688CFEFA7 /* Pods_Runner.framework */; }; F1DBCF1D2E601A39004C2540 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F1DBCF132E601A39004C2540 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + F1E401255BF7F4649BBEC0E4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -62,23 +61,23 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0BA04FBA3F37D0A688CFEFA7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0EB032AD98424F9B253F06E2 /* 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 = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 15F5EAC162214F437B9D5305 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 1EB0E6D3737F83B935118E8E /* 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 = ""; }; - 20A55D0FD9E7CFD1B9BBBEED /* 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 = ""; }; + 167D66D998116642A4545F97 /* 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 = ""; }; + 1D444AAE6AC78C530C74E51F /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1EFA3C3B31029CE6405A8591 /* 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 = ""; }; + 3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 69671B6B1959FCF16D21E200 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; - 71EEBC62F9F81EB3FD345FA2 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; + 3CBE9A216C715CD68229F6EC /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.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 = ""; }; - F1DBCF292E602000004C2540 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 837A0EFB28C7ACD7FF14EC67 /* 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 = ""; }; - 838ED4921F9222D50ECF498B /* 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 = ""; }; + 81AA439A1413A5BB88E56615 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; + 8689E338A774C63931CCE3E4 /* 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 = ""; }; + 951B4C2E3B8A60C543F47868 /* 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 = ""; }; 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; }; @@ -86,10 +85,9 @@ 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 = ""; }; - B60F6CBA44A303DE5389649E /* 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 = ""; }; - B6CA85D4EC683E94C2FB2A88 /* 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 = ""; }; - C9F5BA8D70297B37D581AE50 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; - E21C32C282E5A003D094A6E9 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C334EBA4AE824079ECAEE9EE /* 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 = ""; }; + CF1093DCAFB438AD6653A379 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; F1DBCF132E601A39004C2540 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F1DBCF242E601A7C004C2540 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ @@ -124,7 +122,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4A7698E95402502ED3A27D07 /* Pods_RunnerTests.framework in Frameworks */, + F1E401255BF7F4649BBEC0E4 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -132,7 +130,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B26D75F4FE6FFABCFAC07E43 /* Pods_Runner.framework in Frameworks */, + 925EEBF822EC5FC307568548 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -140,7 +138,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5318C41BD6012F73B0A81F8D /* Pods_ShareExtension.framework in Frameworks */, + 55DE1EF6874060F683F7BE5A /* Pods_ShareExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -155,12 +153,12 @@ path = RunnerTests; sourceTree = ""; }; - 50815AD0039D3C59615FF5FC /* Frameworks */ = { + 5C7D9D0FF0837699EA9220F9 /* Frameworks */ = { isa = PBXGroup; children = ( - 0BA04FBA3F37D0A688CFEFA7 /* Pods_Runner.framework */, - E21C32C282E5A003D094A6E9 /* Pods_RunnerTests.framework */, - 15F5EAC162214F437B9D5305 /* Pods_ShareExtension.framework */, + A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */, + 3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */, + 1D444AAE6AC78C530C74E51F /* Pods_ShareExtension.framework */, ); name = Frameworks; sourceTree = ""; @@ -168,15 +166,15 @@ 8C43905FA2E52A883F49D605 /* Pods */ = { isa = PBXGroup; children = ( - 20A55D0FD9E7CFD1B9BBBEED /* Pods-Runner.debug.xcconfig */, - 838ED4921F9222D50ECF498B /* Pods-Runner.release.xcconfig */, - B60F6CBA44A303DE5389649E /* Pods-Runner.profile.xcconfig */, - B6CA85D4EC683E94C2FB2A88 /* Pods-RunnerTests.debug.xcconfig */, - 837A0EFB28C7ACD7FF14EC67 /* Pods-RunnerTests.release.xcconfig */, - 1EB0E6D3737F83B935118E8E /* Pods-RunnerTests.profile.xcconfig */, - 71EEBC62F9F81EB3FD345FA2 /* Pods-ShareExtension.debug.xcconfig */, - 69671B6B1959FCF16D21E200 /* Pods-ShareExtension.release.xcconfig */, - C9F5BA8D70297B37D581AE50 /* Pods-ShareExtension.profile.xcconfig */, + 0EB032AD98424F9B253F06E2 /* Pods-Runner.debug.xcconfig */, + 8689E338A774C63931CCE3E4 /* Pods-Runner.release.xcconfig */, + C334EBA4AE824079ECAEE9EE /* Pods-Runner.profile.xcconfig */, + 951B4C2E3B8A60C543F47868 /* Pods-RunnerTests.debug.xcconfig */, + 1EFA3C3B31029CE6405A8591 /* Pods-RunnerTests.release.xcconfig */, + 167D66D998116642A4545F97 /* Pods-RunnerTests.profile.xcconfig */, + 81AA439A1413A5BB88E56615 /* Pods-ShareExtension.debug.xcconfig */, + CF1093DCAFB438AD6653A379 /* Pods-ShareExtension.release.xcconfig */, + 3CBE9A216C715CD68229F6EC /* Pods-ShareExtension.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -201,7 +199,7 @@ 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 8C43905FA2E52A883F49D605 /* Pods */, - 50815AD0039D3C59615FF5FC /* Frameworks */, + 5C7D9D0FF0837699EA9220F9 /* Frameworks */, ); sourceTree = ""; }; @@ -238,7 +236,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - B2C308F843502D753F643BC6 /* [CP] Check Pods Manifest.lock */, + C9B5EBE3027E91A8CD4D83EE /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 3F0B71E2AB4D863902C09B3E /* Frameworks */, @@ -257,15 +255,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - FEB5EADAC290174A94F41862 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, + 0D14FD100D11330B7967BF40 /* [CP] Check Pods Manifest.lock */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, F1DBCF1E2E601A39004C2540 /* Embed Foundation Extensions */, - AA915538CD255B5C833E6AB7 /* [CP] Copy Pods Resources */, + 19E24AB51CA4721E8EB2EF0A /* [CP] Copy Pods Resources */, + 9740EEB61CF901F6004384FC /* Run Script */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( ); @@ -281,7 +279,7 @@ isa = PBXNativeTarget; buildConfigurationList = F1DBCF232E601A39004C2540 /* Build configuration list for PBXNativeTarget "ShareExtension" */; buildPhases = ( - 5A0A281EDF7C33D1547603FC /* [CP] Check Pods Manifest.lock */, + 9B6E10307D13776C37514922 /* [CP] Check Pods Manifest.lock */, F1DBCF0F2E601A39004C2540 /* Sources */, F1DBCF102E601A39004C2540 /* Frameworks */, F1DBCF112E601A39004C2540 /* Resources */, @@ -370,6 +368,43 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0D14FD100D11330B7967BF40 /* [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; + }; + 19E24AB51CA4721E8EB2EF0A /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -377,7 +412,7 @@ files = ( ); inputPaths = ( - + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -386,7 +421,22 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 5A0A281EDF7C33D1547603FC /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + 9B6E10307D13776C37514922 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -408,37 +458,7 @@ 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 */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - AA915538CD255B5C833E6AB7 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - B2C308F843502D753F643BC6 /* [CP] Check Pods Manifest.lock */ = { + C9B5EBE3027E91A8CD4D83EE /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -460,28 +480,6 @@ 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; }; - FEB5EADAC290174A94F41862 /* [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; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -627,7 +625,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B6CA85D4EC683E94C2FB2A88 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 951B4C2E3B8A60C543F47868 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -645,7 +643,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 837A0EFB28C7ACD7FF14EC67 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 1EFA3C3B31029CE6405A8591 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -661,7 +659,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1EB0E6D3737F83B935118E8E /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 167D66D998116642A4545F97 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -847,7 +845,7 @@ }; F1DBCF1F2E601A39004C2540 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 71EEBC62F9F81EB3FD345FA2 /* Pods-ShareExtension.debug.xcconfig */; + baseConfigurationReference = 81AA439A1413A5BB88E56615 /* Pods-ShareExtension.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -890,7 +888,7 @@ }; F1DBCF202E601A39004C2540 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 69671B6B1959FCF16D21E200 /* Pods-ShareExtension.release.xcconfig */; + baseConfigurationReference = CF1093DCAFB438AD6653A379 /* Pods-ShareExtension.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -930,7 +928,7 @@ }; F1DBCF212E601A39004C2540 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C9F5BA8D70297B37D581AE50 /* Pods-ShareExtension.profile.xcconfig */; + baseConfigurationReference = 3CBE9A216C715CD68229F6EC /* Pods-ShareExtension.profile.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; diff --git a/lib/features/chat/providers/text_to_speech_provider.dart b/lib/features/chat/providers/text_to_speech_provider.dart new file mode 100644 index 0000000..33615c0 --- /dev/null +++ b/lib/features/chat/providers/text_to_speech_provider.dart @@ -0,0 +1,261 @@ +import 'dart:async'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../services/text_to_speech_service.dart'; + +enum TtsPlaybackStatus { idle, initializing, loading, speaking, paused, error } + +class TextToSpeechState { + final bool initialized; + final bool available; + final TtsPlaybackStatus status; + final String? activeMessageId; + final String? errorMessage; + + const TextToSpeechState({ + this.initialized = false, + this.available = false, + this.status = TtsPlaybackStatus.idle, + this.activeMessageId, + this.errorMessage, + }); + + bool get isSpeaking => status == TtsPlaybackStatus.speaking; + bool get isBusy => + status == TtsPlaybackStatus.loading || + status == TtsPlaybackStatus.initializing; + + TextToSpeechState copyWith({ + bool? initialized, + bool? available, + TtsPlaybackStatus? status, + String? activeMessageId, + bool clearActiveMessageId = false, + String? errorMessage, + bool clearErrorMessage = false, + }) { + return TextToSpeechState( + initialized: initialized ?? this.initialized, + available: available ?? this.available, + status: status ?? this.status, + activeMessageId: clearActiveMessageId + ? null + : activeMessageId ?? this.activeMessageId, + errorMessage: clearErrorMessage + ? null + : errorMessage ?? this.errorMessage, + ); + } +} + +class TextToSpeechController extends StateNotifier { + TextToSpeechController(this._service) : super(const TextToSpeechState()) { + _service.bindHandlers( + onStart: _handleStart, + onComplete: _handleCompletion, + onCancel: _handleCancellation, + onPause: _handlePause, + onContinue: _handleContinue, + onError: _handleError, + ); + } + + final TextToSpeechService _service; + Future? _initializationFuture; + + Future _ensureInitialized() { + final existing = _initializationFuture; + if (existing != null) { + return existing; + } + + state = state.copyWith( + status: TtsPlaybackStatus.initializing, + clearErrorMessage: true, + ); + + final future = _service + .initialize() + .then((available) { + if (!mounted) { + return available; + } + + state = state.copyWith( + initialized: true, + available: available, + status: TtsPlaybackStatus.idle, + ); + return available; + }) + .catchError((error, _) { + if (!mounted) { + return false; + } + + state = state.copyWith( + initialized: true, + available: false, + status: TtsPlaybackStatus.error, + errorMessage: error.toString(), + clearActiveMessageId: true, + ); + return false; + }); + + _initializationFuture = future; + future.whenComplete(() { + _initializationFuture = null; + }); + + return future; + } + + Future toggleForMessage({ + required String messageId, + required String text, + }) async { + if (text.trim().isEmpty) { + return; + } + + final available = await _ensureInitialized(); + if (!available) { + if (!mounted) { + return; + } + state = state.copyWith( + status: TtsPlaybackStatus.error, + errorMessage: 'Text-to-speech unavailable', + clearActiveMessageId: true, + ); + return; + } + + final isCurrentlyActive = + state.activeMessageId == messageId && + state.status != TtsPlaybackStatus.idle; + + if (isCurrentlyActive) { + await stop(); + return; + } + + state = state.copyWith( + status: TtsPlaybackStatus.loading, + activeMessageId: messageId, + clearErrorMessage: true, + ); + + try { + await _service.speak(text); + if (!mounted) { + return; + } + if (state.status == TtsPlaybackStatus.loading) { + state = state.copyWith(status: TtsPlaybackStatus.speaking); + } + } catch (e) { + if (!mounted) { + return; + } + state = state.copyWith( + status: TtsPlaybackStatus.error, + errorMessage: e.toString(), + clearActiveMessageId: true, + ); + } + } + + Future pause() async { + if (!state.initialized || !state.available) { + return; + } + await _service.pause(); + } + + Future stop() async { + await _service.stop(); + if (!mounted) { + return; + } + state = state.copyWith( + status: TtsPlaybackStatus.idle, + clearActiveMessageId: true, + clearErrorMessage: true, + ); + } + + void _handleStart() { + if (!mounted) { + return; + } + state = state.copyWith(status: TtsPlaybackStatus.speaking); + } + + void _handleCompletion() { + if (!mounted) { + return; + } + state = state.copyWith( + status: TtsPlaybackStatus.idle, + clearActiveMessageId: true, + ); + } + + void _handleCancellation() { + if (!mounted) { + return; + } + state = state.copyWith( + status: TtsPlaybackStatus.idle, + clearActiveMessageId: true, + ); + } + + void _handlePause() { + if (!mounted) { + return; + } + state = state.copyWith(status: TtsPlaybackStatus.paused); + } + + void _handleContinue() { + if (!mounted) { + return; + } + state = state.copyWith(status: TtsPlaybackStatus.speaking); + } + + void _handleError(String message) { + if (!mounted) { + return; + } + state = state.copyWith( + status: TtsPlaybackStatus.error, + errorMessage: message, + clearActiveMessageId: true, + ); + } + + @override + void dispose() { + unawaited(_service.stop()); + super.dispose(); + } +} + +final textToSpeechServiceProvider = Provider((ref) { + final service = TextToSpeechService(); + ref.onDispose(() { + unawaited(service.dispose()); + }); + return service; +}); + +final textToSpeechControllerProvider = + StateNotifierProvider((ref) { + final service = ref.watch(textToSpeechServiceProvider); + return TextToSpeechController(service); + }); diff --git a/lib/features/chat/services/text_to_speech_service.dart b/lib/features/chat/services/text_to_speech_service.dart new file mode 100644 index 0000000..3e275db --- /dev/null +++ b/lib/features/chat/services/text_to_speech_service.dart @@ -0,0 +1,151 @@ +import 'dart:async'; +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_tts/flutter_tts.dart'; + +/// Lightweight wrapper around FlutterTts to centralize configuration +class TextToSpeechService { + final FlutterTts _tts = FlutterTts(); + bool _initialized = false; + bool _available = false; + + VoidCallback? _onStart; + VoidCallback? _onComplete; + VoidCallback? _onCancel; + VoidCallback? _onPause; + VoidCallback? _onContinue; + void Function(String message)? _onError; + + bool get isInitialized => _initialized; + bool get isAvailable => _available; + + /// Register callbacks for TTS lifecycle events + void bindHandlers({ + VoidCallback? onStart, + VoidCallback? onComplete, + VoidCallback? onCancel, + VoidCallback? onPause, + VoidCallback? onContinue, + void Function(String message)? onError, + }) { + _onStart = onStart; + _onComplete = onComplete; + _onCancel = onCancel; + _onPause = onPause; + _onContinue = onContinue; + _onError = onError; + + _tts.setStartHandler(_handleStart); + _tts.setCompletionHandler(_handleComplete); + _tts.setCancelHandler(_handleCancel); + _tts.setPauseHandler(_handlePause); + _tts.setContinueHandler(_handleContinue); + _tts.setErrorHandler(_handleError); + } + + /// Initialize the native TTS engine lazily + Future initialize() async { + if (_initialized) { + return _available; + } + + try { + await _tts.awaitSpeakCompletion(false); + if (!kIsWeb && Platform.isIOS) { + await _tts.setIosAudioCategory(IosTextToSpeechAudioCategory.playback, [ + IosTextToSpeechAudioCategoryOptions.mixWithOthers, + IosTextToSpeechAudioCategoryOptions.defaultToSpeaker, + IosTextToSpeechAudioCategoryOptions.allowBluetooth, + IosTextToSpeechAudioCategoryOptions.allowBluetoothA2DP, + ]); + } + _available = true; + } catch (e) { + _available = false; + _onError?.call(e.toString()); + } + + _initialized = true; + return _available; + } + + Future speak(String text) async { + if (text.trim().isEmpty) { + throw ArgumentError('Cannot speak empty text'); + } + + if (!_initialized) { + await initialize(); + } + + if (!_available) { + throw StateError('Text-to-speech is unavailable on this device'); + } + + await _tts.stop(); + final result = await _tts.speak(text); + if (result == null) { + return; + } + + if (result is int && result != 1) { + _onError?.call('Text-to-speech engine returned code $result'); + } + } + + Future pause() async { + if (!_initialized || !_available) { + return; + } + + try { + await _tts.pause(); + } catch (e) { + _onError?.call(e.toString()); + } + } + + Future stop() async { + if (!_initialized) { + return; + } + + try { + await _tts.stop(); + } catch (e) { + _onError?.call(e.toString()); + } + } + + Future dispose() async { + await stop(); + } + + void _handleStart() { + _onStart?.call(); + } + + void _handleComplete() { + _onComplete?.call(); + } + + void _handleCancel() { + _onCancel?.call(); + } + + void _handlePause() { + _onPause?.call(); + } + + void _handleContinue() { + _onContinue?.call(); + } + + void _handleError(dynamic message) { + final safeMessage = message == null + ? 'Unknown TTS error' + : message.toString(); + _onError?.call(safeMessage); + } +} diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index e78bd62..b3be7d1 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -10,6 +10,7 @@ import '../../../shared/widgets/markdown/streaming_markdown_widget.dart'; import '../../../core/utils/reasoning_parser.dart'; import '../../../core/utils/message_segments.dart'; import '../../../core/utils/tool_calls_parser.dart'; +import '../providers/text_to_speech_provider.dart'; import 'enhanced_image_attachment.dart'; import 'package:conduit/l10n/app_localizations.dart'; import 'enhanced_attachment.dart'; @@ -54,6 +55,7 @@ class _AssistantMessageWidgetState extends ConsumerState Widget? _cachedAvatar; bool _allowTypingIndicator = false; Timer? _typingGateTimer; + String _ttsPlainText = ''; // press state handled by shared ChatActionButton @override @@ -154,8 +156,12 @@ class _AssistantMessageWidgetState extends ConsumerState } } + final segments = out.isEmpty ? [MessageSegment.text(raw)] : out; + final speechText = _buildTtsPlainText(segments, raw); + setState(() { - _segments = out.isEmpty ? [MessageSegment.text(raw)] : out; + _segments = segments; + _ttsPlainText = speechText; }); _updateTypingIndicatorGate(); } @@ -179,6 +185,73 @@ class _AssistantMessageWidgetState extends ConsumerState } } + String get _messageId { + try { + final dynamic idValue = widget.message.id; + if (idValue == null) { + return ''; + } + return idValue.toString(); + } catch (_) { + return ''; + } + } + + String _buildTtsPlainText(List segments, String fallback) { + if (segments.isEmpty) { + return _sanitizeForSpeech(fallback); + } + + final buffer = StringBuffer(); + for (final segment in segments) { + if (!segment.isText) { + continue; + } + final text = segment.text ?? ''; + final sanitized = _sanitizeForSpeech(text); + if (sanitized.isEmpty) { + continue; + } + if (buffer.isNotEmpty) { + buffer.writeln(); + buffer.writeln(); + } + buffer.write(sanitized); + } + + final result = buffer.toString().trim(); + if (result.isEmpty) { + return _sanitizeForSpeech(fallback); + } + return result; + } + + String _sanitizeForSpeech(String input) { + if (input.isEmpty) { + return ''; + } + + var text = input; + text = text.replaceAll(RegExp(r'```'), ' '); + text = text.replaceAll(RegExp(r'`'), ''); + text = text.replaceAll(RegExp(r'!\[(.*?)\]\((.*?)\)'), r'$1'); + text = text.replaceAll(RegExp(r'\[(.*?)\]\((.*?)\)'), r'$1'); + text = text.replaceAll(RegExp(r'\*\*'), ''); + text = text.replaceAll(RegExp(r'__'), ''); + text = text.replaceAll(RegExp(r'\*'), ''); + text = text.replaceAll(RegExp(r'_'), ''); + text = text.replaceAll(RegExp(r'~'), ''); + text = text.replaceAll(RegExp(r'^[-*+]\s+', multiLine: true), ''); + text = text.replaceAll(RegExp(r'^>\s?', multiLine: true), ''); + text = text.replaceAll(' ', ' '); + text = text.replaceAll('&', '&'); + text = text.replaceAll('<', '<'); + text = text.replaceAll('>', '>'); + text = text.replaceAll(RegExp(r'[ \t]{2,}'), ' '); + text = text.replaceAll(RegExp(r'\n{3,}'), '\n\n'); + return text.trim(); + } + // No streaming-specific markdown fixes needed here; handled by Markdown widget Widget _buildToolCallTile(ToolCallEntry tc) { @@ -888,21 +961,65 @@ class _AssistantMessageWidgetState extends ConsumerState } Widget _buildActionButtons() { + final l10n = AppLocalizations.of(context)!; + final ttsState = ref.watch(textToSpeechControllerProvider); + final messageId = _messageId; + final hasSpeechText = _ttsPlainText.trim().isNotEmpty; final isErrorMessage = widget.message.content.contains('⚠️') || widget.message.content.contains('Error') || widget.message.content.contains('timeout') || widget.message.content.contains('retry options'); + final isActiveMessage = ttsState.activeMessageId == messageId; + final isSpeaking = + isActiveMessage && ttsState.status == TtsPlaybackStatus.speaking; + final isPaused = + isActiveMessage && ttsState.status == TtsPlaybackStatus.paused; + final isBusy = + isActiveMessage && + (ttsState.status == TtsPlaybackStatus.loading || + ttsState.status == TtsPlaybackStatus.initializing); + final bool disableDueToStreaming = widget.isStreaming && !isActiveMessage; + final bool ttsAvailable = !ttsState.initialized || ttsState.available; + final bool showStopState = + isActiveMessage && (isSpeaking || isPaused || isBusy); + final bool shouldShowTtsButton = hasSpeechText && messageId.isNotEmpty; + final bool canStartTts = + shouldShowTtsButton && !disableDueToStreaming && ttsAvailable; + + VoidCallback? ttsOnTap; + if (showStopState || canStartTts) { + ttsOnTap = () { + if (messageId.isEmpty) { + return; + } + ref + .read(textToSpeechControllerProvider.notifier) + .toggleForMessage(messageId: messageId, text: _ttsPlainText); + }; + } + + final IconData listenIcon = Platform.isIOS + ? CupertinoIcons.speaker_2_fill + : Icons.volume_up; + final IconData stopIcon = Platform.isIOS + ? CupertinoIcons.stop_fill + : Icons.stop; + final IconData ttsIcon = showStopState ? stopIcon : listenIcon; + final String ttsLabel = showStopState ? l10n.ttsStop : l10n.ttsListen; + return Wrap( spacing: 8, runSpacing: 8, children: [ + if (shouldShowTtsButton) + _buildActionButton(icon: ttsIcon, label: ttsLabel, onTap: ttsOnTap), _buildActionButton( icon: Platform.isIOS ? CupertinoIcons.doc_on_clipboard : Icons.content_copy, - label: AppLocalizations.of(context)!.copy, + label: l10n.copy, onTap: widget.onCopy, ), if (isErrorMessage) ...[ @@ -910,13 +1027,13 @@ class _AssistantMessageWidgetState extends ConsumerState icon: Platform.isIOS ? CupertinoIcons.arrow_clockwise : Icons.refresh, - label: AppLocalizations.of(context)!.retry, + label: l10n.retry, onTap: widget.onRegenerate, ), ] else ...[ _buildActionButton( icon: Platform.isIOS ? CupertinoIcons.refresh : Icons.refresh, - label: AppLocalizations.of(context)!.regenerate, + label: l10n.regenerate, onTap: widget.onRegenerate, ), ], diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index b1304c4..8ed6895 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -217,6 +217,8 @@ "imageGeneration": "Bildgenerierung", "imageGenerationDescription": "Bilder aus deinen Prompts erstellen.", "copy": "Kopieren", + "ttsListen": "Anhören", + "ttsStop": "Stoppen", "edit": "Bearbeiten", "regenerate": "Neu generieren", "noConversationsYet": "Noch keine Unterhaltungen" diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b739f7a..80a1768 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -441,6 +441,14 @@ "@imageGenerationDescription": {"description": "Explains creating images via model prompts."}, "copy": "Copy", "@copy": {"description": "Action to copy text to clipboard."}, + "ttsListen": "Listen", + "@ttsListen": { + "description": "Action to play the assistant message using text to speech" + }, + "ttsStop": "Stop", + "@ttsStop": { + "description": "Action to stop text to speech playback" + }, "edit": "Edit", "@edit": {"description": "Action to edit an item/message."}, "regenerate": "Regenerate", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a938ff2..f49fb7d 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -217,6 +217,8 @@ "imageGeneration": "Génération d'images", "imageGenerationDescription": "Créez des images à partir de vos prompts.", "copy": "Copier", + "ttsListen": "Écouter", + "ttsStop": "Arrêter", "edit": "Modifier", "regenerate": "Régénérer", "noConversationsYet": "Aucune conversation pour l'instant" diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index e9973b9..7d25ba8 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -217,6 +217,8 @@ "imageGeneration": "Generazione immagini", "imageGenerationDescription": "Crea immagini dai tuoi prompt.", "copy": "Copia", + "ttsListen": "Ascolta", + "ttsStop": "Interrompi", "edit": "Modifica", "regenerate": "Rigenera", "noConversationsYet": "Ancora nessuna conversazione" diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cbaaf33..b933b04 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1290,6 +1290,18 @@ abstract class AppLocalizations { /// **'Copy'** String get copy; + /// Action to play the assistant message using text to speech + /// + /// In en, this message translates to: + /// **'Listen'** + String get ttsListen; + + /// Action to stop text to speech playback + /// + /// In en, this message translates to: + /// **'Stop'** + String get ttsStop; + /// Action to edit an item/message. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 1bbf1f2..90829e5 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -658,6 +658,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get copy => 'Kopieren'; + @override + String get ttsListen => 'Anhören'; + + @override + String get ttsStop => 'Stoppen'; + @override String get edit => 'Bearbeiten'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 1b5d4b6..99cf853 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -653,6 +653,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get copy => 'Copy'; + @override + String get ttsListen => 'Listen'; + + @override + String get ttsStop => 'Stop'; + @override String get edit => 'Edit'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index e46a011..1f92cbf 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -664,6 +664,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get copy => 'Copier'; + @override + String get ttsListen => 'Écouter'; + + @override + String get ttsStop => 'Arrêter'; + @override String get edit => 'Modifier'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 3b8b8bd..7376547 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -655,6 +655,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get copy => 'Copia'; + @override + String get ttsListen => 'Ascolta'; + + @override + String get ttsStop => 'Interrompi'; + @override String get edit => 'Modifica'; diff --git a/pubspec.lock b/pubspec.lock index e7d3157..e969181 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -456,6 +456,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_tts: + dependency: "direct main" + description: + name: flutter_tts + sha256: cbb3fd43b946e62398560235469e6113e4fe26c40eab1b7cb5e7c417503fb3a8 + url: "https://pub.dev" + source: hosted + version: "3.8.5" flutter_web_plugins: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index d55f4da..7c0ae14 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: # Platform Features record: ^6.1.1 stts: ^1.2.5 + flutter_tts: ^3.8.5 image_picker: ^1.2.0 file_picker: ^10.3.2 path_provider: ^2.1.4