diff --git a/.github/workflows/objective-c-xcode.yml b/.github/workflows/objective-c-xcode.yml new file mode 100644 index 0000000..add007b --- /dev/null +++ b/.github/workflows/objective-c-xcode.yml @@ -0,0 +1,30 @@ +name: Xcode - Build and Analyze + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + name: Build and analyse default scheme using xcodebuild command + runs-on: macos-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set Default Scheme + run: | + scheme_list=$(xcodebuild -list -json | tr -d "\n") + default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") + echo $default | cat >default + echo Using default scheme: $default + - name: Build + env: + scheme: ${{ 'default' }} + run: | + if [ $scheme = default ]; then scheme=$(cat default); fi + if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi + file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` + xcodebuild clean build analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" | xcpretty && exit ${PIPESTATUS[0]} diff --git a/.gitignore b/.gitignore index da62fc9..cc654ba 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,48 @@ -AGENT.md + +.vscode/settings.json + +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 689d79d..0000000 --- a/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "libs/EvenDemoApp"] - path = libs/EvenDemoApp - url = git@github.com:even-realities/EvenDemoApp.git -[submodule "libs/even_glasses"] - path = libs/even_glasses - url = https://github.com/emingenc/even_glasses -[submodule "libs/g1_flutter_blue_plus"] - path = libs/g1_flutter_blue_plus - url = git@github.com:emingenc/g1_flutter_blue_plus.git diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..4212cc8 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: ios + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/Helix.xcodeproj/project.pbxproj b/Helix.xcodeproj/project.pbxproj deleted file mode 100644 index d849d38..0000000 --- a/Helix.xcodeproj/project.pbxproj +++ /dev/null @@ -1,563 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 77; - objects = { - -/* Begin PBXContainerItemProxy section */ - DA26EA942D4F40C000B353E6 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DA26EA7B2D4F40BF00B353E6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = DA26EA822D4F40BF00B353E6; - remoteInfo = Helix; - }; - DA26EA9E2D4F40C000B353E6 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DA26EA7B2D4F40BF00B353E6 /* Project object */; - proxyType = 1; - remoteGlobalIDString = DA26EA822D4F40BF00B353E6; - remoteInfo = Helix; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - DA26EA832D4F40BF00B353E6 /* Helix.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Helix.app; sourceTree = BUILT_PRODUCTS_DIR; }; - DA26EA932D4F40C000B353E6 /* HelixTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HelixTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DA26EA9D2D4F40C000B353E6 /* HelixUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HelixUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFileSystemSynchronizedRootGroup section */ - DA26EA852D4F40BF00B353E6 /* Helix */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = Helix; - sourceTree = ""; - }; - DA26EA962D4F40C000B353E6 /* HelixTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = HelixTests; - sourceTree = ""; - }; - DA26EAA02D4F40C000B353E6 /* HelixUITests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = HelixUITests; - sourceTree = ""; - }; -/* End PBXFileSystemSynchronizedRootGroup section */ - -/* Begin PBXFrameworksBuildPhase section */ - DA26EA802D4F40BF00B353E6 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DA26EA902D4F40C000B353E6 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DA26EA9A2D4F40C000B353E6 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - DA26EA7A2D4F40BF00B353E6 = { - isa = PBXGroup; - children = ( - DA26EA852D4F40BF00B353E6 /* Helix */, - DA26EA962D4F40C000B353E6 /* HelixTests */, - DA26EAA02D4F40C000B353E6 /* HelixUITests */, - DA26EA842D4F40BF00B353E6 /* Products */, - ); - sourceTree = ""; - }; - DA26EA842D4F40BF00B353E6 /* Products */ = { - isa = PBXGroup; - children = ( - DA26EA832D4F40BF00B353E6 /* Helix.app */, - DA26EA932D4F40C000B353E6 /* HelixTests.xctest */, - DA26EA9D2D4F40C000B353E6 /* HelixUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - DA26EA822D4F40BF00B353E6 /* Helix */ = { - isa = PBXNativeTarget; - buildConfigurationList = DA26EAA72D4F40C000B353E6 /* Build configuration list for PBXNativeTarget "Helix" */; - buildPhases = ( - DA26EA7F2D4F40BF00B353E6 /* Sources */, - DA26EA802D4F40BF00B353E6 /* Frameworks */, - DA26EA812D4F40BF00B353E6 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - fileSystemSynchronizedGroups = ( - DA26EA852D4F40BF00B353E6 /* Helix */, - ); - name = Helix; - packageProductDependencies = ( - ); - productName = Helix; - productReference = DA26EA832D4F40BF00B353E6 /* Helix.app */; - productType = "com.apple.product-type.application"; - }; - DA26EA922D4F40C000B353E6 /* HelixTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = DA26EAAA2D4F40C000B353E6 /* Build configuration list for PBXNativeTarget "HelixTests" */; - buildPhases = ( - DA26EA8F2D4F40C000B353E6 /* Sources */, - DA26EA902D4F40C000B353E6 /* Frameworks */, - DA26EA912D4F40C000B353E6 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - DA26EA952D4F40C000B353E6 /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - DA26EA962D4F40C000B353E6 /* HelixTests */, - ); - name = HelixTests; - packageProductDependencies = ( - ); - productName = HelixTests; - productReference = DA26EA932D4F40C000B353E6 /* HelixTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - DA26EA9C2D4F40C000B353E6 /* HelixUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = DA26EAAD2D4F40C000B353E6 /* Build configuration list for PBXNativeTarget "HelixUITests" */; - buildPhases = ( - DA26EA992D4F40C000B353E6 /* Sources */, - DA26EA9A2D4F40C000B353E6 /* Frameworks */, - DA26EA9B2D4F40C000B353E6 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - DA26EA9F2D4F40C000B353E6 /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - DA26EAA02D4F40C000B353E6 /* HelixUITests */, - ); - name = HelixUITests; - packageProductDependencies = ( - ); - productName = HelixUITests; - productReference = DA26EA9D2D4F40C000B353E6 /* HelixUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - DA26EA7B2D4F40BF00B353E6 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1620; - LastUpgradeCheck = 1620; - TargetAttributes = { - DA26EA822D4F40BF00B353E6 = { - CreatedOnToolsVersion = 16.2; - }; - DA26EA922D4F40C000B353E6 = { - CreatedOnToolsVersion = 16.2; - TestTargetID = DA26EA822D4F40BF00B353E6; - }; - DA26EA9C2D4F40C000B353E6 = { - CreatedOnToolsVersion = 16.2; - TestTargetID = DA26EA822D4F40BF00B353E6; - }; - }; - }; - buildConfigurationList = DA26EA7E2D4F40BF00B353E6 /* Build configuration list for PBXProject "Helix" */; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = DA26EA7A2D4F40BF00B353E6; - minimizedProjectReferenceProxies = 1; - preferredProjectObjectVersion = 77; - productRefGroup = DA26EA842D4F40BF00B353E6 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - DA26EA822D4F40BF00B353E6 /* Helix */, - DA26EA922D4F40C000B353E6 /* HelixTests */, - DA26EA9C2D4F40C000B353E6 /* HelixUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - DA26EA812D4F40BF00B353E6 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DA26EA912D4F40C000B353E6 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DA26EA9B2D4F40C000B353E6 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - DA26EA7F2D4F40BF00B353E6 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DA26EA8F2D4F40C000B353E6 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - DA26EA992D4F40C000B353E6 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - DA26EA952D4F40C000B353E6 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DA26EA822D4F40BF00B353E6 /* Helix */; - targetProxy = DA26EA942D4F40C000B353E6 /* PBXContainerItemProxy */; - }; - DA26EA9F2D4F40C000B353E6 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DA26EA822D4F40BF00B353E6 /* Helix */; - targetProxy = DA26EA9E2D4F40C000B353E6 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - DA26EAA52D4F40C000B353E6 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - DA26EAA62D4F40C000B353E6 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - DA26EAA82D4F40C000B353E6 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Helix/Preview Content\""; - DEVELOPMENT_TEAM = 4SA9UFLZMT; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ArtJiang.Helix; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - DA26EAA92D4F40C000B353E6 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"Helix/Preview Content\""; - DEVELOPMENT_TEAM = 4SA9UFLZMT; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ArtJiang.Helix; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - DA26EAAB2D4F40C000B353E6 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4SA9UFLZMT; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ArtJiang.HelixTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Helix.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Helix"; - }; - name = Debug; - }; - DA26EAAC2D4F40C000B353E6 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4SA9UFLZMT; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.2; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ArtJiang.HelixTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Helix.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Helix"; - }; - name = Release; - }; - DA26EAAE2D4F40C000B353E6 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4SA9UFLZMT; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ArtJiang.HelixUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Helix; - }; - name = Debug; - }; - DA26EAAF2D4F40C000B353E6 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 4SA9UFLZMT; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = ArtJiang.HelixUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Helix; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - DA26EA7E2D4F40BF00B353E6 /* Build configuration list for PBXProject "Helix" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DA26EAA52D4F40C000B353E6 /* Debug */, - DA26EAA62D4F40C000B353E6 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DA26EAA72D4F40C000B353E6 /* Build configuration list for PBXNativeTarget "Helix" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DA26EAA82D4F40C000B353E6 /* Debug */, - DA26EAA92D4F40C000B353E6 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DA26EAAA2D4F40C000B353E6 /* Build configuration list for PBXNativeTarget "HelixTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DA26EAAB2D4F40C000B353E6 /* Debug */, - DA26EAAC2D4F40C000B353E6 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - DA26EAAD2D4F40C000B353E6 /* Build configuration list for PBXNativeTarget "HelixUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DA26EAAE2D4F40C000B353E6 /* Debug */, - DA26EAAF2D4F40C000B353E6 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = DA26EA7B2D4F40BF00B353E6 /* Project object */; -} diff --git a/Helix/Assets.xcassets/AccentColor.colorset/Contents.json b/Helix/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/Helix/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Helix/Assets.xcassets/AppIcon.appiconset/Contents.json b/Helix/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 2305880..0000000 --- a/Helix/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "tinted" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Helix/Assets.xcassets/Contents.json b/Helix/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Helix/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Helix/ContentView.swift b/Helix/ContentView.swift deleted file mode 100644 index 85189c1..0000000 --- a/Helix/ContentView.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ContentView.swift -// Helix -// -// - -import SwiftUI - -struct ContentView: View { - @StateObject private var appCoordinator = AppCoordinator() - - var body: some View { - NavigationStack { - MainTabView() - .environmentObject(appCoordinator) - } - } -} - -#Preview { - ContentView() -} diff --git a/Helix/Core/AI/ClaimDetectionService.swift b/Helix/Core/AI/ClaimDetectionService.swift deleted file mode 100644 index b49894c..0000000 --- a/Helix/Core/AI/ClaimDetectionService.swift +++ /dev/null @@ -1,417 +0,0 @@ -import Foundation -import Combine -import NaturalLanguage - -class ClaimDetectionService { - private let nlProcessor = NLTagger(tagSchemes: [.nameType, .lexicalClass]) - private let semanticAnalyzer = SemanticAnalyzer() - private let patternMatcher = PatternMatcher() - - func detectClaims(in text: String) -> AnyPublisher<[FactualClaim], LLMError> { - return Future<[FactualClaim], LLMError> { [weak self] promise in - guard let self = self else { - promise(.failure(.serviceUnavailable)) - return - } - - DispatchQueue.global(qos: .userInitiated).async { - let claims = self.performClaimDetection(in: text) - promise(.success(claims)) - } - } - .eraseToAnyPublisher() - } - - private func performClaimDetection(in text: String) -> [FactualClaim] { - var detectedClaims: [FactualClaim] = [] - - // 1. Pattern-based detection - let patternClaims = patternMatcher.detectClaims(in: text) - detectedClaims.append(contentsOf: patternClaims) - - // 2. Semantic analysis - let semanticClaims = semanticAnalyzer.detectClaims(in: text) - detectedClaims.append(contentsOf: semanticClaims) - - // 3. Named entity recognition - let entityClaims = detectEntityBasedClaims(in: text) - detectedClaims.append(contentsOf: entityClaims) - - // 4. Statistical statement detection - let statisticalClaims = detectStatisticalClaims(in: text) - detectedClaims.append(contentsOf: statisticalClaims) - - // Remove duplicates and filter by confidence - return deduplicateAndFilter(claims: detectedClaims) - } - - private func detectEntityBasedClaims(in text: String) -> [FactualClaim] { - nlProcessor.string = text - var claims: [FactualClaim] = [] - - let range = text.startIndex.. [FactualClaim] { - var claims: [FactualClaim] = [] - - // Patterns for statistical claims - let statisticalPatterns = [ - #"\b\d+(?:\.\d+)?%"#, // Percentages - #"\b\d+(?:,\d{3})*(?:\.\d+)?\s+(?:million|billion|trillion|thousand)"#, // Large numbers - #"\b\d+(?:\.\d+)?\s+(?:times|fold)"#, // Multipliers - #"\b(?:increased|decreased|rose|fell|grew|dropped)\s+by\s+\d+(?:\.\d+)?%?"#, // Change statistics - #"\b\d+(?:\.\d+)?\s+(?:degrees|celsius|fahrenheit)"#, // Temperature - #"\b\d{4}\s+(?:years?|AD|BC|CE|BCE)"#, // Years/dates - ] - - for pattern in statisticalPatterns { - let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) - let nsRange = NSRange(text.startIndex.., category: ClaimCategory) -> FactualClaim? { - // Extract sentence containing the entity - let sentenceRange = expandToSentence(from: range, in: text) - let sentence = String(text[sentenceRange]) - - // Check if sentence contains factual indicators - let factualIndicators = [ - "is", "was", "are", "were", "has", "have", "had", - "contains", "includes", "measures", "weighs", - "born", "died", "founded", "established", - "located", "situated", "discovered", "invented" - ] - - let lowercaseSentence = sentence.lowercased() - let containsFactualIndicator = factualIndicators.contains { lowercaseSentence.contains($0) } - - if containsFactualIndicator { - return FactualClaim( - text: sentence.trimmingCharacters(in: .whitespacesAndNewlines), - confidence: 0.6, - category: category, - extractionMethod: .entityRecognition, - context: sentence, - position: ClaimPosition( - startIndex: sentenceRange.lowerBound, - endIndex: sentenceRange.upperBound, - characterRange: NSRange(sentenceRange, in: text) - ) - ) - } - - return nil - } - - private func mapEntityToCategory(_ tag: NLTag) -> ClaimCategory { - switch tag { - case .personalName: - return .biographical - case .placeName: - return .geographical - case .organizationName: - return .general - default: - return .general - } - } - - private func expandToSentence(from range: Range, in text: String) -> Range { - let sentenceEnders: Set = [".", "!", "?"] - - // Find sentence start - var start = range.lowerBound - while start > text.startIndex { - let prevIndex = text.index(before: start) - if sentenceEnders.contains(text[prevIndex]) { - break - } - start = prevIndex - } - - // Find sentence end - var end = range.upperBound - while end < text.endIndex { - if sentenceEnders.contains(text[end]) { - end = text.index(after: end) - break - } - end = text.index(after: end) - } - - return start.., in text: String, contextWords: Int = 10) -> String { - let words = text.components(separatedBy: .whitespacesAndNewlines) - let claimText = String(text[range]) - - // Find the claim in the words array - guard let claimWordIndex = words.firstIndex(where: { claimText.contains($0) }) else { - return claimText - } - - let startIndex = max(0, claimWordIndex - contextWords) - let endIndex = min(words.count, claimWordIndex + contextWords) - - return words[startIndex.. [FactualClaim] { - var uniqueClaims: [FactualClaim] = [] - let minConfidence: Float = 0.5 - - for claim in claims { - // Filter by confidence - guard claim.confidence >= minConfidence else { continue } - - // Check for duplicates - let isDuplicate = uniqueClaims.contains { existingClaim in - let similarity = calculateSimilarity(claim.text, existingClaim.text) - return similarity > 0.8 - } - - if !isDuplicate { - uniqueClaims.append(claim) - } - } - - // Sort by confidence - return uniqueClaims.sorted { $0.confidence > $1.confidence } - } - - private func calculateSimilarity(_ text1: String, _ text2: String) -> Float { - let words1 = Set(text1.lowercased().components(separatedBy: .whitespacesAndNewlines)) - let words2 = Set(text2.lowercased().components(separatedBy: .whitespacesAndNewlines)) - - let intersection = words1.intersection(words2) - let union = words1.union(words2) - - return union.isEmpty ? 0.0 : Float(intersection.count) / Float(union.count) - } -} - -// MARK: - Pattern Matcher - -class PatternMatcher { - private let factualPatterns: [FactualPattern] = [ - // Geographical claims - FactualPattern( - pattern: #"\b\w+\s+is\s+(?:located|situated)\s+in\s+\w+"#, - category: .geographical, - confidence: 0.8 - ), - FactualPattern( - pattern: #"\b\w+\s+has\s+a\s+population\s+of\s+[\d,]+"#, - category: .statistical, - confidence: 0.9 - ), - - // Historical claims - FactualPattern( - pattern: #"\b\w+\s+(?:was\s+born|died)\s+in\s+\d{4}"#, - category: .biographical, - confidence: 0.8 - ), - FactualPattern( - pattern: #"\b\w+\s+(?:founded|established)\s+in\s+\d{4}"#, - category: .historical, - confidence: 0.8 - ), - - // Scientific claims - FactualPattern( - pattern: #"\b\w+\s+(?:boils|melts|freezes)\s+at\s+\d+(?:\.\d+)?\s+degrees"#, - category: .scientific, - confidence: 0.9 - ), - FactualPattern( - pattern: #"\b\w+\s+(?:weighs|measures)\s+\d+(?:\.\d+)?\s+\w+"#, - category: .scientific, - confidence: 0.7 - ), - - // General factual statements - FactualPattern( - pattern: #"\b(?:there\s+are|there\s+were)\s+\d+\s+\w+"#, - category: .statistical, - confidence: 0.7 - ), - FactualPattern( - pattern: #"\b\w+\s+is\s+the\s+(?:capital|largest|smallest)\s+\w+\s+in\s+\w+"#, - category: .geographical, - confidence: 0.8 - ) - ] - - func detectClaims(in text: String) -> [FactualClaim] { - var claims: [FactualClaim] = [] - - for pattern in factualPatterns { - let regex = try? NSRegularExpression(pattern: pattern.pattern, options: [.caseInsensitive]) - let nsRange = NSRange(text.startIndex.. [FactualClaim] { - guard let embedding = embedding else { return [] } - - var claims: [FactualClaim] = [] - - // Split into sentences - let sentences = text.components(separatedBy: CharacterSet(charactersIn: ".!?")) - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - - for sentence in sentences { - if let claim = analyzeSemanticContent(sentence, embedding: embedding) { - claims.append(claim) - } - } - - return claims - } - - private func analyzeSemanticContent(_ sentence: String, embedding: NLEmbedding) -> FactualClaim? { - // Keywords that often indicate factual claims - let factualKeywords = [ - "is", "was", "are", "were", "has", "have", "contains", - "measures", "weighs", "located", "founded", "born", "died", - "discovered", "invented", "established", "population", "temperature" - ] - - let words = sentence.lowercased().components(separatedBy: .whitespacesAndNewlines) - let factualWordCount = words.filter { factualKeywords.contains($0) }.count - - // Calculate semantic confidence based on factual keyword density - let confidence = min(Float(factualWordCount) / Float(words.count) * 2.0, 1.0) - - guard confidence > 0.3 else { return nil } - - // Determine category based on semantic content - let category = determineSemanticCategory(sentence, embedding: embedding) - - return FactualClaim( - text: sentence, - confidence: confidence, - category: category, - extractionMethod: .semanticAnalysis, - context: sentence, - position: ClaimPosition( - startIndex: sentence.startIndex, - endIndex: sentence.endIndex, - characterRange: NSRange(location: 0, length: sentence.count) - ) - ) - } - - private func determineSemanticCategory(_ sentence: String, embedding: NLEmbedding) -> ClaimCategory { - let categoryKeywords: [ClaimCategory: [String]] = [ - .statistical: ["number", "percent", "population", "million", "billion", "thousand"], - .geographical: ["located", "country", "city", "river", "mountain", "continent"], - .historical: ["year", "century", "founded", "established", "war", "battle"], - .scientific: ["temperature", "weight", "mass", "discovery", "element", "formula"], - .biographical: ["born", "died", "age", "person", "author", "president", "leader"], - .financial: ["cost", "price", "money", "dollar", "economy", "market"], - .medical: ["disease", "treatment", "medicine", "health", "symptom", "therapy"] - ] - - let words = sentence.lowercased().components(separatedBy: .whitespacesAndNewlines) - - var bestCategory: ClaimCategory = .general - var maxScore = 0 - - for (category, keywords) in categoryKeywords { - let score = keywords.filter { keyword in - words.contains { $0.contains(keyword) } - }.count - - if score > maxScore { - maxScore = score - bestCategory = category - } - } - - return bestCategory - } -} \ No newline at end of file diff --git a/Helix/Core/AI/LLMService.swift b/Helix/Core/AI/LLMService.swift deleted file mode 100644 index 1436a55..0000000 --- a/Helix/Core/AI/LLMService.swift +++ /dev/null @@ -1,756 +0,0 @@ -import Foundation -import Combine - -protocol LLMServiceProtocol { - func analyzeConversation(_ context: ConversationContext) -> AnyPublisher - func analyzeWithCustomPrompt(_ prompt: String, context: ConversationContext) -> AnyPublisher - func factCheck(_ claim: String, context: ConversationContext?) -> AnyPublisher - func summarizeConversation(_ messages: [ConversationMessage]) -> AnyPublisher - func detectClaims(in text: String) -> AnyPublisher<[FactualClaim], LLMError> - func extractActionItems(from messages: [ConversationMessage]) -> AnyPublisher<[ActionItem], LLMError> - func setCurrentPersona(_ persona: AIPersona) - func generatePersonalizedResponse(_ messages: [ConversationMessage], conversationContext: Helix.ConversationContext) -> AnyPublisher -} - -struct ConversationContext { - let messages: [ConversationMessage] - let speakers: [Speaker] - let metadata: ConversationMetadata - let analysisType: AnalysisType - let timestamp: TimeInterval - - init(messages: [ConversationMessage], speakers: [Speaker], analysisType: AnalysisType, metadata: ConversationMetadata = ConversationMetadata()) { - self.messages = messages - self.speakers = speakers - self.analysisType = analysisType - self.metadata = metadata - self.timestamp = Date().timeIntervalSince1970 - } -} - -struct ConversationMetadata { - let sessionId: UUID - let location: String? - let tags: [String] - let priority: AnalysisPriority - - init(sessionId: UUID = UUID(), location: String? = nil, tags: [String] = [], priority: AnalysisPriority = .medium) { - self.sessionId = sessionId - self.location = location - self.tags = tags - self.priority = priority - } -} - -enum AnalysisType: String, CaseIterable { - case factCheck = "fact_check" - case summarization = "summarization" - case actionItems = "action_items" - case sentiment = "sentiment" - case keyTopics = "key_topics" - case translation = "translation" - case clarification = "clarification" -} - -enum AnalysisPriority: String { - case low = "low" - case medium = "medium" - case high = "high" - case critical = "critical" -} - -struct AnalysisResult { - let id: UUID - let type: AnalysisType - let content: AnalysisContent - let confidence: Float - let sources: [Source] - let timestamp: TimeInterval - let processingTime: TimeInterval - let provider: LLMProvider - - init(type: AnalysisType, content: AnalysisContent, confidence: Float = 0.0, sources: [Source] = [], provider: LLMProvider = .openai) { - self.id = UUID() - self.type = type - self.content = content - self.confidence = confidence - self.sources = sources - self.timestamp = Date().timeIntervalSince1970 - self.processingTime = 0.0 - self.provider = provider - } -} - -enum AnalysisContent { - case factCheck(FactCheckResult) - case summary(String) - case actionItems([ActionItem]) - case sentiment(SentimentAnalysis) - case topics([String]) - case translation(TranslationResult) - case text(String) -} - -struct FactCheckResult { - let claim: String - let isAccurate: Bool - let explanation: String - let sources: [VerificationSource] - let confidence: Float - let alternativeInfo: String? - let category: ClaimCategory - let severity: FactCheckSeverity - - enum FactCheckSeverity { - case minor - case significant - case critical - } -} - -struct FactualClaim { - let text: String - let confidence: Float - let category: ClaimCategory - let extractionMethod: ExtractionMethod - let context: String - let position: ClaimPosition -} - -struct ClaimPosition { - let startIndex: String.Index - let endIndex: String.Index - let characterRange: NSRange -} - -enum ClaimCategory: String, CaseIterable { - case statistical = "statistical" - case historical = "historical" - case scientific = "scientific" - case geographical = "geographical" - case biographical = "biographical" - case general = "general" - case financial = "financial" - case medical = "medical" - case legal = "legal" -} - -enum ExtractionMethod { - case patternMatching - case semanticAnalysis - case entityRecognition - case contextualAnalysis -} - -struct VerificationSource { - let title: String - let url: String? - let reliability: SourceReliability - let lastUpdated: Date? - let summary: String? -} - -enum SourceReliability: String { - case high = "high" - case medium = "medium" - case low = "low" - case unknown = "unknown" -} - -struct ActionItem { - let id: UUID - let description: String - let assignee: UUID? - let dueDate: Date? - let priority: ActionItemPriority - let category: ActionItemCategory - let status: ActionItemStatus - - init(description: String, assignee: UUID? = nil, dueDate: Date? = nil, priority: ActionItemPriority = .medium, category: ActionItemCategory = .general) { - self.id = UUID() - self.description = description - self.assignee = assignee - self.dueDate = dueDate - self.priority = priority - self.category = category - self.status = .pending - } -} - -enum ActionItemPriority: String { - case low = "low" - case medium = "medium" - case high = "high" - case urgent = "urgent" -} - -enum ActionItemCategory: String { - case general = "general" - case followUp = "follow_up" - case decision = "decision" - case research = "research" - case communication = "communication" -} - -enum ActionItemStatus: String { - case pending = "pending" - case inProgress = "in_progress" - case completed = "completed" - case cancelled = "cancelled" -} - -struct SentimentAnalysis { - let overallSentiment: Sentiment - let speakerSentiments: [UUID: Sentiment] - let emotionalTone: EmotionalTone - let confidence: Float -} - -enum Sentiment: String { - case positive = "positive" - case negative = "negative" - case neutral = "neutral" - case mixed = "mixed" -} - -enum EmotionalTone: String { - case formal = "formal" - case casual = "casual" - case tense = "tense" - case relaxed = "relaxed" - case excited = "excited" - case concerned = "concerned" -} - -struct TranslationResult { - let originalText: String - let translatedText: String - let sourceLanguage: String - let targetLanguage: String - let confidence: Float -} - -struct Source { - let id: UUID - let title: String - let url: String? - let type: SourceType - let reliability: SourceReliability - - init(title: String, url: String? = nil, type: SourceType = .web, reliability: SourceReliability = .medium) { - self.id = UUID() - self.title = title - self.url = url - self.type = type - self.reliability = reliability - } -} - -enum SourceType: String { - case web = "web" - case academic = "academic" - case news = "news" - case government = "government" - case encyclopedia = "encyclopedia" - case database = "database" -} - -enum LLMProvider: String, CaseIterable { - case openai = "openai" - case anthropic = "anthropic" - case local = "local" - - var displayName: String { - switch self { - case .openai: return "OpenAI" - case .anthropic: return "Anthropic" - case .local: return "Local Model" - } - } -} - -enum LLMError: Error { - case networkError(Error) - case authenticationFailed - case rateLimitExceeded - case modelUnavailable - case invalidRequest - case responseParsingFailed - case contextTooLarge - case serviceUnavailable - case quotaExceeded - - var localizedDescription: String { - switch self { - case .networkError(let error): - return "Network error: \(error.localizedDescription)" - case .authenticationFailed: - return "Authentication failed" - case .rateLimitExceeded: - return "Rate limit exceeded" - case .modelUnavailable: - return "Model unavailable" - case .invalidRequest: - return "Invalid request" - case .responseParsingFailed: - return "Failed to parse response" - case .contextTooLarge: - return "Context too large for model" - case .serviceUnavailable: - return "Service unavailable" - case .quotaExceeded: - return "Usage quota exceeded" - } - } -} - -// MARK: - LLM Service Implementation - -class LLMService: LLMServiceProtocol { - private let providers: [LLMProvider: LLMProviderProtocol] - private let rateLimiter: RateLimiter - private let cacheManager: LLMCacheManager - private let configManager: LLMConfigManager - private let promptManager: PromptManagerProtocol - private let contextDetector: ContextDetectorProtocol - - private var currentProvider: LLMProvider = .openai - private let fallbackProviders: [LLMProvider] = [.anthropic, .openai] - private var currentPersona: AIPersona? - - init(providers: [LLMProvider: LLMProviderProtocol], - promptManager: PromptManagerProtocol = PromptManager(), - contextDetector: ContextDetectorProtocol = ContextDetector(), - rateLimiter: RateLimiter = RateLimiter(), - cacheManager: LLMCacheManager = LLMCacheManager()) { - self.providers = providers - self.promptManager = promptManager - self.contextDetector = contextDetector - self.rateLimiter = rateLimiter - self.cacheManager = cacheManager - self.configManager = LLMConfigManager() - } - - func analyzeConversation(_ context: ConversationContext) -> AnyPublisher { - // Check cache first - if let cachedResult = cacheManager.getCachedResult(for: context) { - return Just(cachedResult) - .setFailureType(to: LLMError.self) - .eraseToAnyPublisher() - } - - // Select appropriate provider based on analysis type - let provider = selectProvider(for: context.analysisType) - - return executeWithFallback(context: context, providers: [provider] + fallbackProviders) - .handleEvents(receiveOutput: { [weak self] result in - self?.cacheManager.cacheResult(result, for: context) - }) - .eraseToAnyPublisher() - } - - // MARK: - Convenience LLMServiceProtocol methods - func factCheck(_ claim: String, context: ConversationContext? = nil) -> AnyPublisher { - // Build minimal context if none provided - let ctx: ConversationContext = context ?? ConversationContext( - messages: [ConversationMessage(id: UUID(), content: claim, speakerId: nil, - confidence: 1.0, timestamp: Date().timeIntervalSince1970, - isFinal: true, wordTimings: [], originalText: claim)], - speakers: [], analysisType: .factCheck - ) - return analyzeConversation(ctx) - .tryMap { result in - guard case .factCheck(let fc) = result.content else { - throw LLMError.responseParsingFailed - } - return fc - } - .mapError { $0 as? LLMError ?? .responseParsingFailed } - .eraseToAnyPublisher() - } - - func summarizeConversation(_ messages: [ConversationMessage]) -> AnyPublisher { - let ctx = ConversationContext(messages: messages, speakers: [], analysisType: .summarization) - return analyzeConversation(ctx) - .tryMap { result in - if case .summary(let text) = result.content { - return text - } - throw LLMError.responseParsingFailed - } - .mapError { $0 as? LLMError ?? .responseParsingFailed } - .eraseToAnyPublisher() - } - - func detectClaims(in text: String) -> AnyPublisher<[FactualClaim], LLMError> { - // Simple rule-based claim detection: sentences containing digits - let sentences = text.components(separatedBy: CharacterSet(charactersIn: ".!?")) - let claims = sentences.compactMap { sentence -> FactualClaim? in - guard sentence.rangeOfCharacter(from: .decimalDigits) != nil else { return nil } - let trimmed = sentence.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return nil } - let start = text.range(of: trimmed)?.lowerBound ?? text.startIndex - let end = text.range(of: trimmed)?.upperBound ?? text.endIndex - let nsRange = NSRange(start.. AnyPublisher<[ActionItem], LLMError> { - let items = messages.map { msg in - ActionItem(description: msg.content, assignee: msg.speakerId) - } - return Just(items) - .setFailureType(to: LLMError.self) - .eraseToAnyPublisher() - } - - func analyzeWithCustomPrompt(_ prompt: String, context: ConversationContext) -> AnyPublisher { - // Use prompt manager to build a custom context - let customCtx = ConversationContext(messages: context.messages, speakers: context.speakers, analysisType: context.analysisType) - return analyzeConversation(customCtx) - } - - func setCurrentPersona(_ persona: AIPersona) { - currentPersona = persona - } - - func generatePersonalizedResponse(_ messages: [ConversationMessage], conversationContext: ConversationContext) -> AnyPublisher { - let ctx = ConversationContext(messages: messages, speakers: conversationContext.speakers, analysisType: .clarification) - return analyzeConversation(ctx) - .tryMap { result in - switch result.content { - case .text(let str): return str - default: return "" - } - } - .mapError { $0 as? LLMError ?? .responseParsingFailed } - .eraseToAnyPublisher() - } - - func factCheck(_ claim: String, context: ConversationContext?) -> AnyPublisher { - let analysisContext = ConversationContext( - messages: context?.messages ?? [], - speakers: context?.speakers ?? [], - analysisType: .factCheck - ) - - return analyzeConversation(analysisContext) - .compactMap { result in - if case .factCheck(let factCheckResult) = result.content { - return factCheckResult - } else { - return nil - } - } - .mapError { $0 as LLMError } - .eraseToAnyPublisher() - } - - func summarizeConversation(_ messages: [ConversationMessage]) -> AnyPublisher { - let context = ConversationContext( - messages: messages, - speakers: [], - analysisType: .summarization - ) - - return analyzeConversation(context) - .compactMap { result in - if case .summary(let summary) = result.content { - return summary - } else { - return nil - } - } - .eraseToAnyPublisher() - } - - func detectClaims(in text: String) -> AnyPublisher<[FactualClaim], LLMError> { - let claimDetector = ClaimDetectionService() - return claimDetector.detectClaims(in: text) - } - - func extractActionItems(from messages: [ConversationMessage]) -> AnyPublisher<[ActionItem], LLMError> { - let context = ConversationContext( - messages: messages, - speakers: [], - analysisType: .actionItems - ) - - return analyzeConversation(context) - .compactMap { result in - if case .actionItems(let items) = result.content { - return items - } else { - return nil - } - } - .eraseToAnyPublisher() - } - - func analyzeWithCustomPrompt(_ prompt: String, context: ConversationContext) -> AnyPublisher { - // Create enhanced context with custom prompt - var enhancedContext = context - enhancedContext.metadata.tags.append("custom_prompt") - - // Use current persona if available, otherwise create temporary one - let persona = currentPersona ?? AIPersona( - name: "Custom Assistant", - description: "Custom prompt analysis", - systemPrompt: prompt, - tone: .balanced - ) - - return executeWithFallback(context: enhancedContext, providers: [currentProvider] + fallbackProviders) - .eraseToAnyPublisher() - } - - func setCurrentPersona(_ persona: AIPersona) { - currentPersona = persona - promptManager.setCurrentPersona(persona) - } - - func generatePersonalizedResponse(_ messages: [ConversationMessage], conversationContext: Helix.ConversationContext) -> AnyPublisher { - // Detect conversation context automatically - let detectedContext = contextDetector.detectContext(from: messages) - - // Generate personalized prompt using current persona and context - let systemPrompt = promptManager.generatePrompt(for: detectedContext, with: [ - "conversation_type": detectedContext.description, - "speaker_count": "\(conversationContext.speakers.count)", - "message_count": "\(messages.count)" - ]) - - // Create analysis context for response generation - let analysisContext = ConversationContext( - messages: messages, - speakers: conversationContext.speakers, - analysisType: .clarification, - metadata: ConversationMetadata(tags: ["personalized", "response_generation"]) - ) - - return analyzeWithCustomPrompt(systemPrompt, context: analysisContext) - .compactMap { result in - if case .text(let response) = result.content { - return response - } else { - return "Generated response based on conversation analysis" - } - } - .eraseToAnyPublisher() - } - - private func selectProvider(for analysisType: AnalysisType) -> LLMProvider { - switch analysisType { - case .factCheck: - return .anthropic // Claude is good for fact-checking - case .summarization, .actionItems: - return .openai // GPT is good for structured tasks - case .sentiment, .keyTopics: - return currentProvider - case .translation: - return .openai - case .clarification: - return .anthropic - } - } - - private func executeWithFallback(context: ConversationContext, providers: [LLMProvider]) -> AnyPublisher { - guard let firstProvider = providers.first, - let service = self.providers[firstProvider] else { - return Fail(error: LLMError.serviceUnavailable) - .eraseToAnyPublisher() - } - - return rateLimiter.execute { - service.analyze(context) - } - .catch { error -> AnyPublisher in - let remainingProviders = Array(providers.dropFirst()) - if !remainingProviders.isEmpty { - print("Provider \(firstProvider) failed, trying fallback: \(error)") - return self.executeWithFallback(context: context, providers: remainingProviders) - } else { - return Fail(error: error).eraseToAnyPublisher() - } - } - .eraseToAnyPublisher() - } -} - -// MARK: - LLM Provider Protocol - -protocol LLMProviderProtocol { - var provider: LLMProvider { get } - func analyze(_ context: ConversationContext) -> AnyPublisher - func isAvailable() -> Bool - func estimateCost(for context: ConversationContext) -> Float -} - -// MARK: - Supporting Services - -class RateLimiter { - private let maxRequestsPerMinute: Int = 60 - private let maxRequestsPerHour: Int = 1000 - private var requestTimestamps: [Date] = [] - private let queue = DispatchQueue(label: "rate.limiter", attributes: .concurrent) - - func execute(_ operation: @escaping () -> AnyPublisher) -> AnyPublisher { - return Future { [weak self] promise in - self?.queue.async(flags: .barrier) { - guard let self = self else { - promise(.failure(.serviceUnavailable)) - return - } - - let now = Date() - - // Clean old timestamps - self.requestTimestamps = self.requestTimestamps.filter { timestamp in - now.timeIntervalSince(timestamp) < 3600 // 1 hour - } - - // Check rate limits - let recentRequests = self.requestTimestamps.filter { timestamp in - now.timeIntervalSince(timestamp) < 60 // 1 minute - } - - if recentRequests.count >= self.maxRequestsPerMinute { - promise(.failure(.rateLimitExceeded)) - return - } - - if self.requestTimestamps.count >= self.maxRequestsPerHour { - promise(.failure(.rateLimitExceeded)) - return - } - - // Add current request - self.requestTimestamps.append(now) - - // Execute operation - operation() - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - promise(.failure(error)) - } - }, - receiveValue: { value in - promise(.success(value)) - } - ) - .store(in: &Set()) - } - } - .eraseToAnyPublisher() - } -} - -class LLMCacheManager { - private var cache: [String: CachedResult] = [:] - private let cacheQueue = DispatchQueue(label: "llm.cache", attributes: .concurrent) - private let maxCacheSize = 100 - private let cacheExpirationTime: TimeInterval = 3600 // 1 hour - - struct CachedResult { - let result: AnalysisResult - let timestamp: Date - let accessCount: Int - - var isExpired: Bool { - Date().timeIntervalSince(timestamp) > 3600 - } - } - - func getCachedResult(for context: ConversationContext) -> AnalysisResult? { - let key = generateCacheKey(for: context) - - return cacheQueue.sync { - guard let cached = cache[key], !cached.isExpired else { - cache.removeValue(forKey: key) - return nil - } - - // Update access count - cache[key] = CachedResult( - result: cached.result, - timestamp: cached.timestamp, - accessCount: cached.accessCount + 1 - ) - - return cached.result - } - } - - func cacheResult(_ result: AnalysisResult, for context: ConversationContext) { - let key = generateCacheKey(for: context) - - cacheQueue.async(flags: .barrier) { [weak self] in - guard let self = self else { return } - - // Clean expired entries - self.cleanExpiredEntries() - - // Add new entry - self.cache[key] = CachedResult( - result: result, - timestamp: Date(), - accessCount: 1 - ) - - // Maintain cache size - if self.cache.count > self.maxCacheSize { - self.evictLeastUsed() - } - } - } - - private func generateCacheKey(for context: ConversationContext) -> String { - let messagesHash = context.messages.map { $0.content }.joined().hash - return "\(context.analysisType.rawValue)_\(messagesHash)" - } - - private func cleanExpiredEntries() { - cache = cache.filter { !$0.value.isExpired } - } - - private func evictLeastUsed() { - guard let leastUsedKey = cache.min(by: { $0.value.accessCount < $1.value.accessCount })?.key else { - return - } - cache.removeValue(forKey: leastUsedKey) - } -} - -class LLMConfigManager { - struct LLMConfig { - let maxTokens: Int - let temperature: Float - let topP: Float - let frequencyPenalty: Float - let presencePenalty: Float - } - - private let configs: [AnalysisType: LLMConfig] = [ - .factCheck: LLMConfig(maxTokens: 500, temperature: 0.1, topP: 0.9, frequencyPenalty: 0.0, presencePenalty: 0.0), - .summarization: LLMConfig(maxTokens: 300, temperature: 0.3, topP: 0.9, frequencyPenalty: 0.0, presencePenalty: 0.0), - .actionItems: LLMConfig(maxTokens: 400, temperature: 0.2, topP: 0.9, frequencyPenalty: 0.0, presencePenalty: 0.0), - .sentiment: LLMConfig(maxTokens: 200, temperature: 0.1, topP: 0.9, frequencyPenalty: 0.0, presencePenalty: 0.0), - .keyTopics: LLMConfig(maxTokens: 300, temperature: 0.2, topP: 0.9, frequencyPenalty: 0.0, presencePenalty: 0.0) - ] - - func getConfig(for analysisType: AnalysisType) -> LLMConfig { - return configs[analysisType] ?? LLMConfig(maxTokens: 400, temperature: 0.3, topP: 0.9, frequencyPenalty: 0.0, presencePenalty: 0.0) - } -} \ No newline at end of file diff --git a/Helix/Core/AI/OpenAIProvider.swift b/Helix/Core/AI/OpenAIProvider.swift deleted file mode 100644 index 1ce572b..0000000 --- a/Helix/Core/AI/OpenAIProvider.swift +++ /dev/null @@ -1,482 +0,0 @@ -import Foundation -import Combine - -class OpenAIProvider: LLMProviderProtocol { - let provider: LLMProvider = .openai - - private let apiKey: String - private let baseURL = "https://api.openai.com/v1" - private let session = URLSession.shared - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - - private let model = "gpt-4" - private let maxRetries = 3 - - init(apiKey: String) { - self.apiKey = apiKey - encoder.keyEncodingStrategy = .convertToSnakeCase - decoder.keyDecodingStrategy = .convertFromSnakeCase - } - - func analyze(_ context: ConversationContext) -> AnyPublisher { - let prompt = buildPrompt(for: context) - let request = createChatCompletionRequest(prompt: prompt, analysisType: context.analysisType) - - return executeRequest(request) - .map { [weak self] response in - self?.parseResponse(response, for: context.analysisType) ?? AnalysisResult( - type: context.analysisType, - content: .text("Failed to parse response"), - provider: .openai - ) - } - .mapError { error in - self.mapError(error) - } - .eraseToAnyPublisher() - } - - func isAvailable() -> Bool { - return !apiKey.isEmpty - } - - func estimateCost(for context: ConversationContext) -> Float { - let promptTokens = estimateTokens(for: buildPrompt(for: context)) - let completionTokens = 500 // Estimated - - // GPT-4 pricing (approximate) - let inputCostPer1K: Float = 0.03 - let outputCostPer1K: Float = 0.06 - - let inputCost = Float(promptTokens) / 1000.0 * inputCostPer1K - let outputCost = Float(completionTokens) / 1000.0 * outputCostPer1K - - return inputCost + outputCost - } - - private func buildPrompt(for context: ConversationContext) -> String { - switch context.analysisType { - case .factCheck: - return buildFactCheckPrompt(context) - case .summarization: - return buildSummarizationPrompt(context) - case .actionItems: - return buildActionItemsPrompt(context) - case .sentiment: - return buildSentimentPrompt(context) - case .keyTopics: - return buildTopicsPrompt(context) - case .translation: - return buildTranslationPrompt(context) - case .clarification: - return buildClarificationPrompt(context) - } - } - - private func buildFactCheckPrompt(_ context: ConversationContext) -> String { - let conversationText = context.messages.map { message in - let speakerName = context.speakers.first(where: { $0.id == message.speakerId })?.name ?? "Unknown" - return "\(speakerName): \(message.content)" - }.joined(separator: "\n") - - return """ - You are a fact-checking expert. Analyze the following conversation and identify any factual claims that can be verified. For each claim, determine if it is accurate or inaccurate, provide an explanation, and cite reliable sources when possible. - - Conversation: - \(conversationText) - - For each factual claim you identify, respond with: - 1. The exact claim - 2. Whether it is accurate (true/false) - 3. A clear explanation - 4. Confidence level (0-1) - 5. Category of claim (statistical, historical, scientific, etc.) - 6. Alternative correct information if the claim is false - - Focus on verifiable facts rather than opinions or subjective statements. Be precise and cite authoritative sources when available. - - Response format: JSON array of fact-check results. - """ - } - - private func buildSummarizationPrompt(_ context: ConversationContext) -> String { - let conversationText = context.messages.map { message in - let speakerName = context.speakers.first(where: { $0.id == message.speakerId })?.name ?? "Unknown" - return "\(speakerName): \(message.content)" - }.joined(separator: "\n") - - return """ - Provide a concise summary of the following conversation. Include the main topics discussed, key decisions made, and important points raised by each participant. - - Conversation: - \(conversationText) - - Summary should be: - - 2-3 sentences maximum - - Focused on key outcomes and decisions - - Include speaker attribution for important points - - Professional and objective tone - """ - } - - private func buildActionItemsPrompt(_ context: ConversationContext) -> String { - let conversationText = context.messages.map { message in - let speakerName = context.speakers.first(where: { $0.id == message.speakerId })?.name ?? "Unknown" - return "\(speakerName): \(message.content)" - }.joined(separator: "\n") - - return """ - Extract action items from the following conversation. Identify tasks, commitments, follow-ups, and decisions that require action. - - Conversation: - \(conversationText) - - For each action item, provide: - 1. Clear description of the task - 2. Assigned person (if mentioned) - 3. Due date (if mentioned) - 4. Priority level (low/medium/high/urgent) - 5. Category (follow-up, decision, research, communication, etc.) - - Response format: JSON array of action items. - """ - } - - private func buildSentimentPrompt(_ context: ConversationContext) -> String { - let conversationText = context.messages.map { message in - let speakerName = context.speakers.first(where: { $0.id == message.speakerId })?.name ?? "Unknown" - return "\(speakerName): \(message.content)" - }.joined(separator: "\n") - - return """ - Analyze the sentiment and emotional tone of the following conversation. Provide overall sentiment and per-speaker analysis. - - Conversation: - \(conversationText) - - Analyze: - 1. Overall conversation sentiment (positive/negative/neutral/mixed) - 2. Individual speaker sentiments - 3. Emotional tone (formal/casual/tense/relaxed/excited/concerned) - 4. Confidence level of analysis - - Response format: JSON with sentiment analysis results. - """ - } - - private func buildTopicsPrompt(_ context: ConversationContext) -> String { - let conversationText = context.messages.map { message in - let speakerName = context.speakers.first(where: { $0.id == message.speakerId })?.name ?? "Unknown" - return "\(speakerName): \(message.content)" - }.joined(separator: "\n") - - return """ - Extract the main topics and themes discussed in the following conversation. - - Conversation: - \(conversationText) - - Identify: - 1. 3-5 main topics - 2. Key themes or subjects - 3. Important concepts mentioned - 4. Areas of focus or emphasis - - Response format: JSON array of topic strings. - """ - } - - private func buildTranslationPrompt(_ context: ConversationContext) -> String { - let conversationText = context.messages.map { $0.content }.joined(separator: "\n") - - return """ - Translate the following text to English (if not already in English) or identify the language and provide a high-quality translation. - - Text: - \(conversationText) - - Provide: - 1. Source language identification - 2. High-quality translation - 3. Confidence level - 4. Any cultural context notes if relevant - - Response format: JSON with translation results. - """ - } - - private func buildClarificationPrompt(_ context: ConversationContext) -> String { - let conversationText = context.messages.map { message in - let speakerName = context.speakers.first(where: { $0.id == message.speakerId })?.name ?? "Unknown" - return "\(speakerName): \(message.content)" - }.joined(separator: "\n") - - return """ - Analyze the following conversation for areas that might need clarification or follow-up questions. - - Conversation: - \(conversationText) - - Identify: - 1. Unclear statements or ambiguous references - 2. Missing context or incomplete information - 3. Potential misunderstandings - 4. Areas that might benefit from follow-up questions - - Suggest clarifying questions or points that could be addressed. - - Response format: JSON with clarification suggestions. - """ - } - - private func createChatCompletionRequest(prompt: String, analysisType: AnalysisType) -> ChatCompletionRequest { - let config = LLMConfigManager().getConfig(for: analysisType) - - return ChatCompletionRequest( - model: model, - messages: [ - ChatMessage(role: .user, content: prompt) - ], - maxTokens: config.maxTokens, - temperature: config.temperature, - topP: config.topP, - frequencyPenalty: config.frequencyPenalty, - presencePenalty: config.presencePenalty - ) - } - - private func executeRequest(_ request: ChatCompletionRequest) -> AnyPublisher { - guard let url = URL(string: "\(baseURL)/chat/completions") else { - return Fail(error: LLMError.invalidRequest).eraseToAnyPublisher() - } - - var urlRequest = URLRequest(url: url) - urlRequest.httpMethod = "POST" - urlRequest.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") - urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") - - do { - urlRequest.httpBody = try encoder.encode(request) - } catch { - return Fail(error: error).eraseToAnyPublisher() - } - - return session.dataTaskPublisher(for: urlRequest) - .map(\.data) - .decode(type: ChatCompletionResponse.self, decoder: decoder) - .retry(maxRetries) - .eraseToAnyPublisher() - } - - private func parseResponse(_ response: ChatCompletionResponse, for analysisType: AnalysisType) -> AnalysisResult { - guard let content = response.choices.first?.message.content else { - return AnalysisResult( - type: analysisType, - content: .text("No response content"), - provider: .openai - ) - } - - switch analysisType { - case .factCheck: - return parseFactCheckResponse(content, analysisType: analysisType) - case .summarization: - return AnalysisResult( - type: analysisType, - content: .summary(content), - confidence: 0.8, - provider: .openai - ) - case .actionItems: - return parseActionItemsResponse(content, analysisType: analysisType) - case .sentiment: - return parseSentimentResponse(content, analysisType: analysisType) - case .keyTopics: - return parseTopicsResponse(content, analysisType: analysisType) - case .translation: - return parseTranslationResponse(content, analysisType: analysisType) - case .clarification: - return AnalysisResult( - type: analysisType, - content: .text(content), - confidence: 0.7, - provider: .openai - ) - } - } - - private func parseFactCheckResponse(_ content: String, analysisType: AnalysisType) -> AnalysisResult { - // Simple parsing - in production, use proper JSON parsing - let factCheckResult = FactCheckResult( - claim: "Extracted claim", - isAccurate: content.lowercased().contains("true"), - explanation: content, - sources: [], - confidence: 0.8, - alternativeInfo: nil, - category: .general, - severity: .minor - ) - - return AnalysisResult( - type: analysisType, - content: .factCheck(factCheckResult), - confidence: 0.8, - provider: .openai - ) - } - - private func parseActionItemsResponse(_ content: String, analysisType: AnalysisType) -> AnalysisResult { - // Simple parsing - extract action items from text - let actionItems = content.components(separatedBy: "\n") - .filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } - .map { ActionItem(description: $0.trimmingCharacters(in: .whitespacesAndNewlines)) } - - return AnalysisResult( - type: analysisType, - content: .actionItems(actionItems), - confidence: 0.7, - provider: .openai - ) - } - - private func parseSentimentResponse(_ content: String, analysisType: AnalysisType) -> AnalysisResult { - let sentiment: Sentiment - let lowercased = content.lowercased() - - if lowercased.contains("positive") { - sentiment = .positive - } else if lowercased.contains("negative") { - sentiment = .negative - } else if lowercased.contains("mixed") { - sentiment = .mixed - } else { - sentiment = .neutral - } - - let sentimentAnalysis = SentimentAnalysis( - overallSentiment: sentiment, - speakerSentiments: [:], - emotionalTone: .casual, - confidence: 0.7 - ) - - return AnalysisResult( - type: analysisType, - content: .sentiment(sentimentAnalysis), - confidence: 0.7, - provider: .openai - ) - } - - private func parseTopicsResponse(_ content: String, analysisType: AnalysisType) -> AnalysisResult { - let topics = content.components(separatedBy: "\n") - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - - return AnalysisResult( - type: analysisType, - content: .topics(topics), - confidence: 0.8, - provider: .openai - ) - } - - private func parseTranslationResponse(_ content: String, analysisType: AnalysisType) -> AnalysisResult { - let translationResult = TranslationResult( - originalText: "", - translatedText: content, - sourceLanguage: "auto", - targetLanguage: "en", - confidence: 0.8 - ) - - return AnalysisResult( - type: analysisType, - content: .translation(translationResult), - confidence: 0.8, - provider: .openai - ) - } - - private func estimateTokens(for text: String) -> Int { - // Rough estimate: 1 token ≈ 4 characters for English - return text.count / 4 - } - - private func mapError(_ error: Error) -> LLMError { - if let urlError = error as? URLError { - switch urlError.code { - case .notConnectedToInternet, .networkConnectionLost: - return .networkError(urlError) - case .timedOut: - return .serviceUnavailable - default: - return .networkError(urlError) - } - } - - if error is DecodingError { - return .responseParsingFailed - } - - return .networkError(error) - } -} - -// MARK: - OpenAI API Models - -struct ChatCompletionRequest: Codable { - let model: String - let messages: [ChatMessage] - let maxTokens: Int? - let temperature: Float? - let topP: Float? - let frequencyPenalty: Float? - let presencePenalty: Float? - let stream: Bool? - - init(model: String, messages: [ChatMessage], maxTokens: Int? = nil, temperature: Float? = nil, topP: Float? = nil, frequencyPenalty: Float? = nil, presencePenalty: Float? = nil, stream: Bool = false) { - self.model = model - self.messages = messages - self.maxTokens = maxTokens - self.temperature = temperature - self.topP = topP - self.frequencyPenalty = frequencyPenalty - self.presencePenalty = presencePenalty - self.stream = stream - } -} - -struct ChatMessage: Codable { - let role: ChatRole - let content: String -} - -enum ChatRole: String, Codable { - case system = "system" - case user = "user" - case assistant = "assistant" -} - -struct ChatCompletionResponse: Codable { - let id: String - let object: String - let created: Int - let model: String - let choices: [ChatChoice] - let usage: Usage? -} - -struct ChatChoice: Codable { - let index: Int - let message: ChatMessage - let finishReason: String? -} - -struct Usage: Codable { - let promptTokens: Int - let completionTokens: Int - let totalTokens: Int -} \ No newline at end of file diff --git a/Helix/Core/AI/PromptManager.swift b/Helix/Core/AI/PromptManager.swift deleted file mode 100644 index e35f68b..0000000 --- a/Helix/Core/AI/PromptManager.swift +++ /dev/null @@ -1,634 +0,0 @@ -// -// PromptManager.swift -// Helix -// - -import Foundation -import Combine - -// MARK: - AI Persona Definition - -struct AIPersona: Codable, Identifiable, Hashable { - let id: UUID - var name: String - var description: String - var systemPrompt: String - var tone: PersonaTone - var expertise: [String] - var contextualBehaviors: [ConversationContext: String] - var isBuiltIn: Bool - var version: Int - var createdDate: Date - var lastModified: Date - - init(name: String, description: String, systemPrompt: String, tone: PersonaTone = .balanced, expertise: [String] = [], isBuiltIn: Bool = false) { - self.id = UUID() - self.name = name - self.description = description - self.systemPrompt = systemPrompt - self.tone = tone - self.expertise = expertise - self.contextualBehaviors = [:] - self.isBuiltIn = isBuiltIn - self.version = 1 - self.createdDate = Date() - self.lastModified = Date() - } -} - -enum PersonaTone: String, Codable, CaseIterable { - case professional = "professional" - case casual = "casual" - case friendly = "friendly" - case analytical = "analytical" - case creative = "creative" - case empathetic = "empathetic" - case authoritative = "authoritative" - case balanced = "balanced" - - var description: String { - switch self { - case .professional: return "Professional and formal communication style" - case .casual: return "Relaxed and informal conversation tone" - case .friendly: return "Warm and approachable personality" - case .analytical: return "Data-driven and logical approach" - case .creative: return "Imaginative and innovative thinking" - case .empathetic: return "Understanding and emotionally aware" - case .authoritative: return "Confident and knowledgeable guidance" - case .balanced: return "Adaptive tone based on context" - } - } -} - -// MARK: - Conversation Context Detection - -enum ConversationContext: String, Codable, CaseIterable { - case meeting = "meeting" - case casual = "casual" - case interview = "interview" - case presentation = "presentation" - case negotiation = "negotiation" - case learning = "learning" - case social = "social" - case professional = "professional" - case creative = "creative" - case problem_solving = "problem_solving" - case debate = "debate" - case brainstorming = "brainstorming" - - var description: String { - switch self { - case .meeting: return "Business meeting or formal discussion" - case .casual: return "Informal conversation" - case .interview: return "Job interview or formal questioning" - case .presentation: return "Presenting information to audience" - case .negotiation: return "Negotiating terms or agreements" - case .learning: return "Educational or instructional context" - case .social: return "Social gathering or networking" - case .professional: return "Professional work environment" - case .creative: return "Creative collaboration or artistic work" - case .problem_solving: return "Working through problems or challenges" - case .debate: return "Formal or informal debate" - case .brainstorming: return "Generating ideas and solutions" - } - } - - var keywords: [String] { - switch self { - case .meeting: return ["meeting", "agenda", "action items", "minutes", "discussion"] - case .casual: return ["chat", "talk", "hang out", "catch up", "conversation"] - case .interview: return ["interview", "questions", "candidate", "position", "qualifications"] - case .presentation: return ["present", "slides", "audience", "demonstrate", "explain"] - case .negotiation: return ["negotiate", "deal", "terms", "agreement", "compromise"] - case .learning: return ["learn", "teach", "explain", "understand", "education"] - case .social: return ["party", "event", "networking", "social", "friends"] - case .professional: return ["work", "business", "professional", "corporate", "office"] - case .creative: return ["creative", "design", "art", "brainstorm", "innovative"] - case .problem_solving: return ["problem", "solution", "fix", "troubleshoot", "resolve"] - case .debate: return ["debate", "argue", "discuss", "opinion", "perspective"] - case .brainstorming: return ["brainstorm", "ideas", "creative", "generate", "think"] - } - } -} - -// MARK: - Prompt Template - -struct PromptTemplate: Codable, Identifiable, Hashable { - let id: UUID - var name: String - var description: String - var template: String - var variables: [PromptVariable] - var category: PromptCategory - var isBuiltIn: Bool - var usageCount: Int - var lastUsed: Date? - var createdDate: Date - - init(name: String, description: String, template: String, variables: [PromptVariable] = [], category: PromptCategory, isBuiltIn: Bool = false) { - self.id = UUID() - self.name = name - self.description = description - self.template = template - self.variables = variables - self.category = category - self.isBuiltIn = isBuiltIn - self.usageCount = 0 - self.lastUsed = nil - self.createdDate = Date() - } - - func render(with values: [String: String] = [:]) -> String { - var rendered = template - for variable in variables { - let placeholder = "{{\(variable.name)}}" - let value = values[variable.name] ?? variable.defaultValue - rendered = rendered.replacingOccurrences(of: placeholder, with: value) - } - return rendered - } -} - -struct PromptVariable: Codable, Hashable { - let name: String - let description: String - let type: VariableType - let defaultValue: String - let isRequired: Bool - let options: [String]? - - enum VariableType: String, Codable { - case text = "text" - case number = "number" - case boolean = "boolean" - case selection = "selection" - case multiSelection = "multiSelection" - } -} - -enum PromptCategory: String, Codable, CaseIterable { - case factChecking = "fact_checking" - case summarization = "summarization" - case analysis = "analysis" - case coaching = "coaching" - case creative = "creative" - case professional = "professional" - case educational = "educational" - case social = "social" - case custom = "custom" - - var displayName: String { - switch self { - case .factChecking: return "Fact Checking" - case .summarization: return "Summarization" - case .analysis: return "Analysis" - case .coaching: return "Coaching" - case .creative: return "Creative" - case .professional: return "Professional" - case .educational: return "Educational" - case .social: return "Social" - case .custom: return "Custom" - } - } -} - -// MARK: - Context Detector - -protocol ContextDetectorProtocol { - func detectContext(from messages: [ConversationMessage]) -> ConversationContext - func getContextConfidence(for context: ConversationContext, from messages: [ConversationMessage]) -> Float -} - -class ContextDetector: ContextDetectorProtocol { - private let keywordWeights: [ConversationContext: Float] = [ - .meeting: 1.0, - .interview: 0.9, - .presentation: 0.8, - .negotiation: 0.8, - .professional: 0.7, - .learning: 0.6, - .creative: 0.6, - .problem_solving: 0.6, - .debate: 0.5, - .brainstorming: 0.5, - .social: 0.4, - .casual: 0.3 - ] - - func detectContext(from messages: [ConversationMessage]) -> ConversationContext { - let scores = ConversationContext.allCases.map { context in - (context, getContextConfidence(for: context, from: messages)) - } - - return scores.max(by: { $0.1 < $1.1 })?.0 ?? .casual - } - - func getContextConfidence(for context: ConversationContext, from messages: [ConversationMessage]) -> Float { - guard !messages.isEmpty else { return 0 } - - let combinedText = messages.map(\.content).joined(separator: " ").lowercased() - let keywords = context.keywords - - let keywordMatches = keywords.reduce(0) { count, keyword in - let occurrences = combinedText.components(separatedBy: keyword.lowercased()).count - 1 - return count + occurrences - } - - let baseScore = Float(keywordMatches) / Float(keywords.count) - let weightedScore = baseScore * (keywordWeights[context] ?? 0.5) - - return min(weightedScore, 1.0) - } -} - -// MARK: - Prompt Manager - -protocol PromptManagerProtocol { - var availablePersonas: AnyPublisher<[AIPersona], Never> { get } - var availableTemplates: AnyPublisher<[PromptTemplate], Never> { get } - var currentPersona: AnyPublisher { get } - - func setCurrentPersona(_ persona: AIPersona) - func createCustomPersona(_ persona: AIPersona) throws - func updatePersona(_ persona: AIPersona) throws - func deletePersona(_ personaId: UUID) throws - - func createTemplate(_ template: PromptTemplate) throws - func updateTemplate(_ template: PromptTemplate) throws - func deleteTemplate(_ templateId: UUID) throws - - func generatePrompt(for context: ConversationContext, with data: [String: String]) -> String - func getPersonaForContext(_ context: ConversationContext) -> AIPersona? - func resetToDefaults() -} - -class PromptManager: PromptManagerProtocol, ObservableObject { - private let personasSubject = CurrentValueSubject<[AIPersona], Never>([]) - private let templatesSubject = CurrentValueSubject<[PromptTemplate], Never>([]) - private let currentPersonaSubject = CurrentValueSubject(nil) - - private let contextDetector: ContextDetectorProtocol - private let storage: PromptStorageProtocol - - var availablePersonas: AnyPublisher<[AIPersona], Never> { - personasSubject.eraseToAnyPublisher() - } - - var availableTemplates: AnyPublisher<[PromptTemplate], Never> { - templatesSubject.eraseToAnyPublisher() - } - - var currentPersona: AnyPublisher { - currentPersonaSubject.eraseToAnyPublisher() - } - - init(contextDetector: ContextDetectorProtocol = ContextDetector(), storage: PromptStorageProtocol = PromptStorage()) { - self.contextDetector = contextDetector - self.storage = storage - - loadStoredData() - initializeDefaultPersonas() - initializeDefaultTemplates() - } - - // MARK: - Persona Management - - func setCurrentPersona(_ persona: AIPersona) { - currentPersonaSubject.send(persona) - storage.saveCurrentPersona(persona) - } - - func createCustomPersona(_ persona: AIPersona) throws { - var newPersona = persona - newPersona.isBuiltIn = false - - var personas = personasSubject.value - personas.append(newPersona) - personasSubject.send(personas) - - try storage.savePersonas(personas) - } - - func updatePersona(_ persona: AIPersona) throws { - guard !persona.isBuiltIn else { - throw PromptError.cannotModifyBuiltInPersona - } - - var personas = personasSubject.value - if let index = personas.firstIndex(where: { $0.id == persona.id }) { - var updatedPersona = persona - updatedPersona.version += 1 - updatedPersona.lastModified = Date() - personas[index] = updatedPersona - - personasSubject.send(personas) - try storage.savePersonas(personas) - } - } - - func deletePersona(_ personaId: UUID) throws { - var personas = personasSubject.value - - guard let index = personas.firstIndex(where: { $0.id == personaId }) else { - throw PromptError.personaNotFound - } - - guard !personas[index].isBuiltIn else { - throw PromptError.cannotDeleteBuiltInPersona - } - - personas.remove(at: index) - personasSubject.send(personas) - try storage.savePersonas(personas) - } - - // MARK: - Template Management - - func createTemplate(_ template: PromptTemplate) throws { - var templates = templatesSubject.value - templates.append(template) - templatesSubject.send(templates) - - try storage.saveTemplates(templates) - } - - func updateTemplate(_ template: PromptTemplate) throws { - guard !template.isBuiltIn else { - throw PromptError.cannotModifyBuiltInTemplate - } - - var templates = templatesSubject.value - if let index = templates.firstIndex(where: { $0.id == template.id }) { - templates[index] = template - templatesSubject.send(templates) - try storage.saveTemplates(templates) - } - } - - func deleteTemplate(_ templateId: UUID) throws { - var templates = templatesSubject.value - - guard let index = templates.firstIndex(where: { $0.id == templateId }) else { - throw PromptError.templateNotFound - } - - guard !templates[index].isBuiltIn else { - throw PromptError.cannotDeleteBuiltInTemplate - } - - templates.remove(at: index) - templatesSubject.send(templates) - try storage.saveTemplates(templates) - } - - // MARK: - Prompt Generation - - func generatePrompt(for context: ConversationContext, with data: [String: String] = [:]) -> String { - let persona = currentPersonaSubject.value ?? getPersonaForContext(context) ?? getDefaultPersona() - let contextualBehavior = persona.contextualBehaviors[context] ?? "" - - var prompt = persona.systemPrompt - - if !contextualBehavior.isEmpty { - prompt += "\n\nContext-specific instructions for \(context.description):\n\(contextualBehavior)" - } - - // Add data placeholders if provided - for (key, value) in data { - prompt = prompt.replacingOccurrences(of: "{{\(key)}}", with: value) - } - - return prompt - } - - func getPersonaForContext(_ context: ConversationContext) -> AIPersona? { - let personas = personasSubject.value - - // Look for personas with specific contextual behaviors for this context - return personas.first { persona in - persona.contextualBehaviors.keys.contains(context) - } - } - - func resetToDefaults() { - initializeDefaultPersonas() - initializeDefaultTemplates() - - if let defaultPersona = personasSubject.value.first(where: { $0.name == "General Assistant" }) { - setCurrentPersona(defaultPersona) - } - } - - // MARK: - Private Methods - - private func loadStoredData() { - if let storedPersonas = storage.loadPersonas() { - personasSubject.send(storedPersonas) - } - - if let storedTemplates = storage.loadTemplates() { - templatesSubject.send(storedTemplates) - } - - if let currentPersona = storage.loadCurrentPersona() { - currentPersonaSubject.send(currentPersona) - } - } - - private func getDefaultPersona() -> AIPersona { - return personasSubject.value.first(where: { $0.name == "General Assistant" }) ?? - personasSubject.value.first ?? - AIPersona(name: "Default", description: "Default assistant", systemPrompt: "You are a helpful assistant.") - } - - private func initializeDefaultPersonas() { - let defaultPersonas = [ - AIPersona( - name: "General Assistant", - description: "Balanced assistant for general conversation analysis", - systemPrompt: "You are an intelligent assistant helping analyze conversations in real-time. Provide helpful, accurate, and contextually appropriate responses. Focus on being helpful while being concise for display on smart glasses.", - tone: .balanced, - expertise: ["general knowledge", "conversation analysis"], - isBuiltIn: true - ), - - AIPersona( - name: "Fact Checker", - description: "Specialized in verifying claims and providing accurate information", - systemPrompt: "You are a fact-checking specialist. Analyze statements for accuracy, provide corrections when needed, and cite reliable sources. Be precise and focus on verifiable information.", - tone: .analytical, - expertise: ["fact checking", "research", "verification"], - isBuiltIn: true - ), - - AIPersona( - name: "Meeting Assistant", - description: "Optimized for business meetings and professional discussions", - systemPrompt: "You are a professional meeting assistant. Track action items, summarize key points, and provide meeting insights. Focus on productivity and clear communication.", - tone: .professional, - expertise: ["meetings", "business", "productivity"], - isBuiltIn: true - ), - - AIPersona( - name: "Social Coach", - description: "Provides social interaction guidance and communication tips", - systemPrompt: "You are a social interaction coach. Provide helpful suggestions for conversations, detect social cues, and offer communication advice. Be supportive and encouraging.", - tone: .empathetic, - expertise: ["social skills", "communication", "relationships"], - isBuiltIn: true - ), - - AIPersona( - name: "Learning Companion", - description: "Educational support for learning conversations", - systemPrompt: "You are an educational companion. Help explain concepts, provide definitions, and support learning discussions. Make complex topics accessible and engaging.", - tone: .friendly, - expertise: ["education", "explanations", "learning"], - isBuiltIn: true - ) - ] - - // Add contextual behaviors - var personas = defaultPersonas - personas[1].contextualBehaviors[.meeting] = "Focus on identifying actionable items and key decisions. Summarize complex discussions clearly." - personas[2].contextualBehaviors[.interview] = "Provide strategic coaching for interview responses. Highlight strengths and suggest improvements." - personas[3].contextualBehaviors[.social] = "Offer conversation starters and help navigate social dynamics gracefully." - personas[4].contextualBehaviors[.learning] = "Break down complex concepts into digestible parts. Encourage questions and exploration." - - if personasSubject.value.isEmpty { - personasSubject.send(personas) - try? storage.savePersonas(personas) - - // Set default current persona - if let defaultPersona = personas.first { - currentPersonaSubject.send(defaultPersona) - storage.saveCurrentPersona(defaultPersona) - } - } - } - - private func initializeDefaultTemplates() { - let defaultTemplates = [ - PromptTemplate( - name: "Fact Check Analysis", - description: "Template for analyzing factual claims", - template: "Analyze this claim for accuracy: '{{claim}}'. Provide verification status, explanation, and reliable sources if available.", - variables: [ - PromptVariable(name: "claim", description: "The factual claim to verify", type: .text, defaultValue: "", isRequired: true, options: nil) - ], - category: .factChecking, - isBuiltIn: true - ), - - PromptTemplate( - name: "Meeting Summary", - description: "Template for summarizing meeting discussions", - template: "Summarize this meeting discussion focusing on: {{focus_areas}}. Include key decisions, action items, and next steps.", - variables: [ - PromptVariable(name: "focus_areas", description: "Specific areas to focus on", type: .text, defaultValue: "key decisions and action items", isRequired: false, options: nil) - ], - category: .summarization, - isBuiltIn: true - ), - - PromptTemplate( - name: "Communication Coaching", - description: "Template for providing communication feedback", - template: "Analyze this conversation for communication effectiveness. Focus on {{analysis_type}} and provide constructive feedback.", - variables: [ - PromptVariable(name: "analysis_type", description: "Type of analysis to perform", type: .selection, defaultValue: "overall communication", isRequired: false, options: ["overall communication", "persuasion techniques", "active listening", "clarity", "emotional intelligence"]) - ], - category: .coaching, - isBuiltIn: true - ) - ] - - if templatesSubject.value.isEmpty { - templatesSubject.send(defaultTemplates) - try? storage.saveTemplates(defaultTemplates) - } - } -} - -// MARK: - Errors - -enum PromptError: LocalizedError { - case personaNotFound - case templateNotFound - case cannotModifyBuiltInPersona - case cannotDeleteBuiltInPersona - case cannotModifyBuiltInTemplate - case cannotDeleteBuiltInTemplate - case invalidTemplate - case storageFailed - - var errorDescription: String? { - switch self { - case .personaNotFound: - return "Persona not found" - case .templateNotFound: - return "Template not found" - case .cannotModifyBuiltInPersona: - return "Cannot modify built-in persona" - case .cannotDeleteBuiltInPersona: - return "Cannot delete built-in persona" - case .cannotModifyBuiltInTemplate: - return "Cannot modify built-in template" - case .cannotDeleteBuiltInTemplate: - return "Cannot delete built-in template" - case .invalidTemplate: - return "Invalid template format" - case .storageFailed: - return "Failed to save to storage" - } - } -} - -// MARK: - Storage Protocol - -protocol PromptStorageProtocol { - func savePersonas(_ personas: [AIPersona]) throws - func loadPersonas() -> [AIPersona]? - func saveTemplates(_ templates: [PromptTemplate]) throws - func loadTemplates() -> [PromptTemplate]? - func saveCurrentPersona(_ persona: AIPersona) - func loadCurrentPersona() -> AIPersona? -} - -class PromptStorage: PromptStorageProtocol { - private let userDefaults = UserDefaults.standard - private let personasKey = "ai_personas" - private let templatesKey = "prompt_templates" - private let currentPersonaKey = "current_persona" - - func savePersonas(_ personas: [AIPersona]) throws { - let data = try JSONEncoder().encode(personas) - userDefaults.set(data, forKey: personasKey) - } - - func loadPersonas() -> [AIPersona]? { - guard let data = userDefaults.data(forKey: personasKey) else { return nil } - return try? JSONDecoder().decode([AIPersona].self, from: data) - } - - func saveTemplates(_ templates: [PromptTemplate]) throws { - let data = try JSONEncoder().encode(templates) - userDefaults.set(data, forKey: templatesKey) - } - - func loadTemplates() -> [PromptTemplate]? { - guard let data = userDefaults.data(forKey: templatesKey) else { return nil } - return try? JSONDecoder().decode([PromptTemplate].self, from: data) - } - - func saveCurrentPersona(_ persona: AIPersona) { - let data = try? JSONEncoder().encode(persona) - userDefaults.set(data, forKey: currentPersonaKey) - } - - func loadCurrentPersona() -> AIPersona? { - guard let data = userDefaults.data(forKey: currentPersonaKey) else { return nil } - return try? JSONDecoder().decode(AIPersona.self, from: data) - } -} \ No newline at end of file diff --git a/Helix/Core/AI/SpecializedModes.swift b/Helix/Core/AI/SpecializedModes.swift deleted file mode 100644 index e7cbffe..0000000 --- a/Helix/Core/AI/SpecializedModes.swift +++ /dev/null @@ -1,779 +0,0 @@ -// -// SpecializedModes.swift -// Helix -// - -import Foundation -import Combine - -// MARK: - Specialized Mode Definitions - -enum SpecializedMode: String, CaseIterable, Codable { - case ghostWriter = "ghost_writer" - case devilsAdvocate = "devils_advocate" - case wingman = "wingman" - case sherlockHolmes = "sherlock_holmes" - case therapyAssistant = "therapy_assistant" - case speedNetworking = "speed_networking" - case interview = "interview" - case creativeCollaboration = "creative_collaboration" - - var displayName: String { - switch self { - case .ghostWriter: return "Ghost Writer" - case .devilsAdvocate: return "Devil's Advocate" - case .wingman: return "Wingman" - case .sherlockHolmes: return "Sherlock Holmes" - case .therapyAssistant: return "Therapy Assistant" - case .speedNetworking: return "Speed Networking" - case .interview: return "Interview Coach" - case .creativeCollaboration: return "Creative Collaborator" - } - } - - var description: String { - switch self { - case .ghostWriter: - return "Generates responses for you to read aloud in conversations" - case .devilsAdvocate: - return "Presents counter-arguments to strengthen your positions" - case .wingman: - return "Social interaction coaching for personal relationships" - case .sherlockHolmes: - return "Analyzes micro-expressions and verbal cues for insights" - case .therapyAssistant: - return "Therapeutic communication technique suggestions" - case .speedNetworking: - return "Rapid conversation starters and networking tips" - case .interview: - return "Question preparation and response coaching" - case .creativeCollaboration: - return "Brainstorming facilitation and idea generation" - } - } - - var icon: String { - switch self { - case .ghostWriter: return "pencil.and.outline" - case .devilsAdvocate: return "flame" - case .wingman: return "heart.circle" - case .sherlockHolmes: return "magnifyingglass.circle" - case .therapyAssistant: return "heart.text.square" - case .speedNetworking: return "person.2.circle" - case .interview: return "person.crop.circle.badge.questionmark" - case .creativeCollaboration: return "lightbulb.circle" - } - } -} - -// MARK: - Mode Configuration - -struct ModeConfiguration: Codable { - let mode: SpecializedMode - var isEnabled: Bool - var customSettings: [String: String] - var triggerPhrases: [String] - var autoActivation: Bool - var confidenceThreshold: Float - var responseStyle: ResponseStyle - - init(mode: SpecializedMode) { - self.mode = mode - self.isEnabled = true - self.customSettings = [:] - self.triggerPhrases = [] - self.autoActivation = false - self.confidenceThreshold = 0.7 - self.responseStyle = .balanced - } -} - -enum ResponseStyle: String, CaseIterable, Codable { - case concise = "concise" - case detailed = "detailed" - case balanced = "balanced" - case creative = "creative" - case analytical = "analytical" - - var description: String { - switch self { - case .concise: return "Brief and to the point" - case .detailed: return "Comprehensive and thorough" - case .balanced: return "Moderate level of detail" - case .creative: return "Imaginative and innovative" - case .analytical: return "Data-driven and logical" - } - } -} - -// MARK: - Mode Response - -struct ModeResponse { - let id: UUID - let mode: SpecializedMode - let content: String - let alternatives: [String] - let confidence: Float - let context: ResponseContext - let timing: ResponseTiming - let metadata: [String: Any] - - init(mode: SpecializedMode, content: String, alternatives: [String] = [], confidence: Float = 1.0, context: ResponseContext = .general) { - self.id = UUID() - self.mode = mode - self.content = content - self.alternatives = alternatives - self.confidence = confidence - self.context = context - self.timing = ResponseTiming.immediate - self.metadata = [:] - } -} - -enum ResponseContext: String, Codable { - case general = "general" - case professional = "professional" - case social = "social" - case academic = "academic" - case creative = "creative" - case personal = "personal" -} - -enum ResponseTiming: String, Codable { - case immediate = "immediate" - case delayed = "delayed" - case onDemand = "on_demand" -} - -// MARK: - Specialized Modes Manager - -protocol SpecializedModesManagerProtocol { - var activeMode: AnyPublisher { get } - var availableModes: AnyPublisher<[SpecializedMode], Never> { get } - var modeConfigurations: AnyPublisher<[SpecializedMode: ModeConfiguration], Never> { get } - - func activateMode(_ mode: SpecializedMode) - func deactivateMode() - func configureMode(_ mode: SpecializedMode, configuration: ModeConfiguration) - func generateResponse(for context: ModeContext) -> AnyPublisher - func detectModeFromContext(_ context: ModeContext) -> SpecializedMode? -} - -class SpecializedModesManager: SpecializedModesManagerProtocol, ObservableObject { - private let activeModeSubject = CurrentValueSubject(nil) - private let availableModesSubject = CurrentValueSubject<[SpecializedMode], Never>(SpecializedMode.allCases) - private let modeConfigurationsSubject = CurrentValueSubject<[SpecializedMode: ModeConfiguration], Never>([:]) - - private let modeHandlers: [SpecializedMode: SpecializedModeHandler] - private let llmService: LLMServiceProtocol - private let contextAnalyzer: ModeContextAnalyzer - - var activeMode: AnyPublisher { - activeModeSubject.eraseToAnyPublisher() - } - - var availableModes: AnyPublisher<[SpecializedMode], Never> { - availableModesSubject.eraseToAnyPublisher() - } - - var modeConfigurations: AnyPublisher<[SpecializedMode: ModeConfiguration], Never> { - modeConfigurationsSubject.eraseToAnyPublisher() - } - - init(llmService: LLMServiceProtocol) { - self.llmService = llmService - self.contextAnalyzer = ModeContextAnalyzer() - - // Initialize mode handlers - self.modeHandlers = [ - .ghostWriter: GhostWriterMode(llmService: llmService), - .devilsAdvocate: DevilsAdvocateMode(llmService: llmService), - .wingman: WingmanMode(llmService: llmService), - .sherlockHolmes: SherlockHolmesMode(llmService: llmService), - .therapyAssistant: TherapyAssistantMode(llmService: llmService), - .speedNetworking: SpeedNetworkingMode(llmService: llmService), - .interview: InterviewMode(llmService: llmService), - .creativeCollaboration: CreativeCollaborationMode(llmService: llmService) - ] - - initializeDefaultConfigurations() - } - - func activateMode(_ mode: SpecializedMode) { - activeModeSubject.send(mode) - print("Activated specialized mode: \(mode.displayName)") - } - - func deactivateMode() { - activeModeSubject.send(nil) - print("Deactivated specialized mode") - } - - func configureMode(_ mode: SpecializedMode, configuration: ModeConfiguration) { - var configurations = modeConfigurationsSubject.value - configurations[mode] = configuration - modeConfigurationsSubject.send(configurations) - } - - func generateResponse(for context: ModeContext) -> AnyPublisher { - guard let activeMode = activeModeSubject.value else { - return Fail(error: ModeError.noActiveModePresent) - .eraseToAnyPublisher() - } - - guard let handler = modeHandlers[activeMode] else { - return Fail(error: ModeError.modeHandlerNotFound) - .eraseToAnyPublisher() - } - - let configuration = modeConfigurationsSubject.value[activeMode] ?? ModeConfiguration(mode: activeMode) - - return handler.generateResponse(for: context, configuration: configuration) - } - - func detectModeFromContext(_ context: ModeContext) -> SpecializedMode? { - return contextAnalyzer.detectOptimalMode(from: context) - } - - private func initializeDefaultConfigurations() { - var configurations: [SpecializedMode: ModeConfiguration] = [:] - - for mode in SpecializedMode.allCases { - configurations[mode] = ModeConfiguration(mode: mode) - } - - modeConfigurationsSubject.send(configurations) - } -} - -// MARK: - Mode Context - -struct ModeContext { - let messages: [ConversationMessage] - let speakers: [Speaker] - let currentSpeaker: Speaker? - let conversationType: ConversationContext - let environmentalFactors: EnvironmentalFactors - let userPreferences: UserPreferences - let timestamp: TimeInterval - - init(messages: [ConversationMessage], speakers: [Speaker], currentSpeaker: Speaker? = nil, conversationType: ConversationContext = .casual) { - self.messages = messages - self.speakers = speakers - self.currentSpeaker = currentSpeaker - self.conversationType = conversationType - self.environmentalFactors = EnvironmentalFactors() - self.userPreferences = UserPreferences() - self.timestamp = Date().timeIntervalSince1970 - } -} - -struct EnvironmentalFactors { - let noiseLevel: Float - let location: String? - let timeOfDay: TimeOfDay - let socialContext: SocialContext - - init(noiseLevel: Float = 0.0, location: String? = nil, timeOfDay: TimeOfDay = .unknown, socialContext: SocialContext = .unknown) { - self.noiseLevel = noiseLevel - self.location = location - self.timeOfDay = timeOfDay - self.socialContext = socialContext - } -} - -enum TimeOfDay: String, Codable { - case morning = "morning" - case afternoon = "afternoon" - case evening = "evening" - case night = "night" - case unknown = "unknown" -} - -enum SocialContext: String, Codable { - case formal = "formal" - case informal = "informal" - case professional = "professional" - case personal = "personal" - case public = "public" - case private = "private" - case unknown = "unknown" -} - -struct UserPreferences { - let responseLength: ResponseLength - let humorLevel: HumorLevel - let assertivenessLevel: AssertivenessLevel - let culturalContext: String? - - init(responseLength: ResponseLength = .medium, humorLevel: HumorLevel = .moderate, assertivenessLevel: AssertivenessLevel = .balanced, culturalContext: String? = nil) { - self.responseLength = responseLength - self.humorLevel = humorLevel - self.assertivenessLevel = assertivenessLevel - self.culturalContext = culturalContext - } -} - -enum ResponseLength: String, CaseIterable, Codable { - case brief = "brief" - case medium = "medium" - case detailed = "detailed" -} - -enum HumorLevel: String, CaseIterable, Codable { - case none = "none" - case subtle = "subtle" - case moderate = "moderate" - case high = "high" -} - -enum AssertivenessLevel: String, CaseIterable, Codable { - case passive = "passive" - case balanced = "balanced" - case assertive = "assertive" - case aggressive = "aggressive" -} - -// MARK: - Mode Handler Protocol - -protocol SpecializedModeHandler { - var mode: SpecializedMode { get } - func generateResponse(for context: ModeContext, configuration: ModeConfiguration) -> AnyPublisher - func isApplicable(for context: ModeContext) -> Bool - func getConfidence(for context: ModeContext) -> Float -} - -// MARK: - Ghost Writer Mode - -class GhostWriterMode: SpecializedModeHandler { - let mode: SpecializedMode = .ghostWriter - private let llmService: LLMServiceProtocol - - init(llmService: LLMServiceProtocol) { - self.llmService = llmService - } - - func generateResponse(for context: ModeContext, configuration: ModeConfiguration) -> AnyPublisher { - let prompt = createGhostWriterPrompt(context: context, configuration: configuration) - - return llmService.analyzeWithCustomPrompt(prompt, context: createLLMContext(from: context)) - .map { analysisResult in - let content = self.extractResponseContent(from: analysisResult) - let alternatives = self.generateAlternatives(content: content, context: context) - - return ModeResponse( - mode: .ghostWriter, - content: content, - alternatives: alternatives, - confidence: analysisResult.confidence, - context: self.mapToResponseContext(context.conversationType) - ) - } - .mapError { _ in ModeError.responseGenerationFailed } - .eraseToAnyPublisher() - } - - func isApplicable(for context: ModeContext) -> Bool { - // Ghost writer is applicable when user needs help responding - return context.messages.count > 0 && context.currentSpeaker?.isCurrentUser == false - } - - func getConfidence(for context: ModeContext) -> Float { - // Higher confidence in formal or professional settings - switch context.conversationType { - case .meeting, .professional, .interview: return 0.9 - case .negotiation, .presentation: return 0.8 - default: return 0.6 - } - } - - private func createGhostWriterPrompt(context: ModeContext, configuration: ModeConfiguration) -> String { - let recentMessages = Array(context.messages.suffix(3)) - let conversationText = recentMessages.map { "\($0.content)" }.joined(separator: "\n") - - let styleInstruction = getStyleInstruction(for: configuration.responseStyle) - let lengthInstruction = getLengthInstruction(for: context.userPreferences.responseLength) - - return """ - You are a Ghost Writer assistant. Generate a natural, contextually appropriate response that the user can speak aloud in this conversation. - - Conversation context: - \(conversationText) - - Instructions: - - \(styleInstruction) - - \(lengthInstruction) - - Make it sound natural and conversational - - Consider the tone and style of the conversation - - Provide a response that advances the conversation meaningfully - - Generate 1-2 sentences that the user can say next: - """ - } - - private func getStyleInstruction(for style: ResponseStyle) -> String { - switch style { - case .concise: return "Keep the response brief and to the point" - case .detailed: return "Provide a thoughtful, comprehensive response" - case .balanced: return "Strike a balance between brevity and completeness" - case .creative: return "Use creative and engaging language" - case .analytical: return "Focus on logical reasoning and facts" - } - } - - private func getLengthInstruction(for length: ResponseLength) -> String { - switch length { - case .brief: return "Maximum 1 sentence" - case .medium: return "1-2 sentences" - case .detailed: return "2-3 sentences maximum" - } - } - - private func generateAlternatives(content: String, context: ModeContext) -> [String] { - // Generate alternative phrasings (simplified implementation) - return [ - "Alternative: " + content.replacingOccurrences(of: "I think", with: "In my opinion"), - "Alternative: " + content.replacingOccurrences(of: "Yes", with: "Absolutely") - ] - } - - private func extractResponseContent(from result: AnalysisResult) -> String { - switch result.content { - case .text(let text): return text - default: return "I'd like to add my perspective on this topic." - } - } - - private func createLLMContext(from context: ModeContext) -> ConversationContext { - return ConversationContext( - messages: context.messages, - speakers: context.speakers, - analysisType: .clarification, - metadata: ConversationMetadata(tags: ["ghost_writer"]) - ) - } - - private func mapToResponseContext(_ conversationType: ConversationContext) -> ResponseContext { - switch conversationType { - case .meeting, .professional: return .professional - case .social: return .social - case .learning: return .academic - case .creative: return .creative - default: return .general - } - } -} - -// MARK: - Devil's Advocate Mode - -class DevilsAdvocateMode: SpecializedModeHandler { - let mode: SpecializedMode = .devilsAdvocate - private let llmService: LLMServiceProtocol - - init(llmService: LLMServiceProtocol) { - self.llmService = llmService - } - - func generateResponse(for context: ModeContext, configuration: ModeConfiguration) -> AnyPublisher { - let prompt = createDevilsAdvocatePrompt(context: context, configuration: configuration) - - return llmService.analyzeWithCustomPrompt(prompt, context: createLLMContext(from: context)) - .map { analysisResult in - let content = self.extractResponseContent(from: analysisResult) - - return ModeResponse( - mode: .devilsAdvocate, - content: content, - confidence: analysisResult.confidence, - context: .professional - ) - } - .mapError { _ in ModeError.responseGenerationFailed } - .eraseToAnyPublisher() - } - - func isApplicable(for context: ModeContext) -> Bool { - // Devil's advocate is useful in debates, discussions, and decision-making - return context.conversationType == .debate || - context.conversationType == .meeting || - context.conversationType == .problem_solving - } - - func getConfidence(for context: ModeContext) -> Float { - switch context.conversationType { - case .debate, .problem_solving: return 0.9 - case .meeting, .brainstorming: return 0.7 - default: return 0.4 - } - } - - private func createDevilsAdvocatePrompt(context: ModeContext, configuration: ModeConfiguration) -> String { - let recentMessages = Array(context.messages.suffix(3)) - let conversationText = recentMessages.map { "\($0.content)" }.joined(separator: "\n") - - return """ - You are a Devil's Advocate assistant. Identify potential counterarguments, weaknesses, or alternative perspectives to strengthen the discussion. - - Recent conversation: - \(conversationText) - - Provide constructive counterpoints or alternative viewpoints that could: - - Challenge assumptions - - Identify potential risks or downsides - - Present alternative solutions - - Strengthen the overall argument through critical examination - - Be respectful but thought-provoking in your analysis: - """ - } - - private func extractResponseContent(from result: AnalysisResult) -> String { - switch result.content { - case .text(let text): return text - default: return "Consider this alternative perspective..." - } - } - - private func createLLMContext(from context: ModeContext) -> ConversationContext { - return ConversationContext( - messages: context.messages, - speakers: context.speakers, - analysisType: .clarification, - metadata: ConversationMetadata(tags: ["devils_advocate", "critical_thinking"]) - ) - } -} - -// MARK: - Placeholder Mode Implementations - -class WingmanMode: SpecializedModeHandler { - let mode: SpecializedMode = .wingman - private let llmService: LLMServiceProtocol - - init(llmService: LLMServiceProtocol) { - self.llmService = llmService - } - - func generateResponse(for context: ModeContext, configuration: ModeConfiguration) -> AnyPublisher { - // Implementation for social interaction coaching - return Just(ModeResponse(mode: .wingman, content: "Great conversation starter: Ask about their interests in this topic.")) - .setFailureType(to: ModeError.self) - .eraseToAnyPublisher() - } - - func isApplicable(for context: ModeContext) -> Bool { - return context.conversationType == .social - } - - func getConfidence(for context: ModeContext) -> Float { - return context.conversationType == .social ? 0.8 : 0.3 - } -} - -class SherlockHolmesMode: SpecializedModeHandler { - let mode: SpecializedMode = .sherlockHolmes - private let llmService: LLMServiceProtocol - - init(llmService: LLMServiceProtocol) { - self.llmService = llmService - } - - func generateResponse(for context: ModeContext, configuration: ModeConfiguration) -> AnyPublisher { - // Implementation for observation and deduction analysis - return Just(ModeResponse(mode: .sherlockHolmes, content: "Observation: Notice the change in speaking pace when discussing financial topics.")) - .setFailureType(to: ModeError.self) - .eraseToAnyPublisher() - } - - func isApplicable(for context: ModeContext) -> Bool { - return true // Can analyze any conversation - } - - func getConfidence(for context: ModeContext) -> Float { - return 0.6 - } -} - -class TherapyAssistantMode: SpecializedModeHandler { - let mode: SpecializedMode = .therapyAssistant - private let llmService: LLMServiceProtocol - - init(llmService: LLMServiceProtocol) { - self.llmService = llmService - } - - func generateResponse(for context: ModeContext, configuration: ModeConfiguration) -> AnyPublisher { - // Implementation for therapeutic communication suggestions - return Just(ModeResponse(mode: .therapyAssistant, content: "Try reflecting their emotions: 'It sounds like this situation is really frustrating for you.'")) - .setFailureType(to: ModeError.self) - .eraseToAnyPublisher() - } - - func isApplicable(for context: ModeContext) -> Bool { - return context.environmentalFactors.socialContext == .personal - } - - func getConfidence(for context: ModeContext) -> Float { - return 0.7 - } -} - -class SpeedNetworkingMode: SpecializedModeHandler { - let mode: SpecializedMode = .speedNetworking - private let llmService: LLMServiceProtocol - - init(llmService: LLMServiceProtocol) { - self.llmService = llmService - } - - func generateResponse(for context: ModeContext, configuration: ModeConfiguration) -> AnyPublisher { - return Just(ModeResponse(mode: .speedNetworking, content: "Time for a transition: 'That's fascinating! How did you get started in that field?'")) - .setFailureType(to: ModeError.self) - .eraseToAnyPublisher() - } - - func isApplicable(for context: ModeContext) -> Bool { - return context.conversationType == .social || context.conversationType == .professional - } - - func getConfidence(for context: ModeContext) -> Float { - return 0.6 - } -} - -class InterviewMode: SpecializedModeHandler { - let mode: SpecializedMode = .interview - private let llmService: LLMServiceProtocol - - init(llmService: LLMServiceProtocol) { - self.llmService = llmService - } - - func generateResponse(for context: ModeContext, configuration: ModeConfiguration) -> AnyPublisher { - return Just(ModeResponse(mode: .interview, content: "Strong answer structure: Situation, Task, Action, Result. Highlight your specific contribution.")) - .setFailureType(to: ModeError.self) - .eraseToAnyPublisher() - } - - func isApplicable(for context: ModeContext) -> Bool { - return context.conversationType == .interview - } - - func getConfidence(for context: ModeContext) -> Float { - return context.conversationType == .interview ? 0.9 : 0.2 - } -} - -class CreativeCollaborationMode: SpecializedModeHandler { - let mode: SpecializedMode = .creativeCollaboration - private let llmService: LLMServiceProtocol - - init(llmService: LLMServiceProtocol) { - self.llmService = llmService - } - - func generateResponse(for context: ModeContext, configuration: ModeConfiguration) -> AnyPublisher { - return Just(ModeResponse(mode: .creativeCollaboration, content: "Build on that idea: 'What if we took that concept and applied it to...'")) - .setFailureType(to: ModeError.self) - .eraseToAnyPublisher() - } - - func isApplicable(for context: ModeContext) -> Bool { - return context.conversationType == .creative || context.conversationType == .brainstorming - } - - func getConfidence(for context: ModeContext) -> Float { - return context.conversationType == .creative ? 0.8 : 0.4 - } -} - -// MARK: - Mode Context Analyzer - -class ModeContextAnalyzer { - func detectOptimalMode(from context: ModeContext) -> SpecializedMode? { - let handlers: [SpecializedModeHandler] = [ - GhostWriterMode(llmService: MockLLMService()), - DevilsAdvocateMode(llmService: MockLLMService()), - WingmanMode(llmService: MockLLMService()), - InterviewMode(llmService: MockLLMService()) - ] - - return handlers - .filter { $0.isApplicable(for: context) } - .max(by: { $0.getConfidence(for: context) < $1.getConfidence(for: context) })? - .mode - } -} - -// MARK: - Mock LLM Service for Mode Handlers - -private class MockLLMService: LLMServiceProtocol { - func analyzeConversation(_ context: ConversationContext) -> AnyPublisher { - return Just(AnalysisResult(type: .clarification, content: .text("Mock response"))) - .setFailureType(to: LLMError.self) - .eraseToAnyPublisher() - } - - func analyzeWithCustomPrompt(_ prompt: String, context: ConversationContext) -> AnyPublisher { - return Just(AnalysisResult(type: .clarification, content: .text("Mock custom response"))) - .setFailureType(to: LLMError.self) - .eraseToAnyPublisher() - } - - func factCheck(_ claim: String, context: ConversationContext?) -> AnyPublisher { - return Just(FactCheckResult(claim: claim, isAccurate: true, explanation: "Mock", sources: [], confidence: 0.8, alternativeInfo: nil, category: .general, severity: .minor)) - .setFailureType(to: LLMError.self) - .eraseToAnyPublisher() - } - - func summarizeConversation(_ messages: [ConversationMessage]) -> AnyPublisher { - return Just("Mock summary") - .setFailureType(to: LLMError.self) - .eraseToAnyPublisher() - } - - func detectClaims(in text: String) -> AnyPublisher<[FactualClaim], LLMError> { - return Just([]) - .setFailureType(to: LLMError.self) - .eraseToAnyPublisher() - } - - func extractActionItems(from messages: [ConversationMessage]) -> AnyPublisher<[ActionItem], LLMError> { - return Just([]) - .setFailureType(to: LLMError.self) - .eraseToAnyPublisher() - } - - func setCurrentPersona(_ persona: AIPersona) {} - - func generatePersonalizedResponse(_ messages: [ConversationMessage], conversationContext: Helix.ConversationContext) -> AnyPublisher { - return Just("Mock personalized response") - .setFailureType(to: LLMError.self) - .eraseToAnyPublisher() - } -} - -// MARK: - Errors - -enum ModeError: LocalizedError { - case noActiveModePresent - case modeHandlerNotFound - case responseGenerationFailed - case invalidConfiguration - case contextInsufficientForMode - - var errorDescription: String? { - switch self { - case .noActiveModePresent: - return "No specialized mode is currently active" - case .modeHandlerNotFound: - return "Handler for the specified mode was not found" - case .responseGenerationFailed: - return "Failed to generate response for the current mode" - case .invalidConfiguration: - return "Invalid configuration for the specified mode" - case .contextInsufficientForMode: - return "Insufficient context to activate the requested mode" - } - } -} \ No newline at end of file diff --git a/Helix/Core/Audio/AdvancedRecordingManager.swift b/Helix/Core/Audio/AdvancedRecordingManager.swift deleted file mode 100644 index d43c7f5..0000000 --- a/Helix/Core/Audio/AdvancedRecordingManager.swift +++ /dev/null @@ -1,799 +0,0 @@ -// -// AdvancedRecordingManager.swift -// Helix -// - -import Foundation -import AVFoundation -import Combine - -// MARK: - Recording Configuration - -struct AdvancedRecordingSettings { - let sampleRate: Double - let channels: UInt32 - let bitDepth: UInt32 - let format: AudioFormat - let compressionLevel: CompressionLevel - let autoGainControl: Bool - let noiseSuppressionLevel: Float - let enableExtensionMicrophone: Bool - let recordingQuality: RecordingQuality - - static let `default` = AdvancedRecordingSettings( - sampleRate: 48000, - channels: 2, - bitDepth: 24, - format: .wav, - compressionLevel: .lossless, - autoGainControl: true, - noiseSuppressionLevel: 0.5, - enableExtensionMicrophone: false, - recordingQuality: .high - ) - - static let highFidelity = AdvancedRecordingSettings( - sampleRate: 96000, - channels: 2, - bitDepth: 32, - format: .flac, - compressionLevel: .lossless, - autoGainControl: false, - noiseSuppressionLevel: 0.3, - enableExtensionMicrophone: true, - recordingQuality: .studio - ) -} - -enum AudioFormat: String, CaseIterable, Codable { - case wav = "wav" - case flac = "flac" - case mp3 = "mp3" - case aac = "aac" - case m4a = "m4a" - - var displayName: String { - switch self { - case .wav: return "WAV (Uncompressed)" - case .flac: return "FLAC (Lossless)" - case .mp3: return "MP3 (Compressed)" - case .aac: return "AAC (High Quality)" - case .m4a: return "M4A (Apple)" - } - } - - var fileExtension: String { - return rawValue - } - - var avAudioFormat: AVAudioFormat.AudioFileFormat { - switch self { - case .wav: return .wav - case .flac: return .wav // FLAC will be handled separately - case .mp3: return .mp3 - case .aac: return .mp4 - case .m4a: return .m4a - } - } -} - -enum CompressionLevel: String, CaseIterable, Codable { - case lossless = "lossless" - case high = "high" - case medium = "medium" - case low = "low" - - var compressionQuality: Float { - switch self { - case .lossless: return 1.0 - case .high: return 0.8 - case .medium: return 0.6 - case .low: return 0.4 - } - } -} - -enum RecordingQuality: String, CaseIterable, Codable { - case studio = "studio" - case high = "high" - case medium = "medium" - case low = "low" - case voice = "voice" - - var description: String { - switch self { - case .studio: return "Studio Quality (96kHz/32-bit)" - case .high: return "High Quality (48kHz/24-bit)" - case .medium: return "Medium Quality (44.1kHz/16-bit)" - case .low: return "Low Quality (22kHz/16-bit)" - case .voice: return "Voice Optimized (16kHz/16-bit)" - } - } - - var sampleRate: Double { - switch self { - case .studio: return 96000 - case .high: return 48000 - case .medium: return 44100 - case .low: return 22050 - case .voice: return 16000 - } - } - - var bitDepth: UInt32 { - switch self { - case .studio: return 32 - case .high: return 24 - case .medium, .low, .voice: return 16 - } - } -} - -// MARK: - Advanced Recording Manager - -protocol AdvancedRecordingManagerProtocol { - var isRecording: AnyPublisher { get } - var currentSettings: AnyPublisher { get } - var recordingLevel: AnyPublisher { get } - var recordingDuration: AnyPublisher { get } - var audioBuffer: AnyPublisher { get } - var externalMicrophones: AnyPublisher<[ExternalMicrophone], Never> { get } - - func updateSettings(_ settings: AdvancedRecordingSettings) throws - func startRecording() throws - func stopRecording() -> AnyPublisher - func pauseRecording() throws - func resumeRecording() throws - func cancelRecording() - - func connectExternalMicrophone(_ microphone: ExternalMicrophone) -> AnyPublisher - func disconnectExternalMicrophone() - func testMicrophone() -> AnyPublisher -} - -class AdvancedRecordingManager: AdvancedRecordingManagerProtocol, ObservableObject { - private let isRecordingSubject = CurrentValueSubject(false) - private let currentSettingsSubject = CurrentValueSubject(.default) - private let recordingLevelSubject = CurrentValueSubject(0.0) - private let recordingDurationSubject = CurrentValueSubject(0.0) - private let audioBufferSubject = PassthroughSubject() - private let externalMicrophonesSubject = CurrentValueSubject<[ExternalMicrophone], Never>([]) - - private var audioEngine: AVAudioEngine - private var audioFile: AVAudioFile? - private var recordingStartTime: Date? - private var isPaused = false - private var cancellables = Set() - - // Audio processing chain - private let mixerNode: AVAudioMixerNode - private let effectsChain: AudioEffectsChain - private let levelMonitor: AudioLevelMonitor - private let qualityEnhancer: AudioQualityEnhancer - - var isRecording: AnyPublisher { - isRecordingSubject.eraseToAnyPublisher() - } - - var currentSettings: AnyPublisher { - currentSettingsSubject.eraseToAnyPublisher() - } - - var recordingLevel: AnyPublisher { - recordingLevelSubject.eraseToAnyPublisher() - } - - var recordingDuration: AnyPublisher { - recordingDurationSubject.eraseToAnyPublisher() - } - - var audioBuffer: AnyPublisher { - audioBufferSubject.eraseToAnyPublisher() - } - - var externalMicrophones: AnyPublisher<[ExternalMicrophone], Never> { - externalMicrophonesSubject.eraseToAnyPublisher() - } - - init() { - self.audioEngine = AVAudioEngine() - self.mixerNode = AVAudioMixerNode() - self.effectsChain = AudioEffectsChain() - self.levelMonitor = AudioLevelMonitor() - self.qualityEnhancer = AudioQualityEnhancer() - - setupAudioEngine() - startLevelMonitoring() - startDurationMonitoring() - } - - // MARK: - Recording Control - - func updateSettings(_ settings: AdvancedRecordingSettings) throws { - guard !isRecordingSubject.value else { - throw RecordingError.cannotChangeSettingsWhileRecording - } - - currentSettingsSubject.send(settings) - try reconfigureAudioEngine(for: settings) - } - - func startRecording() throws { - guard !isRecordingSubject.value else { - throw RecordingError.alreadyRecording - } - - let settings = currentSettingsSubject.value - - // Request recording permission - guard await requestRecordingPermission() else { - throw RecordingError.permissionDenied - } - - // Configure audio session - try configureAudioSession(for: settings) - - // Create audio file - audioFile = try createAudioFile(with: settings) - - // Start audio engine - try audioEngine.start() - - recordingStartTime = Date() - isPaused = false - isRecordingSubject.send(true) - - print("Advanced recording started with settings: \(settings)") - } - - func stopRecording() -> AnyPublisher { - return Future { [weak self] promise in - guard let self = self else { - promise(.failure(.engineNotInitialized)) - return - } - - guard self.isRecordingSubject.value else { - promise(.failure(.notRecording)) - return - } - - // Stop audio engine - self.audioEngine.stop() - - // Finalize audio file - self.audioFile = nil - - // Calculate recording duration - let duration = self.recordingDurationSubject.value - - // Create recording result - let result = RecordingResult( - duration: duration, - fileURL: self.getRecordingFileURL(), - settings: self.currentSettingsSubject.value, - quality: self.calculateRecordingQuality(), - fileSize: self.getFileSize(), - averageLevel: self.levelMonitor.averageLevel, - peakLevel: self.levelMonitor.peakLevel - ) - - self.isRecordingSubject.send(false) - self.recordingStartTime = nil - self.recordingDurationSubject.send(0.0) - - promise(.success(result)) - } - .eraseToAnyPublisher() - } - - func pauseRecording() throws { - guard isRecordingSubject.value else { - throw RecordingError.notRecording - } - - guard !isPaused else { - throw RecordingError.alreadyPaused - } - - audioEngine.pause() - isPaused = true - - print("Recording paused") - } - - func resumeRecording() throws { - guard isRecordingSubject.value else { - throw RecordingError.notRecording - } - - guard isPaused else { - throw RecordingError.notPaused - } - - try audioEngine.start() - isPaused = false - - print("Recording resumed") - } - - func cancelRecording() { - if isRecordingSubject.value { - audioEngine.stop() - isRecordingSubject.send(false) - } - - // Clean up any recording files - if let fileURL = getRecordingFileURL() { - try? FileManager.default.removeItem(at: fileURL) - } - - recordingStartTime = nil - recordingDurationSubject.send(0.0) - isPaused = false - - print("Recording cancelled") - } - - // MARK: - External Microphone Support - - func connectExternalMicrophone(_ microphone: ExternalMicrophone) -> AnyPublisher { - return Future { [weak self] promise in - guard let self = self else { - promise(.failure(.engineNotInitialized)) - return - } - - // Configure external microphone - self.configureExternalMicrophone(microphone) { result in - switch result { - case .success: - var microphones = self.externalMicrophonesSubject.value - microphones.append(microphone) - self.externalMicrophonesSubject.send(microphones) - promise(.success(())) - - case .failure(let error): - promise(.failure(error)) - } - } - } - .eraseToAnyPublisher() - } - - func disconnectExternalMicrophone() { - // Disconnect current external microphone - externalMicrophonesSubject.send([]) - - // Reconfigure audio engine for built-in microphone - try? reconfigureAudioEngine(for: currentSettingsSubject.value) - } - - func testMicrophone() -> AnyPublisher { - return Future { [weak self] promise in - guard let self = self else { - promise(.failure(.engineNotInitialized)) - return - } - - // Perform microphone test - self.performMicrophoneTest { result in - promise(.success(result)) - } - } - .eraseToAnyPublisher() - } - - // MARK: - Private Methods - - private func setupAudioEngine() { - // Configure audio engine with processing chain - audioEngine.attach(mixerNode) - audioEngine.attach(effectsChain.noiseReductionNode) - audioEngine.attach(effectsChain.gainControlNode) - audioEngine.attach(qualityEnhancer.equalizerNode) - - // Connect audio processing chain - let inputNode = audioEngine.inputNode - - audioEngine.connect(inputNode, to: effectsChain.noiseReductionNode, format: inputNode.inputFormat(forBus: 0)) - audioEngine.connect(effectsChain.noiseReductionNode, to: effectsChain.gainControlNode, format: inputNode.inputFormat(forBus: 0)) - audioEngine.connect(effectsChain.gainControlNode, to: qualityEnhancer.equalizerNode, format: inputNode.inputFormat(forBus: 0)) - audioEngine.connect(qualityEnhancer.equalizerNode, to: mixerNode, format: inputNode.inputFormat(forBus: 0)) - audioEngine.connect(mixerNode, to: audioEngine.mainMixerNode, format: inputNode.inputFormat(forBus: 0)) - - // Install audio tap for processing - installAudioTap() - } - - private func installAudioTap() { - let inputNode = audioEngine.inputNode - let format = inputNode.inputFormat(forBus: 0) - - inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, time in - guard let self = self else { return } - - // Monitor audio level - self.levelMonitor.processBuffer(buffer) - self.recordingLevelSubject.send(self.levelMonitor.currentLevel) - - // Create processed audio for transcription - let processedAudio = ProcessedAudio( - buffer: buffer, - timestamp: time.sampleTime, - sampleRate: format.sampleRate, - channelCount: Int(format.channelCount) - ) - - self.audioBufferSubject.send(processedAudio) - } - } - - private func reconfigureAudioEngine(for settings: AdvancedRecordingSettings) throws { - // Stop engine if running - if audioEngine.isRunning { - audioEngine.stop() - } - - // Remove existing taps - audioEngine.inputNode.removeTap(onBus: 0) - - // Configure effects chain - effectsChain.configureNoiseReduction(level: settings.noiseSuppressionLevel) - effectsChain.configureAutoGainControl(enabled: settings.autoGainControl) - - // Configure quality enhancer - qualityEnhancer.configureForRecordingQuality(settings.recordingQuality) - - // Reinstall audio tap - installAudioTap() - } - - private func requestRecordingPermission() async -> Bool { - return await withCheckedContinuation { continuation in - AVAudioSession.sharedInstance().requestRecordPermission { granted in - continuation.resume(returning: granted) - } - } - } - - private func configureAudioSession(for settings: AdvancedRecordingSettings) throws { - let audioSession = AVAudioSession.sharedInstance() - - try audioSession.setCategory(.playAndRecord, mode: .measurement, options: [.defaultToSpeaker, .allowBluetooth]) - try audioSession.setPreferredSampleRate(settings.sampleRate) - try audioSession.setPreferredIOBufferDuration(0.01) // 10ms buffer for low latency - try audioSession.setActive(true) - } - - private func createAudioFile(with settings: AdvancedRecordingSettings) throws -> AVAudioFile { - let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] - let fileName = "recording_\(Date().timeIntervalSince1970).\(settings.format.fileExtension)" - let fileURL = documentsPath.appendingPathComponent(fileName) - - let format = AVAudioFormat( - standardFormatWithSampleRate: settings.sampleRate, - channels: settings.channels - )! - - return try AVAudioFile(forWriting: fileURL, settings: format.settings) - } - - private func startLevelMonitoring() { - levelMonitor.levelPublisher - .sink { [weak self] level in - self?.recordingLevelSubject.send(level) - } - .store(in: &cancellables) - } - - private func startDurationMonitoring() { - Timer.publish(every: 0.1, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - guard let self = self, - let startTime = self.recordingStartTime, - self.isRecordingSubject.value && !self.isPaused else { - return - } - - let duration = Date().timeIntervalSince(startTime) - self.recordingDurationSubject.send(duration) - } - .store(in: &cancellables) - } - - private func getRecordingFileURL() -> URL? { - // Return the current recording file URL - return audioFile?.url - } - - private func calculateRecordingQuality() -> RecordingQualityMetrics { - return RecordingQualityMetrics( - snr: levelMonitor.signalToNoiseRatio, - thd: qualityEnhancer.totalHarmonicDistortion, - dynamicRange: levelMonitor.dynamicRange, - averageLevel: levelMonitor.averageLevel, - peakLevel: levelMonitor.peakLevel - ) - } - - private func getFileSize() -> Int64 { - guard let fileURL = getRecordingFileURL(), - let attributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path) else { - return 0 - } - - return attributes[.size] as? Int64 ?? 0 - } - - private func configureExternalMicrophone(_ microphone: ExternalMicrophone, completion: @escaping (Result) -> Void) { - // Configure external microphone (implementation depends on microphone type) - DispatchQueue.global().async { - // Simulate external microphone configuration - Thread.sleep(forTimeInterval: 1.0) - - DispatchQueue.main.async { - completion(.success(())) - } - } - } - - private func performMicrophoneTest(completion: @escaping (MicrophoneTestResult) -> Void) { - // Perform comprehensive microphone test - DispatchQueue.global().async { - let result = MicrophoneTestResult( - frequency: 1000, // 1kHz test tone - level: -20, // dB - snr: 60, // dB - distortion: 0.01, // 1% THD - latency: 10, // 10ms - passed: true - ) - - DispatchQueue.main.async { - completion(result) - } - } - } -} - -// MARK: - Supporting Types - -struct ExternalMicrophone: Identifiable, Codable { - let id: UUID - let name: String - let type: MicrophoneType - let connectionType: ConnectionType - let specifications: MicrophoneSpecs - - init(name: String, type: MicrophoneType, connectionType: ConnectionType, specifications: MicrophoneSpecs) { - self.id = UUID() - self.name = name - self.type = type - self.connectionType = connectionType - self.specifications = specifications - } -} - -enum MicrophoneType: String, Codable { - case lavalier = "lavalier" - case shotgun = "shotgun" - case studio = "studio" - case headset = "headset" - case wireless = "wireless" - case usb = "usb" -} - -enum ConnectionType: String, Codable { - case bluetooth = "bluetooth" - case lightning = "lightning" - case usbc = "usbc" - case wireless = "wireless" - case builtin = "builtin" -} - -struct MicrophoneSpecs: Codable { - let frequencyResponse: FrequencyRange - let sensitivity: Float // dB - let maxSPL: Float // dB - let snr: Float // dB - let batteryLife: TimeInterval? // seconds, nil for wired -} - -struct FrequencyRange: Codable { - let minimum: Float // Hz - let maximum: Float // Hz -} - -struct RecordingResult { - let duration: TimeInterval - let fileURL: URL? - let settings: AdvancedRecordingSettings - let quality: RecordingQualityMetrics - let fileSize: Int64 - let averageLevel: Float - let peakLevel: Float -} - -struct RecordingQualityMetrics { - let snr: Float // Signal-to-noise ratio in dB - let thd: Float // Total harmonic distortion percentage - let dynamicRange: Float // Dynamic range in dB - let averageLevel: Float // Average recording level - let peakLevel: Float // Peak recording level -} - -struct MicrophoneTestResult { - let frequency: Float // Hz - let level: Float // dB - let snr: Float // dB - let distortion: Float // Percentage - let latency: TimeInterval // ms - let passed: Bool -} - -// MARK: - Audio Processing Components - -class AudioEffectsChain { - let noiseReductionNode: AVAudioUnitEffect - let gainControlNode: AVAudioUnitEffect - - init() { - // Initialize audio effect nodes (simplified for this example) - self.noiseReductionNode = AVAudioUnitEffect() - self.gainControlNode = AVAudioUnitEffect() - } - - func configureNoiseReduction(level: Float) { - // Configure noise reduction level (0.0 to 1.0) - print("Configuring noise reduction level: \(level)") - } - - func configureAutoGainControl(enabled: Bool) { - // Configure automatic gain control - print("Auto gain control: \(enabled ? "enabled" : "disabled")") - } -} - -class AudioQualityEnhancer { - let equalizerNode: AVAudioUnitEQ - - init() { - self.equalizerNode = AVAudioUnitEQ(numberOfBands: 10) - } - - func configureForRecordingQuality(_ quality: RecordingQuality) { - // Configure EQ based on recording quality - switch quality { - case .studio: - configureStudioEQ() - case .high: - configureHighQualityEQ() - case .medium: - configureMediumQualityEQ() - case .low, .voice: - configureVoiceOptimizedEQ() - } - } - - var totalHarmonicDistortion: Float { - // Calculate THD (simplified) - return 0.01 // 1% - } - - private func configureStudioEQ() { - // Flat response for studio recording - for i in 0..() - - var levelPublisher: AnyPublisher { - levelSubject.eraseToAnyPublisher() - } - - var currentLevel: Float { _currentLevel } - var averageLevel: Float { _averageLevel } - var peakLevel: Float { _peakLevel } - var signalToNoiseRatio: Float { _signalToNoiseRatio } - var dynamicRange: Float { _dynamicRange } - - func processBuffer(_ buffer: AVAudioPCMBuffer) { - guard let channelData = buffer.floatChannelData else { return } - - let frameLength = Int(buffer.frameLength) - let channelCount = Int(buffer.format.channelCount) - - var sum: Float = 0.0 - var peak: Float = 0.0 - - for channel in 0.. { get } - var isRecording: Bool { get } - - func startRecording() throws - func stopRecording() - func configure(sampleRate: Double, bufferDuration: TimeInterval) throws -} - -class AudioManager: NSObject, AudioManagerProtocol { - private let audioEngine = AVAudioEngine() - private let audioSession = AVAudioSession.sharedInstance() - private let processingQueue = DispatchQueue(label: "audio.processing", qos: .userInteractive) - - // Test mode when running under XCTest - private let isTesting: Bool = ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil - private var testRecording = false - private var testSampleRate: Double = 16000.0 - private var testBufferDuration: TimeInterval = 0.005 - - private let audioSubject = PassthroughSubject() - private var cancellables = Set() - - var audioPublisher: AnyPublisher { - audioSubject.eraseToAnyPublisher() - } - - var isRecording: Bool { - isTesting ? testRecording : audioEngine.isRunning - } - - override init() { - super.init() - setupAudioSession() - } - - func startRecording() throws { - guard !isRecording else { return } - if isTesting { - // simulate audio in tests - testRecording = true - scheduleTestAudio() - } else { - try configureAudioEngine() - try audioEngine.start() - print("Audio recording started") - } - } - - func stopRecording() { - if isTesting { - testRecording = false - } else if audioEngine.isRunning { - audioEngine.stop() - audioEngine.inputNode.removeTap(onBus: 0) - print("Audio recording stopped") - } - } - - func configure(sampleRate: Double = 16000.0, bufferDuration: TimeInterval = 0.005) throws { - if isTesting { - testSampleRate = sampleRate - testBufferDuration = bufferDuration - } else { - try audioSession.setPreferredSampleRate(sampleRate) - try audioSession.setPreferredIOBufferDuration(bufferDuration) - } - } - - private func setupAudioSession() { - do { - try audioSession.setCategory(.playAndRecord, mode: .measurement, options: [.defaultToSpeaker, .allowBluetooth]) - try audioSession.setActive(true) - } catch { - audioSubject.send(completion: .failure(.sessionSetupFailed(error))) - } - } - - private func configureAudioEngine() throws { - let inputNode = audioEngine.inputNode - let inputFormat = inputNode.outputFormat(forBus: 0) - - // Configure format for 16kHz mono - guard let format = AVAudioFormat(commonFormat: .pcmFormatFloat32, - sampleRate: 16000, - channels: 1, - interleaved: false) else { - throw AudioError.formatConfigurationFailed - } - - inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, time in - self?.processAudioBuffer(buffer, at: time) - } - } - - private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, at time: AVAudioTime) { - processingQueue.async { [weak self] in - guard let self = self else { return } - let processedAudio = ProcessedAudio( - buffer: buffer, - timestamp: time.sampleTime, - sampleRate: buffer.format.sampleRate, - channelCount: Int(buffer.format.channelCount) - ) - self.audioSubject.send(processedAudio) - } - } - - // MARK: - Test audio simulation - private func scheduleTestAudio() { - guard testRecording else { return } - // send mock buffer after specified duration - processingQueue.asyncAfter(deadline: .now() + testBufferDuration) { [weak self] in - guard let self = self, self.testRecording else { return } - // create silent buffer - let format = AVAudioFormat(standardFormatWithSampleRate: self.testSampleRate, channels: 1)! - let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 1024)! - buffer.frameLength = 1024 - let processed = ProcessedAudio( - buffer: buffer, - timestamp: AVAudioFramePosition(Date().timeIntervalSince1970 * self.testSampleRate), - sampleRate: self.testSampleRate, - channelCount: 1 - ) - self.audioSubject.send(processed) - // schedule next - self.scheduleTestAudio() - } - } -} - -// MARK: - Data Models - -struct ProcessedAudio { - let buffer: AVAudioPCMBuffer - let timestamp: AVAudioFramePosition - let sampleRate: Double - let channelCount: Int - let id: UUID = UUID() - - var duration: TimeInterval { - Double(buffer.frameLength) / sampleRate - } -} - -enum AudioError: Error { - case sessionSetupFailed(Error) - case formatConfigurationFailed - case recordingStartFailed(Error) - case processingFailed(Error) - case permissionDenied - - var localizedDescription: String { - switch self { - case .sessionSetupFailed(let error): - return "Audio session setup failed: \(error.localizedDescription)" - case .formatConfigurationFailed: - return "Audio format configuration failed" - case .recordingStartFailed(let error): - return "Recording start failed: \(error.localizedDescription)" - case .processingFailed(let error): - return "Audio processing failed: \(error.localizedDescription)" - case .permissionDenied: - return "Microphone permission denied" - } - } -} \ No newline at end of file diff --git a/Helix/Core/Audio/NoiseReductionProcessor.swift b/Helix/Core/Audio/NoiseReductionProcessor.swift deleted file mode 100644 index 35bd5a3..0000000 --- a/Helix/Core/Audio/NoiseReductionProcessor.swift +++ /dev/null @@ -1,228 +0,0 @@ -import AVFoundation -import Accelerate - -protocol NoiseReductionProcessorProtocol { - func processBuffer(_ buffer: AVAudioPCMBuffer) -> AVAudioPCMBuffer - func updateNoiseProfile(_ buffer: AVAudioPCMBuffer) - func setReductionLevel(_ level: Float) -} - -class NoiseReductionProcessor: NoiseReductionProcessorProtocol { - private var noiseProfile: [Float] = [] - private var reductionLevel: Float = 0.5 - private let fftSize: Int = 1024 - private let overlapFactor: Float = 0.5 - - private var fftSetup: FFTSetup? - private var window: [Float] = [] - - init() { - setupFFT() - setupWindow() - } - - deinit { - if let fftSetup = fftSetup { - vDSP_destroy_fftsetup(fftSetup) - } - } - - func processBuffer(_ buffer: AVAudioPCMBuffer) -> AVAudioPCMBuffer { - guard let inputData = buffer.floatChannelData?[0], - let outputBuffer = AVAudioPCMBuffer(pcmFormat: buffer.format, frameCapacity: buffer.frameCapacity) else { - return buffer - } - - let frameCount = Int(buffer.frameLength) - let outputData = outputBuffer.floatChannelData![0] - - // Apply spectral subtraction noise reduction - performSpectralSubtraction(input: inputData, output: outputData, frameCount: frameCount) - - outputBuffer.frameLength = buffer.frameLength - return outputBuffer - } - - func updateNoiseProfile(_ buffer: AVAudioPCMBuffer) { - guard let inputData = buffer.floatChannelData?[0] else { return } - - let frameCount = Int(buffer.frameLength) - - // Calculate power spectrum for noise profiling - let powerSpectrum = calculatePowerSpectrum(input: inputData, frameCount: frameCount) - - if noiseProfile.isEmpty { - noiseProfile = powerSpectrum - } else { - // Update noise profile with exponential smoothing - let alpha: Float = 0.1 - for i in 0.., output: UnsafeMutablePointer, frameCount: Int) { - guard !noiseProfile.isEmpty, - let fftSetup = fftSetup else { - // No noise profile available, copy input to output - memcpy(output, input, frameCount * MemoryLayout.size) - return - } - - let hopSize = Int(Float(fftSize) * (1.0 - overlapFactor)) - var position = 0 - - // Initialize output buffer - memset(output, 0, frameCount * MemoryLayout.size) - - while position + fftSize <= frameCount { - // Apply windowing - var windowedFrame = Array(repeating: Float(0), count: fftSize) - for i in 0.., frameCount: Int) -> [Float] { - guard frameCount >= fftSize else { return [] } - - var windowedFrame = Array(repeating: Float(0), count: fftSize) - for i in 0.. [DSPComplex] { - guard let fftSetup = fftSetup else { return [] } - - let halfSize = fftSize / 2 - var realPart = Array(repeating: Float(0), count: halfSize) - var imagPart = Array(repeating: Float(0), count: halfSize) - - // Prepare input for vDSP - for i in 0.. [Float] { - guard let fftSetup = fftSetup, - spectrum.count == fftSize / 2 else { return [] } - - let halfSize = fftSize / 2 - var realPart = spectrum.map { $0.real } - var imagPart = spectrum.map { $0.imaginary } - - var splitComplex = DSPSplitComplex(realp: &realPart, imagp: &imagPart) - vDSP_fft_zrip(fftSetup, &splitComplex, 1, vDSP_Length(log2(Float(fftSize))), Int32(FFT_INVERSE)) - - var result = Array(repeating: Float(0), count: fftSize) - for i in 0.. [DSPComplex] { - guard spectrum.count == noiseProfile.count else { return spectrum } - - var result: [DSPComplex] = [] - - for i in 0.., frameCount: Int) { - var maxValue: Float = 0 - vDSP_maxv(output, 1, &maxValue, vDSP_Length(frameCount)) - - if maxValue > 0 { - let scale = 0.95 / maxValue - vDSP_vsmul(output, 1, &scale, output, 1, vDSP_Length(frameCount)) - } - } -} - -// MARK: - Supporting Types - -struct DSPComplex { - let real: Float - let imaginary: Float - - init(real: Float, imag: Float) { - self.real = real - self.imaginary = imag - } -} \ No newline at end of file diff --git a/Helix/Core/Audio/SpeakerDiarizationEngine.swift b/Helix/Core/Audio/SpeakerDiarizationEngine.swift deleted file mode 100644 index 2e13b64..0000000 --- a/Helix/Core/Audio/SpeakerDiarizationEngine.swift +++ /dev/null @@ -1,501 +0,0 @@ -import AVFoundation -import Accelerate -import Foundation - -protocol SpeakerDiarizationEngineProtocol { - func identifySpeaker(in buffer: AVAudioPCMBuffer) -> SpeakerIdentification? - func trainSpeakerModel(samples: [AVAudioPCMBuffer], speakerId: UUID) -> Bool - func addSpeaker(id: UUID, name: String?, isCurrentUser: Bool) - func removeSpeaker(id: UUID) - func getCurrentSpeakers() -> [Speaker] - func resetSpeakerModels() -} - -struct SpeakerIdentification { - let speakerId: UUID - let confidence: Float - let audioSegment: AudioSegment - let embedding: SpeakerEmbedding - let timestamp: TimeInterval -} - -struct AudioSegment { - let startTime: TimeInterval - let endTime: TimeInterval - let buffer: AVAudioPCMBuffer - let energy: Float -} - -struct SpeakerEmbedding { - let features: [Float] - let dimension: Int - - init(features: [Float]) { - self.features = features - self.dimension = features.count - } - - func distance(to other: SpeakerEmbedding) -> Float { - guard features.count == other.features.count else { return Float.greatestFiniteMagnitude } - - var distance: Float = 0.0 - vDSP_distancesq(features, 1, other.features, 1, &distance, vDSP_Length(features.count)) - return sqrt(distance) - } - - func cosineSimilarity(to other: SpeakerEmbedding) -> Float { - guard features.count == other.features.count else { return -1.0 } - - var dotProduct: Float = 0.0 - var normA: Float = 0.0 - var normB: Float = 0.0 - - vDSP_dotpr(features, 1, other.features, 1, &dotProduct, vDSP_Length(features.count)) - vDSP_svesq(features, 1, &normA, vDSP_Length(features.count)) - vDSP_svesq(other.features, 1, &normB, vDSP_Length(features.count)) - - let denominator = sqrt(normA) * sqrt(normB) - return denominator > 0 ? dotProduct / denominator : -1.0 - } -} - -struct Speaker { - let id: UUID - let name: String? - let isCurrentUser: Bool - var voiceModel: SpeakerModel? - let createdAt: Date - var lastSeen: Date? - - init(id: UUID = UUID(), name: String? = nil, isCurrentUser: Bool = false) { - self.id = id - self.name = name - self.isCurrentUser = isCurrentUser - self.createdAt = Date() - } -} - -struct SpeakerModel { - let speakerId: UUID - let embeddings: [SpeakerEmbedding] - let centroid: SpeakerEmbedding - let threshold: Float - let trainingCount: Int - - init(speakerId: UUID, embeddings: [SpeakerEmbedding]) { - self.speakerId = speakerId - self.embeddings = embeddings - self.centroid = SpeakerModel.calculateCentroid(from: embeddings) - self.threshold = SpeakerModel.calculateThreshold(from: embeddings, centroid: self.centroid) - self.trainingCount = embeddings.count - } - - private static func calculateCentroid(from embeddings: [SpeakerEmbedding]) -> SpeakerEmbedding { - guard !embeddings.isEmpty else { - return SpeakerEmbedding(features: []) - } - - let dimension = embeddings.first?.dimension ?? 0 - var centroidFeatures = Array(repeating: Float(0), count: dimension) - - for embedding in embeddings { - for i in 0.. Float { - guard embeddings.count > 1 else { return 0.5 } - - let distances = embeddings.map { centroid.distance(to: $0) } - let mean = distances.reduce(0, +) / Float(distances.count) - - let variance = distances.map { pow($0 - mean, 2) }.reduce(0, +) / Float(distances.count) - let standardDeviation = sqrt(variance) - - // Threshold is mean + 2 standard deviations - return mean + 2 * standardDeviation - } - - func matches(_ embedding: SpeakerEmbedding) -> (matches: Bool, confidence: Float) { - let distance = centroid.distance(to: embedding) - let similarity = centroid.cosineSimilarity(to: embedding) - - let distanceMatch = distance <= threshold - let similarityThreshold: Float = 0.7 - let similarityMatch = similarity >= similarityThreshold - - let confidence = max(0.0, min(1.0, (similarityThreshold + similarity) / 2.0)) - - return (distanceMatch && similarityMatch, confidence) - } -} - -class SpeakerDiarizationEngine: SpeakerDiarizationEngineProtocol { - private var speakers: [UUID: Speaker] = [:] - private var speakerModels: [UUID: SpeakerModel] = [:] - private let featureExtractor = VoiceFeatureExtractor() - - private let similarityThreshold: Float = 0.7 - private let minSamplesForTraining = 5 - private let maxSpeakers = 8 - - private let processingQueue = DispatchQueue(label: "speaker.diarization", qos: .userInitiated) - - func identifySpeaker(in buffer: AVAudioPCMBuffer) -> SpeakerIdentification? { - guard let embedding = featureExtractor.extractFeatures(from: buffer) else { - return nil - } - - var bestMatch: (speakerId: UUID, confidence: Float)? - var bestDistance: Float = Float.greatestFiniteMagnitude - - for (speakerId, model) in speakerModels { - let result = model.matches(embedding) - - if result.matches && result.confidence > (bestMatch?.confidence ?? 0) { - bestMatch = (speakerId, result.confidence) - bestDistance = model.centroid.distance(to: embedding) - } - } - - if let match = bestMatch { - let audioSegment = AudioSegment( - startTime: Date().timeIntervalSince1970, - endTime: Date().timeIntervalSince1970 + Double(buffer.frameLength) / buffer.format.sampleRate, - buffer: buffer, - energy: calculateEnergy(buffer) - ) - - // Update last seen time - speakers[match.speakerId]?.lastSeen = Date() - - return SpeakerIdentification( - speakerId: match.speakerId, - confidence: match.confidence, - audioSegment: audioSegment, - embedding: embedding, - timestamp: Date().timeIntervalSince1970 - ) - } - - return nil - } - - func trainSpeakerModel(samples: [AVAudioPCMBuffer], speakerId: UUID) -> Bool { - guard samples.count >= minSamplesForTraining else { - print("Not enough samples for training: \(samples.count) < \(minSamplesForTraining)") - return false - } - - var embeddings: [SpeakerEmbedding] = [] - - for sample in samples { - if let embedding = featureExtractor.extractFeatures(from: sample) { - embeddings.append(embedding) - } - } - - guard embeddings.count >= minSamplesForTraining else { - print("Failed to extract enough features for training") - return false - } - - let model = SpeakerModel(speakerId: speakerId, embeddings: embeddings) - speakerModels[speakerId] = model - - if var speaker = speakers[speakerId] { - speaker.voiceModel = model - speakers[speakerId] = speaker - } - - print("Trained speaker model for \(speakerId) with \(embeddings.count) samples") - return true - } - - func addSpeaker(id: UUID, name: String?, isCurrentUser: Bool = false) { - let speaker = Speaker(id: id, name: name, isCurrentUser: isCurrentUser) - speakers[id] = speaker - print("Added speaker: \(name ?? "Unknown") (\(id))") - } - - func removeSpeaker(id: UUID) { - speakers.removeValue(forKey: id) - speakerModels.removeValue(forKey: id) - print("Removed speaker: \(id)") - } - - func getCurrentSpeakers() -> [Speaker] { - return Array(speakers.values) - } - - func resetSpeakerModels() { - speakerModels.removeAll() - for speakerId in speakers.keys { - speakers[speakerId]?.voiceModel = nil - } - print("Reset all speaker models") - } - - private func calculateEnergy(_ buffer: AVAudioPCMBuffer) -> Float { - guard let audioData = buffer.floatChannelData?[0] else { return 0.0 } - - var energy: Float = 0.0 - vDSP_rmsqv(audioData, 1, &energy, vDSP_Length(buffer.frameLength)) - - return 20.0 * log10(max(energy, 1e-10)) - } -} - -// MARK: - Voice Feature Extractor - -class VoiceFeatureExtractor { - private let fftSize = 512 - private let melFilterCount = 13 - private let sampleRate: Double = 16000 - - func extractFeatures(from buffer: AVAudioPCMBuffer) -> SpeakerEmbedding? { - guard let audioData = buffer.floatChannelData?[0], - buffer.frameLength > 0 else { - return nil - } - - let frameLength = Int(buffer.frameLength) - - // Extract MFCC features - let mfccFeatures = extractMFCC(audioData: audioData, frameLength: frameLength) - - // Extract additional prosodic features - let prosodyFeatures = extractProsodyFeatures(audioData: audioData, frameLength: frameLength, sampleRate: buffer.format.sampleRate) - - // Combine all features - var allFeatures = mfccFeatures - allFeatures.append(contentsOf: prosodyFeatures) - - return SpeakerEmbedding(features: allFeatures) - } - - private func extractMFCC(audioData: UnsafePointer, frameLength: Int) -> [Float] { - // Pre-emphasis filter - var preEmphasized = Array(repeating: Float(0), count: frameLength) - let alpha: Float = 0.97 - preEmphasized[0] = audioData[0] - for i in 1.., frameLength: Int, sampleRate: Double) -> [Float] { - var features: [Float] = [] - - // Fundamental frequency (F0) estimation - let f0 = estimateFundamentalFrequency(audioData: audioData, frameLength: frameLength, sampleRate: sampleRate) - features.append(f0) - - // Energy - var energy: Float = 0.0 - vDSP_rmsqv(audioData, 1, &energy, vDSP_Length(frameLength)) - features.append(20.0 * log10(max(energy, 1e-10))) - - // Zero crossing rate - var zcr: Float = 0.0 - for i in 1..= 0) != (audioData[i-1] >= 0) { - zcr += 1 - } - } - zcr /= Float(frameLength - 1) - features.append(zcr) - - // Spectral centroid - let spectralCentroid = calculateSpectralCentroid(audioData: audioData, frameLength: frameLength, sampleRate: sampleRate) - features.append(spectralCentroid) - - return features - } - - private func calculatePowerSpectrum(_ input: [Float]) -> [Float] { - let paddedSize = max(fftSize, input.count) - let log2Size = vDSP_Length(log2(Float(paddedSize))) - let actualFFTSize = Int(pow(2, ceil(log2(Float(paddedSize))))) - - guard let fftSetup = vDSP_create_fftsetup(log2Size, Int32(kFFTRadix2)) else { - return Array(repeating: 0, count: actualFFTSize / 2) - } - - defer { - vDSP_destroy_fftsetup(fftSetup) - } - - let halfSize = actualFFTSize / 2 - var paddedInput = Array(repeating: Float(0), count: actualFFTSize) - - for i in 0.. [Float] { - let melFilters = createMelFilterBank(fftSize: fftSize, numFilters: melFilterCount, sampleRate: sampleRate) - var melSpectrum = Array(repeating: Float(0), count: melFilterCount) - - for i in 0.. [Float] { - let numCoeffs = min(13, melSpectrum.count) - var mfcc = Array(repeating: Float(0), count: numCoeffs) - - for i in 0.. [[Float]] { - let lowFreq: Float = 0 - let highFreq = Float(sampleRate / 2) - - func hzToMel(_ hz: Float) -> Float { - return 2595 * log10(1 + hz / 700) - } - - func melToHz(_ mel: Float) -> Float { - return 700 * (pow(10, mel / 2595) - 1) - } - - let lowMel = hzToMel(lowFreq) - let highMel = hzToMel(highFreq) - - var melPoints = Array(repeating: Float(0), count: numFilters + 2) - for i in 0.. left { - filterBank[i][j] = Float(j - left) / Float(center - left) - } - } - - for j in center.. center { - filterBank[i][j] = Float(right - j) / Float(right - center) - } - } - } - - return filterBank - } - - private func estimateFundamentalFrequency(audioData: UnsafePointer, frameLength: Int, sampleRate: Double) -> Float { - // Simple autocorrelation-based F0 estimation - let minPeriod = Int(sampleRate / 800) // 800 Hz max - let maxPeriod = Int(sampleRate / 50) // 50 Hz min - - var maxCorrelation: Float = 0.0 - var bestPeriod = 0 - - for period in minPeriod...min(maxPeriod, frameLength / 2) { - var correlation: Float = 0.0 - - for i in 0..<(frameLength - period) { - correlation += audioData[i] * audioData[i + period] - } - - if correlation > maxCorrelation { - maxCorrelation = correlation - bestPeriod = period - } - } - - return bestPeriod > 0 ? Float(sampleRate) / Float(bestPeriod) : 0.0 - } - - private func calculateSpectralCentroid(audioData: UnsafePointer, frameLength: Int, sampleRate: Double) -> Float { - let powerSpectrum = calculatePowerSpectrum(Array(UnsafeBufferPointer(start: audioData, count: frameLength))) - - var weightedSum: Float = 0.0 - var magnitudeSum: Float = 0.0 - - for i in 1.. 0 ? weightedSum / magnitudeSum : 0.0 - } -} \ No newline at end of file diff --git a/Helix/Core/Audio/VoiceActivityDetector.swift b/Helix/Core/Audio/VoiceActivityDetector.swift deleted file mode 100644 index 611b8b3..0000000 --- a/Helix/Core/Audio/VoiceActivityDetector.swift +++ /dev/null @@ -1,224 +0,0 @@ -import AVFoundation -import Accelerate - -protocol VoiceActivityDetectorProtocol { - func detectVoiceActivity(in buffer: AVAudioPCMBuffer) -> VoiceActivityResult - func updateBackground(with buffer: AVAudioPCMBuffer) - func setSensitivity(_ sensitivity: Float) -} - -struct VoiceActivityResult { - let hasVoice: Bool - let confidence: Float - let energy: Float - let spectralCentroid: Float - let zeroCrossingRate: Float - let timestamp: TimeInterval -} - -class VoiceActivityDetector: VoiceActivityDetectorProtocol { - private var backgroundEnergyLevel: Float = 0.0 - private var backgroundSpectralCentroid: Float = 0.0 - private var sensitivity: Float = 0.5 - private let adaptationRate: Float = 0.01 - - // Thresholds for voice detection - private let energyThresholdMultiplier: Float = 2.5 - private let spectralCentroidThreshold: Float = 1000.0 - private let zeroCrossingRateThreshold: Float = 0.1 - - private var frameCount: Int = 0 - - func detectVoiceActivity(in buffer: AVAudioPCMBuffer) -> VoiceActivityResult { - guard let audioData = buffer.floatChannelData?[0] else { - return VoiceActivityResult( - hasVoice: false, - confidence: 0.0, - energy: 0.0, - spectralCentroid: 0.0, - zeroCrossingRate: 0.0, - timestamp: Date().timeIntervalSince1970 - ) - } - - let frameLength = Int(buffer.frameLength) - let sampleRate = buffer.format.sampleRate - - // Calculate audio features - let energy = calculateEnergy(audioData, frameLength: frameLength) - let spectralCentroid = calculateSpectralCentroid(audioData, frameLength: frameLength, sampleRate: sampleRate) - let zeroCrossingRate = calculateZeroCrossingRate(audioData, frameLength: frameLength, sampleRate: sampleRate) - - // Determine voice activity - let hasVoice = isVoiceDetected(energy: energy, spectralCentroid: spectralCentroid, zeroCrossingRate: zeroCrossingRate) - let confidence = calculateConfidence(energy: energy, spectralCentroid: spectralCentroid, zeroCrossingRate: zeroCrossingRate) - - return VoiceActivityResult( - hasVoice: hasVoice, - confidence: confidence, - energy: energy, - spectralCentroid: spectralCentroid, - zeroCrossingRate: zeroCrossingRate, - timestamp: Date().timeIntervalSince1970 - ) - } - - func updateBackground(with buffer: AVAudioPCMBuffer) { - guard let audioData = buffer.floatChannelData?[0] else { return } - - let frameLength = Int(buffer.frameLength) - let sampleRate = buffer.format.sampleRate - - let energy = calculateEnergy(audioData, frameLength: frameLength) - let spectralCentroid = calculateSpectralCentroid(audioData, frameLength: frameLength, sampleRate: sampleRate) - - // Update background levels with exponential smoothing - if frameCount == 0 { - backgroundEnergyLevel = energy - backgroundSpectralCentroid = spectralCentroid - } else { - backgroundEnergyLevel = adaptationRate * energy + (1 - adaptationRate) * backgroundEnergyLevel - backgroundSpectralCentroid = adaptationRate * spectralCentroid + (1 - adaptationRate) * backgroundSpectralCentroid - } - - frameCount += 1 - } - - func setSensitivity(_ sensitivity: Float) { - self.sensitivity = max(0.0, min(1.0, sensitivity)) - } - - private func calculateEnergy(_ audioData: UnsafePointer, frameLength: Int) -> Float { - var energy: Float = 0.0 - - // Calculate RMS energy - vDSP_rmsqv(audioData, 1, &energy, vDSP_Length(frameLength)) - - // Convert to dB - let energyDB = 20.0 * log10(max(energy, 1e-10)) - - return energyDB - } - - private func calculateSpectralCentroid(_ audioData: UnsafePointer, frameLength: Int, sampleRate: Double) -> Float { - guard frameLength > 0 else { return 0.0 } - - // Calculate FFT size (next power of 2) - let fftSize = Int(pow(2, ceil(log2(Double(frameLength))))) - let halfFFTSize = fftSize / 2 - - // Prepare data for FFT - var fftInput = Array(repeating: Float(0), count: fftSize) - for i in 0.. 0 ? weightedSum / magnitudeSum : 0.0 - } - - private func calculateZeroCrossingRate(_ audioData: UnsafePointer, frameLength: Int, sampleRate: Double) -> Float { - guard frameLength > 1 else { return 0.0 } - - var zeroCrossings = 0 - - for i in 1..= 0) != (audioData[i-1] >= 0) { - zeroCrossings += 1 - } - } - - return Float(zeroCrossings) / Float(frameLength - 1) * Float(sampleRate) / 2.0 - } - - private func calculateMagnitudeSpectrum(_ input: [Float], fftSize: Int) -> [Float] { - let halfSize = fftSize / 2 - let log2Size = vDSP_Length(log2(Float(fftSize))) - - guard let fftSetup = vDSP_create_fftsetup(log2Size, Int32(kFFTRadix2)) else { - return Array(repeating: 0, count: halfSize) - } - - defer { - vDSP_destroy_fftsetup(fftSetup) - } - - var realPart = Array(repeating: Float(0), count: halfSize) - var imagPart = Array(repeating: Float(0), count: halfSize) - - // Prepare input for vDSP (interleaved to split) - for i in 0.. Bool { - // Energy-based detection - let energyThreshold = backgroundEnergyLevel + (energyThresholdMultiplier * (1.0 - sensitivity)) - let energyCondition = energy > energyThreshold - - // Spectral centroid-based detection (voice typically has higher spectral centroid than noise) - let spectralCondition = spectralCentroid > spectralCentroidThreshold - - // Zero crossing rate condition (voice has moderate ZCR) - let zcrCondition = zeroCrossingRate > zeroCrossingRateThreshold && zeroCrossingRate < 10 * zeroCrossingRateThreshold - - // Combine conditions - return energyCondition && (spectralCondition || zcrCondition) - } - - private func calculateConfidence(energy: Float, spectralCentroid: Float, zeroCrossingRate: Float) -> Float { - let energyThreshold = backgroundEnergyLevel + energyThresholdMultiplier - let energyConfidence = max(0.0, min(1.0, (energy - backgroundEnergyLevel) / energyThreshold)) - - let spectralConfidence = max(0.0, min(1.0, spectralCentroid / (2 * spectralCentroidThreshold))) - - let zcrConfidence: Float - if zeroCrossingRate < zeroCrossingRateThreshold { - zcrConfidence = 0.0 - } else if zeroCrossingRate > 10 * zeroCrossingRateThreshold { - zcrConfidence = 0.0 - } else { - zcrConfidence = 1.0 - abs(zeroCrossingRate - 5 * zeroCrossingRateThreshold) / (5 * zeroCrossingRateThreshold) - } - - // Weighted combination - return 0.5 * energyConfidence + 0.3 * spectralConfidence + 0.2 * zcrConfidence - } -} \ No newline at end of file diff --git a/Helix/Core/Display/RealTimeTranscriptionDisplay.swift b/Helix/Core/Display/RealTimeTranscriptionDisplay.swift deleted file mode 100644 index e269ae0..0000000 --- a/Helix/Core/Display/RealTimeTranscriptionDisplay.swift +++ /dev/null @@ -1,652 +0,0 @@ -// -// RealTimeTranscriptionDisplay.swift -// Helix -// - -import Foundation -import SwiftUI -import Combine - -// MARK: - Transcription Display Configuration - -struct TranscriptionDisplaySettings { - var textSize: TextSize - var textColor: Color - var backgroundColor: Color - var fontFamily: FontFamily - var displayMode: DisplayMode - var position: DisplayPosition - var scrollBehavior: ScrollBehavior - var fadeInAnimation: Bool - var wordHighlighting: Bool - var speakerColors: [UUID: Color] - var maxVisibleLines: Int - var autoHideDelay: TimeInterval - var confidence: ConfidenceDisplay - - static let `default` = TranscriptionDisplaySettings( - textSize: .medium, - textColor: .primary, - backgroundColor: .clear, - fontFamily: .system, - displayMode: .overlay, - position: .bottom, - scrollBehavior: .smooth, - fadeInAnimation: true, - wordHighlighting: true, - speakerColors: [:], - maxVisibleLines: 3, - autoHideDelay: 5.0, - confidence: .minimal - ) - - static let glassesOptimized = TranscriptionDisplaySettings( - textSize: .large, - textColor: .white, - backgroundColor: Color.black.opacity(0.3), - fontFamily: .monospace, - displayMode: .overlay, - position: .center, - scrollBehavior: .snap, - fadeInAnimation: true, - wordHighlighting: false, - speakerColors: [:], - maxVisibleLines: 2, - autoHideDelay: 3.0, - confidence: .none - ) -} - -enum TextSize: String, CaseIterable, Codable { - case small = "small" - case medium = "medium" - case large = "large" - case extraLarge = "extra_large" - - var scaleFactor: CGFloat { - switch self { - case .small: return 0.8 - case .medium: return 1.0 - case .large: return 1.2 - case .extraLarge: return 1.5 - } - } -} - -enum FontFamily: String, CaseIterable, Codable { - case system = "system" - case monospace = "monospace" - case serif = "serif" - case sansSerif = "sans_serif" - - var font: Font { - switch self { - case .system: return .system(.body) - case .monospace: return .system(.body, design: .monospaced) - case .serif: return .system(.body, design: .serif) - case .sansSerif: return .system(.body, design: .default) - } - } -} - -enum DisplayMode: String, CaseIterable, Codable { - case overlay = "overlay" - case sidebar = "sidebar" - case popup = "popup" - case floating = "floating" - case fullscreen = "fullscreen" - - var description: String { - switch self { - case .overlay: return "Overlay on screen" - case .sidebar: return "Side panel" - case .popup: return "Popup window" - case .floating: return "Floating window" - case .fullscreen: return "Full screen" - } - } -} - -enum DisplayPosition: String, CaseIterable, Codable { - case top = "top" - case center = "center" - case bottom = "bottom" - case left = "left" - case right = "right" - case topLeft = "top_left" - case topRight = "top_right" - case bottomLeft = "bottom_left" - case bottomRight = "bottom_right" -} - -enum ScrollBehavior: String, CaseIterable, Codable { - case smooth = "smooth" - case snap = "snap" - case instant = "instant" - case typewriter = "typewriter" -} - -enum ConfidenceDisplay: String, CaseIterable, Codable { - case none = "none" - case minimal = "minimal" - case detailed = "detailed" - case color_coded = "color_coded" -} - -// MARK: - Transcription Display Item - -struct TranscriptionDisplayItem: Identifiable, Hashable { - let id: UUID - let text: String - let speakerId: UUID? - let speakerName: String - let timestamp: TimeInterval - let confidence: Float - let isFinal: Bool - let wordTimings: [WordTiming] - let isCurrentSpeaker: Bool - - init(from message: ConversationMessage, speakerName: String = "Unknown", isCurrentSpeaker: Bool = false) { - self.id = UUID() - self.text = message.content - self.speakerId = message.speakerId - self.speakerName = speakerName - self.timestamp = message.timestamp - self.confidence = message.confidence - self.isFinal = message.isFinal - self.wordTimings = message.wordTimings - self.isCurrentSpeaker = isCurrentSpeaker - } -} - -struct WordTiming: Codable, Hashable { - let word: String - let startTime: TimeInterval - let endTime: TimeInterval - let confidence: Float -} - -// MARK: - Real-Time Transcription Display - -protocol RealTimeTranscriptionDisplayProtocol { - var displayItems: AnyPublisher<[TranscriptionDisplayItem], Never> { get } - var settings: AnyPublisher { get } - var isVisible: AnyPublisher { get } - - func updateSettings(_ newSettings: TranscriptionDisplaySettings) - func addTranscriptionItem(_ item: TranscriptionDisplayItem) - func updateTranscriptionItem(_ item: TranscriptionDisplayItem) - func clearDisplay() - func show() - func hide() - func toggleVisibility() -} - -class RealTimeTranscriptionDisplay: RealTimeTranscriptionDisplayProtocol, ObservableObject { - private let displayItemsSubject = CurrentValueSubject<[TranscriptionDisplayItem], Never>([]) - private let settingsSubject = CurrentValueSubject(.default) - private let isVisibleSubject = CurrentValueSubject(true) - - private var autoHideTimer: Timer? - private var cancellables = Set() - - var displayItems: AnyPublisher<[TranscriptionDisplayItem], Never> { - displayItemsSubject.eraseToAnyPublisher() - } - - var settings: AnyPublisher { - settingsSubject.eraseToAnyPublisher() - } - - var isVisible: AnyPublisher { - isVisibleSubject.eraseToAnyPublisher() - } - - init() { - setupAutoHide() - } - - func updateSettings(_ newSettings: TranscriptionDisplaySettings) { - settingsSubject.send(newSettings) - setupAutoHide() - } - - func addTranscriptionItem(_ item: TranscriptionDisplayItem) { - var items = displayItemsSubject.value - items.append(item) - - // Limit the number of visible items - let maxItems = settingsSubject.value.maxVisibleLines - if items.count > maxItems { - items = Array(items.suffix(maxItems)) - } - - displayItemsSubject.send(items) - resetAutoHideTimer() - - if !isVisibleSubject.value { - show() - } - } - - func updateTranscriptionItem(_ item: TranscriptionDisplayItem) { - var items = displayItemsSubject.value - - if let index = items.firstIndex(where: { $0.id == item.id }) { - items[index] = item - } else { - // If item doesn't exist, add it - items.append(item) - } - - displayItemsSubject.send(items) - resetAutoHideTimer() - } - - func clearDisplay() { - displayItemsSubject.send([]) - hide() - } - - func show() { - isVisibleSubject.send(true) - resetAutoHideTimer() - } - - func hide() { - isVisibleSubject.send(false) - autoHideTimer?.invalidate() - } - - func toggleVisibility() { - if isVisibleSubject.value { - hide() - } else { - show() - } - } - - private func setupAutoHide() { - let settings = settingsSubject.value - if settings.autoHideDelay > 0 { - resetAutoHideTimer() - } - } - - private func resetAutoHideTimer() { - autoHideTimer?.invalidate() - - let settings = settingsSubject.value - guard settings.autoHideDelay > 0 else { return } - - autoHideTimer = Timer.scheduledTimer(withTimeInterval: settings.autoHideDelay, repeats: false) { [weak self] _ in - self?.hide() - } - } -} - -// MARK: - SwiftUI Views - -struct TranscriptionDisplayView: View { - @ObservedObject private var display: RealTimeTranscriptionDisplay - @State private var settings: TranscriptionDisplaySettings - @State private var items: [TranscriptionDisplayItem] = [] - @State private var isVisible: Bool = true - - init(display: RealTimeTranscriptionDisplay) { - self.display = display - self._settings = State(initialValue: .default) - } - - var body: some View { - Group { - if isVisible && !items.isEmpty { - content - .opacity(isVisible ? 1.0 : 0.0) - .animation(.easeInOut(duration: 0.3), value: isVisible) - } - } - .onReceive(display.displayItems) { newItems in - withAnimation(settings.fadeInAnimation ? .easeInOut(duration: 0.2) : .none) { - items = newItems - } - } - .onReceive(display.settings) { newSettings in - settings = newSettings - } - .onReceive(display.isVisible) { visible in - withAnimation(.easeInOut(duration: 0.3)) { - isVisible = visible - } - } - } - - @ViewBuilder - private var content: some View { - switch settings.displayMode { - case .overlay: - overlayContent - case .sidebar: - sidebarContent - case .popup: - popupContent - case .floating: - floatingContent - case .fullscreen: - fullscreenContent - } - } - - private var overlayContent: some View { - VStack(alignment: .leading, spacing: 4) { - ForEach(items) { item in - TranscriptionItemView(item: item, settings: settings) - } - } - .padding() - .background(settings.backgroundColor) - .cornerRadius(8) - .position(for: settings.position) - } - - private var sidebarContent: some View { - VStack(alignment: .leading, spacing: 8) { - Text("Live Transcription") - .font(.headline) - .foregroundColor(settings.textColor) - - ScrollView { - LazyVStack(alignment: .leading, spacing: 4) { - ForEach(items) { item in - TranscriptionItemView(item: item, settings: settings) - .padding(.horizontal) - } - } - } - } - .frame(width: 300) - .background(settings.backgroundColor) - } - - private var popupContent: some View { - VStack(alignment: .leading, spacing: 4) { - ForEach(items) { item in - TranscriptionItemView(item: item, settings: settings) - } - } - .padding() - .background(settings.backgroundColor) - .cornerRadius(12) - .shadow(radius: 10) - .scaleEffect(isVisible ? 1.0 : 0.8) - .animation(.spring(), value: isVisible) - } - - private var floatingContent: some View { - VStack(alignment: .leading, spacing: 4) { - ForEach(items) { item in - TranscriptionItemView(item: item, settings: settings) - } - } - .padding() - .background(settings.backgroundColor) - .cornerRadius(8) - .shadow(radius: 5) - .gesture( - DragGesture() - .onEnded { _ in - // Allow dragging to reposition - } - ) - } - - private var fullscreenContent: some View { - VStack { - Spacer() - - ScrollView { - LazyVStack(alignment: .leading, spacing: 8) { - ForEach(items) { item in - TranscriptionItemView(item: item, settings: settings) - .padding(.horizontal) - } - } - } - .frame(maxHeight: 400) - - Spacer() - } - .background(settings.backgroundColor) - } -} - -struct TranscriptionItemView: View { - let item: TranscriptionDisplayItem - let settings: TranscriptionDisplaySettings - - var body: some View { - HStack(alignment: .top, spacing: 8) { - // Speaker indicator - speakerIndicator - - // Transcription content - VStack(alignment: .leading, spacing: 2) { - // Speaker name and timestamp - if !item.speakerName.isEmpty { - HStack { - Text(item.speakerName) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(speakerColor) - - Spacer() - - if settings.confidence != .none { - confidenceIndicator - } - } - } - - // Transcription text - if settings.wordHighlighting && !item.wordTimings.isEmpty { - wordByWordText - } else { - regularText - } - } - } - .animation(.easeInOut(duration: 0.2), value: item.isFinal) - } - - private var speakerIndicator: some View { - Circle() - .fill(speakerColor) - .frame(width: 8, height: 8) - .opacity(item.isCurrentSpeaker ? 1.0 : 0.6) - } - - private var speakerColor: Color { - if let speakerId = item.speakerId, - let color = settings.speakerColors[speakerId] { - return color - } - return item.isCurrentSpeaker ? .blue : .gray - } - - private var confidenceIndicator: some View { - Group { - switch settings.confidence { - case .minimal: - if item.confidence < 0.7 { - Image(systemName: "questionmark.circle") - .foregroundColor(.orange) - .font(.caption) - } - - case .detailed: - Text("\(Int(item.confidence * 100))%") - .font(.caption2) - .foregroundColor(confidenceColor) - - case .color_coded: - Circle() - .fill(confidenceColor) - .frame(width: 6, height: 6) - - case .none: - EmptyView() - } - } - } - - private var confidenceColor: Color { - switch item.confidence { - case 0.9...1.0: return .green - case 0.7..<0.9: return .yellow - case 0.5..<0.7: return .orange - default: return .red - } - } - - private var regularText: some View { - Text(item.text) - .font(settings.fontFamily.font.scaleEffect(settings.textSize.scaleFactor)) - .foregroundColor(settings.textColor) - .opacity(item.isFinal ? 1.0 : 0.7) - .animation(.easeInOut(duration: 0.3), value: item.isFinal) - } - - private var wordByWordText: some View { - // Placeholder for word-by-word highlighting - // This would implement real-time word highlighting based on timing - Text(item.text) - .font(settings.fontFamily.font.scaleEffect(settings.textSize.scaleFactor)) - .foregroundColor(settings.textColor) - .opacity(item.isFinal ? 1.0 : 0.7) - } -} - -// MARK: - View Extensions - -extension View { - func position(for displayPosition: DisplayPosition) -> some View { - switch displayPosition { - case .top: - return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)) - case .center: - return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)) - case .bottom: - return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)) - case .left: - return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)) - case .right: - return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)) - case .topLeft: - return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)) - case .topRight: - return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)) - case .bottomLeft: - return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading)) - case .bottomRight: - return AnyView(self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)) - } - } -} - -// MARK: - Glasses Display Integration - -class GlassesTranscriptionRenderer { - private let glassesManager: GlassesManagerProtocol - private let display: RealTimeTranscriptionDisplay - private var cancellables = Set() - - init(glassesManager: GlassesManagerProtocol, display: RealTimeTranscriptionDisplay) { - self.glassesManager = glassesManager - self.display = display - - setupGlassesSync() - } - - private func setupGlassesSync() { - display.displayItems - .combineLatest(display.settings) - .sink { [weak self] (items, settings) in - self?.renderOnGlasses(items: items, settings: settings) - } - .store(in: &cancellables) - } - - private func renderOnGlasses(items: [TranscriptionDisplayItem], settings: TranscriptionDisplaySettings) { - guard !items.isEmpty else { return } - - // Convert items to HUD content - let latestItem = items.last! - let text = formatForGlasses(item: latestItem, settings: settings) - - let hudContent = HUDContent( - text: text, - style: HUDStyle.transcription, - position: mapToHUDPosition(settings.position), - duration: settings.autoHideDelay, - priority: .medium - ) - - glassesManager.displayContent(hudContent) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - print("Failed to display transcription on glasses: \(error)") - } - }, - receiveValue: { _ in - // Successfully displayed - } - ) - .store(in: &cancellables) - } - - private func formatForGlasses(item: TranscriptionDisplayItem, settings: TranscriptionDisplaySettings) -> String { - var formattedText = "" - - // Add speaker name if enabled - if !item.speakerName.isEmpty && settings.displayMode != .overlay { - formattedText += "\(item.speakerName): " - } - - formattedText += item.text - - // Truncate if too long for glasses display - if formattedText.count > 60 { - formattedText = String(formattedText.prefix(57)) + "..." - } - - return formattedText - } - - private func mapToHUDPosition(_ position: DisplayPosition) -> HUDPosition { - switch position { - case .top: return .topCenter - case .center: return .center - case .bottom: return .bottomCenter - case .left: return .centerLeft - case .right: return .centerRight - case .topLeft: return .topLeft - case .topRight: return .topRight - case .bottomLeft: return .bottomLeft - case .bottomRight: return .bottomRight - } - } -} - -// MARK: - HUD Style Extension - -extension HUDStyle { - static let transcription = HUDStyle( - backgroundColor: Color.black.opacity(0.8), - textColor: .white, - font: .system(.body, design: .monospaced), - cornerRadius: 4, - padding: 8, - border: nil - ) -} \ No newline at end of file diff --git a/Helix/Core/Glasses/GlassesManager.swift b/Helix/Core/Glasses/GlassesManager.swift deleted file mode 100644 index 8b11833..0000000 --- a/Helix/Core/Glasses/GlassesManager.swift +++ /dev/null @@ -1,744 +0,0 @@ -import Foundation -import CoreBluetooth -import Combine - -protocol GlassesManagerProtocol { - var connectionState: AnyPublisher { get } - var batteryLevel: AnyPublisher { get } - var displayCapabilities: AnyPublisher { get } - - func connect() -> AnyPublisher - func disconnect() - func displayText(_ text: String, at position: HUDPosition) -> AnyPublisher - func displayContent(_ content: HUDContent) -> AnyPublisher - func clearDisplay() - func updateDisplaySettings(_ settings: DisplaySettings) - func sendGestureCommand(_ command: GestureCommand) - func startBatteryMonitoring() - func stopBatteryMonitoring() -} - -enum ConnectionState { - case disconnected - case scanning - case connecting - case connected - case error(GlassesError) - - var isConnected: Bool { - if case .connected = self { - return true - } - return false - } -} - -struct DisplayCapabilities { - let maxTextLength: Int - let supportedPositions: [HUDPosition] - let supportedColors: [HUDColor] - let maxConcurrentDisplays: Int - let refreshRate: Float - let resolution: DisplayResolution - - static let `default` = DisplayCapabilities( - maxTextLength: 280, - supportedPositions: [ - HUDPosition(x: 0.5, y: 0.1, alignment: .center, fontSize: .medium), - HUDPosition(x: 0.1, y: 0.5, alignment: .left, fontSize: .small), - HUDPosition(x: 0.9, y: 0.5, alignment: .right, fontSize: .small) - ], - supportedColors: [.white, .green, .red, .blue, .yellow], - maxConcurrentDisplays: 3, - refreshRate: 60.0, - resolution: DisplayResolution(width: 640, height: 400) - ) -} - -struct DisplayResolution { - let width: Int - let height: Int -} - -struct HUDPosition { - let x: Float // 0.0 to 1.0 (left to right) - let y: Float // 0.0 to 1.0 (top to bottom) - let alignment: TextAlignment - let fontSize: FontSize - - static let topCenter = HUDPosition(x: 0.5, y: 0.1, alignment: .center, fontSize: .medium) - static let bottomCenter = HUDPosition(x: 0.5, y: 0.9, alignment: .center, fontSize: .small) - static let topLeft = HUDPosition(x: 0.1, y: 0.1, alignment: .left, fontSize: .small) - static let topRight = HUDPosition(x: 0.9, y: 0.1, alignment: .right, fontSize: .small) -} - -enum TextAlignment: String, CaseIterable { - case left = "left" - case center = "center" - case right = "right" -} - -enum FontSize: String, CaseIterable { - case small = "small" - case medium = "medium" - case large = "large" - - var pointSize: Float { - switch self { - case .small: return 12.0 - case .medium: return 16.0 - case .large: return 20.0 - } - } -} - -struct HUDContent { - let id: String - let text: String - let style: HUDStyle - let position: HUDPosition - let duration: TimeInterval? - let priority: DisplayPriority - let animation: HUDAnimation? - - init(id: String = UUID().uuidString, text: String, style: HUDStyle = HUDStyle(), position: HUDPosition = .topCenter, duration: TimeInterval? = nil, priority: DisplayPriority = .medium, animation: HUDAnimation? = nil) { - self.id = id - self.text = text - self.style = style - self.position = position - self.duration = duration - self.priority = priority - self.animation = animation - } -} - -struct HUDStyle { - let color: HUDColor - let backgroundColor: HUDColor? - let fontSize: FontSize - let isBold: Bool - let isItalic: Bool - let opacity: Float - - init(color: HUDColor = .white, backgroundColor: HUDColor? = nil, fontSize: FontSize = .medium, isBold: Bool = false, isItalic: Bool = false, opacity: Float = 1.0) { - self.color = color - self.backgroundColor = backgroundColor - self.fontSize = fontSize - self.isBold = isBold - self.isItalic = isItalic - self.opacity = opacity - } - - static let factCheck = HUDStyle(color: .red, fontSize: .medium, isBold: true) - static let summary = HUDStyle(color: .blue, fontSize: .small) - static let actionItem = HUDStyle(color: .yellow, fontSize: .small, isBold: true) - static let notification = HUDStyle(color: .green, fontSize: .small) -} - -enum HUDColor: String, CaseIterable { - case white = "white" - case black = "black" - case red = "red" - case green = "green" - case blue = "blue" - case yellow = "yellow" - case orange = "orange" - case purple = "purple" - - var rgbValues: (r: Float, g: Float, b: Float) { - switch self { - case .white: return (1.0, 1.0, 1.0) - case .black: return (0.0, 0.0, 0.0) - case .red: return (1.0, 0.0, 0.0) - case .green: return (0.0, 1.0, 0.0) - case .blue: return (0.0, 0.0, 1.0) - case .yellow: return (1.0, 1.0, 0.0) - case .orange: return (1.0, 0.5, 0.0) - case .purple: return (0.5, 0.0, 1.0) - } - } -} - -enum DisplayPriority: Int, CaseIterable { - case low = 1 - case medium = 2 - case high = 3 - case critical = 4 - - var displayDuration: TimeInterval { - switch self { - case .low: return 3.0 - case .medium: return 5.0 - case .high: return 8.0 - case .critical: return 12.0 - } - } -} - -struct HUDAnimation { - let type: AnimationType - let duration: TimeInterval - let easing: EasingFunction - - enum AnimationType { - case fadeIn - case fadeOut - case slideIn(direction: SlideDirection) - case slideOut(direction: SlideDirection) - case scale(from: Float, to: Float) - case none - } - - enum SlideDirection { - case left, right, up, down - } - - enum EasingFunction { - case linear - case easeIn - case easeOut - case easeInOut - } - - static let fadeIn = HUDAnimation(type: .fadeIn, duration: 0.3, easing: .easeOut) - static let fadeOut = HUDAnimation(type: .fadeOut, duration: 0.3, easing: .easeIn) - static let slideInFromTop = HUDAnimation(type: .slideIn(direction: .up), duration: 0.4, easing: .easeOut) -} - -struct DisplaySettings { - let brightness: Float // 0.0 to 1.0 - let contrast: Float // 0.0 to 1.0 - let autoAdjustBrightness: Bool - let defaultPosition: HUDPosition - let maxDisplayTime: TimeInterval - let enableAnimations: Bool - - static let `default` = DisplaySettings( - brightness: 0.8, - contrast: 0.9, - autoAdjustBrightness: true, - defaultPosition: .topCenter, - maxDisplayTime: 10.0, - enableAnimations: true - ) -} - -enum GestureCommand { - case tap - case doubleTap - case swipeLeft - case swipeRight - case swipeUp - case swipeDown - case longPress - case dismiss - case next - case previous - case confirm - case cancel -} - -enum GlassesError: Error { - case bluetoothUnavailable - case deviceNotFound - case connectionFailed - case authenticationFailed - case communicationTimeout - case displayError(String) - case batteryLow - case firmwareUpdateRequired - case hardwareError - case serviceUnavailable - - var localizedDescription: String { - switch self { - case .bluetoothUnavailable: - return "Bluetooth is not available or disabled" - case .deviceNotFound: - return "Even Realities glasses not found" - case .connectionFailed: - return "Failed to connect to glasses" - case .authenticationFailed: - return "Authentication with glasses failed" - case .communicationTimeout: - return "Communication timeout with glasses" - case .displayError(let message): - return "Display error: \(message)" - case .batteryLow: - return "Glasses battery is low" - case .firmwareUpdateRequired: - return "Firmware update required" - case .hardwareError: - return "Hardware error detected" - case .serviceUnavailable: - return "Glasses service unavailable" - } - } -} - -class GlassesManager: NSObject, GlassesManagerProtocol { - private let centralManager: CBCentralManager - private var peripheral: CBPeripheral? - private var characteristics: [CBUUID: CBCharacteristic] = [:] - - private let connectionStateSubject = CurrentValueSubject(.disconnected) - private let batteryLevelSubject = CurrentValueSubject(0.0) - private let displayCapabilitiesSubject = CurrentValueSubject(.default) - - private var displayQueue: [HUDContent] = [] - private var currentDisplays: [String: HUDContent] = [:] - private var displaySettings = DisplaySettings.default - - private let processingQueue = DispatchQueue(label: "glasses.processing", qos: .userInteractive) - private var cancellables = Set() - - // Even Realities specific UUIDs (example UUIDs - replace with actual ones) - private let serviceUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABC") - private let displayCharacteristicUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABD") - private let batteryCharacteristicUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABE") - private let gestureCharacteristicUUID = CBUUID(string: "12345678-1234-1234-1234-123456789ABF") - - var connectionState: AnyPublisher { - connectionStateSubject.eraseToAnyPublisher() - } - - var batteryLevel: AnyPublisher { - batteryLevelSubject.eraseToAnyPublisher() - } - - var displayCapabilities: AnyPublisher { - displayCapabilitiesSubject.eraseToAnyPublisher() - } - - override init() { - centralManager = CBCentralManager() - super.init() - centralManager.delegate = self - - setupDisplayTimer() - } - - func connect() -> AnyPublisher { - return Future { [weak self] promise in - guard let self = self else { - promise(.failure(.serviceUnavailable)) - return - } - - self.processingQueue.async { - guard self.centralManager.state == .poweredOn else { - promise(.failure(.bluetoothUnavailable)) - return - } - - self.connectionStateSubject.send(.scanning) - - // Start scanning for Even Realities glasses - self.centralManager.scanForPeripherals( - withServices: [self.serviceUUID], - options: [CBCentralManagerScanOptionAllowDuplicatesKey: false] - ) - - // Set timeout for scanning - DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { - if self.connectionStateSubject.value == .scanning { - self.centralManager.stopScan() - promise(.failure(.deviceNotFound)) - } - } - - // Store promise for completion when connected - self.connectionPromise = promise - } - } - .eraseToAnyPublisher() - } - - func disconnect() { - processingQueue.async { [weak self] in - guard let self = self else { return } - - if let peripheral = self.peripheral { - self.centralManager.cancelPeripheralConnection(peripheral) - } - - self.peripheral = nil - self.characteristics.removeAll() - self.connectionStateSubject.send(.disconnected) - - print("Disconnected from glasses") - } - } - - func displayText(_ text: String, at position: HUDPosition) -> AnyPublisher { - let content = HUDContent(text: text, position: position) - return displayContent(content) - } - - func displayContent(_ content: HUDContent) -> AnyPublisher { - return Future { [weak self] promise in - guard let self = self else { - promise(.failure(.serviceUnavailable)) - return - } - - self.processingQueue.async { - guard self.connectionStateSubject.value.isConnected else { - promise(.failure(.connectionFailed)) - return - } - - // Add to display queue - self.displayQueue.append(content) - self.processDisplayQueue() - - promise(.success(())) - } - } - .eraseToAnyPublisher() - } - - func clearDisplay() { - processingQueue.async { [weak self] in - guard let self = self else { return } - - self.displayQueue.removeAll() - self.currentDisplays.removeAll() - - let clearCommand = GlassesCommand.clearDisplay - self.sendCommand(clearCommand) - } - } - - func updateDisplaySettings(_ settings: DisplaySettings) { - processingQueue.async { [weak self] in - guard let self = self else { return } - - self.displaySettings = settings - - let settingsCommand = GlassesCommand.updateSettings(settings) - self.sendCommand(settingsCommand) - } - } - - func sendGestureCommand(_ command: GestureCommand) { - processingQueue.async { [weak self] in - guard let self = self else { return } - - let gestureCommand = GlassesCommand.gesture(command) - self.sendCommand(gestureCommand) - } - } - - func startBatteryMonitoring() { - guard let characteristic = characteristics[batteryCharacteristicUUID], - let peripheral = peripheral else { return } - - peripheral.setNotifyValue(true, for: characteristic) - print("Started battery monitoring") - } - - func stopBatteryMonitoring() { - guard let characteristic = characteristics[batteryCharacteristicUUID], - let peripheral = peripheral else { return } - - peripheral.setNotifyValue(false, for: characteristic) - print("Stopped battery monitoring") - } - - // Private properties for connection handling - private var connectionPromise: ((Result) -> Void)? - - private func setupDisplayTimer() { - Timer.publish(every: 0.1, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - self?.updateDisplays() - } - .store(in: &cancellables) - } - - private func processDisplayQueue() { - guard !displayQueue.isEmpty else { return } - - // Sort by priority - displayQueue.sort { $0.priority.rawValue > $1.priority.rawValue } - - let maxConcurrent = displayCapabilitiesSubject.value.maxConcurrentDisplays - - while currentDisplays.count < maxConcurrent && !displayQueue.isEmpty { - let content = displayQueue.removeFirst() - currentDisplays[content.id] = content - - let displayCommand = GlassesCommand.displayContent(content) - sendCommand(displayCommand) - } - } - - private func updateDisplays() { - let now = Date().timeIntervalSince1970 - var expiredDisplays: [String] = [] - - for (id, content) in currentDisplays { - if let duration = content.duration, - now - content.timestamp > duration { - expiredDisplays.append(id) - } - } - - for id in expiredDisplays { - currentDisplays.removeValue(forKey: id) - let clearCommand = GlassesCommand.clearContent(id) - sendCommand(clearCommand) - } - - // Process queue if we have capacity - if currentDisplays.count < displayCapabilitiesSubject.value.maxConcurrentDisplays { - processDisplayQueue() - } - } - - private func sendCommand(_ command: GlassesCommand) { - guard let peripheral = peripheral, - let characteristic = characteristics[displayCharacteristicUUID] else { - print("Cannot send command: peripheral or characteristic not available") - return - } - - do { - let data = try command.encode() - peripheral.writeValue(data, for: characteristic, type: .withResponse) - } catch { - print("Failed to encode command: \(error)") - } - } -} - -// MARK: - CBCentralManagerDelegate - -extension GlassesManager: CBCentralManagerDelegate { - func centralManagerDidUpdateState(_ central: CBCentralManager) { - switch central.state { - case .poweredOn: - print("Bluetooth powered on") - case .poweredOff: - connectionStateSubject.send(.error(.bluetoothUnavailable)) - case .unsupported: - connectionStateSubject.send(.error(.bluetoothUnavailable)) - case .unauthorized: - connectionStateSubject.send(.error(.bluetoothUnavailable)) - case .resetting: - connectionStateSubject.send(.disconnected) - case .unknown: - break - @unknown default: - break - } - } - - func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { - print("Discovered peripheral: \(peripheral.name ?? "Unknown")") - - // Check if this is an Even Realities device - if isEvenRealitiesDevice(peripheral, advertisementData: advertisementData) { - self.peripheral = peripheral - peripheral.delegate = self - - central.stopScan() - connectionStateSubject.send(.connecting) - central.connect(peripheral, options: nil) - } - } - - func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - print("Connected to peripheral: \(peripheral.name ?? "Unknown")") - - connectionStateSubject.send(.connected) - connectionPromise?(.success(())) - connectionPromise = nil - - // Discover services - peripheral.discoverServices([serviceUUID]) - } - - func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { - print("Failed to connect to peripheral: \(error?.localizedDescription ?? "Unknown error")") - - connectionStateSubject.send(.error(.connectionFailed)) - connectionPromise?(.failure(.connectionFailed)) - connectionPromise = nil - } - - func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { - print("Disconnected from peripheral: \(error?.localizedDescription ?? "Intentional disconnect")") - - self.peripheral = nil - characteristics.removeAll() - connectionStateSubject.send(.disconnected) - } - - private func isEvenRealitiesDevice(_ peripheral: CBPeripheral, advertisementData: [String: Any]) -> Bool { - // Check device name - if let name = peripheral.name?.lowercased(), - name.contains("even") || name.contains("realities") { - return true - } - - // Check advertisement data for Even Realities specific identifiers - if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID], - serviceUUIDs.contains(serviceUUID) { - return true - } - - return false - } -} - -// MARK: - CBPeripheralDelegate - -extension GlassesManager: CBPeripheralDelegate { - func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - if let error = error { - print("Error discovering services: \(error.localizedDescription)") - return - } - - guard let services = peripheral.services else { return } - - for service in services { - if service.uuid == serviceUUID { - peripheral.discoverCharacteristics([ - displayCharacteristicUUID, - batteryCharacteristicUUID, - gestureCharacteristicUUID - ], for: service) - } - } - } - - func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - if let error = error { - print("Error discovering characteristics: \(error.localizedDescription)") - return - } - - guard let characteristics = service.characteristics else { return } - - for characteristic in characteristics { - self.characteristics[characteristic.uuid] = characteristic - - // Enable notifications for battery and gesture characteristics - if characteristic.uuid == batteryCharacteristicUUID || - characteristic.uuid == gestureCharacteristicUUID { - peripheral.setNotifyValue(true, for: characteristic) - } - } - - print("Discovered \(characteristics.count) characteristics") - - // Request initial battery level - if let batteryCharacteristic = self.characteristics[batteryCharacteristicUUID] { - peripheral.readValue(for: batteryCharacteristic) - } - } - - func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - if let error = error { - print("Error updating characteristic value: \(error.localizedDescription)") - return - } - - guard let data = characteristic.value else { return } - - switch characteristic.uuid { - case batteryCharacteristicUUID: - handleBatteryUpdate(data) - case gestureCharacteristicUUID: - handleGestureUpdate(data) - default: - break - } - } - - private func handleBatteryUpdate(_ data: Data) { - guard let batteryLevel = data.first else { return } - - let level = Float(batteryLevel) / 100.0 - batteryLevelSubject.send(level) - - print("Battery level: \(Int(level * 100))%") - - if level < 0.15 { - connectionStateSubject.send(.error(.batteryLow)) - } - } - - private func handleGestureUpdate(_ data: Data) { - // Parse gesture data and handle accordingly - // This would be implemented based on Even Realities protocol - print("Received gesture data: \(data)") - } -} - -// MARK: - Glasses Command Protocol - -enum GlassesCommand { - case displayContent(HUDContent) - case clearContent(String) - case clearDisplay - case updateSettings(DisplaySettings) - case gesture(GestureCommand) - - func encode() throws -> Data { - // This would implement the actual Even Realities protocol - // For now, return placeholder data - let commandData: [String: Any] - - switch self { - case .displayContent(let content): - commandData = [ - "type": "display", - "id": content.id, - "text": content.text, - "position": [ - "x": content.position.x, - "y": content.position.y - ], - "style": [ - "color": content.style.color.rawValue, - "fontSize": content.style.fontSize.rawValue - ] - ] - case .clearContent(let id): - commandData = [ - "type": "clear", - "id": id - ] - case .clearDisplay: - commandData = [ - "type": "clearAll" - ] - case .updateSettings(let settings): - commandData = [ - "type": "settings", - "brightness": settings.brightness, - "contrast": settings.contrast - ] - case .gesture(let gesture): - commandData = [ - "type": "gesture", - "command": "\(gesture)" - ] - } - - return try JSONSerialization.data(withJSONObject: commandData) - } -} - -// MARK: - Extensions - -extension HUDContent { - var timestamp: TimeInterval { - Date().timeIntervalSince1970 - } -} \ No newline at end of file diff --git a/Helix/Core/Glasses/HUDRenderer.swift b/Helix/Core/Glasses/HUDRenderer.swift deleted file mode 100644 index f76e27f..0000000 --- a/Helix/Core/Glasses/HUDRenderer.swift +++ /dev/null @@ -1,537 +0,0 @@ -import Foundation -import Combine - -protocol HUDRendererProtocol { - func render(_ content: HUDContent) -> AnyPublisher - func updateContent(_ content: HUDContent, with animation: HUDAnimation?) - func clearAll() - func setPriority(_ priority: DisplayPriority, for contentId: String) - func getActiveDisplays() -> [HUDContent] - func setDisplayCapabilities(_ capabilities: DisplayCapabilities) -} - -enum RenderError: Error { - case contentTooLong - case invalidPosition - case displayFull - case renderingFailed(String) - case hardwareError - case contextLost - - var localizedDescription: String { - switch self { - case .contentTooLong: - return "Content exceeds maximum display length" - case .invalidPosition: - return "Invalid display position" - case .displayFull: - return "Display capacity exceeded" - case .renderingFailed(let message): - return "Rendering failed: \(message)" - case .hardwareError: - return "Hardware rendering error" - case .contextLost: - return "Rendering context lost" - } - } -} - -class HUDRenderer: HUDRendererProtocol { - private let glassesManager: GlassesManagerProtocol - private var activeDisplays: [String: ActiveDisplay] = [:] - private var displayCapabilities: DisplayCapabilities = .default - private var renderingSettings: RenderingSettings = .default - - private let renderingQueue = DispatchQueue(label: "hud.rendering", qos: .userInteractive) - private var cancellables = Set() - - private struct ActiveDisplay { - let content: HUDContent - let renderTime: Date - let expirationTime: Date? - var isVisible: Bool - - init(content: HUDContent) { - self.content = content - self.renderTime = Date() - self.expirationTime = content.duration.map { Date().addingTimeInterval($0) } - self.isVisible = true - } - - var isExpired: Bool { - guard let expirationTime = expirationTime else { return false } - return Date() > expirationTime - } - } - - struct RenderingSettings { - let maxTextLength: Int - let wordWrapEnabled: Bool - let autoScroll: Bool - let fadeInDuration: TimeInterval - let fadeOutDuration: TimeInterval - let displayTimeout: TimeInterval - - static let `default` = RenderingSettings( - maxTextLength: 280, - wordWrapEnabled: true, - autoScroll: true, - fadeInDuration: 0.3, - fadeOutDuration: 0.3, - displayTimeout: 10.0 - ) - } - - init(glassesManager: GlassesManagerProtocol) { - self.glassesManager = glassesManager - - setupSubscriptions() - startExpirationTimer() - } - - func render(_ content: HUDContent) -> AnyPublisher { - return Future { [weak self] promise in - guard let self = self else { - promise(.failure(.contextLost)) - return - } - - self.renderingQueue.async { - do { - try self.validateContent(content) - let processedContent = self.processContent(content) - - // Check if we can display more content - if self.activeDisplays.count >= self.displayCapabilities.maxConcurrentDisplays { - self.handleDisplayOverflow(for: processedContent) - } - - // Add to active displays - let activeDisplay = ActiveDisplay(content: processedContent) - self.activeDisplays[processedContent.id] = activeDisplay - - // Send to glasses - self.glassesManager.displayContent(processedContent) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - promise(.failure(.renderingFailed(error.localizedDescription))) - } else { - promise(.success(())) - } - }, - receiveValue: { _ in } - ) - .store(in: &self.cancellables) - - } catch { - promise(.failure(error as? RenderError ?? .renderingFailed(error.localizedDescription))) - } - } - } - .eraseToAnyPublisher() - } - - func updateContent(_ content: HUDContent, with animation: HUDAnimation? = nil) { - renderingQueue.async { [weak self] in - guard let self = self else { return } - - // Update existing content - if var activeDisplay = self.activeDisplays[content.id] { - let updatedContent = HUDContent( - id: content.id, - text: content.text, - style: content.style, - position: content.position, - duration: content.duration, - priority: content.priority, - animation: animation - ) - - activeDisplay = ActiveDisplay(content: updatedContent) - self.activeDisplays[content.id] = activeDisplay - - self.glassesManager.displayContent(updatedContent) - .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) - .store(in: &self.cancellables) - } else { - // Render as new content - self.render(content) - .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) - .store(in: &self.cancellables) - } - } - } - - func clearAll() { - renderingQueue.async { [weak self] in - guard let self = self else { return } - - self.activeDisplays.removeAll() - self.glassesManager.clearDisplay() - } - } - - func setPriority(_ priority: DisplayPriority, for contentId: String) { - renderingQueue.async { [weak self] in - guard let self = self, - var activeDisplay = self.activeDisplays[contentId] else { return } - - // Update priority - let updatedContent = HUDContent( - id: activeDisplay.content.id, - text: activeDisplay.content.text, - style: activeDisplay.content.style, - position: activeDisplay.content.position, - duration: activeDisplay.content.duration, - priority: priority, - animation: activeDisplay.content.animation - ) - - activeDisplay = ActiveDisplay(content: updatedContent) - self.activeDisplays[contentId] = activeDisplay - - // Re-evaluate display order - self.reevaluateDisplayOrder() - } - } - - func getActiveDisplays() -> [HUDContent] { - return renderingQueue.sync { - return activeDisplays.values.map { $0.content } - } - } - - func setDisplayCapabilities(_ capabilities: DisplayCapabilities) { - renderingQueue.async { [weak self] in - guard let self = self else { return } - - self.displayCapabilities = capabilities - - // Update rendering settings based on capabilities - self.updateRenderingSettings(for: capabilities) - - // Re-evaluate current displays if we now have less capacity - if self.activeDisplays.count > capabilities.maxConcurrentDisplays { - self.enforceDisplayLimit() - } - } - } - - private func setupSubscriptions() { - glassesManager.displayCapabilities - .sink { [weak self] capabilities in - self?.setDisplayCapabilities(capabilities) - } - .store(in: &cancellables) - } - - private func startExpirationTimer() { - Timer.publish(every: 0.5, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - self?.cleanupExpiredDisplays() - } - .store(in: &cancellables) - } - - private func validateContent(_ content: HUDContent) throws { - // Validate text length - if content.text.count > displayCapabilities.maxTextLength { - throw RenderError.contentTooLong - } - - // Validate position - if content.position.x < 0 || content.position.x > 1 || - content.position.y < 0 || content.position.y > 1 { - throw RenderError.invalidPosition - } - - // Check if position is supported - let isPositionSupported = displayCapabilities.supportedPositions.contains { supportedPos in - abs(supportedPos.x - content.position.x) < 0.1 && - abs(supportedPos.y - content.position.y) < 0.1 - } - - if !isPositionSupported && !displayCapabilities.supportedPositions.isEmpty { - throw RenderError.invalidPosition - } - } - - private func processContent(_ content: HUDContent) -> HUDContent { - var processedText = content.text - - // Apply word wrapping if needed - if renderingSettings.wordWrapEnabled { - processedText = applyWordWrapping(to: processedText) - } - - // Truncate if still too long - if processedText.count > renderingSettings.maxTextLength { - let endIndex = processedText.index(processedText.startIndex, offsetBy: renderingSettings.maxTextLength - 3) - processedText = String(processedText[.. 50 { - processedText = formatForAutoScroll(processedText) - } - - return HUDContent( - id: content.id, - text: processedText, - style: content.style, - position: optimizePosition(content.position), - duration: content.duration ?? renderingSettings.displayTimeout, - priority: content.priority, - animation: content.animation ?? defaultAnimation(for: content.priority) - ) - } - - private func applyWordWrapping(to text: String) -> String { - let maxLineLength = 40 // Characters per line for glasses display - let words = text.components(separatedBy: .whitespaces) - var lines: [String] = [] - var currentLine = "" - - for word in words { - if currentLine.isEmpty { - currentLine = word - } else if (currentLine.count + word.count + 1) <= maxLineLength { - currentLine += " " + word - } else { - lines.append(currentLine) - currentLine = word - } - } - - if !currentLine.isEmpty { - lines.append(currentLine) - } - - return lines.joined(separator: "\n") - } - - private func formatForAutoScroll(_ text: String) -> String { - // Add markers for auto-scrolling - return "🔄 " + text - } - - private func optimizePosition(_ position: HUDPosition) -> HUDPosition { - // Find the closest supported position - guard !displayCapabilities.supportedPositions.isEmpty else { return position } - - let closestPosition = displayCapabilities.supportedPositions.min { pos1, pos2 in - let distance1 = sqrt(pow(pos1.x - position.x, 2) + pow(pos1.y - position.y, 2)) - let distance2 = sqrt(pow(pos2.x - position.x, 2) + pow(pos2.y - position.y, 2)) - return distance1 < distance2 - } - - return closestPosition ?? position - } - - private func defaultAnimation(for priority: DisplayPriority) -> HUDAnimation? { - switch priority { - case .critical: - return HUDAnimation(type: .scale(from: 0.8, to: 1.0), duration: 0.4, easing: .easeOut) - case .high: - return .slideInFromTop - case .medium: - return .fadeIn - case .low: - return nil - } - } - - private func handleDisplayOverflow(for content: HUDContent) { - // Find the lowest priority display that's not critical - let sortedDisplays = activeDisplays.values.sorted { display1, display2 in - if display1.content.priority.rawValue != display2.content.priority.rawValue { - return display1.content.priority.rawValue < display2.content.priority.rawValue - } - return display1.renderTime < display2.renderTime // Older first - } - - // Remove lowest priority display if the new content has higher priority - if let lowestPriorityDisplay = sortedDisplays.first, - lowestPriorityDisplay.content.priority.rawValue < content.priority.rawValue { - - removeDisplay(lowestPriorityDisplay.content.id) - } - } - - private func enforceDisplayLimit() { - let maxDisplays = displayCapabilities.maxConcurrentDisplays - let excessCount = activeDisplays.count - maxDisplays - - guard excessCount > 0 else { return } - - // Sort by priority (lowest first) and age (oldest first) - let sortedDisplays = activeDisplays.values.sorted { display1, display2 in - if display1.content.priority.rawValue != display2.content.priority.rawValue { - return display1.content.priority.rawValue < display2.content.priority.rawValue - } - return display1.renderTime < display2.renderTime - } - - // Remove excess displays - for i in 0.. display2.content.priority.rawValue - } - return display1.renderTime > display2.renderTime - } - - // Keep only the highest priority displays within capacity - let maxDisplays = displayCapabilities.maxConcurrentDisplays - - for (index, display) in sortedDisplays.enumerated() { - if index >= maxDisplays { - removeDisplay(display.content.id) - } - } - } - - private func removeDisplay(_ id: String) { - activeDisplays.removeValue(forKey: id) - - // Send clear command to glasses - glassesManager.displayContent(HUDContent(id: id, text: "", style: HUDStyle(), position: .topCenter)) - .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) - .store(in: &cancellables) - } - - private func cleanupExpiredDisplays() { - renderingQueue.async { [weak self] in - guard let self = self else { return } - - let now = Date() - var expiredIds: [String] = [] - - for (id, activeDisplay) in self.activeDisplays { - if activeDisplay.isExpired { - expiredIds.append(id) - } - } - - for id in expiredIds { - self.removeDisplay(id) - } - } - } - - private func updateRenderingSettings(for capabilities: DisplayCapabilities) { - renderingSettings = RenderingSettings( - maxTextLength: capabilities.maxTextLength, - wordWrapEnabled: capabilities.resolution.width < 800, - autoScroll: capabilities.resolution.width < 600, - fadeInDuration: 0.3, - fadeOutDuration: 0.3, - displayTimeout: 10.0 - ) - } -} - -// MARK: - HUD Content Factory - -class HUDContentFactory { - static func createFactCheckDisplay(_ result: FactCheckResult) -> HUDContent { - let text = result.isAccurate ? - "✓ Confirmed" : - "✗ \(result.explanation)" - - let style = result.isAccurate ? - HUDStyle(color: .green, fontSize: .medium, isBold: true) : - HUDStyle(color: .red, fontSize: .medium, isBold: true) - - return HUDContent( - text: text, - style: style, - position: .topCenter, - duration: result.isAccurate ? 3.0 : 8.0, - priority: result.severity == .critical ? .critical : .high, - animation: .slideInFromTop - ) - } - - static func createSummaryDisplay(_ summary: String) -> HUDContent { - return HUDContent( - text: "📝 " + summary, - style: .summary, - position: .bottomCenter, - duration: 6.0, - priority: .medium, - animation: .fadeIn - ) - } - - static func createActionItemDisplay(_ actionItem: ActionItem) -> HUDContent { - let priorityIcon = actionItem.priority == .urgent ? "🚨" : "📋" - let text = "\(priorityIcon) \(actionItem.description)" - - return HUDContent( - text: text, - style: .actionItem, - position: .topRight, - duration: actionItem.priority.displayDuration, - priority: mapActionItemPriority(actionItem.priority), - animation: .slideInFromTop - ) - } - - static func createNotificationDisplay(_ message: String, priority: DisplayPriority = .medium) -> HUDContent { - return HUDContent( - text: "💬 " + message, - style: .notification, - position: .topLeft, - duration: priority.displayDuration, - priority: priority, - animation: .fadeIn - ) - } - - private static func mapActionItemPriority(_ priority: ActionItemPriority) -> DisplayPriority { - switch priority { - case .low: return .low - case .medium: return .medium - case .high: return .high - case .urgent: return .critical - } - } -} - -// MARK: - Display Position Helper - -extension HUDPosition { - static func dynamicPosition(avoiding conflicts: [HUDContent]) -> HUDPosition { - let availablePositions: [HUDPosition] = [ - .topCenter, .topLeft, .topRight, - .bottomCenter, - HUDPosition(x: 0.3, y: 0.5, alignment: .left, fontSize: .small), - HUDPosition(x: 0.7, y: 0.5, alignment: .right, fontSize: .small) - ] - - // Find position that doesn't conflict with existing content - for position in availablePositions { - let hasConflict = conflicts.contains { content in - abs(content.position.x - position.x) < 0.2 && - abs(content.position.y - position.y) < 0.2 - } - - if !hasConflict { - return position - } - } - - // Default to top center if all positions are occupied - return .topCenter - } -} \ No newline at end of file diff --git a/Helix/Core/Intelligence/CognitiveEnhancementSuite.swift b/Helix/Core/Intelligence/CognitiveEnhancementSuite.swift deleted file mode 100644 index 3e09d0f..0000000 --- a/Helix/Core/Intelligence/CognitiveEnhancementSuite.swift +++ /dev/null @@ -1,766 +0,0 @@ -// -// CognitiveEnhancementSuite.swift -// Helix -// - -import Foundation -import Combine -import Vision -import CoreLocation - -// MARK: - Memory Palace System - -struct MemoryPalace: Codable, Identifiable { - let id: UUID - var name: String - var description: String - var locations: [MemoryLocation] - var associatedTopics: [String] - var createdDate: Date - var lastUsed: Date - var usageCount: Int - - init(name: String, description: String) { - self.id = UUID() - self.name = name - self.description = description - self.locations = [] - self.associatedTopics = [] - self.createdDate = Date() - self.lastUsed = Date() - self.usageCount = 0 - } -} - -struct MemoryLocation: Codable, Identifiable { - let id: UUID - var name: String - var description: String - var position: SpatialPosition - var associatedInformation: [MemoryItem] - var visualCues: [VisualCue] - var createdDate: Date - - init(name: String, description: String, position: SpatialPosition) { - self.id = UUID() - self.name = name - self.description = description - self.position = position - self.associatedInformation = [] - self.visualCues = [] - self.createdDate = Date() - } -} - -struct SpatialPosition: Codable { - let x: Float - let y: Float - let z: Float - let orientation: Float // 0-360 degrees -} - -struct MemoryItem: Codable, Identifiable { - let id: UUID - let content: String - let type: MemoryItemType - let associatedConversation: UUID? - let createdDate: Date - let strength: Float // 0.0 to 1.0 - var lastAccessed: Date - - init(content: String, type: MemoryItemType, associatedConversation: UUID? = nil) { - self.id = UUID() - self.content = content - self.type = type - self.associatedConversation = associatedConversation - self.createdDate = Date() - self.strength = 1.0 - self.lastAccessed = Date() - } -} - -enum MemoryItemType: String, Codable, CaseIterable { - case fact = "fact" - case person = "person" - case event = "event" - case concept = "concept" - case reminder = "reminder" - case insight = "insight" -} - -struct VisualCue: Codable, Identifiable { - let id: UUID - let type: VisualCueType - let description: String - let color: CueColor - let size: CueSize - let animation: CueAnimation? - - init(type: VisualCueType, description: String, color: CueColor = .blue, size: CueSize = .medium) { - self.id = UUID() - self.type = type - self.description = description - self.color = color - self.size = size - self.animation = nil - } -} - -enum VisualCueType: String, Codable { - case icon = "icon" - case shape = "shape" - case text = "text" - case image = "image" -} - -enum CueColor: String, Codable, CaseIterable { - case red = "red" - case blue = "blue" - case green = "green" - case yellow = "yellow" - case purple = "purple" - case orange = "orange" - case white = "white" -} - -enum CueSize: String, Codable { - case small = "small" - case medium = "medium" - case large = "large" -} - -enum CueAnimation: String, Codable { - case pulse = "pulse" - case fade = "fade" - case bounce = "bounce" - case rotate = "rotate" -} - -// MARK: - Memory Palace Manager - -protocol MemoryPalaceManagerProtocol { - var memoryPalaces: AnyPublisher<[MemoryPalace], Never> { get } - var activeMemoryPalace: AnyPublisher { get } - - func createMemoryPalace(_ palace: MemoryPalace) throws - func updateMemoryPalace(_ palace: MemoryPalace) throws - func deleteMemoryPalace(_ palaceId: UUID) throws - func activateMemoryPalace(_ palaceId: UUID) - func deactivateMemoryPalace() - - func addMemoryItem(_ item: MemoryItem, to locationId: UUID) throws - func linkConversationToMemory(_ conversationId: UUID, item: MemoryItem) - func retrieveMemoriesFor(topic: String) -> [MemoryItem] - func generateMemoryPalaceFor(topic: String) -> MemoryPalace -} - -class MemoryPalaceManager: MemoryPalaceManagerProtocol, ObservableObject { - private let memoryPalacesSubject = CurrentValueSubject<[MemoryPalace], Never>([]) - private let activeMemoryPalaceSubject = CurrentValueSubject(nil) - - private let storage: MemoryPalaceStorage - private let memoryAssociator: MemoryAssociator - - var memoryPalaces: AnyPublisher<[MemoryPalace], Never> { - memoryPalacesSubject.eraseToAnyPublisher() - } - - var activeMemoryPalace: AnyPublisher { - activeMemoryPalaceSubject.eraseToAnyPublisher() - } - - init(storage: MemoryPalaceStorage = MemoryPalaceStorage()) { - self.storage = storage - self.memoryAssociator = MemoryAssociator() - - loadStoredPalaces() - createDefaultPalaces() - } - - func createMemoryPalace(_ palace: MemoryPalace) throws { - var palaces = memoryPalacesSubject.value - palaces.append(palace) - memoryPalacesSubject.send(palaces) - - try storage.save(palaces) - } - - func updateMemoryPalace(_ palace: MemoryPalace) throws { - var palaces = memoryPalacesSubject.value - - if let index = palaces.firstIndex(where: { $0.id == palace.id }) { - palaces[index] = palace - memoryPalacesSubject.send(palaces) - - try storage.save(palaces) - } - } - - func deleteMemoryPalace(_ palaceId: UUID) throws { - var palaces = memoryPalacesSubject.value - palaces.removeAll { $0.id == palaceId } - memoryPalacesSubject.send(palaces) - - if activeMemoryPalaceSubject.value?.id == palaceId { - activeMemoryPalaceSubject.send(nil) - } - - try storage.save(palaces) - } - - func activateMemoryPalace(_ palaceId: UUID) { - let palace = memoryPalacesSubject.value.first { $0.id == palaceId } - activeMemoryPalaceSubject.send(palace) - } - - func deactivateMemoryPalace() { - activeMemoryPalaceSubject.send(nil) - } - - func addMemoryItem(_ item: MemoryItem, to locationId: UUID) throws { - var palaces = memoryPalacesSubject.value - - for (palaceIndex, palace) in palaces.enumerated() { - for (locationIndex, location) in palace.locations.enumerated() { - if location.id == locationId { - palaces[palaceIndex].locations[locationIndex].associatedInformation.append(item) - memoryPalacesSubject.send(palaces) - - try storage.save(palaces) - return - } - } - } - - throw MemoryPalaceError.locationNotFound - } - - func linkConversationToMemory(_ conversationId: UUID, item: MemoryItem) { - var enhancedItem = item - enhancedItem.lastAccessed = Date() - - // Find relevant memory palace and location - let relevantPalace = findRelevantPalace(for: item) - - if let palace = relevantPalace { - try? addMemoryItem(enhancedItem, to: palace.locations.first?.id ?? UUID()) - } - } - - func retrieveMemoriesFor(topic: String) -> [MemoryItem] { - let palaces = memoryPalacesSubject.value - - return palaces.flatMap { palace in - palace.locations.flatMap { location in - location.associatedInformation.filter { item in - item.content.localizedCaseInsensitiveContains(topic) || - palace.associatedTopics.contains { $0.localizedCaseInsensitiveContains(topic) } - } - } - } - } - - func generateMemoryPalaceFor(topic: String) -> MemoryPalace { - var palace = MemoryPalace(name: "\(topic) Palace", description: "Generated memory palace for \(topic)") - - // Create 5 standard locations - let locations = [ - MemoryLocation(name: "Entrance", description: "Starting point for \(topic)", position: SpatialPosition(x: 0, y: 0, z: 0, orientation: 0)), - MemoryLocation(name: "Central Hall", description: "Main concepts of \(topic)", position: SpatialPosition(x: 10, y: 0, z: 0, orientation: 90)), - MemoryLocation(name: "Left Wing", description: "Details and examples", position: SpatialPosition(x: 10, y: 10, z: 0, orientation: 180)), - MemoryLocation(name: "Right Wing", description: "Related topics", position: SpatialPosition(x: 10, y: -10, z: 0, orientation: 0)), - MemoryLocation(name: "Archive", description: "Historical context", position: SpatialPosition(x: 20, y: 0, z: 0, orientation: 270)) - ] - - palace.locations = locations - palace.associatedTopics = [topic] - - return palace - } - - private func loadStoredPalaces() { - if let stored = storage.load() { - memoryPalacesSubject.send(stored) - } - } - - private func createDefaultPalaces() { - guard memoryPalacesSubject.value.isEmpty else { return } - - let defaultPalace = generateMemoryPalaceFor(topic: "General Knowledge") - try? createMemoryPalace(defaultPalace) - } - - private func findRelevantPalace(for item: MemoryItem) -> MemoryPalace? { - let palaces = memoryPalacesSubject.value - - return palaces.first { palace in - palace.associatedTopics.contains { topic in - item.content.localizedCaseInsensitiveContains(topic) - } - } ?? palaces.first - } -} - -// MARK: - Name and Face Recognition - -struct PersonProfile: Codable, Identifiable { - let id: UUID - var name: String - var faceEmbedding: Data? - var personalInfo: PersonalInfo - var conversationHistory: [UUID] // Conversation IDs - var lastSeen: Date? - var interactionCount: Int - var relationshipType: RelationshipType - var tags: [String] - - init(name: String, personalInfo: PersonalInfo = PersonalInfo()) { - self.id = UUID() - self.name = name - self.faceEmbedding = nil - self.personalInfo = personalInfo - self.conversationHistory = [] - self.lastSeen = nil - self.interactionCount = 0 - self.relationshipType = .acquaintance - self.tags = [] - } -} - -struct PersonalInfo: Codable { - var company: String? - var jobTitle: String? - var interests: [String] - var notes: [String] - var importantDates: [ImportantDate] - var contactInformation: ContactInfo? - var socialMediaHandles: [String: String] // Platform: Handle - - init() { - self.company = nil - self.jobTitle = nil - self.interests = [] - self.notes = [] - self.importantDates = [] - self.contactInformation = nil - self.socialMediaHandles = [:] - } -} - -struct ImportantDate: Codable, Identifiable { - let id: UUID - let date: Date - let description: String - let type: DateType - - init(date: Date, description: String, type: DateType) { - self.id = UUID() - self.date = date - self.description = description - self.type = type - } -} - -enum DateType: String, Codable, CaseIterable { - case birthday = "birthday" - case anniversary = "anniversary" - case meeting = "meeting" - case deadline = "deadline" - case reminder = "reminder" -} - -struct ContactInfo: Codable { - var email: String? - var phone: String? - var address: String? - var website: String? -} - -enum RelationshipType: String, Codable, CaseIterable { - case family = "family" - case friend = "friend" - case colleague = "colleague" - case acquaintance = "acquaintance" - case professional = "professional" - case client = "client" - case vendor = "vendor" -} - -// MARK: - Face Recognition Manager - -protocol FaceRecognitionManagerProtocol { - var recognizedPersons: AnyPublisher<[PersonProfile], Never> { get } - var isEnabled: AnyPublisher { get } - - func enableFaceRecognition() - func disableFaceRecognition() - func addPersonProfile(_ profile: PersonProfile, faceImage: Data?) throws - func updatePersonProfile(_ profile: PersonProfile) throws - func recognizeFace(from imageData: Data) -> AnyPublisher - func trainFaceModel(for personId: UUID, with images: [Data]) -> AnyPublisher -} - -class FaceRecognitionManager: FaceRecognitionManagerProtocol, ObservableObject { - private let recognizedPersonsSubject = CurrentValueSubject<[PersonProfile], Never>([]) - private let isEnabledSubject = CurrentValueSubject(false) - - private let storage: PersonProfileStorage - private let faceAnalyzer: FaceAnalyzer - - var recognizedPersons: AnyPublisher<[PersonProfile], Never> { - recognizedPersonsSubject.eraseToAnyPublisher() - } - - var isEnabled: AnyPublisher { - isEnabledSubject.eraseToAnyPublisher() - } - - init() { - self.storage = PersonProfileStorage() - self.faceAnalyzer = FaceAnalyzer() - - loadStoredProfiles() - } - - func enableFaceRecognition() { - isEnabledSubject.send(true) - print("Face recognition enabled") - } - - func disableFaceRecognition() { - isEnabledSubject.send(false) - print("Face recognition disabled") - } - - func addPersonProfile(_ profile: PersonProfile, faceImage: Data?) throws { - var enhancedProfile = profile - - if let imageData = faceImage { - enhancedProfile.faceEmbedding = try faceAnalyzer.generateEmbedding(from: imageData) - } - - var profiles = recognizedPersonsSubject.value - profiles.append(enhancedProfile) - recognizedPersonsSubject.send(profiles) - - try storage.save(profiles) - } - - func updatePersonProfile(_ profile: PersonProfile) throws { - var profiles = recognizedPersonsSubject.value - - if let index = profiles.firstIndex(where: { $0.id == profile.id }) { - profiles[index] = profile - recognizedPersonsSubject.send(profiles) - - try storage.save(profiles) - } - } - - func recognizeFace(from imageData: Data) -> AnyPublisher { - guard isEnabledSubject.value else { - return Just(nil) - .setFailureType(to: FaceRecognitionError.self) - .eraseToAnyPublisher() - } - - return faceAnalyzer.recognizeFace(imageData: imageData, knownProfiles: recognizedPersonsSubject.value) - } - - func trainFaceModel(for personId: UUID, with images: [Data]) -> AnyPublisher { - return faceAnalyzer.trainModel(personId: personId, images: images) - } - - private func loadStoredProfiles() { - if let stored = storage.load() { - recognizedPersonsSubject.send(stored) - } - } -} - -// MARK: - Attention Direction System - -struct AttentionCue: Identifiable { - let id: UUID - let type: AttentionCueType - let direction: AttentionDirection - let intensity: Float // 0.0 to 1.0 - let priority: AttentionPriority - let duration: TimeInterval - let reason: String - - init(type: AttentionCueType, direction: AttentionDirection, intensity: Float, priority: AttentionPriority, reason: String, duration: TimeInterval = 3.0) { - self.id = UUID() - self.type = type - self.direction = direction - self.intensity = intensity - self.priority = priority - self.duration = duration - self.reason = reason - } -} - -enum AttentionCueType: String, CaseIterable { - case visual = "visual" - case audio = "audio" - case haptic = "haptic" - case combined = "combined" -} - -enum AttentionDirection: String, CaseIterable { - case left = "left" - case right = "right" - case forward = "forward" - case behind = "behind" - case up = "up" - case down = "down" -} - -enum AttentionPriority: String, CaseIterable { - case low = "low" - case medium = "medium" - case high = "high" - case urgent = "urgent" -} - -protocol AttentionDirectionSystemProtocol { - var activeCues: AnyPublisher<[AttentionCue], Never> { get } - var settings: AnyPublisher { get } - - func updateSettings(_ newSettings: AttentionSettings) - func addAttentionCue(_ cue: AttentionCue) - func clearCues() - func detectActiveSpeaker(from audioLevels: [UUID: Float]) -> UUID? - func generateDirectionalCue(for speakerId: UUID, speakers: [Speaker]) -> AttentionCue? -} - -struct AttentionSettings: Codable { - var isEnabled: Bool - var enabledCueTypes: Set - var sensitivity: Float // 0.0 to 1.0 - var autoHighlightActiveSpeaker: Bool - var eyeTrackingIntegration: Bool - var maxConcurrentCues: Int - - static let `default` = AttentionSettings( - isEnabled: true, - enabledCueTypes: [.visual], - sensitivity: 0.5, - autoHighlightActiveSpeaker: true, - eyeTrackingIntegration: false, - maxConcurrentCues: 3 - ) -} - -class AttentionDirectionSystem: AttentionDirectionSystemProtocol, ObservableObject { - private let activeCuesSubject = CurrentValueSubject<[AttentionCue], Never>([]) - private let settingsSubject = CurrentValueSubject(.default) - - private let spatialAudioAnalyzer: SpatialAudioAnalyzer - private var cueExpirationTimers: [UUID: Timer] = [:] - - var activeCues: AnyPublisher<[AttentionCue], Never> { - activeCuesSubject.eraseToAnyPublisher() - } - - var settings: AnyPublisher { - settingsSubject.eraseToAnyPublisher() - } - - init() { - self.spatialAudioAnalyzer = SpatialAudioAnalyzer() - } - - func updateSettings(_ newSettings: AttentionSettings) { - settingsSubject.send(newSettings) - } - - func addAttentionCue(_ cue: AttentionCue) { - var cues = activeCuesSubject.value - - // Remove oldest cue if at max capacity - let settings = settingsSubject.value - if cues.count >= settings.maxConcurrentCues { - if let oldestCue = cues.min(by: { $0.priority.rawValue < $1.priority.rawValue }) { - removeCue(oldestCue.id) - } - } - - cues.append(cue) - activeCuesSubject.send(cues) - - // Set expiration timer - let timer = Timer.scheduledTimer(withTimeInterval: cue.duration, repeats: false) { [weak self] _ in - self?.removeCue(cue.id) - } - cueExpirationTimers[cue.id] = timer - } - - func clearCues() { - // Cancel all timers - cueExpirationTimers.values.forEach { $0.invalidate() } - cueExpirationTimers.removeAll() - - activeCuesSubject.send([]) - } - - func detectActiveSpeaker(from audioLevels: [UUID: Float]) -> UUID? { - return audioLevels.max(by: { $0.value < $1.value })?.key - } - - func generateDirectionalCue(for speakerId: UUID, speakers: [Speaker]) -> AttentionCue? { - guard let speaker = speakers.first(where: { $0.id == speakerId }) else { - return nil - } - - // Simplified directional logic (in real implementation, would use spatial audio analysis) - let direction: AttentionDirection = .forward // Placeholder - - return AttentionCue( - type: .visual, - direction: direction, - intensity: 0.7, - priority: .medium, - reason: "\(speaker.name) is speaking" - ) - } - - private func removeCue(_ cueId: UUID) { - var cues = activeCuesSubject.value - cues.removeAll { $0.id == cueId } - activeCuesSubject.send(cues) - - cueExpirationTimers[cueId]?.invalidate() - cueExpirationTimers.removeValue(forKey: cueId) - } -} - -// MARK: - Supporting Classes - -class MemoryAssociator { - func findAssociations(for item: MemoryItem, in palaces: [MemoryPalace]) -> [MemoryItem] { - // Find related memory items based on content similarity - return [] - } -} - -class MemoryPalaceStorage { - private let userDefaults = UserDefaults.standard - private let key = "memory_palaces" - - func save(_ palaces: [MemoryPalace]) throws { - let data = try JSONEncoder().encode(palaces) - userDefaults.set(data, forKey: key) - } - - func load() -> [MemoryPalace]? { - guard let data = userDefaults.data(forKey: key) else { return nil } - return try? JSONDecoder().decode([MemoryPalace].self, from: data) - } -} - -class PersonProfileStorage { - private let userDefaults = UserDefaults.standard - private let key = "person_profiles" - - func save(_ profiles: [PersonProfile]) throws { - let data = try JSONEncoder().encode(profiles) - userDefaults.set(data, forKey: key) - } - - func load() -> [PersonProfile]? { - guard let data = userDefaults.data(forKey: key) else { return nil } - return try? JSONDecoder().decode([PersonProfile].self, from: data) - } -} - -class FaceAnalyzer { - func generateEmbedding(from imageData: Data) throws -> Data { - // In real implementation, would use Vision framework for face detection and embedding - return Data() // Placeholder - } - - func recognizeFace(imageData: Data, knownProfiles: [PersonProfile]) -> AnyPublisher { - return Future { promise in - // Simulate face recognition processing - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { - // In real implementation, would compare face embeddings - promise(.success(nil)) // No match found - } - } - .eraseToAnyPublisher() - } - - func trainModel(personId: UUID, images: [Data]) -> AnyPublisher { - return Future { promise in - // Simulate model training - DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) { - promise(.success(())) - } - } - .eraseToAnyPublisher() - } -} - -class SpatialAudioAnalyzer { - func analyzeDirection(for audioData: Data) -> AttentionDirection { - // Analyze audio data to determine direction - return .forward // Placeholder - } - - func calculateIntensity(for audioLevel: Float) -> Float { - return min(max(audioLevel / 100.0, 0.0), 1.0) - } -} - -// MARK: - Errors - -enum MemoryPalaceError: LocalizedError { - case locationNotFound - case palaceNotFound - case invalidMemoryItem - case storageFailed - - var errorDescription: String? { - switch self { - case .locationNotFound: return "Memory location not found" - case .palaceNotFound: return "Memory palace not found" - case .invalidMemoryItem: return "Invalid memory item" - case .storageFailed: return "Failed to save memory palace" - } - } -} - -enum FaceRecognitionError: LocalizedError { - case noFaceDetected - case multiplefacesDetected - case embeddingGenerationFailed - case modelTrainingFailed - case permissionDenied - case deviceNotSupported - - var errorDescription: String? { - switch self { - case .noFaceDetected: return "No face detected in image" - case .multipleTracesDetected: return "Multiple faces detected" - case .embeddingGenerationFailed: return "Failed to generate face embedding" - case .modelTrainingFailed: return "Face model training failed" - case .permissionDenied: return "Camera permission denied" - case .deviceNotSupported: return "Face recognition not supported on this device" - } - } -} - -// MARK: - Extensions for AttentionCueType Set Codable - -extension Set: @retroactive RawRepresentable where Element: RawRepresentable, Element.RawValue == String { - public var rawValue: String { - return Array(self).map { $0.rawValue }.joined(separator: ",") - } - - public init?(rawValue: String) { - let elements = rawValue.components(separatedBy: ",").compactMap { Element(rawValue: $0) } - self.init(elements) - } -} \ No newline at end of file diff --git a/Helix/Core/Transcription/SpeechRecognitionService.swift b/Helix/Core/Transcription/SpeechRecognitionService.swift deleted file mode 100644 index 391aed7..0000000 --- a/Helix/Core/Transcription/SpeechRecognitionService.swift +++ /dev/null @@ -1,418 +0,0 @@ -import Speech -import AVFoundation -import Combine - -protocol SpeechRecognitionServiceProtocol { - var transcriptionPublisher: AnyPublisher { get } - var isRecognizing: Bool { get } - - func startStreamingRecognition() - func stopRecognition() - func setLanguage(_ locale: Locale) - func addCustomVocabulary(_ words: [String]) - func processAudioBuffer(_ buffer: AVAudioPCMBuffer) -} - -struct TranscriptionResult { - let text: String - let speakerId: UUID? - let confidence: Float - let isFinal: Bool - let timestamp: TimeInterval - let wordTimings: [WordTiming] - let alternatives: [String] - - init(text: String, speakerId: UUID? = nil, confidence: Float = 0.0, isFinal: Bool = false, wordTimings: [WordTiming] = [], alternatives: [String] = []) { - self.text = text - self.speakerId = speakerId - self.confidence = confidence - self.isFinal = isFinal - self.timestamp = Date().timeIntervalSince1970 - self.wordTimings = wordTimings - self.alternatives = alternatives - } -} - -struct WordTiming { - let word: String - let startTime: TimeInterval - let endTime: TimeInterval - let confidence: Float -} - -enum TranscriptionError: Error { - case permissionDenied - case recognitionNotAvailable - case audioEngineError(Error) - case recognitionFailed(Error) - case invalidAudioFormat - case serviceUnavailable - - var localizedDescription: String { - switch self { - case .permissionDenied: - return "Speech recognition permission denied" - case .recognitionNotAvailable: - return "Speech recognition not available on this device" - case .audioEngineError(let error): - return "Audio engine error: \(error.localizedDescription)" - case .recognitionFailed(let error): - return "Speech recognition failed: \(error.localizedDescription)" - case .invalidAudioFormat: - return "Invalid audio format for speech recognition" - case .serviceUnavailable: - return "Speech recognition service unavailable" - } - } -} - -class SpeechRecognitionService: NSObject, SpeechRecognitionServiceProtocol { - private let speechRecognizer: SFSpeechRecognizer - private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest? - private var recognitionTask: SFSpeechRecognitionTask? - - private let transcriptionSubject = PassthroughSubject() - private let processingQueue = DispatchQueue(label: "speech.recognition", qos: .userInitiated) - - private var currentLocale: Locale = Locale(identifier: "en-US") - private var customVocabulary: [String] = [] - private var isCurrentlyRecognizing = false - - // Configuration - private let maxRecognitionDuration: TimeInterval = 60.0 - private let silenceTimeout: TimeInterval = 3.0 - - var transcriptionPublisher: AnyPublisher { - transcriptionSubject.eraseToAnyPublisher() - } - - var isRecognizing: Bool { - isCurrentlyRecognizing - } - - override init() { - guard let recognizer = SFSpeechRecognizer(locale: currentLocale) else { - fatalError("Speech recognizer not available for locale: \(currentLocale)") - } - - self.speechRecognizer = recognizer - super.init() - - speechRecognizer.delegate = self - requestPermissions() - } - - func startStreamingRecognition() { - guard !isCurrentlyRecognizing else { - print("Speech recognition already in progress") - return - } - - guard speechRecognizer.isAvailable else { - transcriptionSubject.send(completion: .failure(.recognitionNotAvailable)) - return - } - - processingQueue.async { [weak self] in - guard let self = self else { return } - self.setupRecognitionRequest() - } - } - - func stopRecognition() { - guard isCurrentlyRecognizing else { return } - processingQueue.async { [weak self] in - self?.cleanupRecognition() - } - } - - func setLanguage(_ locale: Locale) { - stopRecognition() - - currentLocale = locale - guard let newRecognizer = SFSpeechRecognizer(locale: locale) else { - print("Speech recognizer not available for locale: \(locale)") - return - } - - // Note: In a real implementation, you would replace the recognizer - // For this demo, we'll just update the locale reference - print("Updated speech recognition locale to: \(locale.identifier)") - } - - func addCustomVocabulary(_ words: [String]) { - customVocabulary.append(contentsOf: words) - print("Added \(words.count) words to custom vocabulary") - } - - func processAudioBuffer(_ buffer: AVAudioPCMBuffer) { - guard isCurrentlyRecognizing, - let request = recognitionRequest else { - return - } - - processingQueue.async { - request.append(buffer) - } - } - - private func requestPermissions() { - SFSpeechRecognizer.requestAuthorization { [weak self] status in - DispatchQueue.main.async { - switch status { - case .authorized: - print("Speech recognition authorized") - case .denied, .restricted, .notDetermined: - self?.transcriptionSubject.send(completion: .failure(.permissionDenied)) - @unknown default: - self?.transcriptionSubject.send(completion: .failure(.permissionDenied)) - } - } - } - } - - private func setupRecognitionRequest() { - // Cancel and clean up any existing task - recognitionTask?.cancel() - recognitionRequest?.endAudio() - recognitionTask = nil - - // Create new recognition request - recognitionRequest = SFSpeechAudioBufferRecognitionRequest() - - guard let recognitionRequest = recognitionRequest else { - transcriptionSubject.send(completion: .failure(.serviceUnavailable)) - return - } - - // Configure recognition request - recognitionRequest.shouldReportPartialResults = true - recognitionRequest.requiresOnDeviceRecognition = false - - // Add context strings for better recognition - if !customVocabulary.isEmpty { - recognitionRequest.contextualStrings = customVocabulary - } - - // Start recognition task - recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest!) { [weak self] result, error in - self?.handleRecognitionResult(result: result, error: error) - } - isCurrentlyRecognizing = true - private func handleRecognitionResult(result: SFSpeechRecognitionResult?, error: Error?) { - if let error = error { - transcriptionSubject.send(completion: .failure(.recognitionFailed(error))) - isCurrentlyRecognizing = false - return - } - guard let result = result else { return } - // Build word timings - let segments = result.bestTranscription.segments - let wordTimings = segments.map { seg in - WordTiming( - word: seg.substring, - startTime: seg.timestamp, - endTime: seg.timestamp + seg.duration, - confidence: seg.confidence - ) - } - // Confidence: use result.transcriptions first best confidence average - let confidences = segments.map { $0.confidence } - let avgConfidence = confidences.isEmpty ? 0.0 : confidences.reduce(0, +) / Float(confidences.count) - // Alternatives - let alternatives = result.transcriptions.map { $0.formattedString } - let transcription = TranscriptionResult( - text: result.bestTranscription.formattedString, - speakerId: nil, - confidence: avgConfidence, - isFinal: result.isFinal, - wordTimings: wordTimings, - alternatives: alternatives - ) - transcriptionSubject.send(transcription) - if result.isFinal { - // After final result, end audio or prepare for next - } - } - - private func cleanupRecognition() { - recognitionRequest?.endAudio() - recognitionTask?.cancel() - recognitionRequest = nil - recognitionTask = nil - isCurrentlyRecognizing = false - } - - isCurrentlyRecognizing = true - print("Started speech recognition") - } - - private func handleRecognitionResult(result: SFSpeechRecognitionResult?, error: Error?) { - if let error = error { - transcriptionSubject.send(completion: .failure(.recognitionFailed(error))) - cleanupRecognition() - return - } - - guard let result = result else { return } - - let transcription = result.bestTranscription - let isFinal = result.isFinal - - // Extract word timings - let wordTimings = transcription.segments.map { segment in - WordTiming( - word: segment.substring, - startTime: segment.timestamp, - endTime: segment.timestamp + segment.duration, - confidence: segment.confidence - ) - } - - // Get alternative transcriptions - let alternatives = result.transcriptions.dropFirst().map { $0.formattedString } - - let transcriptionResult = TranscriptionResult( - text: transcription.formattedString, - speakerId: nil, // Will be set by speaker identification - confidence: transcription.segments.map { $0.confidence }.reduce(0, +) / Float(transcription.segments.count), - isFinal: isFinal, - wordTimings: wordTimings, - alternatives: Array(alternatives.prefix(3)) - ) - - transcriptionSubject.send(transcriptionResult) - - if isFinal { - // Restart recognition for continuous transcription - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - if self?.isCurrentlyRecognizing == true { - self?.setupRecognitionRequest() - } - } - } - } - - private func cleanupRecognition() { - recognitionTask?.cancel() - recognitionTask = nil - - recognitionRequest?.endAudio() - recognitionRequest = nil - - isCurrentlyRecognizing = false - print("Stopped speech recognition") - } -} - -// MARK: - SFSpeechRecognizerDelegate - -extension SpeechRecognitionService: SFSpeechRecognizerDelegate { - func speechRecognizer(_ speechRecognizer: SFSpeechRecognizer, availabilityDidChange available: Bool) { - if !available && isCurrentlyRecognizing { - transcriptionSubject.send(completion: .failure(.serviceUnavailable)) - cleanupRecognition() - } - - print("Speech recognizer availability changed: \(available)") - } -} - -// MARK: - Transcription Processor - -class TranscriptionProcessor { - private let punctuationModel = PunctuationModel() - private let spellingCorrector = SpellingCorrector() - - func processTranscription(_ result: TranscriptionResult) -> TranscriptionResult { - var processedText = result.text - - // Apply post-processing improvements - processedText = addPunctuation(to: processedText) - processedText = correctSpelling(in: processedText) - processedText = capitalizeSentences(in: processedText) - - return TranscriptionResult( - text: processedText, - speakerId: result.speakerId, - confidence: result.confidence, - isFinal: result.isFinal, - wordTimings: result.wordTimings, - alternatives: result.alternatives - ) - } - - private func addPunctuation(to text: String) -> String { - return punctuationModel.addPunctuation(to: text) - } - - private func correctSpelling(in text: String) -> String { - return spellingCorrector.correctSpelling(in: text) - } - - private func capitalizeSentences(in text: String) -> String { - let sentences = text.components(separatedBy: CharacterSet(charactersIn: ".!?")) - let capitalizedSentences = sentences.map { sentence in - let trimmed = sentence.trimmingCharacters(in: .whitespaces) - guard !trimmed.isEmpty else { return sentence } - return trimmed.prefix(1).uppercased() + trimmed.dropFirst() - } - - return capitalizedSentences.joined(separator: ". ") - } -} - -// MARK: - Supporting Models - -class PunctuationModel { - private let pauseThreshold: TimeInterval = 0.5 - private let sentenceEndWords = Set(["period", "stop", "end", "finished"]) - - func addPunctuation(to text: String) -> String { - var result = text - - // Simple rule-based punctuation addition - result = result.replacingOccurrences(of: " period", with: ".") - result = result.replacingOccurrences(of: " comma", with: ",") - result = result.replacingOccurrences(of: " question mark", with: "?") - result = result.replacingOccurrences(of: " exclamation mark", with: "!") - - // Add periods at natural sentence boundaries - let words = result.components(separatedBy: " ") - if let lastWord = words.last?.lowercased(), - sentenceEndWords.contains(lastWord) { - result = result.replacingOccurrences(of: lastWord, with: ".") - } - - return result - } -} - -class SpellingCorrector { - private let commonCorrections: [String: String] = [ - "cant": "can't", - "wont": "won't", - "dont": "don't", - "isnt": "isn't", - "wasnt": "wasn't", - "werent": "weren't", - "shouldnt": "shouldn't", - "couldnt": "couldn't", - "wouldnt": "wouldn't" - ] - - func correctSpelling(in text: String) -> String { - var result = text - - for (incorrect, correct) in commonCorrections { - let pattern = "\\b\(incorrect)\\b" - result = result.replacingOccurrences( - of: pattern, - with: correct, - options: [.regularExpression, .caseInsensitive] - ) - } - - return result - } -} \ No newline at end of file diff --git a/Helix/Core/Transcription/TranscriptionCoordinator.swift b/Helix/Core/Transcription/TranscriptionCoordinator.swift deleted file mode 100644 index a8963e3..0000000 --- a/Helix/Core/Transcription/TranscriptionCoordinator.swift +++ /dev/null @@ -1,412 +0,0 @@ -import Foundation -import Combine -import AVFoundation - -protocol TranscriptionCoordinatorProtocol { - var conversationPublisher: AnyPublisher { get } - - func startConversationTranscription() - func stopConversationTranscription() - func addSpeaker(_ speaker: Speaker) - func trainSpeaker(_ speakerId: UUID, with samples: [AVAudioPCMBuffer]) -} - -struct ConversationUpdate { - let message: ConversationMessage - let speaker: Speaker? - let isNewSpeaker: Bool - let timestamp: TimeInterval -} - -struct ConversationMessage { - let id: UUID - let content: String - let speakerId: UUID? - let confidence: Float - let timestamp: TimeInterval - let isFinal: Bool - let wordTimings: [WordTiming] - let originalText: String - - init(from transcriptionResult: TranscriptionResult, speakerId: UUID? = nil) { - self.id = UUID() - self.content = transcriptionResult.text - self.speakerId = speakerId ?? transcriptionResult.speakerId - self.confidence = transcriptionResult.confidence - self.timestamp = transcriptionResult.timestamp - self.isFinal = transcriptionResult.isFinal - self.wordTimings = transcriptionResult.wordTimings - self.originalText = transcriptionResult.text - } -} - -class TranscriptionCoordinator: TranscriptionCoordinatorProtocol { - private let audioManager: AudioManagerProtocol - private let speechRecognizer: SpeechRecognitionServiceProtocol - private let speakerDiarization: SpeakerDiarizationEngineProtocol - private let voiceActivityDetector: VoiceActivityDetectorProtocol - private let transcriptionProcessor: TranscriptionProcessor - private let noiseReducer: NoiseReductionProcessorProtocol - - private let conversationSubject = PassthroughSubject() - private var cancellables = Set() - - private var isTranscribing = false - private var currentSpeakers: [UUID: Speaker] = [:] - private var unknownSpeakerCounter = 0 - private var lastVoiceActivity: TimeInterval = 0 - private var backgroundNoiseProfile: AVAudioPCMBuffer? - - // Configuration - private let minSpeechDuration: TimeInterval = 0.5 - private let maxSilenceDuration: TimeInterval = 2.0 - private let speakerChangeThreshold: Float = 0.3 - - var conversationPublisher: AnyPublisher { - conversationSubject.eraseToAnyPublisher() - } - - init( - audioManager: AudioManagerProtocol, - speechRecognizer: SpeechRecognitionServiceProtocol, - speakerDiarization: SpeakerDiarizationEngineProtocol, - voiceActivityDetector: VoiceActivityDetectorProtocol, - transcriptionProcessor: TranscriptionProcessor = TranscriptionProcessor(), - noiseReducer: NoiseReductionProcessorProtocol - ) { - self.audioManager = audioManager - self.speechRecognizer = speechRecognizer - self.speakerDiarization = speakerDiarization - self.voiceActivityDetector = voiceActivityDetector - self.transcriptionProcessor = transcriptionProcessor - self.noiseReducer = noiseReducer - - setupSubscriptions() - } - - func startConversationTranscription() { - guard !isTranscribing else { - print("Transcription already in progress") - return - } - - do { - try audioManager.startRecording() - speechRecognizer.startStreamingRecognition() - isTranscribing = true - print("Started conversation transcription") - } catch { - conversationSubject.send(completion: .failure(.audioEngineError(error))) - } - } - - func stopConversationTranscription() { - guard isTranscribing else { return } - - audioManager.stopRecording() - speechRecognizer.stopRecognition() - isTranscribing = false - print("Stopped conversation transcription") - } - - func addSpeaker(_ speaker: Speaker) { - currentSpeakers[speaker.id] = speaker - speakerDiarization.addSpeaker(id: speaker.id, name: speaker.name, isCurrentUser: speaker.isCurrentUser) - print("Added speaker: \(speaker.name ?? "Unknown") (\(speaker.id))") - } - - func trainSpeaker(_ speakerId: UUID, with samples: [AVAudioPCMBuffer]) { - guard currentSpeakers[speakerId] != nil else { - print("Cannot train unknown speaker: \(speakerId)") - return - } - - let success = speakerDiarization.trainSpeakerModel(samples: samples, speakerId: speakerId) - if success { - print("Successfully trained speaker model for: \(speakerId)") - } else { - print("Failed to train speaker model for: \(speakerId)") - } - } - - private func setupSubscriptions() { - // Audio processing pipeline - audioManager.audioPublisher - .receive(on: DispatchQueue.global(qos: .userInitiated)) - .sink( - receiveCompletion: { [weak self] completion in - if case .failure(let error) = completion { - self?.conversationSubject.send(completion: .failure(.audioEngineError(error))) - } - }, - receiveValue: { [weak self] processedAudio in - self?.processAudioFrame(processedAudio) - } - ) - .store(in: &cancellables) - - // Transcription processing - speechRecognizer.transcriptionPublisher - .receive(on: DispatchQueue.global(qos: .userInitiated)) - .sink( - receiveCompletion: { [weak self] completion in - if case .failure(let error) = completion { - self?.conversationSubject.send(completion: .failure(error)) - } - }, - receiveValue: { [weak self] transcriptionResult in - self?.processTranscriptionResult(transcriptionResult) - } - ) - .store(in: &cancellables) - } - - private func processAudioFrame(_ processedAudio: ProcessedAudio) { - // Apply noise reduction - let cleanedBuffer = noiseReducer.processBuffer(processedAudio.buffer) - - // Detect voice activity - let voiceActivity = voiceActivityDetector.detectVoiceActivity(in: cleanedBuffer) - - // Update background noise profile during silence - if !voiceActivity.hasVoice { - voiceActivityDetector.updateBackground(with: cleanedBuffer) - noiseReducer.updateNoiseProfile(cleanedBuffer) - } else { - lastVoiceActivity = Date().timeIntervalSince1970 - - // Send audio to speech recognizer if voice is detected - speechRecognizer.processAudioBuffer(cleanedBuffer) - } - } - - private func processTranscriptionResult(_ result: TranscriptionResult) { - // Skip empty or very short transcriptions - guard !result.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, - result.text.count > 2 else { - return - } - - // Process transcription for better quality - let processedResult = transcriptionProcessor.processTranscription(result) - - // Attempt speaker identification - let speakerInfo = identifySpeakerForTranscription(processedResult) - - // Create conversation message - let message = ConversationMessage( - from: processedResult, - speakerId: speakerInfo.speakerId - ) - // Determine if this is a new speaker - let isNew = (message.speakerId != nil) && (currentSpeakers[message.speakerId!] == nil) - // Lookup speaker object if exists - let speakerObj = message.speakerId.flatMap { currentSpeakers[$0] } - // Send update downstream - let update = ConversationUpdate( - message: message, - speaker: speakerObj, - isNewSpeaker: isNew, - timestamp: message.timestamp - ) - conversationSubject.send(update) - - // Create conversation update - let update = ConversationUpdate( - message: message, - speaker: speakerInfo.speaker, - isNewSpeaker: speakerInfo.isNewSpeaker, - timestamp: Date().timeIntervalSince1970 - ) - - // Send update - DispatchQueue.main.async { [weak self] in - self?.conversationSubject.send(update) - } - } - - private func identifySpeakerForTranscription(_ result: TranscriptionResult) -> (speakerId: UUID?, speaker: Speaker?, isNewSpeaker: Bool) { - // For now, we'll use a simplified approach since we don't have the actual audio buffer - // In a complete implementation, this would analyze the audio characteristics - - if let explicitSpeakerId = result.speakerId, - let speaker = currentSpeakers[explicitSpeakerId] { - return (explicitSpeakerId, speaker, false) - } - - // Check if we can identify based on existing speaker models - // This would require the actual audio buffer in a real implementation - - // For demo purposes, create unknown speaker if we have multiple speakers - if currentSpeakers.count > 1 { - // Simple heuristic: alternate between known speakers or create new ones - let unknownSpeakerId = UUID() - let unknownSpeaker = Speaker( - id: unknownSpeakerId, - name: "Speaker \(unknownSpeakerCounter + 1)", - isCurrentUser: false - ) - - unknownSpeakerCounter += 1 - addSpeaker(unknownSpeaker) - - return (unknownSpeakerId, unknownSpeaker, true) - } - - // Default to first speaker or current user - if let firstSpeaker = currentSpeakers.values.first { - return (firstSpeaker.id, firstSpeaker, false) - } - - // Create default speaker if none exist - let defaultSpeakerId = UUID() - let defaultSpeaker = Speaker( - id: defaultSpeakerId, - name: "Current User", - isCurrentUser: true - ) - - addSpeaker(defaultSpeaker) - return (defaultSpeakerId, defaultSpeaker, true) - } -} - -// MARK: - Conversation Context Manager - -class ConversationContextManager { - private var conversationHistory: [ConversationMessage] = [] - private var speakers: [UUID: Speaker] = [:] - private let maxHistorySize = 100 - private let contextWindowSize = 20 - - func addMessage(_ message: ConversationMessage) { - conversationHistory.append(message) - - // Maintain history size limit - if conversationHistory.count > maxHistorySize { - conversationHistory.removeFirst(conversationHistory.count - maxHistorySize) - } - } - - func addSpeaker(_ speaker: Speaker) { - speakers[speaker.id] = speaker - } - - func getRecentContext(messageCount: Int = 20) -> [ConversationMessage] { - let count = min(messageCount, conversationHistory.count) - return Array(conversationHistory.suffix(count)) - } - - func getConversationSummary() -> ConversationSummary { - let totalMessages = conversationHistory.count - let speakerCount = Set(conversationHistory.compactMap { $0.speakerId }).count - let averageConfidence = conversationHistory.map { $0.confidence }.reduce(0, +) / Float(max(totalMessages, 1)) - - let startTime = conversationHistory.first?.timestamp ?? Date().timeIntervalSince1970 - let endTime = conversationHistory.last?.timestamp ?? Date().timeIntervalSince1970 - let duration = endTime - startTime - - return ConversationSummary( - messageCount: totalMessages, - speakerCount: speakerCount, - duration: duration, - averageConfidence: averageConfidence, - startTime: startTime, - endTime: endTime - ) - } - - func getSpeakerStatistics() -> [SpeakerStatistics] { - var speakerStats: [UUID: SpeakerStatistics] = [:] - - for message in conversationHistory { - guard let speakerId = message.speakerId else { continue } - - if speakerStats[speakerId] == nil { - speakerStats[speakerId] = SpeakerStatistics( - speakerId: speakerId, - speaker: speakers[speakerId], - messageCount: 0, - totalWords: 0, - averageConfidence: 0.0, - speakingTime: 0.0 - ) - } - - let wordCount = message.content.components(separatedBy: .whitespacesAndNewlines).count - let messageDuration = message.wordTimings.last?.endTime ?? 0.0 - (message.wordTimings.first?.startTime ?? 0.0) - - speakerStats[speakerId]?.messageCount += 1 - speakerStats[speakerId]?.totalWords += wordCount - speakerStats[speakerId]?.averageConfidence = (speakerStats[speakerId]?.averageConfidence ?? 0.0 + message.confidence) / 2.0 - speakerStats[speakerId]?.speakingTime += messageDuration - } - - return Array(speakerStats.values) - } - - func clearHistory() { - conversationHistory.removeAll() - } - - func exportConversation() -> ConversationExport { - return ConversationExport( - messages: conversationHistory, - speakers: Array(speakers.values), - summary: getConversationSummary(), - exportDate: Date() - ) - } -} - -// MARK: - Supporting Types - -struct ConversationSummary { - let messageCount: Int - let speakerCount: Int - let duration: TimeInterval - let averageConfidence: Float - let startTime: TimeInterval - let endTime: TimeInterval -} - -struct SpeakerStatistics { - let speakerId: UUID - let speaker: Speaker? - var messageCount: Int - var totalWords: Int - var averageConfidence: Float - var speakingTime: TimeInterval - - var wordsPerMessage: Float { - messageCount > 0 ? Float(totalWords) / Float(messageCount) : 0.0 - } - - var wordsPerMinute: Float { - speakingTime > 0 ? Float(totalWords) / Float(speakingTime / 60.0) : 0.0 - } -} - -struct ConversationExport: Codable { - let messages: [ConversationMessage] - let speakers: [Speaker] - let summary: ConversationSummary - let exportDate: Date -} - -// Make types Codable for export functionality -extension ConversationMessage: Codable { - enum CodingKeys: String, CodingKey { - case id, content, speakerId, confidence, timestamp, isFinal, wordTimings, originalText - } -} - -extension WordTiming: Codable {} - -extension Speaker: Codable { - enum CodingKeys: String, CodingKey { - case id, name, isCurrentUser, createdAt, lastSeen - } -} - -extension ConversationSummary: Codable {} \ No newline at end of file diff --git a/Helix/HelixApp.swift b/Helix/HelixApp.swift deleted file mode 100644 index 05e5844..0000000 --- a/Helix/HelixApp.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// HelixApp.swift -// Helix -// -// Created by Art Jiang on 2/1/25. -// - -import SwiftUI - -@main -struct HelixApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} diff --git a/Helix/Preview Content/Preview Assets.xcassets/Contents.json b/Helix/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Helix/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Helix/UI/Coordinators/AppCoordinator.swift b/Helix/UI/Coordinators/AppCoordinator.swift deleted file mode 100644 index eb940b3..0000000 --- a/Helix/UI/Coordinators/AppCoordinator.swift +++ /dev/null @@ -1,345 +0,0 @@ -import Foundation -import Combine -import AVFoundation - -@MainActor -class AppCoordinator: ObservableObject { - // Core services - private let audioManager: AudioManagerProtocol - private let speechRecognizer: SpeechRecognitionServiceProtocol - private let speakerDiarization: SpeakerDiarizationEngineProtocol - private let voiceActivityDetector: VoiceActivityDetectorProtocol - private let noiseReducer: NoiseReductionProcessorProtocol - // Transcription service - let transcriptionCoordinator: TranscriptionCoordinatorProtocol - private let llmService: LLMServiceProtocol - private let glassesManager: GlassesManagerProtocol - private let hudRenderer: HUDRendererProtocol - private let conversationContext: ConversationContextManager - - // Published state - @Published var isRecording = false - @Published var connectionState: ConnectionState = .disconnected - @Published var batteryLevel: Float = 0.0 - @Published var currentConversation: [ConversationMessage] = [] - @Published var recentAnalysis: [AnalysisResult] = [] - @Published var speakers: [Speaker] = [] - @Published var isProcessing = false - @Published var errorMessage: String? - - // Settings - @Published var settings = AppSettings() - - // Conversation timing - private var conversationStartDate: Date? - private var durationTimer: AnyCancellable? - - /// Number of messages in the current conversation - var messageCount: Int { - currentConversation.count - } - - /// Elapsed duration of the current conversation (seconds) - @Published var conversationDuration: TimeInterval = 0 - - private var cancellables = Set() - - init() { - // Initialize core services - self.audioManager = AudioManager() - self.speechRecognizer = SpeechRecognitionService() - self.speakerDiarization = SpeakerDiarizationEngine() - self.voiceActivityDetector = VoiceActivityDetector() - self.noiseReducer = NoiseReductionProcessor() - - self.transcriptionCoordinator = TranscriptionCoordinator( - audioManager: audioManager, - speechRecognizer: speechRecognizer, - speakerDiarization: speakerDiarization, - voiceActivityDetector: voiceActivityDetector, - noiseReducer: noiseReducer - ) - - // Initialize AI services - let openAIProvider = OpenAIProvider(apiKey: settings.openAIKey) - self.llmService = LLMService(providers: [.openai: openAIProvider]) - - // Initialize glasses services - self.glassesManager = GlassesManager() - self.hudRenderer = HUDRenderer(glassesManager: glassesManager) - - // Initialize conversation management - self.conversationContext = ConversationContextManager() - // Initialize conversation view model - self.conversationViewModel = ConversationViewModel(transcriptionCoordinator: transcriptionCoordinator) - - setupSubscriptions() - setupDefaultSpeakers() - } - - // MARK: - Public Interface - - func startConversation() { - guard !isRecording else { return } - - isRecording = true - isProcessing = true - // Reset conversation history and timing - currentConversation.removeAll() - conversationStartDate = Date() - // Reset duration and start timer - conversationDuration = 0 - durationTimer?.cancel() - durationTimer = Timer.publish(every: 1.0, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - guard let self = self, let start = self.conversationStartDate else { return } - self.conversationDuration = Date().timeIntervalSince(start) - } - - transcriptionCoordinator.startConversationTranscription() - } - - func stopConversation() { - guard isRecording else { return } - - isRecording = false - isProcessing = false - // Stop duration timer - durationTimer?.cancel() - - transcriptionCoordinator.stopConversationTranscription() - } - - func connectToGlasses() { - glassesManager.connect() - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - if case .failure(let error) = completion { - self?.errorMessage = error.localizedDescription - } - }, - receiveValue: { [weak self] _ in - self?.errorMessage = nil - } - ) - .store(in: &cancellables) - } - - func disconnectFromGlasses() { - glassesManager.disconnect() - } - - func addSpeaker(name: String, isCurrentUser: Bool = false) { - let speaker = Speaker(name: name, isCurrentUser: isCurrentUser) - speakers.append(speaker) - transcriptionCoordinator.addSpeaker(speaker) - conversationContext.addSpeaker(speaker) - } - - func trainSpeaker(_ speakerId: UUID, with samples: [AVAudioPCMBuffer]) { - transcriptionCoordinator.trainSpeaker(speakerId, with: samples) - } - - func clearConversation() { - // Clear all conversation data and timing - currentConversation.removeAll() - recentAnalysis.removeAll() - conversationContext.clearHistory() - hudRenderer.clearAll() - conversationStartDate = nil - conversationDuration = 0 - durationTimer?.cancel() - } - - func exportConversation() -> ConversationExport { - return conversationContext.exportConversation() - } - - func updateSettings(_ newSettings: AppSettings) { - settings = newSettings - - // Update service configurations - configureServices(with: newSettings) - } - - // MARK: - Private Methods - - private func setupSubscriptions() { - // Glasses connection state - glassesManager.connectionState - .receive(on: DispatchQueue.main) - .assign(to: \.connectionState, on: self) - .store(in: &cancellables) - - // Battery level - glassesManager.batteryLevel - .receive(on: DispatchQueue.main) - .assign(to: \.batteryLevel, on: self) - .store(in: &cancellables) - - // Conversation updates - transcriptionCoordinator.conversationPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - if case .failure(let error) = completion { - self?.errorMessage = error.localizedDescription - self?.isProcessing = false - } - } receiveValue: { [weak self] update in - self?.handleConversationUpdate(update) - } - .store(in: &cancellables) - } - - private func setupDefaultSpeakers() { - // Add current user as default speaker - let currentUser = Speaker(name: "You", isCurrentUser: true) - speakers.append(currentUser) - transcriptionCoordinator.addSpeaker(currentUser) - conversationContext.addSpeaker(currentUser) - } - - private func handleConversationUpdate(_ update: ConversationUpdate) { - // Add message to conversation - currentConversation.append(update.message) - conversationContext.addMessage(update.message) - - // Update speakers list if new speaker - if update.isNewSpeaker, let speaker = update.speaker { - if !speakers.contains(where: { $0.id == speaker.id }) { - speakers.append(speaker) - } - } - - // Process for AI analysis if enabled - if settings.enableFactChecking || settings.enableAutoSummary { - processMessageForAnalysis(update.message) - } - - isProcessing = false - } - - private func processMessageForAnalysis(_ message: ConversationMessage) { - guard !message.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } - - let context = ConversationContext( - messages: Array(currentConversation.suffix(5)), // Last 5 messages for context - speakers: speakers, - analysisType: .factCheck - ) - - // Detect claims first - llmService.detectClaims(in: message.content) - .flatMap { [weak self] claims -> AnyPublisher<[AnalysisResult], LLMError> in - guard let self = self, !claims.isEmpty else { - return Just([]).setFailureType(to: LLMError.self).eraseToAnyPublisher() - } - - let factCheckPublishers = claims.map { claim in - self.llmService.factCheck(claim.text, context: context) - .map { factCheckResult in - AnalysisResult( - type: .factCheck, - content: .factCheck(factCheckResult), - confidence: factCheckResult.confidence, - provider: .openai - ) - } - } - - return Publishers.MergeMany(factCheckPublishers) - .collect() - .eraseToAnyPublisher() - } - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - if case .failure(let error) = completion { - print("Analysis failed: \(error)") - self?.errorMessage = "Analysis failed: \(error.localizedDescription)" - } - }, - receiveValue: { [weak self] results in - self?.handleAnalysisResults(results) - } - ) - .store(in: &cancellables) - } - - private func handleAnalysisResults(_ results: [AnalysisResult]) { - recentAnalysis.append(contentsOf: results) - - // Display critical results on HUD - for result in results { - if case .factCheck(let factCheckResult) = result.content, - !factCheckResult.isAccurate && factCheckResult.severity == .critical { - - let hudContent = HUDContentFactory.createFactCheckDisplay(factCheckResult) - hudRenderer.render(hudContent) - .sink(receiveCompletion: { _ in }, receiveValue: { _ in }) - .store(in: &cancellables) - } - } - } - - private func configureServices(with settings: AppSettings) { - // Configure audio settings - do { - try audioManager.configure( - sampleRate: 16000, - bufferDuration: settings.audioBufferDuration - ) - } catch { - errorMessage = "Failed to configure audio: \(error.localizedDescription)" - } - - // Configure speech recognition - if let language = settings.primaryLanguage { - speechRecognizer.setLanguage(language) - } - - // Configure noise reduction - noiseReducer.setReductionLevel(settings.noiseReductionLevel) - - // Configure voice activity detection - voiceActivityDetector.setSensitivity(settings.voiceSensitivity) - } -} - -// MARK: - App Settings - -struct AppSettings: Codable { - var openAIKey: String = "" - var anthropicKey: String = "" - var enableFactChecking: Bool = true - var enableAutoSummary: Bool = true - var enableActionItems: Bool = true - var primaryLanguage: Locale? = Locale(identifier: "en-US") - var audioBufferDuration: TimeInterval = 0.005 - var noiseReductionLevel: Float = 0.5 - var voiceSensitivity: Float = 0.5 - var glassesAutoConnect: Bool = true - var displayBrightness: Float = 0.8 - var factCheckSeverityFilter: FactCheckResult.FactCheckSeverity = .significant - var maxConversationHistory: Int = 100 - var autoExport: Bool = false - var privacyMode: Bool = false - - static let `default` = AppSettings() -} - -// MARK: - Extensions - -extension AppCoordinator { - /// Whether the glasses are currently connected - var isConnectedToGlasses: Bool { - connectionState.isConnected - } - - /// Number of unique speakers in the current conversation - var speakerCount: Int { - Set(currentConversation.compactMap { $0.speakerId }).count - } -} \ No newline at end of file diff --git a/Helix/UI/Coordinators/ConversationViewModel.swift b/Helix/UI/Coordinators/ConversationViewModel.swift deleted file mode 100644 index b0af688..0000000 --- a/Helix/UI/Coordinators/ConversationViewModel.swift +++ /dev/null @@ -1,57 +0,0 @@ -import Foundation -import Combine -import Helix_Core_Transcription - -/// ViewModel to drive ConversationView using TranscriptionCoordinator -@MainActor -class ConversationViewModel: ObservableObject { - /// Published conversation messages - @Published var messages: [ConversationMessage] = [] - /// Recording state - @Published var isRecording: Bool = false - /// Processing indicator - @Published var isProcessing: Bool = false - /// Error message - @Published var errorMessage: String? - - private let transcriptionCoordinator: TranscriptionCoordinatorProtocol - private var cancellables = Set() - - init(transcriptionCoordinator: TranscriptionCoordinatorProtocol) { - self.transcriptionCoordinator = transcriptionCoordinator - setupBindings() - } - - private func setupBindings() { - transcriptionCoordinator.conversationPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - if case .failure(let error) = completion { - self?.errorMessage = error.localizedDescription - self?.isProcessing = false - } - } receiveValue: { [weak self] update in - guard let self = self else { return } - self.messages.append(update.message) - self.isProcessing = false - } - .store(in: &cancellables) - } - - /// Start transcription - func start() { - guard !isRecording else { return } - messages.removeAll() - isRecording = true - isProcessing = true - transcriptionCoordinator.startConversationTranscription() - } - - /// Stop transcription - func stop() { - guard isRecording else { return } - isRecording = false - isProcessing = false - transcriptionCoordinator.stopConversationTranscription() - } -} \ No newline at end of file diff --git a/Helix/UI/ViewModels/ConversationViewModel.swift b/Helix/UI/ViewModels/ConversationViewModel.swift deleted file mode 100644 index c26e874..0000000 --- a/Helix/UI/ViewModels/ConversationViewModel.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Foundation -import Combine -import Helix_Core_Transcription - -/// ViewModel for live conversation transcription -@MainActor -class ConversationViewModel: ObservableObject { - @Published var messages: [ConversationMessage] = [] - @Published var isRecording: Bool = false - @Published var isProcessing: Bool = false - @Published var errorMessage: String? - - private let transcriptionCoordinator: TranscriptionCoordinatorProtocol - private var cancellables = Set() - - init(transcriptionCoordinator: TranscriptionCoordinatorProtocol) { - self.transcriptionCoordinator = transcriptionCoordinator - subscribeToTranscription() - } - - /// Start live transcription - func start() { - guard !isRecording else { return } - messages.removeAll() - isRecording = true - isProcessing = true - transcriptionCoordinator.startConversationTranscription() - } - - /// Stop live transcription - func stop() { - guard isRecording else { return } - isRecording = false - isProcessing = false - transcriptionCoordinator.stopConversationTranscription() - } - - private func subscribeToTranscription() { - transcriptionCoordinator.conversationPublisher - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - if case .failure(let error) = completion { - self?.errorMessage = error.localizedDescription - self?.isProcessing = false - } - }, receiveValue: { [weak self] update in - self?.messages.append(update.message) - self?.isProcessing = false - }) - .store(in: &cancellables) - } -} \ No newline at end of file diff --git a/Helix/UI/Views/AnalysisView.swift b/Helix/UI/Views/AnalysisView.swift deleted file mode 100644 index dceabd0..0000000 --- a/Helix/UI/Views/AnalysisView.swift +++ /dev/null @@ -1,639 +0,0 @@ -import SwiftUI - -struct AnalysisView: View { - @EnvironmentObject var coordinator: AppCoordinator - @State private var selectedAnalysisType: AnalysisType = .factCheck - - var body: some View { - NavigationView { - VStack { - if coordinator.recentAnalysis.isEmpty { - EmptyAnalysisView() - } else { - AnalysisContentView(selectedType: $selectedAnalysisType) - } - } - .navigationTitle("Analysis") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Menu { - ForEach(AnalysisType.allCases, id: \.self) { type in - Button(type.displayName) { - selectedAnalysisType = type - } - } - } label: { - Image(systemName: "line.3.horizontal.decrease.circle") - } - } - } - } - } -} - -struct EmptyAnalysisView: View { - var body: some View { - VStack(spacing: 24) { - Image(systemName: "brain.head.profile") - .font(.system(size: 60)) - .foregroundColor(.secondary) - - VStack(spacing: 8) { - Text("No Analysis Available") - .font(.title2) - .fontWeight(.semibold) - - Text("Start a conversation to see AI-powered analysis including fact-checking, summaries, and insights.") - .font(.body) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal) - } - - VStack(spacing: 12) { - AnalysisFeatureRow( - icon: "checkmark.circle", - title: "Fact Checking", - description: "Real-time verification of claims and statements" - ) - - AnalysisFeatureRow( - icon: "doc.text", - title: "Auto Summary", - description: "Key points and decisions from conversations" - ) - - AnalysisFeatureRow( - icon: "list.bullet", - title: "Action Items", - description: "Extracted tasks and follow-ups" - ) - - AnalysisFeatureRow( - icon: "heart.text.square", - title: "Sentiment Analysis", - description: "Emotional tone and mood tracking" - ) - } - .padding() - } - } -} - -struct AnalysisFeatureRow: View { - let icon: String - let title: String - let description: String - - var body: some View { - HStack(spacing: 12) { - Image(systemName: icon) - .font(.title3) - .foregroundColor(.blue) - .frame(width: 24) - - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.subheadline) - .fontWeight(.medium) - - Text(description) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - } -} - -struct AnalysisContentView: View { - @EnvironmentObject var coordinator: AppCoordinator - @Binding var selectedType: AnalysisType - - private var filteredAnalysis: [AnalysisResult] { - coordinator.recentAnalysis.filter { $0.type == selectedType } - } - - var body: some View { - VStack { - // Analysis type picker - AnalysisTypePicker(selectedType: $selectedType) - .padding(.horizontal) - - if filteredAnalysis.isEmpty { - NoAnalysisForTypeView(type: selectedType) - } else { - // Analysis results - List(filteredAnalysis, id: \.id) { result in - AnalysisResultCard(result: result) - .listRowSeparator(.hidden) - .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) - } - .listStyle(.plain) - } - } - } -} - -struct AnalysisTypePicker: View { - @Binding var selectedType: AnalysisType - - var body: some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 12) { - ForEach(AnalysisType.allCases, id: \.self) { type in - Button(action: { - selectedType = type - }) { - HStack(spacing: 6) { - Image(systemName: type.iconName) - .font(.caption) - - Text(type.displayName) - .font(.caption) - .fontWeight(.medium) - } - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(selectedType == type ? Color.blue : Color(.systemGray5)) - .foregroundColor(selectedType == type ? .white : .primary) - .cornerRadius(16) - } - } - } - .padding(.horizontal) - } - } -} - -struct NoAnalysisForTypeView: View { - let type: AnalysisType - - var body: some View { - VStack(spacing: 16) { - Image(systemName: type.iconName) - .font(.system(size: 40)) - .foregroundColor(.secondary) - - Text("No \(type.displayName) Available") - .font(.headline) - .foregroundColor(.secondary) - - Text(type.emptyStateDescription) - .font(.body) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } -} - -struct AnalysisResultCard: View { - let result: AnalysisResult - @State private var isExpanded = false - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - // Header - HStack { - HStack(spacing: 8) { - Image(systemName: result.type.iconName) - .foregroundColor(result.type.color) - - Text(result.type.displayName) - .font(.headline) - .fontWeight(.semibold) - } - - Spacer() - - VStack(alignment: .trailing, spacing: 2) { - ConfidenceIndicator(confidence: result.confidence) - - Text(formatTimestamp(result.timestamp)) - .font(.caption2) - .foregroundColor(.secondary) - } - } - - // Content - AnalysisContentCard(content: result.content, isExpanded: $isExpanded) - - // Sources (if available) - if !result.sources.isEmpty { - SourcesView(sources: result.sources) - } - } - .padding() - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2) - } - - private func formatTimestamp(_ timestamp: TimeInterval) -> String { - let date = Date(timeIntervalSince1970: timestamp) - let formatter = RelativeDateTimeFormatter() - formatter.unitsStyle = .abbreviated - return formatter.localizedString(for: date, relativeTo: Date()) - } -} - -struct AnalysisContentCard: View { - let content: AnalysisContent - @Binding var isExpanded: Bool - - var body: some View { - switch content { - case .factCheck(let result): - FactCheckContentView(result: result, isExpanded: $isExpanded) - case .summary(let text): - SummaryContentView(text: text) - case .actionItems(let items): - ActionItemsContentView(items: items) - case .sentiment(let analysis): - SentimentContentView(analysis: analysis) - case .topics(let topics): - TopicsContentView(topics: topics) - case .translation(let result): - TranslationContentView(result: result) - case .text(let text): - Text(text) - .font(.body) - } - } -} - -struct FactCheckContentView: View { - let result: FactCheckResult - @Binding var isExpanded: Bool - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - // Claim - Text("Claim:") - .font(.caption) - .foregroundColor(.secondary) - - Text(result.claim) - .font(.body) - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(Color(.systemGray6)) - .cornerRadius(8) - - // Result - HStack { - Image(systemName: result.isAccurate ? "checkmark.circle.fill" : "xmark.circle.fill") - .foregroundColor(result.isAccurate ? .green : .red) - - Text(result.isAccurate ? "Accurate" : "Inaccurate") - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(result.isAccurate ? .green : .red) - - Spacer() - - Button(action: { - withAnimation(.easeInOut(duration: 0.3)) { - isExpanded.toggle() - } - }) { - Image(systemName: isExpanded ? "chevron.up" : "chevron.down") - .font(.caption) - .foregroundColor(.secondary) - } - } - - // Explanation (expandable) - if isExpanded { - VStack(alignment: .leading, spacing: 8) { - Text("Explanation:") - .font(.caption) - .foregroundColor(.secondary) - - Text(result.explanation) - .font(.body) - - if let alternativeInfo = result.alternativeInfo { - Text("Correct Information:") - .font(.caption) - .foregroundColor(.secondary) - - Text(alternativeInfo) - .font(.body) - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(Color.green.opacity(0.1)) - .cornerRadius(8) - } - } - .transition(.slide) - } - } - } -} - -struct SummaryContentView: View { - let text: String - - var body: some View { - Text(text) - .font(.body) - .padding() - .background(Color.blue.opacity(0.1)) - .cornerRadius(8) - } -} - -struct ActionItemsContentView: View { - let items: [ActionItem] - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - ForEach(items, id: \.id) { item in - HStack { - Image(systemName: "circle") - .foregroundColor(item.priority.color) - - Text(item.description) - .font(.body) - - Spacer() - - Text(item.priority.rawValue.uppercased()) - .font(.caption2) - .fontWeight(.medium) - .foregroundColor(item.priority.color) - } - .padding(.vertical, 4) - } - } - } -} - -struct SentimentContentView: View { - let analysis: SentimentAnalysis - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Overall Sentiment:") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - SentimentBadge(sentiment: analysis.overallSentiment) - } - - HStack { - Text("Emotional Tone:") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text(analysis.emotionalTone.rawValue.capitalized) - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color(.systemGray5)) - .cornerRadius(8) - } - } - } -} - -struct SentimentBadge: View { - let sentiment: Sentiment - - var body: some View { - HStack(spacing: 4) { - Image(systemName: sentiment.iconName) - .font(.caption2) - - Text(sentiment.rawValue.capitalized) - .font(.caption) - .fontWeight(.medium) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(sentiment.color.opacity(0.2)) - .foregroundColor(sentiment.color) - .cornerRadius(8) - } -} - -struct TopicsContentView: View { - let topics: [String] - - var body: some View { - LazyVGrid(columns: [ - GridItem(.adaptive(minimum: 100)) - ], spacing: 8) { - ForEach(topics, id: \.self) { topic in - Text(topic) - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color(.systemGray5)) - .cornerRadius(12) - } - } - } -} - -struct TranslationContentView: View { - let result: TranslationResult - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text("Original (\(result.sourceLanguage)):") - .font(.caption) - .foregroundColor(.secondary) - - Text(result.originalText) - .font(.body) - .padding() - .background(Color(.systemGray6)) - .cornerRadius(8) - - Text("Translation (\(result.targetLanguage)):") - .font(.caption) - .foregroundColor(.secondary) - - Text(result.translatedText) - .font(.body) - .padding() - .background(Color.blue.opacity(0.1)) - .cornerRadius(8) - } - } -} - -struct SourcesView: View { - let sources: [Source] - @State private var isExpanded = false - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Button(action: { - withAnimation { - isExpanded.toggle() - } - }) { - HStack { - Text("Sources (\(sources.count))") - .font(.caption) - .foregroundColor(.blue) - - Spacer() - - Image(systemName: isExpanded ? "chevron.up" : "chevron.down") - .font(.caption2) - .foregroundColor(.blue) - } - } - - if isExpanded { - ForEach(sources, id: \.id) { source in - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(source.title) - .font(.caption) - .fontWeight(.medium) - - if let url = source.url { - Text(url) - .font(.caption2) - .foregroundColor(.blue) - .lineLimit(1) - } - } - - Spacer() - - ReliabilityBadge(reliability: source.reliability) - } - .padding(.vertical, 2) - } - .transition(.slide) - } - } - } -} - -struct ReliabilityBadge: View { - let reliability: SourceReliability - - var body: some View { - Text(reliability.rawValue.capitalized) - .font(.caption2) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(reliability.color.opacity(0.2)) - .foregroundColor(reliability.color) - .cornerRadius(4) - } -} - -// MARK: - Extensions - -extension AnalysisType { - var displayName: String { - switch self { - case .factCheck: return "Fact Check" - case .summarization: return "Summary" - case .actionItems: return "Action Items" - case .sentiment: return "Sentiment" - case .keyTopics: return "Topics" - case .translation: return "Translation" - case .clarification: return "Clarification" - } - } - - var iconName: String { - switch self { - case .factCheck: return "checkmark.circle" - case .summarization: return "doc.text" - case .actionItems: return "list.bullet" - case .sentiment: return "heart.text.square" - case .keyTopics: return "tag" - case .translation: return "globe" - case .clarification: return "questionmark.circle" - } - } - - var color: Color { - switch self { - case .factCheck: return .red - case .summarization: return .blue - case .actionItems: return .orange - case .sentiment: return .purple - case .keyTopics: return .green - case .translation: return .cyan - case .clarification: return .yellow - } - } - - var emptyStateDescription: String { - switch self { - case .factCheck: return "Fact-checking results will appear here when claims are detected in conversations." - case .summarization: return "Conversation summaries will be generated automatically during discussions." - case .actionItems: return "Action items and tasks will be extracted from conversations." - case .sentiment: return "Sentiment analysis will show the emotional tone of conversations." - case .keyTopics: return "Key topics and themes will be identified from conversation content." - case .translation: return "Translation results will appear when non-English content is detected." - case .clarification: return "Clarification suggestions will help improve conversation understanding." - } - } -} - -extension ActionItemPriority { - var color: Color { - switch self { - case .low: return .green - case .medium: return .orange - case .high: return .red - case .urgent: return .purple - } - } -} - -extension Sentiment { - var iconName: String { - switch self { - case .positive: return "face.smiling" - case .negative: return "face.dashed" - case .neutral: return "face.expressionless" - case .mixed: return "face.expressionless" - } - } - - var color: Color { - switch self { - case .positive: return .green - case .negative: return .red - case .neutral: return .gray - case .mixed: return .orange - } - } -} - -extension SourceReliability { - var color: Color { - switch self { - case .high: return .green - case .medium: return .orange - case .low: return .red - case .unknown: return .gray - } - } -} - -#Preview { - AnalysisView() - .environmentObject(AppCoordinator()) -} \ No newline at end of file diff --git a/Helix/UI/Views/ConversationView.swift b/Helix/UI/Views/ConversationView.swift deleted file mode 100644 index 9972a03..0000000 --- a/Helix/UI/Views/ConversationView.swift +++ /dev/null @@ -1,428 +0,0 @@ -import SwiftUI - -struct ConversationView: View { - @EnvironmentObject var coordinator: AppCoordinator - @StateObject private var viewModel: ConversationViewModel - @State private var showingSpeakerSheet = false - @State private var isAutoScrollEnabled = true - - /// Initialize with a ViewModel - init(viewModel: ConversationViewModel) { - _viewModel = StateObject(wrappedValue: viewModel) - } - - var body: some View { - NavigationView { - VStack(spacing: 0) { - // Status Bar - // Status Bar showing recording state and stats - StatusBarView(viewModel: viewModel) - .padding(.horizontal) - .padding(.top, 8) - - Divider() - - // Conversation Messages - // Conversation messages list - ConversationScrollView(viewModel: viewModel, isAutoScrollEnabled: $isAutoScrollEnabled) - - Divider() - - // Control Panel - // Controls for recording, speakers, glasses - ControlPanelView(viewModel: viewModel, showingSpeakerSheet: $showingSpeakerSheet) - .padding() - } - .navigationTitle("Live Conversation") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Menu { - Button("Add Speaker") { - showingSpeakerSheet = true - } - - Button("Clear Conversation") { - coordinator.clearConversation() - } - - Button("Export Conversation") { - exportConversation() - } - - Toggle("Auto-scroll", isOn: $isAutoScrollEnabled) - } label: { - Image(systemName: "ellipsis.circle") - } - } - } - } - .sheet(isPresented: $showingSpeakerSheet) { - AddSpeakerSheet() - } - .alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) { - Button("OK") { - viewModel.errorMessage = nil - } - } message: { - Text(viewModel.errorMessage ?? "") - } - } - - private func exportConversation() { - let export = coordinator.exportConversation() - // TODO: Implement export functionality - print("Exporting conversation: \(export)") - } -} - -struct StatusBarView: View { - @EnvironmentObject var coordinator: AppCoordinator - - var body: some View { - HStack { - // Recording Status - HStack(spacing: 8) { - Circle() - .fill(coordinator.isRecording ? .red : .gray) - .frame(width: 8, height: 8) - .scaleEffect(coordinator.isRecording ? 1.2 : 1.0) - .animation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true), value: coordinator.isRecording) - - Text(coordinator.isRecording ? "Recording" : "Stopped") - .font(.caption) - .foregroundColor(coordinator.isRecording ? .red : .secondary) - } - - Spacer() - - // Glasses Connection - HStack(spacing: 4) { - Image(systemName: coordinator.isConnectedToGlasses ? "eyeglasses" : "eyeglasses.slash") - .foregroundColor(coordinator.isConnectedToGlasses ? .green : .gray) - - if coordinator.isConnectedToGlasses { - BatteryIndicator(level: coordinator.batteryLevel) - } - } - .font(.caption) - - Spacer() - - // Stats - VStack(alignment: .trailing, spacing: 2) { - Text("\(coordinator.messageCount) messages") - .font(.caption2) - .foregroundColor(.secondary) - - Text(formatDuration(coordinator.conversationDuration)) - .font(.caption2) - .foregroundColor(.secondary) - } - } - .padding(.vertical, 8) - .padding(.horizontal, 12) - .background(Color(.systemGray6)) - .cornerRadius(8) - } - - private func formatDuration(_ duration: TimeInterval) -> String { - let minutes = Int(duration) / 60 - let seconds = Int(duration) % 60 - return String(format: "%02d:%02d", minutes, seconds) - } -} - -struct BatteryIndicator: View { - let level: Float - - var body: some View { - HStack(spacing: 2) { - RoundedRectangle(cornerRadius: 1) - .stroke(batteryColor, lineWidth: 1) - .frame(width: 16, height: 8) - .overlay( - RoundedRectangle(cornerRadius: 0.5) - .fill(batteryColor) - .frame(width: CGFloat(level) * 14, height: 6) - .offset(x: (CGFloat(level) - 1) * 7) - ) - - RoundedRectangle(cornerRadius: 0.5) - .fill(batteryColor) - .frame(width: 2, height: 4) - } - } - - private var batteryColor: Color { - switch level { - case 0.5...1.0: return .green - case 0.2..<0.5: return .orange - default: return .red - } - } -} - -struct ConversationScrollView: View { - @EnvironmentObject var coordinator: AppCoordinator - @Binding var isAutoScrollEnabled: Bool - - var body: some View { - ScrollViewReader { proxy in - ScrollView { - LazyVStack(alignment: .leading, spacing: 12) { - ForEach(coordinator.currentConversation, id: \.id) { message in - MessageBubble(message: message) - .id(message.id) - } - - if coordinator.isProcessing { - ProcessingIndicator() - } - } - .padding() - } - .onChange(of: coordinator.currentConversation.count) { _ in - if isAutoScrollEnabled, let lastMessage = coordinator.currentConversation.last { - withAnimation(.easeOut(duration: 0.3)) { - proxy.scrollTo(lastMessage.id, anchor: .bottom) - } - } - } - } - } -} - -struct MessageBubble: View { - @EnvironmentObject var coordinator: AppCoordinator - let message: ConversationMessage - - private var speaker: Speaker? { - coordinator.speakers.first { $0.id == message.speakerId } - } - - private var isCurrentUser: Bool { - speaker?.isCurrentUser ?? false - } - - var body: some View { - HStack { - if isCurrentUser { - Spacer() - } - - VStack(alignment: isCurrentUser ? .trailing : .leading, spacing: 4) { - // Speaker name and timestamp - HStack { - Text(speaker?.name ?? "Unknown Speaker") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text(formatTimestamp(message.timestamp)) - .font(.caption2) - .foregroundColor(.secondary) - } - - // Message content - Text(message.content) - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(isCurrentUser ? Color.blue : Color(.systemGray5)) - .foregroundColor(isCurrentUser ? .white : .primary) - .cornerRadius(16) - - // Confidence indicator - if message.confidence > 0 { - ConfidenceIndicator(confidence: message.confidence) - } - } - .frame(maxWidth: 280, alignment: isCurrentUser ? .trailing : .leading) - - if !isCurrentUser { - Spacer() - } - } - } - - private func formatTimestamp(_ timestamp: TimeInterval) -> String { - let date = Date(timeIntervalSince1970: timestamp) - let formatter = DateFormatter() - formatter.timeStyle = .short - return formatter.string(from: date) - } -} - -struct ConfidenceIndicator: View { - let confidence: Float - - var body: some View { - HStack(spacing: 2) { - ForEach(0..<5, id: \.self) { index in - Circle() - .fill(index < Int(confidence * 5) ? confidenceColor : Color.gray.opacity(0.3)) - .frame(width: 4, height: 4) - } - } - } - - private var confidenceColor: Color { - switch confidence { - case 0.8...1.0: return .green - case 0.6..<0.8: return .orange - default: return .red - } - } -} - -struct ProcessingIndicator: View { - @State private var isAnimating = false - - var body: some View { - HStack { - Spacer() - - HStack(spacing: 4) { - ForEach(0..<3, id: \.self) { index in - Circle() - .fill(Color.blue) - .frame(width: 8, height: 8) - .scaleEffect(isAnimating ? 1.2 : 0.8) - .animation( - Animation.easeInOut(duration: 0.6) - .repeatForever() - .delay(Double(index) * 0.2), - value: isAnimating - ) - } - - Text("Processing...") - .font(.caption) - .foregroundColor(.secondary) - } - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(Color(.systemGray6)) - .cornerRadius(16) - - Spacer() - } - .onAppear { - isAnimating = true - } - } -} - -struct ControlPanelView: View { - @EnvironmentObject var coordinator: AppCoordinator - @Binding var showingSpeakerSheet: Bool - - var body: some View { - VStack(spacing: 16) { - // Main record button - Button(action: toggleRecording) { - ZStack { - Circle() - .fill(coordinator.isRecording ? Color.red : Color.blue) - .frame(width: 80, height: 80) - .scaleEffect(coordinator.isRecording ? 1.1 : 1.0) - .animation(.easeInOut(duration: 0.3), value: coordinator.isRecording) - - Image(systemName: coordinator.isRecording ? "stop.fill" : "mic.fill") - .font(.title) - .foregroundColor(.white) - } - } - .disabled(coordinator.isProcessing) - - // Secondary controls - HStack(spacing: 20) { - Button("Speakers") { - showingSpeakerSheet = true - } - .buttonStyle(.bordered) - - Button("Clear") { - coordinator.clearConversation() - } - .buttonStyle(.bordered) - .disabled(coordinator.currentConversation.isEmpty) - - Button("Connect") { - if coordinator.isConnectedToGlasses { - coordinator.disconnectFromGlasses() - } else { - coordinator.connectToGlasses() - } - } - .buttonStyle(.bordered) - } - } - } - - private func toggleRecording() { - if coordinator.isRecording { - coordinator.stopConversation() - } else { - coordinator.startConversation() - } - } -} - -struct AddSpeakerSheet: View { - @EnvironmentObject var coordinator: AppCoordinator - @Environment(\.dismiss) private var dismiss - @State private var speakerName = "" - @State private var isCurrentUser = false - - var body: some View { - NavigationView { - Form { - Section("Speaker Information") { - TextField("Name", text: $speakerName) - - Toggle("This is me", isOn: $isCurrentUser) - } - - Section("Current Speakers") { - ForEach(coordinator.speakers, id: \.id) { speaker in - HStack { - Text(speaker.name ?? "Unknown") - - Spacer() - - if speaker.isCurrentUser { - Text("You") - .font(.caption) - .foregroundColor(.secondary) - } - } - } - } - } - .navigationTitle("Manage Speakers") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Add") { - coordinator.addSpeaker(name: speakerName, isCurrentUser: isCurrentUser) - dismiss() - } - .disabled(speakerName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) - } - } - } - } -} - -#Preview { - ConversationView() - .environmentObject(AppCoordinator()) -} \ No newline at end of file diff --git a/Helix/UI/Views/GlassesView.swift b/Helix/UI/Views/GlassesView.swift deleted file mode 100644 index 9265594..0000000 --- a/Helix/UI/Views/GlassesView.swift +++ /dev/null @@ -1,438 +0,0 @@ -import SwiftUI - -struct GlassesView: View { - @EnvironmentObject var coordinator: AppCoordinator - @State private var showingTestDisplay = false - @State private var testMessage = "Test message" - - var body: some View { - NavigationView { - List { - ConnectionSection() - - if coordinator.isConnectedToGlasses { - StatusSection() - DisplayTestSection( - showingTestDisplay: $showingTestDisplay, - testMessage: $testMessage - ) - DisplaySettingsSection() - } - } - .navigationTitle("Glasses") - .toolbar { - if coordinator.isConnectedToGlasses { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Disconnect") { - coordinator.disconnectFromGlasses() - } - } - } - } - } - .sheet(isPresented: $showingTestDisplay) { - TestDisplaySheet(testMessage: $testMessage) - } - } -} - -struct ConnectionSection: View { - @EnvironmentObject var coordinator: AppCoordinator - - var body: some View { - Section("Connection") { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Even Realities Glasses") - .font(.headline) - - Text(coordinator.connectionState.statusDescription) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - ConnectionStatusIndicator(state: coordinator.connectionState) - } - .padding(.vertical, 8) - - if !coordinator.isConnectedToGlasses { - Button("Connect to Glasses") { - coordinator.connectToGlasses() - } - .buttonStyle(.bordered) - .disabled(coordinator.connectionState == .scanning || coordinator.connectionState == .connecting) - } - } - } -} - -struct ConnectionStatusIndicator: View { - let state: ConnectionState - - var body: some View { - HStack(spacing: 8) { - Circle() - .fill(state.indicatorColor) - .frame(width: 12, height: 12) - .scaleEffect(state == .scanning || state == .connecting ? 1.2 : 1.0) - .animation(.easeInOut(duration: 0.8).repeatForever(autoreverses: true), value: state == .scanning || state == .connecting) - - Text(state.displayName) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(state.textColor) - } - } -} - -struct StatusSection: View { - @EnvironmentObject var coordinator: AppCoordinator - - var body: some View { - Section("Status") { - StatusRow( - icon: "battery.100", - title: "Battery Level", - value: "\(Int(coordinator.batteryLevel * 100))%", - color: batteryColor - ) - - StatusRow( - icon: "eye", - title: "Display Status", - value: "Active", - color: .green - ) - - StatusRow( - icon: "antenna.radiowaves.left.and.right", - title: "Signal Strength", - value: "Strong", - color: .green - ) - } - } - - private var batteryColor: Color { - switch coordinator.batteryLevel { - case 0.5...1.0: return .green - case 0.2..<0.5: return .orange - default: return .red - } - } -} - -struct StatusRow: View { - let icon: String - let title: String - let value: String - let color: Color - - var body: some View { - HStack { - Image(systemName: icon) - .foregroundColor(color) - .frame(width: 24) - - Text(title) - .font(.body) - - Spacer() - - Text(value) - .font(.body) - .fontWeight(.medium) - .foregroundColor(color) - } - } -} - -struct DisplayTestSection: View { - @EnvironmentObject var coordinator: AppCoordinator - @Binding var showingTestDisplay: Bool - @Binding var testMessage: String - - var body: some View { - Section("Display Test") { - HStack { - TextField("Test message", text: $testMessage) - .textFieldStyle(.roundedBorder) - - Button("Send") { - sendTestMessage() - } - .buttonStyle(.bordered) - .disabled(testMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) - } - - Button("Advanced Test") { - showingTestDisplay = true - } - .buttonStyle(.bordered) - - Button("Clear Display") { - clearDisplay() - } - .buttonStyle(.bordered) - } - } - - private func sendTestMessage() { - // TODO: Implement with actual HUD renderer - print("Sending test message: \(testMessage)") - } - - private func clearDisplay() { - // TODO: Implement with actual HUD renderer - print("Clearing display") - } -} - -struct DisplaySettingsSection: View { - @EnvironmentObject var coordinator: AppCoordinator - @State private var brightness: Double = 0.8 - @State private var autoAdjust = true - - var body: some View { - Section("Display Settings") { - VStack(alignment: .leading, spacing: 8) { - Text("Brightness") - .font(.caption) - .foregroundColor(.secondary) - - HStack { - Image(systemName: "sun.min") - .foregroundColor(.secondary) - - Slider(value: $brightness, in: 0.1...1.0) - .onChange(of: brightness) { newValue in - updateBrightness(newValue) - } - - Image(systemName: "sun.max") - .foregroundColor(.secondary) - } - } - - Toggle("Auto-adjust brightness", isOn: $autoAdjust) - .onChange(of: autoAdjust) { newValue in - updateAutoAdjust(newValue) - } - } - } - - private func updateBrightness(_ value: Double) { - // TODO: Implement with actual glasses manager - print("Updated brightness to: \(value)") - } - - private func updateAutoAdjust(_ enabled: Bool) { - // TODO: Implement with actual glasses manager - print("Auto-adjust brightness: \(enabled)") - } -} - -struct TestDisplaySheet: View { - @EnvironmentObject var coordinator: AppCoordinator - @Environment(\.dismiss) private var dismiss - @Binding var testMessage: String - - @State private var selectedPosition: HUDPosition = .topCenter - @State private var selectedColor: HUDColor = .white - @State private var selectedSize: FontSize = .medium - @State private var duration: Double = 5.0 - @State private var isBold = false - - private let positions: [HUDPosition] = [ - .topLeft, .topCenter, .topRight, - HUDPosition(x: 0.5, y: 0.5, alignment: .center, fontSize: .medium), - HUDPosition(x: 0.1, y: 0.9, alignment: .left, fontSize: .small), - HUDPosition(x: 0.9, y: 0.9, alignment: .right, fontSize: .small) - ] - - var body: some View { - NavigationView { - Form { - Section("Message") { - TextField("Test message", text: $testMessage) - .textFieldStyle(.roundedBorder) - } - - Section("Position") { - Picker("Position", selection: $selectedPosition) { - ForEach(positions, id: \.description) { position in - Text(position.displayName) - .tag(position) - } - } - .pickerStyle(.wheel) - } - - Section("Style") { - Picker("Color", selection: $selectedColor) { - ForEach(HUDColor.allCases, id: \.self) { color in - HStack { - Circle() - .fill(Color(color)) - .frame(width: 16, height: 16) - - Text(color.rawValue.capitalized) - } - .tag(color) - } - } - - Picker("Size", selection: $selectedSize) { - ForEach(FontSize.allCases, id: \.self) { size in - Text(size.rawValue.capitalized) - .tag(size) - } - } - .pickerStyle(.segmented) - - Toggle("Bold", isOn: $isBold) - } - - Section("Duration") { - HStack { - Text("Duration: \(Int(duration))s") - Spacer() - Slider(value: $duration, in: 1...30, step: 1) - } - } - - Section { - Button("Send Test Display") { - sendTestDisplay() - } - .frame(maxWidth: .infinity) - .buttonStyle(.borderedProminent) - .disabled(testMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) - } - } - .navigationTitle("Test Display") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - } - } - } - - private func sendTestDisplay() { - // TODO: Implement with actual HUD renderer - print("Sending test display with settings:") - print("Message: \(testMessage)") - print("Position: \(selectedPosition.displayName)") - print("Color: \(selectedColor.rawValue)") - print("Size: \(selectedSize.rawValue)") - print("Duration: \(duration)") - print("Bold: \(isBold)") - - dismiss() - } -} - -// MARK: - Extensions - -extension ConnectionState { - var statusDescription: String { - switch self { - case .disconnected: - return "Not connected" - case .scanning: - return "Scanning for devices..." - case .connecting: - return "Connecting..." - case .connected: - return "Connected and ready" - case .error(let error): - return "Error: \(error.localizedDescription)" - } - } - - var displayName: String { - switch self { - case .disconnected: - return "Disconnected" - case .scanning: - return "Scanning" - case .connecting: - return "Connecting" - case .connected: - return "Connected" - case .error: - return "Error" - } - } - - var indicatorColor: Color { - switch self { - case .disconnected: - return .gray - case .scanning, .connecting: - return .orange - case .connected: - return .green - case .error: - return .red - } - } - - var textColor: Color { - switch self { - case .error: - return .red - case .connected: - return .green - case .scanning, .connecting: - return .orange - default: - return .secondary - } - } -} - -extension HUDPosition { - var displayName: String { - switch (x, y) { - case (0.1, 0.1): - return "Top Left" - case (0.5, 0.1): - return "Top Center" - case (0.9, 0.1): - return "Top Right" - case (0.5, 0.5): - return "Center" - case (0.1, 0.9): - return "Bottom Left" - case (0.5, 0.9): - return "Bottom Center" - case (0.9, 0.9): - return "Bottom Right" - default: - return "Custom (\(Int(x*100)), \(Int(y*100)))" - } - } - - var description: String { - return "\(x)-\(y)-\(alignment.rawValue)-\(fontSize.rawValue)" - } -} - -extension Color { - init(_ hudColor: HUDColor) { - let rgb = hudColor.rgbValues - self.init(red: Double(rgb.r), green: Double(rgb.g), blue: Double(rgb.b)) - } -} - -#Preview { - GlassesView() - .environmentObject(AppCoordinator()) -} \ No newline at end of file diff --git a/Helix/UI/Views/HistoryView.swift b/Helix/UI/Views/HistoryView.swift deleted file mode 100644 index 87d0112..0000000 --- a/Helix/UI/Views/HistoryView.swift +++ /dev/null @@ -1,690 +0,0 @@ -import SwiftUI - -struct HistoryView: View { - @EnvironmentObject var coordinator: AppCoordinator - @State private var searchText = "" - @State private var selectedConversation: ConversationExport? - @State private var showingExportSheet = false - - // Mock conversation history for demo - @State private var conversationHistory: [ConversationExport] = [] - - var filteredConversations: [ConversationExport] { - if searchText.isEmpty { - return conversationHistory - } else { - return conversationHistory.filter { conversation in - conversation.messages.contains { message in - message.content.localizedCaseInsensitiveContains(searchText) - } - } - } - } - - var body: some View { - NavigationView { - VStack { - if conversationHistory.isEmpty { - EmptyHistoryView() - } else { - ConversationHistoryList( - conversations: filteredConversations, - selectedConversation: $selectedConversation, - showingExportSheet: $showingExportSheet - ) - } - } - .navigationTitle("History") - .searchable(text: $searchText, prompt: "Search conversations") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Menu { - Button("Export Current Session") { - exportCurrentSession() - } - .disabled(coordinator.currentConversation.isEmpty) - - Button("Clear History") { - clearHistory() - } - .disabled(conversationHistory.isEmpty) - - Button("Import Conversation") { - // TODO: Implement import - } - } label: { - Image(systemName: "ellipsis.circle") - } - } - } - } - .sheet(item: $selectedConversation) { conversation in - ConversationDetailView(conversation: conversation) - } - .sheet(isPresented: $showingExportSheet) { - ExportSheet() - } - .onAppear { - loadConversationHistory() - } - } - - private func loadConversationHistory() { - // Load from persistent storage or generate mock data - generateMockHistory() - } - - private func generateMockHistory() { - // Create mock conversation history for demo - let mockSpeakers = [ - Speaker(name: "You", isCurrentUser: true), - Speaker(name: "Alice", isCurrentUser: false), - Speaker(name: "Bob", isCurrentUser: false) - ] - - conversationHistory = (1...5).map { index in - let messages = (1...Int.random(in: 3...8)).map { messageIndex in - ConversationMessage( - content: "This is message \(messageIndex) from conversation \(index). It contains some sample content to demonstrate the conversation history feature.", - speakerId: mockSpeakers.randomElement()?.id, - confidence: Float.random(in: 0.7...0.95), - timestamp: Date().addingTimeInterval(-TimeInterval(index * 3600 + messageIndex * 60)).timeIntervalSince1970, - isFinal: true, - wordTimings: [], - originalText: "Original text \(messageIndex)" - ) - } - - let summary = ConversationSummary( - messageCount: messages.count, - speakerCount: mockSpeakers.count, - duration: TimeInterval(messages.count * 30), - averageConfidence: messages.map(\.confidence).reduce(0, +) / Float(messages.count), - startTime: messages.first?.timestamp ?? 0, - endTime: messages.last?.timestamp ?? 0 - ) - - return ConversationExport( - messages: messages, - speakers: mockSpeakers, - summary: summary, - exportDate: Date().addingTimeInterval(-TimeInterval(index * 3600)) - ) - } - } - - private func exportCurrentSession() { - guard !coordinator.currentConversation.isEmpty else { return } - - let export = coordinator.exportConversation() - conversationHistory.insert(export, at: 0) - showingExportSheet = true - } - - private func clearHistory() { - conversationHistory.removeAll() - } -} - -struct EmptyHistoryView: View { - var body: some View { - VStack(spacing: 24) { - Image(systemName: "clock.arrow.circlepath") - .font(.system(size: 60)) - .foregroundColor(.secondary) - - VStack(spacing: 8) { - Text("No Conversation History") - .font(.title2) - .fontWeight(.semibold) - - Text("Your past conversations will appear here. Start a new conversation to begin building your history.") - .font(.body) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .padding(.horizontal) - } - - VStack(spacing: 12) { - HistoryFeatureRow( - icon: "doc.text.magnifyingglass", - title: "Search Conversations", - description: "Find specific topics or keywords" - ) - - HistoryFeatureRow( - icon: "square.and.arrow.up", - title: "Export & Share", - description: "Save conversations for future reference" - ) - - HistoryFeatureRow( - icon: "chart.bar", - title: "Analytics", - description: "Track conversation patterns and insights" - ) - } - .padding() - } - } -} - -struct HistoryFeatureRow: View { - let icon: String - let title: String - let description: String - - var body: some View { - HStack(spacing: 12) { - Image(systemName: icon) - .font(.title3) - .foregroundColor(.blue) - .frame(width: 24) - - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.subheadline) - .fontWeight(.medium) - - Text(description) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - } -} - -struct ConversationHistoryList: View { - let conversations: [ConversationExport] - @Binding var selectedConversation: ConversationExport? - @Binding var showingExportSheet: Bool - - var body: some View { - List(conversations, id: \.exportDate) { conversation in - ConversationHistoryRow(conversation: conversation) - .onTapGesture { - selectedConversation = conversation - } - .swipeActions(edge: .trailing) { - Button("Export") { - selectedConversation = conversation - showingExportSheet = true - } - .tint(.blue) - - Button("Delete") { - deleteConversation(conversation) - } - .tint(.red) - } - } - .listStyle(.insetGrouped) - } - - private func deleteConversation(_ conversation: ConversationExport) { - // TODO: Implement deletion - print("Deleting conversation from \(conversation.exportDate)") - } -} - -struct ConversationHistoryRow: View { - let conversation: ConversationExport - - private var firstMessage: String { - conversation.messages.first?.content.prefix(80).appending("...") ?? "No content" - } - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(formatDate(conversation.exportDate)) - .font(.headline) - .fontWeight(.medium) - - Spacer() - - Text(formatDuration(conversation.summary.duration)) - .font(.caption) - .foregroundColor(.secondary) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color(.systemGray5)) - .cornerRadius(8) - } - - Text(String(firstMessage)) - .font(.body) - .foregroundColor(.secondary) - .lineLimit(2) - - HStack(spacing: 16) { - ConversationStat( - icon: "message", - value: "\(conversation.summary.messageCount)", - label: "messages" - ) - - ConversationStat( - icon: "person.2", - value: "\(conversation.summary.speakerCount)", - label: "speakers" - ) - - ConversationStat( - icon: "checkmark.circle", - value: "\(Int(conversation.summary.averageConfidence * 100))%", - label: "confidence" - ) - - Spacer() - } - } - .padding(.vertical, 4) - } - - private func formatDate(_ date: Date) -> String { - let formatter = DateFormatter() - if Calendar.current.isDateInToday(date) { - formatter.timeStyle = .short - return "Today at \(formatter.string(from: date))" - } else if Calendar.current.isDateInYesterday(date) { - formatter.timeStyle = .short - return "Yesterday at \(formatter.string(from: date))" - } else { - formatter.dateStyle = .medium - formatter.timeStyle = .short - return formatter.string(from: date) - } - } - - private func formatDuration(_ duration: TimeInterval) -> String { - let minutes = Int(duration) / 60 - let seconds = Int(duration) % 60 - - if minutes > 0 { - return "\(minutes)m \(seconds)s" - } else { - return "\(seconds)s" - } - } -} - -struct ConversationStat: View { - let icon: String - let value: String - let label: String - - var body: some View { - HStack(spacing: 4) { - Image(systemName: icon) - .font(.caption2) - .foregroundColor(.secondary) - - Text(value) - .font(.caption) - .fontWeight(.medium) - - Text(label) - .font(.caption2) - .foregroundColor(.secondary) - } - } -} - -struct ConversationDetailView: View { - let conversation: ConversationExport - @Environment(\.dismiss) private var dismiss - @State private var selectedTab = 0 - - var body: some View { - NavigationView { - TabView(selection: $selectedTab) { - ConversationMessagesView(conversation: conversation) - .tabItem { - Image(systemName: "message") - Text("Messages") - } - .tag(0) - - ConversationStatsView(conversation: conversation) - .tabItem { - Image(systemName: "chart.bar") - Text("Stats") - } - .tag(1) - - ConversationSpeakersView(conversation: conversation) - .tabItem { - Image(systemName: "person.2") - Text("Speakers") - } - .tag(2) - } - .navigationTitle("Conversation Details") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Close") { - dismiss() - } - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Export") { - exportConversation() - } - } - } - } - } - - private func exportConversation() { - // TODO: Implement export functionality - print("Exporting conversation details") - } -} - -struct ConversationMessagesView: View { - let conversation: ConversationExport - - var body: some View { - List(conversation.messages, id: \.id) { message in - MessageDetailRow( - message: message, - speaker: conversation.speakers.first { $0.id == message.speakerId } - ) - } - .listStyle(.plain) - } -} - -struct MessageDetailRow: View { - let message: ConversationMessage - let speaker: Speaker? - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(speaker?.name ?? "Unknown") - .font(.caption) - .fontWeight(.medium) - .foregroundColor(.secondary) - - Spacer() - - Text(formatTimestamp(message.timestamp)) - .font(.caption2) - .foregroundColor(.secondary) - } - - Text(message.content) - .font(.body) - - if message.confidence > 0 { - HStack { - Text("Confidence:") - .font(.caption2) - .foregroundColor(.secondary) - - ConfidenceIndicator(confidence: message.confidence) - - Spacer() - } - } - } - .padding(.vertical, 4) - } - - private func formatTimestamp(_ timestamp: TimeInterval) -> String { - let date = Date(timeIntervalSince1970: timestamp) - let formatter = DateFormatter() - formatter.timeStyle = .medium - return formatter.string(from: date) - } -} - -struct ConversationStatsView: View { - let conversation: ConversationExport - - var body: some View { - List { - Section("Overview") { - StatRow(title: "Duration", value: formatDuration(conversation.summary.duration)) - StatRow(title: "Messages", value: "\(conversation.summary.messageCount)") - StatRow(title: "Speakers", value: "\(conversation.summary.speakerCount)") - StatRow(title: "Average Confidence", value: "\(Int(conversation.summary.averageConfidence * 100))%") - } - - Section("Timeline") { - StatRow(title: "Start Time", value: formatDate(Date(timeIntervalSince1970: conversation.summary.startTime))) - StatRow(title: "End Time", value: formatDate(Date(timeIntervalSince1970: conversation.summary.endTime))) - StatRow(title: "Export Date", value: formatDate(conversation.exportDate)) - } - - Section("Message Distribution") { - ForEach(messagesPerSpeaker, id: \.speakerId) { stat in - HStack { - Text(stat.speakerName) - - Spacer() - - Text("\(stat.messageCount) messages") - .foregroundColor(.secondary) - } - } - } - } - } - - private var messagesPerSpeaker: [SpeakerMessageStat] { - let speakerMessageCounts = Dictionary(grouping: conversation.messages) { $0.speakerId } - .mapValues { $0.count } - - return conversation.speakers.map { speaker in - SpeakerMessageStat( - speakerId: speaker.id, - speakerName: speaker.name ?? "Unknown", - messageCount: speakerMessageCounts[speaker.id] ?? 0 - ) - } - .sorted { $0.messageCount > $1.messageCount } - } - - private func formatDuration(_ duration: TimeInterval) -> String { - let hours = Int(duration) / 3600 - let minutes = (Int(duration) % 3600) / 60 - let seconds = Int(duration) % 60 - - if hours > 0 { - return String(format: "%dh %dm %ds", hours, minutes, seconds) - } else if minutes > 0 { - return String(format: "%dm %ds", minutes, seconds) - } else { - return String(format: "%ds", seconds) - } - } - - private func formatDate(_ date: Date) -> String { - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .medium - return formatter.string(from: date) - } -} - -struct SpeakerMessageStat { - let speakerId: UUID - let speakerName: String - let messageCount: Int -} - -struct StatRow: View { - let title: String - let value: String - - var body: some View { - HStack { - Text(title) - Spacer() - Text(value) - .foregroundColor(.secondary) - } - } -} - -struct ConversationSpeakersView: View { - let conversation: ConversationExport - - var body: some View { - List(conversation.speakers, id: \.id) { speaker in - SpeakerDetailRow(speaker: speaker, conversation: conversation) - } - } -} - -struct SpeakerDetailRow: View { - let speaker: Speaker - let conversation: ConversationExport - - private var speakerMessages: [ConversationMessage] { - conversation.messages.filter { $0.speakerId == speaker.id } - } - - private var averageConfidence: Float { - let confidences = speakerMessages.map { $0.confidence } - return confidences.isEmpty ? 0 : confidences.reduce(0, +) / Float(confidences.count) - } - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(speaker.name ?? "Unknown Speaker") - .font(.headline) - - Spacer() - - if speaker.isCurrentUser { - Text("You") - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue.opacity(0.2)) - .foregroundColor(.blue) - .cornerRadius(8) - } - } - - HStack(spacing: 16) { - SpeakerStat( - title: "Messages", - value: "\(speakerMessages.count)" - ) - - SpeakerStat( - title: "Confidence", - value: "\(Int(averageConfidence * 100))%" - ) - - SpeakerStat( - title: "Words", - value: "\(totalWords)" - ) - } - } - .padding(.vertical, 4) - } - - private var totalWords: Int { - speakerMessages.reduce(0) { total, message in - total + message.content.components(separatedBy: .whitespacesAndNewlines).count - } - } -} - -struct SpeakerStat: View { - let title: String - let value: String - - var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -struct ExportSheet: View { - @Environment(\.dismiss) private var dismiss - @State private var selectedFormat = ExportFormat.json - @State private var includeAnalysis = true - @State private var includeTimestamps = true - - enum ExportFormat: String, CaseIterable { - case json = "JSON" - case csv = "CSV" - case txt = "Text" - case pdf = "PDF" - } - - var body: some View { - NavigationView { - Form { - Section("Export Format") { - Picker("Format", selection: $selectedFormat) { - ForEach(ExportFormat.allCases, id: \.self) { format in - Text(format.rawValue).tag(format) - } - } - .pickerStyle(.segmented) - } - - Section("Options") { - Toggle("Include Analysis Results", isOn: $includeAnalysis) - Toggle("Include Timestamps", isOn: $includeTimestamps) - } - - Section("Preview") { - Text("The exported file will contain conversation messages, speaker information, and metadata in \(selectedFormat.rawValue) format.") - .font(.caption) - .foregroundColor(.secondary) - } - } - .navigationTitle("Export Conversation") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Export") { - performExport() - } - } - } - } - } - - private func performExport() { - // TODO: Implement actual export functionality - print("Exporting in \(selectedFormat.rawValue) format") - print("Include analysis: \(includeAnalysis)") - print("Include timestamps: \(includeTimestamps)") - - dismiss() - } -} - -#Preview { - HistoryView() - .environmentObject(AppCoordinator()) -} \ No newline at end of file diff --git a/Helix/UI/Views/MainTabView.swift b/Helix/UI/Views/MainTabView.swift deleted file mode 100644 index 88c64ed..0000000 --- a/Helix/UI/Views/MainTabView.swift +++ /dev/null @@ -1,51 +0,0 @@ -import SwiftUI - -struct MainTabView: View { - @EnvironmentObject var coordinator: AppCoordinator - @State private var selectedTab = 0 - - var body: some View { - TabView(selection: $selectedTab) { - ConversationView() - .tabItem { - Image(systemName: "waveform.circle") - Text("Conversation") - } - .tag(0) - - AnalysisView() - .tabItem { - Image(systemName: "brain.head.profile") - Text("Analysis") - } - .tag(1) - - GlassesView() - .tabItem { - Image(systemName: "eyeglasses") - Text("Glasses") - } - .tag(2) - - HistoryView() - .tabItem { - Image(systemName: "clock.arrow.circlepath") - Text("History") - } - .tag(3) - - SettingsView() - .tabItem { - Image(systemName: "gearshape") - Text("Settings") - } - .tag(4) - } - .tint(.blue) - } -} - -#Preview { - MainTabView() - .environmentObject(AppCoordinator()) -} \ No newline at end of file diff --git a/Helix/UI/Views/SettingsView.swift b/Helix/UI/Views/SettingsView.swift deleted file mode 100644 index 2531631..0000000 --- a/Helix/UI/Views/SettingsView.swift +++ /dev/null @@ -1,538 +0,0 @@ -import SwiftUI - -struct SettingsView: View { - @EnvironmentObject var coordinator: AppCoordinator - @State private var settings: AppSettings = .default - @State private var showingAPIKeySheet = false - @State private var showingAboutSheet = false - - var body: some View { - NavigationView { - Form { - APIKeysSection( - settings: $settings, - showingAPIKeySheet: $showingAPIKeySheet - ) - - AudioSection(settings: $settings) - - AnalysisSection(settings: $settings) - - GlassesSection(settings: $settings) - - PrivacySection(settings: $settings) - - AboutSection(showingAboutSheet: $showingAboutSheet) - } - .navigationTitle("Settings") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Reset") { - resetSettings() - } - } - } - } - .sheet(isPresented: $showingAPIKeySheet) { - APIKeySheet(settings: $settings) - } - .sheet(isPresented: $showingAboutSheet) { - AboutSheet() - } - .onAppear { - settings = coordinator.settings - } - .onChange(of: settings) { newSettings in - coordinator.updateSettings(newSettings) - } - } - - private func resetSettings() { - settings = .default - } -} - -struct APIKeysSection: View { - @Binding var settings: AppSettings - @Binding var showingAPIKeySheet: Bool - - var body: some View { - Section("AI Services") { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("OpenAI API Key") - .font(.body) - - Text(settings.openAIKey.isEmpty ? "Not configured" : "Configured") - .font(.caption) - .foregroundColor(settings.openAIKey.isEmpty ? .red : .green) - } - - Spacer() - - Button("Configure") { - showingAPIKeySheet = true - } - .buttonStyle(.bordered) - } - - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("Anthropic API Key") - .font(.body) - - Text(settings.anthropicKey.isEmpty ? "Not configured" : "Configured") - .font(.caption) - .foregroundColor(settings.anthropicKey.isEmpty ? .red : .green) - } - - Spacer() - - Button("Configure") { - showingAPIKeySheet = true - } - .buttonStyle(.bordered) - } - } - } -} - -struct AudioSection: View { - @Binding var settings: AppSettings - - var body: some View { - Section("Audio Processing") { - VStack(alignment: .leading, spacing: 8) { - Text("Voice Sensitivity") - .font(.caption) - .foregroundColor(.secondary) - - HStack { - Text("Low") - .font(.caption) - - Slider(value: $settings.voiceSensitivity, in: 0.1...1.0) - - Text("High") - .font(.caption) - } - } - - VStack(alignment: .leading, spacing: 8) { - Text("Noise Reduction") - .font(.caption) - .foregroundColor(.secondary) - - HStack { - Text("Off") - .font(.caption) - - Slider(value: $settings.noiseReductionLevel, in: 0.0...1.0) - - Text("Max") - .font(.caption) - } - } - - Picker("Primary Language", selection: $settings.primaryLanguage) { - Text("English (US)").tag(Locale(identifier: "en-US") as Locale?) - Text("English (UK)").tag(Locale(identifier: "en-GB") as Locale?) - Text("Spanish").tag(Locale(identifier: "es") as Locale?) - Text("French").tag(Locale(identifier: "fr") as Locale?) - Text("German").tag(Locale(identifier: "de") as Locale?) - } - } - } -} - -struct AnalysisSection: View { - @Binding var settings: AppSettings - - var body: some View { - Section("AI Analysis") { - Toggle("Fact Checking", isOn: $settings.enableFactChecking) - - Toggle("Auto Summary", isOn: $settings.enableAutoSummary) - - Toggle("Action Items", isOn: $settings.enableActionItems) - - Picker("Fact-Check Sensitivity", selection: $settings.factCheckSeverityFilter) { - Text("All Claims").tag(FactCheckResult.FactCheckSeverity.minor) - Text("Significant Claims").tag(FactCheckResult.FactCheckSeverity.significant) - Text("Critical Only").tag(FactCheckResult.FactCheckSeverity.critical) - } - - HStack { - Text("Max History") - Spacer() - Stepper("\(settings.maxConversationHistory) messages", - value: $settings.maxConversationHistory, - in: 50...500, - step: 50) - } - } - } -} - -struct GlassesSection: View { - @Binding var settings: AppSettings - - var body: some View { - Section("Glasses Display") { - Toggle("Auto-connect on startup", isOn: $settings.glassesAutoConnect) - - VStack(alignment: .leading, spacing: 8) { - Text("Display Brightness") - .font(.caption) - .foregroundColor(.secondary) - - HStack { - Image(systemName: "sun.min") - .foregroundColor(.secondary) - - Slider(value: $settings.displayBrightness, in: 0.1...1.0) - - Image(systemName: "sun.max") - .foregroundColor(.secondary) - } - } - } - } -} - -struct PrivacySection: View { - @Binding var settings: AppSettings - - var body: some View { - Section("Privacy & Data") { - Toggle("Privacy Mode", isOn: $settings.privacyMode) - - Toggle("Auto Export", isOn: $settings.autoExport) - - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("Data Storage") - .font(.body) - - Text("All data is stored locally on your device") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Manage") { - // TODO: Implement data management - } - .buttonStyle(.bordered) - } - - Button("Clear All Data") { - clearAllData() - } - .foregroundColor(.red) - } - } - - private func clearAllData() { - // TODO: Implement data clearing - print("Clearing all data") - } -} - -struct AboutSection: View { - @Binding var showingAboutSheet: Bool - - var body: some View { - Section("About") { - HStack { - Text("Version") - Spacer() - Text("1.0.0") - .foregroundColor(.secondary) - } - - Button("About Helix") { - showingAboutSheet = true - } - - Button("Privacy Policy") { - openPrivacyPolicy() - } - - Button("Terms of Service") { - openTermsOfService() - } - - Button("Support") { - openSupport() - } - } - } - - private func openPrivacyPolicy() { - // TODO: Open privacy policy - print("Opening privacy policy") - } - - private func openTermsOfService() { - // TODO: Open terms of service - print("Opening terms of service") - } - - private func openSupport() { - // TODO: Open support - print("Opening support") - } -} - -struct APIKeySheet: View { - @Binding var settings: AppSettings - @Environment(\.dismiss) private var dismiss - @State private var openAIKey = "" - @State private var anthropicKey = "" - @State private var showingOpenAIKey = false - @State private var showingAnthropicKey = false - - var body: some View { - NavigationView { - Form { - Section("OpenAI") { - VStack(alignment: .leading, spacing: 8) { - HStack { - if showingOpenAIKey { - TextField("sk-...", text: $openAIKey) - .textFieldStyle(.roundedBorder) - .autocorrectionDisabled() - } else { - SecureField("sk-...", text: $openAIKey) - .textFieldStyle(.roundedBorder) - } - - Button(action: { - showingOpenAIKey.toggle() - }) { - Image(systemName: showingOpenAIKey ? "eye.slash" : "eye") - } - } - - Text("Get your API key from platform.openai.com") - .font(.caption) - .foregroundColor(.secondary) - } - } - - Section("Anthropic") { - VStack(alignment: .leading, spacing: 8) { - HStack { - if showingAnthropicKey { - TextField("sk-ant-...", text: $anthropicKey) - .textFieldStyle(.roundedBorder) - .autocorrectionDisabled() - } else { - SecureField("sk-ant-...", text: $anthropicKey) - .textFieldStyle(.roundedBorder) - } - - Button(action: { - showingAnthropicKey.toggle() - }) { - Image(systemName: showingAnthropicKey ? "eye.slash" : "eye") - } - } - - Text("Get your API key from console.anthropic.com") - .font(.caption) - .foregroundColor(.secondary) - } - } - - Section { - VStack(alignment: .leading, spacing: 8) { - Text("Security Notice") - .font(.headline) - .foregroundColor(.orange) - - Text("API keys are stored securely in your device's keychain and are never transmitted except to the respective AI service providers.") - .font(.caption) - .foregroundColor(.secondary) - } - } - } - .navigationTitle("API Keys") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Cancel") { - dismiss() - } - } - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - saveAPIKeys() - } - } - } - } - .onAppear { - openAIKey = settings.openAIKey - anthropicKey = settings.anthropicKey - } - } - - private func saveAPIKeys() { - settings.openAIKey = openAIKey - settings.anthropicKey = anthropicKey - dismiss() - } -} - -struct AboutSheet: View { - @Environment(\.dismiss) private var dismiss - - var body: some View { - NavigationView { - ScrollView { - VStack(spacing: 24) { - // App Icon and Title - VStack(spacing: 16) { - Image(systemName: "brain.head.profile") - .font(.system(size: 80)) - .foregroundColor(.blue) - - VStack(spacing: 4) { - Text("Helix") - .font(.largeTitle) - .fontWeight(.bold) - - Text("AI-Powered Conversation Analysis") - .font(.headline) - .foregroundColor(.secondary) - } - } - - // Description - VStack(alignment: .leading, spacing: 16) { - Text("About Helix") - .font(.title2) - .fontWeight(.semibold) - - Text("Helix is an advanced conversation analysis tool that works with Even Realities smart glasses to provide real-time AI-powered insights, fact-checking, and conversation intelligence.") - .font(.body) - - Text("Features include:") - .font(.headline) - .padding(.top) - - VStack(alignment: .leading, spacing: 8) { - FeatureBullet(text: "Real-time speech recognition and transcription") - FeatureBullet(text: "AI-powered fact-checking with source attribution") - FeatureBullet(text: "Automatic conversation summarization") - FeatureBullet(text: "Action item extraction and tracking") - FeatureBullet(text: "Speaker identification and diarization") - FeatureBullet(text: "Smart glasses HUD integration") - FeatureBullet(text: "Privacy-first data handling") - } - } - - // Technical Details - VStack(alignment: .leading, spacing: 12) { - Text("Technical Information") - .font(.title3) - .fontWeight(.semibold) - - TechnicalDetail(title: "Version", value: "1.0.0") - TechnicalDetail(title: "Build", value: "2025.01.01") - TechnicalDetail(title: "Platform", value: "iOS 16.0+") - TechnicalDetail(title: "AI Models", value: "OpenAI GPT-4, Anthropic Claude") - TechnicalDetail(title: "Audio Processing", value: "16kHz real-time pipeline") - } - - // Privacy Notice - VStack(alignment: .leading, spacing: 12) { - Text("Privacy & Security") - .font(.title3) - .fontWeight(.semibold) - - Text("Helix prioritizes your privacy:") - .font(.body) - - VStack(alignment: .leading, spacing: 6) { - PrivacyBullet(text: "All conversations are processed locally when possible") - PrivacyBullet(text: "Data is encrypted and stored securely on your device") - PrivacyBullet(text: "No conversation data is stored on our servers") - PrivacyBullet(text: "API keys are protected in the device keychain") - PrivacyBullet(text: "You control all data sharing and export") - } - } - } - .padding() - } - .navigationTitle("About") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - dismiss() - } - } - } - } - } -} - -struct FeatureBullet: View { - let text: String - - var body: some View { - HStack(alignment: .top, spacing: 8) { - Text("•") - .foregroundColor(.blue) - .fontWeight(.bold) - - Text(text) - .font(.body) - } - } -} - -struct TechnicalDetail: View { - let title: String - let value: String - - var body: some View { - HStack { - Text(title) - .font(.body) - .foregroundColor(.secondary) - - Spacer() - - Text(value) - .font(.body) - .fontWeight(.medium) - } - } -} - -struct PrivacyBullet: View { - let text: String - - var body: some View { - HStack(alignment: .top, spacing: 8) { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - .font(.caption) - - Text(text) - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -#Preview { - SettingsView() - .environmentObject(AppCoordinator()) -} \ No newline at end of file diff --git a/HelixTests/AppCoordinatorTests.swift b/HelixTests/AppCoordinatorTests.swift deleted file mode 100644 index f155a1d..0000000 --- a/HelixTests/AppCoordinatorTests.swift +++ /dev/null @@ -1,365 +0,0 @@ -import XCTest -import Combine -@testable import Helix - -@MainActor -class AppCoordinatorTests: XCTestCase { - var coordinator: AppCoordinator! - var cancellables: Set! - - override func setUpWithError() throws { - try super.setUpWithError() - coordinator = AppCoordinator() - cancellables = Set() - } - - override func tearDownWithError() throws { - coordinator = nil - cancellables = nil - try super.tearDownWithError() - } - - func testAppCoordinatorInitialization() { - XCTAssertNotNil(coordinator) - XCTAssertFalse(coordinator.isRecording) - XCTAssertEqual(coordinator.connectionState, .disconnected) - XCTAssertEqual(coordinator.batteryLevel, 0.0) - XCTAssertTrue(coordinator.currentConversation.isEmpty) - XCTAssertTrue(coordinator.recentAnalysis.isEmpty) - XCTAssertFalse(coordinator.speakers.isEmpty) // Should have default current user - XCTAssertFalse(coordinator.isProcessing) - XCTAssertNil(coordinator.errorMessage) - } - - func testStartStopConversation() { - // Test starting conversation - XCTAssertFalse(coordinator.isRecording) - XCTAssertFalse(coordinator.isProcessing) - - coordinator.startConversation() - - XCTAssertTrue(coordinator.isRecording) - XCTAssertTrue(coordinator.isProcessing) - XCTAssertTrue(coordinator.currentConversation.isEmpty) - - // Test stopping conversation - coordinator.stopConversation() - - XCTAssertFalse(coordinator.isRecording) - XCTAssertFalse(coordinator.isProcessing) - } - - func testMultipleStartConversationCalls() { - // First call should work - coordinator.startConversation() - XCTAssertTrue(coordinator.isRecording) - - // Second call should not change state - coordinator.startConversation() - XCTAssertTrue(coordinator.isRecording) - - coordinator.stopConversation() - } - - func testStopConversationWhenNotRecording() { - XCTAssertFalse(coordinator.isRecording) - - // Should not crash or change state - coordinator.stopConversation() - XCTAssertFalse(coordinator.isRecording) - XCTAssertFalse(coordinator.isProcessing) - } - - func testSpeakerManagement() { - let initialSpeakerCount = coordinator.speakers.count - - // Add a new speaker - coordinator.addSpeaker(name: "Test Speaker", isCurrentUser: false) - - XCTAssertEqual(coordinator.speakers.count, initialSpeakerCount + 1) - - let addedSpeaker = coordinator.speakers.last - XCTAssertEqual(addedSpeaker?.name, "Test Speaker") - XCTAssertFalse(addedSpeaker?.isCurrentUser ?? true) - } - - func testCurrentUserSpeaker() { - // Should have a default current user speaker - let currentUserSpeakers = coordinator.speakers.filter { $0.isCurrentUser } - XCTAssertEqual(currentUserSpeakers.count, 1) - XCTAssertEqual(currentUserSpeakers.first?.name, "You") - } - - func testClearConversation() { - // Add some mock data - coordinator.addSpeaker(name: "Test Speaker") - - // Simulate having conversation data - let initialSpeakersCount = coordinator.speakers.count - - coordinator.clearConversation() - - XCTAssertTrue(coordinator.currentConversation.isEmpty) - XCTAssertTrue(coordinator.recentAnalysis.isEmpty) - - // Speakers should remain - XCTAssertEqual(coordinator.speakers.count, initialSpeakersCount) - } - - func testExportConversation() { - let export = coordinator.exportConversation() - - XCTAssertNotNil(export) - XCTAssertEqual(export.messages.count, coordinator.currentConversation.count) - XCTAssertFalse(export.speakers.isEmpty) - XCTAssertNotNil(export.summary) - } - - func testSettingsUpdate() { - var newSettings = coordinator.settings - newSettings.enableFactChecking = false - newSettings.primaryLanguage = Locale(identifier: "es-ES") - - coordinator.updateSettings(newSettings) - - XCTAssertEqual(coordinator.settings.enableFactChecking, false) - XCTAssertEqual(coordinator.settings.primaryLanguage?.identifier, "es-ES") - } - - func testConversationMetrics() { - XCTAssertEqual(coordinator.conversationDuration, 0) - XCTAssertEqual(coordinator.messageCount, 0) - XCTAssertEqual(coordinator.speakerCount, 0) - - // These would change if we had actual conversation data - // In a real test scenario, we would inject mock conversation messages - } - - func testIsConnectedToGlasses() { - XCTAssertFalse(coordinator.isConnectedToGlasses) - - // This would change if we simulated a glasses connection - // In a real test scenario, we would inject a mock glasses manager - } - - func testGlassesConnectionFlow() { - // Initial state - XCTAssertFalse(coordinator.isConnectedToGlasses) - XCTAssertEqual(coordinator.connectionState, .disconnected) - - // Note: In a real test, we would inject mock services - // to actually test the connection flow without real hardware - - coordinator.connectToGlasses() - - // Connection would be attempted (but may fail in test environment) - // The test validates that the method doesn't crash - } - - func testGlassesDisconnection() { - // Should not crash even if not connected - XCTAssertNoThrow(coordinator.disconnectFromGlasses()) - } - - func testErrorHandling() { - // Initial state should have no errors - XCTAssertNil(coordinator.errorMessage) - - // Error handling would be tested with mock services - // that can simulate various error conditions - } -} - -// MARK: - Integration Tests - -@MainActor -class AppCoordinatorIntegrationTests: XCTestCase { - var coordinator: AppCoordinator! - var cancellables: Set! - - override func setUpWithError() throws { - try super.setUpWithError() - coordinator = AppCoordinator() - cancellables = Set() - } - - override func tearDownWithError() throws { - coordinator = nil - cancellables = nil - try super.tearDownWithError() - } - - func testConversationWorkflow() { - let expectation = XCTestExpectation(description: "Conversation workflow should complete") - expectation.expectedFulfillmentCount = 3 - - // Monitor state changes - coordinator.$isRecording - .sink { isRecording in - print("Recording state changed: \(isRecording)") - expectation.fulfill() - } - .store(in: &cancellables) - - coordinator.$isProcessing - .sink { isProcessing in - print("Processing state changed: \(isProcessing)") - expectation.fulfill() - } - .store(in: &cancellables) - - // Start conversation - coordinator.startConversation() - - // Wait briefly then stop - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.coordinator.stopConversation() - expectation.fulfill() - } - - wait(for: [expectation], timeout: 5.0) - } - - func testSpeakerWorkflow() { - let expectation = XCTestExpectation(description: "Speaker workflow should complete") - - // Add speaker - coordinator.addSpeaker(name: "Integration Test Speaker", isCurrentUser: false) - - // Verify speaker was added - let addedSpeaker = coordinator.speakers.first { $0.name == "Integration Test Speaker" } - XCTAssertNotNil(addedSpeaker) - - expectation.fulfill() - wait(for: [expectation], timeout: 1.0) - } - - func testSettingsWorkflow() { - let expectation = XCTestExpectation(description: "Settings workflow should complete") - - let originalSettings = coordinator.settings - - // Update settings - var newSettings = originalSettings - newSettings.enableFactChecking = !originalSettings.enableFactChecking - newSettings.noiseReductionLevel = 0.8 - - coordinator.updateSettings(newSettings) - - // Verify settings were updated - XCTAssertEqual(coordinator.settings.enableFactChecking, newSettings.enableFactChecking) - XCTAssertEqual(coordinator.settings.noiseReductionLevel, 0.8, accuracy: 0.01) - - expectation.fulfill() - wait(for: [expectation], timeout: 1.0) - } -} - -// MARK: - Mock App Coordinator for UI Tests - -class MockAppCoordinator: ObservableObject { - @Published var isRecording = false - @Published var connectionState: ConnectionState = .disconnected - @Published var batteryLevel: Float = 0.75 - @Published var currentConversation: [ConversationMessage] = [] - @Published var recentAnalysis: [AnalysisResult] = [] - @Published var speakers: [Speaker] = [Speaker(name: "You", isCurrentUser: true)] - @Published var isProcessing = false - @Published var errorMessage: String? - @Published var settings = AppSettings.default - - func startConversation() { - isRecording = true - isProcessing = true - - // Simulate adding a message after a delay - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.addMockMessage() - } - } - - func stopConversation() { - isRecording = false - isProcessing = false - } - - func connectToGlasses() { - connectionState = .connecting - - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - self.connectionState = .connected - } - } - - func disconnectFromGlasses() { - connectionState = .disconnected - } - - func addSpeaker(name: String, isCurrentUser: Bool = false) { - let speaker = Speaker(name: name, isCurrentUser: isCurrentUser) - speakers.append(speaker) - } - - func clearConversation() { - currentConversation.removeAll() - recentAnalysis.removeAll() - } - - func exportConversation() -> ConversationExport { - let summary = ConversationSummary( - messageCount: currentConversation.count, - speakerCount: speakers.count, - duration: 300, - averageConfidence: 0.85, - startTime: Date().timeIntervalSince1970 - 300, - endTime: Date().timeIntervalSince1970 - ) - - return ConversationExport( - messages: currentConversation, - speakers: speakers, - summary: summary, - exportDate: Date() - ) - } - - func updateSettings(_ newSettings: AppSettings) { - settings = newSettings - } - - private func addMockMessage() { - let message = ConversationMessage( - content: "This is a mock conversation message for testing purposes.", - speakerId: speakers.first?.id, - confidence: 0.9, - timestamp: Date().timeIntervalSince1970, - isFinal: true, - wordTimings: [], - originalText: "This is a mock conversation message for testing purposes." - ) - - currentConversation.append(message) - isProcessing = false - } - - // Computed properties for compatibility - var isConnectedToGlasses: Bool { - connectionState.isConnected - } - - var conversationDuration: TimeInterval { - guard let first = currentConversation.first, - let last = currentConversation.last else { - return 0 - } - return last.timestamp - first.timestamp - } - - var messageCount: Int { - currentConversation.count - } - - var speakerCount: Int { - Set(currentConversation.compactMap { $0.speakerId }).count - } -} \ No newline at end of file diff --git a/HelixTests/AudioManagerTests.swift b/HelixTests/AudioManagerTests.swift deleted file mode 100644 index aba611a..0000000 --- a/HelixTests/AudioManagerTests.swift +++ /dev/null @@ -1,174 +0,0 @@ -import XCTest -import AVFoundation -import Combine -@testable import Helix - -class AudioManagerTests: XCTestCase { - var audioManager: AudioManager! - var cancellables: Set! - - override func setUpWithError() throws { - try super.setUpWithError() - audioManager = AudioManager() - cancellables = Set() - } - - override func tearDownWithError() throws { - audioManager = nil - cancellables = nil - try super.tearDownWithError() - } - - func testAudioManagerInitialization() { - XCTAssertNotNil(audioManager) - XCTAssertFalse(audioManager.isRecording) - } - - func testAudioConfiguration() throws { - XCTAssertNoThrow(try audioManager.configure(sampleRate: 16000, bufferDuration: 0.005)) - } - - func testStartStopRecording() throws { - // Test starting recording - XCTAssertNoThrow(try audioManager.startRecording()) - XCTAssertTrue(audioManager.isRecording) - - // Test stopping recording - audioManager.stopRecording() - XCTAssertFalse(audioManager.isRecording) - } - - func testAudioPublisherExists() { - let expectation = XCTestExpectation(description: "Audio publisher should exist") - - audioManager.audioPublisher - .sink( - receiveCompletion: { _ in }, - receiveValue: { audio in - XCTAssertNotNil(audio.buffer) - XCTAssertGreaterThan(audio.sampleRate, 0) - expectation.fulfill() - } - ) - .store(in: &cancellables) - - // Start recording to generate audio data - do { - try audioManager.startRecording() - - // Wait briefly for audio data - wait(for: [expectation], timeout: 2.0) - - audioManager.stopRecording() - } catch { - XCTFail("Failed to start recording: \(error)") - } - } - - func testMultipleStartRecordingCalls() throws { - // First call should succeed - XCTAssertNoThrow(try audioManager.startRecording()) - XCTAssertTrue(audioManager.isRecording) - - // Second call should not throw but should not change state - XCTAssertNoThrow(try audioManager.startRecording()) - XCTAssertTrue(audioManager.isRecording) - - audioManager.stopRecording() - } - - func testStopRecordingWhenNotRecording() { - XCTAssertFalse(audioManager.isRecording) - - // Should not crash or throw - XCTAssertNoThrow(audioManager.stopRecording()) - XCTAssertFalse(audioManager.isRecording) - } - - func testProcessedAudioProperties() throws { - let expectation = XCTestExpectation(description: "Audio should have expected properties") - expectation.expectedFulfillmentCount = 1 - - audioManager.audioPublisher - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Audio processing failed: \(error)") - } - }, - receiveValue: { audio in - XCTAssertGreaterThan(audio.duration, 0) - XCTAssertNotEqual(audio.id, UUID()) - XCTAssertEqual(audio.channelCount, 1) // Mono audio - XCTAssertEqual(audio.sampleRate, 16000, accuracy: 100) // Allow some tolerance - expectation.fulfill() - } - ) - .store(in: &cancellables) - - try audioManager.startRecording() - wait(for: [expectation], timeout: 3.0) - audioManager.stopRecording() - } -} - -// MARK: - Mock Audio Manager for Testing - -class MockAudioManager: AudioManagerProtocol { - private let audioSubject = PassthroughSubject() - private(set) var isRecording = false - private var configuredSampleRate: Double = 16000 - private var configuredBufferDuration: TimeInterval = 0.005 - - var audioPublisher: AnyPublisher { - audioSubject.eraseToAnyPublisher() - } - - func startRecording() throws { - guard !isRecording else { return } - isRecording = true - - // Simulate audio data - DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { - self.sendMockAudioData() - } - } - - func stopRecording() { - isRecording = false - } - - func configure(sampleRate: Double, bufferDuration: TimeInterval) throws { - configuredSampleRate = sampleRate - configuredBufferDuration = bufferDuration - } - - private func sendMockAudioData() { - guard isRecording else { return } - - // Create mock audio buffer - let format = AVAudioFormat(standardFormatWithSampleRate: configuredSampleRate, channels: 1)! - let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 1024)! - buffer.frameLength = 1024 - - let processedAudio = ProcessedAudio( - buffer: buffer, - timestamp: AVAudioFramePosition(Date().timeIntervalSince1970 * configuredSampleRate), - sampleRate: configuredSampleRate, - channelCount: 1 - ) - - audioSubject.send(processedAudio) - - // Continue sending data while recording - if isRecording { - DispatchQueue.global().asyncAfter(deadline: .now() + configuredBufferDuration) { - self.sendMockAudioData() - } - } - } - - func simulateError(_ error: AudioError) { - audioSubject.send(completion: .failure(error)) - } -} \ No newline at end of file diff --git a/HelixTests/GlassesManagerTests.swift b/HelixTests/GlassesManagerTests.swift deleted file mode 100644 index 69bd581..0000000 --- a/HelixTests/GlassesManagerTests.swift +++ /dev/null @@ -1,351 +0,0 @@ -import XCTest -import CoreBluetooth -import Combine -@testable import Helix - -class GlassesManagerTests: XCTestCase { - var glassesManager: MockGlassesManager! - var cancellables: Set! - - override func setUpWithError() throws { - try super.setUpWithError() - glassesManager = MockGlassesManager() - cancellables = Set() - } - - override func tearDownWithError() throws { - glassesManager = nil - cancellables = nil - try super.tearDownWithError() - } - - func testGlassesManagerInitialization() { - XCTAssertNotNil(glassesManager) - - let expectation = XCTestExpectation(description: "Initial state should be disconnected") - - glassesManager.connectionState - .sink { state in - XCTAssertEqual(state, .disconnected) - expectation.fulfill() - } - .store(in: &cancellables) - - wait(for: [expectation], timeout: 1.0) - } - - func testGlassesConnection() { - let expectation = XCTestExpectation(description: "Connection should succeed") - - // Monitor connection state changes - var stateChanges: [ConnectionState] = [] - - glassesManager.connectionState - .sink { state in - stateChanges.append(state) - if case .connected = state { - expectation.fulfill() - } - } - .store(in: &cancellables) - - glassesManager.connect() - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Connection failed: \(error)") - } - }, - receiveValue: { _ in - // Connection succeeded - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 5.0) - - // Verify state progression - XCTAssertTrue(stateChanges.contains(.scanning)) - XCTAssertTrue(stateChanges.contains(.connecting)) - XCTAssertTrue(stateChanges.contains(.connected)) - } - - func testDisplayText() { - let expectation = XCTestExpectation(description: "Display text should succeed") - - // First connect - glassesManager.simulateConnection() - - let testText = "Test message for glasses" - let position = HUDPosition.topCenter - - glassesManager.displayText(testText, at: position) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Display failed: \(error)") - } else { - expectation.fulfill() - } - }, - receiveValue: { _ in - // Display succeeded - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 2.0) - } - - func testDisplayContent() { - let expectation = XCTestExpectation(description: "Display content should succeed") - - glassesManager.simulateConnection() - - let content = HUDContent( - text: "Test HUD content", - style: HUDStyle.factCheck, - position: .topCenter, - duration: 5.0, - priority: .high - ) - - glassesManager.displayContent(content) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Display content failed: \(error)") - } else { - expectation.fulfill() - } - }, - receiveValue: { _ in - // Display succeeded - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 2.0) - } - - func testBatteryMonitoring() { - let expectation = XCTestExpectation(description: "Battery level should be received") - - glassesManager.simulateConnection() - - glassesManager.batteryLevel - .sink { level in - XCTAssertGreaterThanOrEqual(level, 0.0) - XCTAssertLessThanOrEqual(level, 1.0) - expectation.fulfill() - } - .store(in: &cancellables) - - glassesManager.startBatteryMonitoring() - glassesManager.simulateBatteryLevel(0.75) - - wait(for: [expectation], timeout: 2.0) - } - - func testDisplayCapabilities() { - let expectation = XCTestExpectation(description: "Display capabilities should be received") - - glassesManager.displayCapabilities - .sink { capabilities in - XCTAssertGreaterThan(capabilities.maxTextLength, 0) - XCTAssertGreaterThan(capabilities.maxConcurrentDisplays, 0) - XCTAssertFalse(capabilities.supportedPositions.isEmpty) - XCTAssertFalse(capabilities.supportedColors.isEmpty) - expectation.fulfill() - } - .store(in: &cancellables) - - wait(for: [expectation], timeout: 1.0) - } - - func testClearDisplay() { - glassesManager.simulateConnection() - - // This should not throw or crash - XCTAssertNoThrow(glassesManager.clearDisplay()) - } - - func testGestureCommands() { - glassesManager.simulateConnection() - - let gestures: [GestureCommand] = [.tap, .swipeLeft, .swipeRight, .dismiss] - - for gesture in gestures { - XCTAssertNoThrow(glassesManager.sendGestureCommand(gesture)) - } - } - - func testDisplaySettings() { - glassesManager.simulateConnection() - - let settings = DisplaySettings( - brightness: 0.8, - contrast: 0.9, - autoAdjustBrightness: true, - defaultPosition: .topCenter, - maxDisplayTime: 10.0, - enableAnimations: true - ) - - XCTAssertNoThrow(glassesManager.updateDisplaySettings(settings)) - } - - func testDisconnection() { - let expectation = XCTestExpectation(description: "Disconnection should complete") - - glassesManager.simulateConnection() - - glassesManager.connectionState - .sink { state in - if case .disconnected = state { - expectation.fulfill() - } - } - .store(in: &cancellables) - - glassesManager.disconnect() - - wait(for: [expectation], timeout: 2.0) - } - - func testConnectionFailure() { - let expectation = XCTestExpectation(description: "Connection failure should be handled") - - glassesManager.shouldFailConnection = true - - glassesManager.connect() - .sink( - receiveCompletion: { completion in - if case .failure = completion { - expectation.fulfill() - } - }, - receiveValue: { _ in - XCTFail("Connection should have failed") - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 3.0) - } -} - -// MARK: - Mock Glasses Manager - -class MockGlassesManager: GlassesManagerProtocol { - private let connectionStateSubject = CurrentValueSubject(.disconnected) - private let batteryLevelSubject = CurrentValueSubject(0.0) - private let displayCapabilitiesSubject = CurrentValueSubject(.default) - - var shouldFailConnection = false - var connectionDelay: TimeInterval = 1.0 - - var connectionState: AnyPublisher { - connectionStateSubject.eraseToAnyPublisher() - } - - var batteryLevel: AnyPublisher { - batteryLevelSubject.eraseToAnyPublisher() - } - - var displayCapabilities: AnyPublisher { - displayCapabilitiesSubject.eraseToAnyPublisher() - } - - func connect() -> AnyPublisher { - return Future { [weak self] promise in - guard let self = self else { - promise(.failure(.serviceUnavailable)) - return - } - - if self.shouldFailConnection { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.connectionStateSubject.send(.error(.deviceNotFound)) - promise(.failure(.deviceNotFound)) - } - return - } - - // Simulate connection process - self.connectionStateSubject.send(.scanning) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - self.connectionStateSubject.send(.connecting) - } - - DispatchQueue.main.asyncAfter(deadline: .now() + self.connectionDelay) { - self.connectionStateSubject.send(.connected) - promise(.success(())) - } - } - .eraseToAnyPublisher() - } - - func disconnect() { - connectionStateSubject.send(.disconnected) - } - - func displayText(_ text: String, at position: HUDPosition) -> AnyPublisher { - let content = HUDContent(text: text, position: position) - return displayContent(content) - } - - func displayContent(_ content: HUDContent) -> AnyPublisher { - return Future { promise in - // Simulate display processing - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - if self.connectionStateSubject.value.isConnected { - promise(.success(())) - } else { - promise(.failure(.connectionFailed)) - } - } - } - .eraseToAnyPublisher() - } - - func clearDisplay() { - // Simulate clearing display - print("Mock: Clearing display") - } - - func updateDisplaySettings(_ settings: DisplaySettings) { - // Simulate updating settings - print("Mock: Updating display settings") - } - - func sendGestureCommand(_ command: GestureCommand) { - // Simulate sending gesture command - print("Mock: Sending gesture command: \(command)") - } - - func startBatteryMonitoring() { - // Simulate starting battery monitoring - print("Mock: Starting battery monitoring") - } - - func stopBatteryMonitoring() { - // Simulate stopping battery monitoring - print("Mock: Stopping battery monitoring") - } - - // MARK: - Test Helper Methods - - func simulateConnection() { - connectionStateSubject.send(.connected) - } - - func simulateBatteryLevel(_ level: Float) { - batteryLevelSubject.send(level) - } - - func simulateError(_ error: GlassesError) { - connectionStateSubject.send(.error(error)) - } -} \ No newline at end of file diff --git a/HelixTests/HelixTests.swift b/HelixTests/HelixTests.swift deleted file mode 100644 index aaf9010..0000000 --- a/HelixTests/HelixTests.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// HelixTests.swift -// HelixTests -// - -import Testing -import XCTest -@testable import Helix - -struct HelixTests { - @Test func basicAppInitialization() async throws { - // Test that the app can initialize without crashing - let coordinator = AppCoordinator() - #expect(coordinator != nil) - } - - @Test func audioManagerCreation() async throws { - let audioManager = AudioManager() - #expect(audioManager != nil) - #expect(!audioManager.isRecording) - } - - @Test func speechRecognitionServiceCreation() async throws { - let speechService = SpeechRecognitionService() - #expect(speechService != nil) - #expect(!speechService.isRecognizing) - } - - @Test func glassesManagerCreation() async throws { - let glassesManager = GlassesManager() - #expect(glassesManager != nil) - } - - @Test func hudContentCreation() async throws { - let content = HUDContent( - text: "Test message", - style: HUDStyle(), - position: HUDPosition.topCenter - ) - - #expect(content.text == "Test message") - #expect(!content.id.isEmpty) - } - - @Test func conversationMessageCreation() async throws { - let message = ConversationMessage( - content: "Test conversation message", - speakerId: UUID(), - confidence: 0.9, - timestamp: Date().timeIntervalSince1970, - isFinal: true, - wordTimings: [], - originalText: "Test conversation message" - ) - - #expect(message.content == "Test conversation message") - #expect(message.confidence == 0.9) - #expect(message.isFinal == true) - } - - @Test func speakerCreation() async throws { - let speaker = Speaker(name: "Test Speaker", isCurrentUser: false) - - #expect(speaker.name == "Test Speaker") - #expect(speaker.isCurrentUser == false) - #expect(speaker.id != UUID()) // Should have a valid UUID - } - - @Test func appSettingsDefaults() async throws { - let settings = AppSettings.default - - #expect(settings.enableFactChecking == true) - #expect(settings.enableAutoSummary == true) - #expect(settings.primaryLanguage?.identifier == "en-US") - #expect(settings.noiseReductionLevel == 0.5) - } - - @Test func factCheckResultCreation() async throws { - let result = FactCheckResult( - claim: "Test claim", - isAccurate: true, - explanation: "Test explanation", - sources: [], - confidence: 0.85, - alternativeInfo: nil, - category: .general, - severity: .minor - ) - - #expect(result.claim == "Test claim") - #expect(result.isAccurate == true) - #expect(result.confidence == 0.85) - #expect(result.category == .general) - } - - @Test func analysisResultCreation() async throws { - let factCheck = FactCheckResult( - claim: "Test", - isAccurate: true, - explanation: "Explanation", - sources: [], - confidence: 0.8, - alternativeInfo: nil, - category: .general, - severity: .minor - ) - - let result = AnalysisResult( - type: .factCheck, - content: .factCheck(factCheck), - confidence: 0.8, - provider: .openai - ) - - #expect(result.type == .factCheck) - #expect(result.confidence == 0.8) - #expect(result.provider == .openai) - } - - @Test func hudPositionConstants() async throws { - #expect(HUDPosition.topCenter.x == 0.5) - #expect(HUDPosition.topCenter.y == 0.1) - #expect(HUDPosition.topCenter.alignment == .center) - - #expect(HUDPosition.topLeft.x == 0.1) - #expect(HUDPosition.topLeft.alignment == .left) - - #expect(HUDPosition.topRight.x == 0.9) - #expect(HUDPosition.topRight.alignment == .right) - } -} - -// MARK: - Integration Test Suite - -class HelixIntegrationTests: XCTestCase { - - func testCompleteSystemInitialization() { - let coordinator = AppCoordinator() - - XCTAssertNotNil(coordinator) - XCTAssertFalse(coordinator.isRecording) - XCTAssertEqual(coordinator.connectionState, .disconnected) - XCTAssertTrue(coordinator.currentConversation.isEmpty) - XCTAssertFalse(coordinator.speakers.isEmpty) // Should have default user - } - - func testAudioToTranscriptionPipeline() { - let audioManager = MockAudioManager() - let speechService = MockSpeechRecognitionService() - - XCTAssertNotNil(audioManager) - XCTAssertNotNil(speechService) - - // Test that services can be initialized together - XCTAssertFalse(audioManager.isRecording) - XCTAssertFalse(speechService.isRecognizing) - } - - func testLLMToGlassesPipeline() { - let llmService = LLMService(providers: [:]) - let glassesManager = MockGlassesManager() - - XCTAssertNotNil(llmService) - XCTAssertNotNil(glassesManager) - } - - func testEndToEndDataFlow() { - // This test validates that all the data structures - // can flow through the complete pipeline - - // 1. Create audio data - let format = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1)! - let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 1024)! - buffer.frameLength = 1024 - - let processedAudio = ProcessedAudio( - buffer: buffer, - timestamp: 0, - sampleRate: 16000, - channelCount: 1 - ) - XCTAssertNotNil(processedAudio) - - // 2. Create transcription result - let transcription = TranscriptionResult( - text: "Test transcription", - confidence: 0.9, - isFinal: true - ) - XCTAssertNotNil(transcription) - - // 3. Create conversation message - let message = ConversationMessage(from: transcription) - XCTAssertEqual(message.content, "Test transcription") - - // 4. Create analysis result - let factCheck = FactCheckResult( - claim: "Test claim", - isAccurate: true, - explanation: "Explanation", - sources: [], - confidence: 0.8, - alternativeInfo: nil, - category: .general, - severity: .minor - ) - - let analysis = AnalysisResult( - type: .factCheck, - content: .factCheck(factCheck), - confidence: 0.8 - ) - XCTAssertNotNil(analysis) - - // 5. Create HUD content - let hudContent = HUDContentFactory.createFactCheckDisplay(factCheck) - XCTAssertNotNil(hudContent) - XCTAssertFalse(hudContent.text.isEmpty) - } -} diff --git a/HelixTests/LLMServiceTests.swift b/HelixTests/LLMServiceTests.swift deleted file mode 100644 index ac0adc9..0000000 --- a/HelixTests/LLMServiceTests.swift +++ /dev/null @@ -1,393 +0,0 @@ -import XCTest -import Combine -@testable import Helix - -class LLMServiceTests: XCTestCase { - var llmService: LLMService! - var mockOpenAIProvider: MockLLMProvider! - var mockAnthropicProvider: MockLLMProvider! - var cancellables: Set! - - override func setUpWithError() throws { - try super.setUpWithError() - - mockOpenAIProvider = MockLLMProvider(provider: .openai) - mockAnthropicProvider = MockLLMProvider(provider: .anthropic) - - llmService = LLMService( - providers: [ - .openai: mockOpenAIProvider, - .anthropic: mockAnthropicProvider - ] - ) - - cancellables = Set() - } - - override func tearDownWithError() throws { - llmService = nil - mockOpenAIProvider = nil - mockAnthropicProvider = nil - cancellables = nil - try super.tearDownWithError() - } - - func testFactCheckingService() { - let expectation = XCTestExpectation(description: "Fact checking should complete") - - let claim = "The United States has 50 states" - - llmService.factCheck(claim, context: nil) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Fact checking failed: \(error)") - } - }, - receiveValue: { result in - XCTAssertEqual(result.claim, claim) - XCTAssertTrue(result.isAccurate) - XCTAssertGreaterThan(result.confidence, 0.5) - XCTAssertNotNil(result.explanation) - expectation.fulfill() - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 5.0) - } - - func testConversationSummarization() { - let expectation = XCTestExpectation(description: "Summarization should complete") - - let messages = createMockConversationMessages() - - llmService.summarizeConversation(messages) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Summarization failed: \(error)") - } - }, - receiveValue: { summary in - XCTAssertFalse(summary.isEmpty) - XCTAssertLessThan(summary.count, 500) // Summary should be concise - expectation.fulfill() - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 5.0) - } - - func testClaimDetection() { - let expectation = XCTestExpectation(description: "Claim detection should complete") - - let text = "The Earth has a population of 8 billion people. Water boils at 100 degrees Celsius." - - llmService.detectClaims(in: text) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Claim detection failed: \(error)") - } - }, - receiveValue: { claims in - XCTAssertGreaterThan(claims.count, 0) - - for claim in claims { - XCTAssertFalse(claim.text.isEmpty) - XCTAssertGreaterThan(claim.confidence, 0.0) - XCTAssertLessThanOrEqual(claim.confidence, 1.0) - } - - expectation.fulfill() - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 5.0) - } - - func testActionItemExtraction() { - let expectation = XCTestExpectation(description: "Action item extraction should complete") - - let messages = createMockActionItemMessages() - - llmService.extractActionItems(from: messages) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Action item extraction failed: \(error)") - } - }, - receiveValue: { actionItems in - XCTAssertGreaterThan(actionItems.count, 0) - - for item in actionItems { - XCTAssertFalse(item.description.isEmpty) - XCTAssertNotNil(item.id) - } - - expectation.fulfill() - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 5.0) - } - - func testConversationAnalysis() { - let expectation = XCTestExpectation(description: "Conversation analysis should complete") - - let context = createMockConversationContext() - - llmService.analyzeConversation(context) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Conversation analysis failed: \(error)") - } - }, - receiveValue: { result in - XCTAssertEqual(result.type, context.analysisType) - XCTAssertGreaterThan(result.confidence, 0.0) - XCTAssertLessThanOrEqual(result.confidence, 1.0) - XCTAssertNotNil(result.content) - expectation.fulfill() - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 5.0) - } - - func testProviderFailover() { - let expectation = XCTestExpectation(description: "Provider failover should work") - - // Make the primary provider fail - mockOpenAIProvider.shouldFail = true - - let context = createMockConversationContext() - - llmService.analyzeConversation(context) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - XCTFail("Analysis should succeed with failover: \(error)") - } - }, - receiveValue: { result in - // Should succeed with Anthropic provider - XCTAssertEqual(result.provider, .anthropic) - expectation.fulfill() - } - ) - .store(in: &cancellables) - - wait(for: [expectation], timeout: 5.0) - } - - func testRateLimiting() { - let expectation = XCTestExpectation(description: "Rate limiting should work") - expectation.expectedFulfillmentCount = 5 - - // Send multiple rapid requests - for _ in 0..<5 { - let context = createMockConversationContext() - - llmService.analyzeConversation(context) - .sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - // Some requests might be rate limited - if case .rateLimitExceeded = error { - expectation.fulfill() - } - } - }, - receiveValue: { _ in - expectation.fulfill() - } - ) - .store(in: &cancellables) - } - - wait(for: [expectation], timeout: 10.0) - } - - // MARK: - Helper Methods - - private func createMockConversationMessages() -> [ConversationMessage] { - let speaker1 = UUID() - let speaker2 = UUID() - - return [ - ConversationMessage( - content: "Let's discuss the quarterly results.", - speakerId: speaker1, - confidence: 0.9, - timestamp: Date().timeIntervalSince1970 - 300, - isFinal: true, - wordTimings: [], - originalText: "Let's discuss the quarterly results." - ), - ConversationMessage( - content: "Revenue increased by 15% this quarter.", - speakerId: speaker2, - confidence: 0.85, - timestamp: Date().timeIntervalSince1970 - 250, - isFinal: true, - wordTimings: [], - originalText: "Revenue increased by 15% this quarter." - ), - ConversationMessage( - content: "That's excellent news! What drove the growth?", - speakerId: speaker1, - confidence: 0.92, - timestamp: Date().timeIntervalSince1970 - 200, - isFinal: true, - wordTimings: [], - originalText: "That's excellent news! What drove the growth?" - ) - ] - } - - private func createMockActionItemMessages() -> [ConversationMessage] { - return [ - ConversationMessage( - content: "We need to follow up with the client by Friday.", - speakerId: UUID(), - confidence: 0.9, - timestamp: Date().timeIntervalSince1970, - isFinal: true, - wordTimings: [], - originalText: "We need to follow up with the client by Friday." - ), - ConversationMessage( - content: "Please send me the report after the meeting.", - speakerId: UUID(), - confidence: 0.88, - timestamp: Date().timeIntervalSince1970, - isFinal: true, - wordTimings: [], - originalText: "Please send me the report after the meeting." - ) - ] - } - - private func createMockConversationContext() -> ConversationContext { - let messages = createMockConversationMessages() - let speakers = [ - Speaker(name: "Alice", isCurrentUser: false), - Speaker(name: "Bob", isCurrentUser: true) - ] - - return ConversationContext( - messages: messages, - speakers: speakers, - analysisType: .factCheck - ) - } -} - -// MARK: - Mock LLM Provider - -class MockLLMProvider: LLMProviderProtocol { - let provider: LLMProvider - var shouldFail = false - var delay: TimeInterval = 0.5 - - init(provider: LLMProvider) { - self.provider = provider - } - - func analyze(_ context: ConversationContext) -> AnyPublisher { - return Future { [weak self] promise in - guard let self = self else { - promise(.failure(.serviceUnavailable)) - return - } - - DispatchQueue.global().asyncAfter(deadline: .now() + self.delay) { - if self.shouldFail { - promise(.failure(.networkError(URLError(.networkConnectionLost)))) - return - } - - let result = self.createMockAnalysisResult(for: context) - promise(.success(result)) - } - } - .eraseToAnyPublisher() - } - - func isAvailable() -> Bool { - return !shouldFail - } - - func estimateCost(for context: ConversationContext) -> Float { - return 0.01 // Mock cost - } - - private func createMockAnalysisResult(for context: ConversationContext) -> AnalysisResult { - let content: AnalysisContent - - switch context.analysisType { - case .factCheck: - let factCheckResult = FactCheckResult( - claim: "Mock claim", - isAccurate: true, - explanation: "This is a mock explanation", - sources: [], - confidence: 0.85, - alternativeInfo: nil, - category: .general, - severity: .minor - ) - content = .factCheck(factCheckResult) - - case .summarization: - content = .summary("This is a mock summary of the conversation.") - - case .actionItems: - let actionItems = [ - ActionItem(description: "Follow up with client"), - ActionItem(description: "Send report") - ] - content = .actionItems(actionItems) - - case .sentiment: - let sentimentAnalysis = SentimentAnalysis( - overallSentiment: .positive, - speakerSentiments: [:], - emotionalTone: .casual, - confidence: 0.8 - ) - content = .sentiment(sentimentAnalysis) - - case .keyTopics: - content = .topics(["Business", "Growth", "Revenue"]) - - case .translation: - let translation = TranslationResult( - originalText: "Original text", - translatedText: "Translated text", - sourceLanguage: "en", - targetLanguage: "es", - confidence: 0.9 - ) - content = .translation(translation) - - case .clarification: - content = .text("Mock clarification text") - } - - return AnalysisResult( - type: context.analysisType, - content: content, - confidence: 0.85, - provider: provider - ) - } -} \ No newline at end of file diff --git a/HelixTests/SpeechRecognitionServiceTests.swift b/HelixTests/SpeechRecognitionServiceTests.swift deleted file mode 100644 index 0610db0..0000000 --- a/HelixTests/SpeechRecognitionServiceTests.swift +++ /dev/null @@ -1,192 +0,0 @@ -import XCTest -import Speech -import AVFoundation -import Combine -@testable import Helix - -class SpeechRecognitionServiceTests: XCTestCase { - var speechService: SpeechRecognitionService! - var cancellables: Set! - - override func setUpWithError() throws { - try super.setUpWithError() - speechService = SpeechRecognitionService() - cancellables = Set() - } - - override func tearDownWithError() throws { - speechService?.stopRecognition() - speechService = nil - cancellables = nil - try super.tearDownWithError() - } - - func testSpeechServiceInitialization() { - XCTAssertNotNil(speechService) - XCTAssertFalse(speechService.isRecognizing) - } - - func testStartStopRecognition() { - // Note: These tests may fail in simulator without microphone access - guard SFSpeechRecognizer.authorizationStatus() == .authorized else { - throw XCTSkip("Speech recognition not authorized") - } - - speechService.startStreamingRecognition() - // Note: isRecognizing might be delayed due to async setup - - speechService.stopRecognition() - XCTAssertFalse(speechService.isRecognizing) - } - - func testTranscriptionPublisher() { - let expectation = XCTestExpectation(description: "Transcription publisher should exist") - expectation.isInverted = false // We expect this to be fulfilled - - speechService.transcriptionPublisher - .sink( - receiveCompletion: { completion in - switch completion { - case .failure(let error): - print("Transcription error: \(error)") - case .finished: - print("Transcription finished") - } - }, - receiveValue: { result in - XCTAssertNotNil(result.text) - XCTAssertGreaterThanOrEqual(result.confidence, 0.0) - XCTAssertLessThanOrEqual(result.confidence, 1.0) - XCTAssertGreaterThan(result.timestamp, 0) - expectation.fulfill() - } - ) - .store(in: &cancellables) - - // Start recognition and wait briefly - speechService.startStreamingRecognition() - - // We'll wait a short time, but this test might not produce results in CI - wait(for: [expectation], timeout: 1.0) - - speechService.stopRecognition() - } - - func testLanguageConfiguration() { - let locale = Locale(identifier: "es-ES") - XCTAssertNoThrow(speechService.setLanguage(locale)) - } - - func testCustomVocabularyAddition() { - let customWords = ["Helix", "transcription", "Even Realities"] - XCTAssertNoThrow(speechService.addCustomVocabulary(customWords)) - } - - func testAudioBufferProcessing() { - // Create a mock audio buffer - let format = AVAudioFormat(standardFormatWithSampleRate: 16000, channels: 1)! - let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: 1024)! - buffer.frameLength = 1024 - - // This should not crash - XCTAssertNoThrow(speechService.processAudioBuffer(buffer)) - } -} - -// MARK: - Mock Speech Recognition Service - -class MockSpeechRecognitionService: SpeechRecognitionServiceProtocol { - private let transcriptionSubject = PassthroughSubject() - private(set) var isRecognizing = false - private var currentLanguage: Locale = Locale(identifier: "en-US") - private var customVocabulary: [String] = [] - - var transcriptionPublisher: AnyPublisher { - transcriptionSubject.eraseToAnyPublisher() - } - - func startStreamingRecognition() { - isRecognizing = true - - // Simulate transcription results - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { - self.sendMockTranscription() - } - } - - func stopRecognition() { - isRecognizing = false - } - - func setLanguage(_ locale: Locale) { - currentLanguage = locale - } - - func addCustomVocabulary(_ words: [String]) { - customVocabulary.append(contentsOf: words) - } - - func processAudioBuffer(_ buffer: AVAudioPCMBuffer) { - guard isRecognizing else { return } - - // Simulate processing delay - DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { - self.sendMockTranscription() - } - } - - private func sendMockTranscription() { - guard isRecognizing else { return } - - let mockTexts = [ - "This is a test transcription.", - "The weather is nice today.", - "Artificial intelligence is fascinating.", - "Even Realities glasses are innovative.", - "Real-time conversation analysis works well." - ] - - let mockText = mockTexts.randomElement() ?? "Test transcription" - - let result = TranscriptionResult( - text: mockText, - speakerId: UUID(), - confidence: Float.random(in: 0.8...0.95), - isFinal: Bool.random(), - wordTimings: createMockWordTimings(for: mockText), - alternatives: ["Alternative transcription"] - ) - - transcriptionSubject.send(result) - - // Continue if still recognizing - if isRecognizing { - DispatchQueue.global().asyncAfter(deadline: .now() + Double.random(in: 1.0...3.0)) { - self.sendMockTranscription() - } - } - } - - private func createMockWordTimings(for text: String) -> [WordTiming] { - let words = text.components(separatedBy: .whitespacesAndNewlines) - var timings: [WordTiming] = [] - var currentTime: TimeInterval = 0 - - for word in words { - let duration = TimeInterval(word.count) * 0.1 + 0.2 - timings.append(WordTiming( - word: word, - startTime: currentTime, - endTime: currentTime + duration, - confidence: Float.random(in: 0.8...0.95) - )) - currentTime += duration + 0.1 - } - - return timings - } - - func simulateError(_ error: TranscriptionError) { - transcriptionSubject.send(completion: .failure(error)) - } -} \ No newline at end of file diff --git a/HelixTests/TranscriptionCoordinatorTests.swift b/HelixTests/TranscriptionCoordinatorTests.swift deleted file mode 100644 index fafcd3e..0000000 --- a/HelixTests/TranscriptionCoordinatorTests.swift +++ /dev/null @@ -1,110 +0,0 @@ -import XCTest -import AVFoundation -import Combine -@testable import Helix - -// Mocks -class MockSpeakerDiarization: SpeakerDiarizationEngineProtocol { - func identifySpeaker(in buffer: AVAudioPCMBuffer) -> SpeakerIdentification? { nil } - func trainSpeakerModel(samples: [AVAudioPCMBuffer], speakerId: UUID) -> Bool { true } - func addSpeaker(id: UUID, name: String?, isCurrentUser: Bool) {} - func removeSpeaker(id: UUID) {} - func getCurrentSpeakers() -> [Speaker] { [] } - func resetSpeakerModels() {} -} - -class MockVAD: VoiceActivityDetectorProtocol { - func detectVoiceActivity(in buffer: AVAudioPCMBuffer) -> VoiceActivityResult { - return VoiceActivityResult(hasVoice: true, confidence: 1.0, - energy: 0, spectralCentroid: 0, - zeroCrossingRate: 0, - timestamp: Date().timeIntervalSince1970) - } - func updateBackground(with buffer: AVAudioPCMBuffer) {} - func setSensitivity(_ sensitivity: Float) {} -} - -class MockNoiseReducer: NoiseReductionProcessorProtocol { - func processBuffer(_ buffer: AVAudioPCMBuffer) -> AVAudioPCMBuffer { buffer } - func updateNoiseProfile(_ buffer: AVAudioPCMBuffer) {} - func setReductionLevel(_ level: Float) {} -} - -class TranscriptionCoordinatorTests: XCTestCase { - var audioManager: MockAudioManager! - var speechService: MockSpeechRecognitionService! - var diarizer: MockSpeakerDiarization! - var vad: MockVAD! - var noise: MockNoiseReducer! - var coordinator: TranscriptionCoordinator! - var cancellables: Set! - - override func setUp() { - super.setUp() - audioManager = MockAudioManager() - speechService = MockSpeechRecognitionService() - diarizer = MockSpeakerDiarization() - vad = MockVAD() - noise = MockNoiseReducer() - coordinator = TranscriptionCoordinator( - audioManager: audioManager, - speechRecognizer: speechService, - speakerDiarization: diarizer, - voiceActivityDetector: vad, - transcriptionProcessor: TranscriptionProcessor(), - noiseReducer: noise - ) - cancellables = [] - } - - override func tearDown() { - coordinator.stopConversationTranscription() - cancellables = nil - super.tearDown() - } - - func testConversationPublisherReceivesUpdates() { - let expect = expectation(description: "Expect conversation update") - - coordinator.conversationPublisher - .sink(receiveCompletion: { _ in }, receiveValue: { update in - XCTAssertEqual(update.message.content, "Hello world") - XCTAssertNil(update.speaker) - XCTAssertFalse(update.isNewSpeaker) - expect.fulfill() - }) - .store(in: &cancellables) - - // Send a transcription result - let result = TranscriptionResult(text: "Hello world", speakerId: nil, - confidence: 0.9, isFinal: true) - speechService.transcriptionSubject.send(result) - - wait(for: [expect], timeout: 1.0) - } - - func testAddSpeakerAndReceiveUpdate() { - let speakerId = UUID() - let speaker = Speaker(id: speakerId, name: "Alice", isCurrentUser: false) - coordinator.addSpeaker(speaker) - - let expect = expectation(description: "Expect update with speaker info") - - coordinator.conversationPublisher - .sink(receiveCompletion: { _ in }, receiveValue: { update in - XCTAssertEqual(update.message.content, "Test") - XCTAssertNotNil(update.speaker) - XCTAssertEqual(update.speaker?.id, speakerId) - // Since speaker was pre-added, isNewSpeaker should be false - XCTAssertFalse(update.isNewSpeaker) - expect.fulfill() - }) - .store(in: &cancellables) - - let result = TranscriptionResult(text: "Test", speakerId: speakerId, - confidence: 0.8, isFinal: true) - speechService.transcriptionSubject.send(result) - - wait(for: [expect], timeout: 1.0) - } -} \ No newline at end of file diff --git a/HelixUITests/HelixUITests.swift b/HelixUITests/HelixUITests.swift deleted file mode 100644 index d377615..0000000 --- a/HelixUITests/HelixUITests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// HelixUITests.swift -// HelixUITests -// -// Created by Art Jiang on 2/1/25. -// - -import XCTest - -final class HelixUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - @MainActor - func testLaunchPerformance() throws { - if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/HelixUITests/HelixUITestsLaunchTests.swift b/HelixUITests/HelixUITestsLaunchTests.swift deleted file mode 100644 index dcd0ddd..0000000 --- a/HelixUITests/HelixUITestsLaunchTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// HelixUITestsLaunchTests.swift -// HelixUITests -// -// Created by Art Jiang on 2/1/25. -// - -import XCTest - -final class HelixUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -} diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..ddac44d --- /dev/null +++ b/PLAN.md @@ -0,0 +1,1047 @@ +# Helix Epic 1.2: ConversationTab Integration - TDD Implementation Plan + +## Epic Overview +**Epic 1.2** focuses on connecting the UI to the working AudioService implementation, ensuring the ConversationTab properly integrates with real audio functionality instead of fake data. + +### Linear Context +- **Epic ID**: ART-10 (Epic 1.2: ConversationTab Integration) +- **Priority**: P0 (Urgent) +- **Estimate**: 5 story points +- **Dependencies**: Epic 1.1 (AudioService fixes) - **COMPLETED** + +### User Stories Included +1. **US 1.2.1**: Connect UI to AudioService (ART-11) +2. **US 1.2.2**: Live Waveform Visualization (ART-12) + +## Current State Analysis + +### What Works ✅ +- AudioService implementation is complete with real functionality +- ConversationTab UI exists with proper visual design +- Recording button and waveform widgets are implemented +- Permission handling is working +- Audio level detection and streaming is functional + +### Critical Issues ❌ +1. **UI is subscribed to AudioService streams but functionality gaps exist** +2. **Waveform shows real audio but needs optimization** +3. **Recording button connects to service but state management needs refinement** +4. **Timer shows real recording duration but UI polish needed** + +## TDD Implementation Strategy + +### Phase 1: Test Infrastructure Setup +Focus on creating comprehensive test coverage for UI-AudioService integration + +### Phase 2: UI Connection Fixes +Connect the ConversationTab to real AudioService streams with TDD approach + +### Phase 3: Waveform Optimization +Optimize the ReactiveWaveform for smooth 30fps real-time updates + +### Phase 4: Integration Testing +End-to-end testing of complete recording workflow + +--- + +## Detailed Implementation Chunks + +### Chunk 1: Test Infrastructure for UI-AudioService Integration (2 hours) +**Goal**: Establish comprehensive testing framework for UI-service integration + +**TDD Steps**: +1. Write failing tests for UI-AudioService state synchronization +2. Write failing tests for stream subscription management +3. Write failing tests for error handling in UI layer +4. Implement test helpers and mocks +5. Establish baseline test coverage + +**Deliverables**: +- `test/widget/conversation_tab_test.dart` - Widget tests +- `test/integration/ui_audio_integration_test.dart` - Integration tests +- Enhanced test helpers for UI testing +- Test coverage baseline established + +--- + +### Chunk 2: Recording Button State Management (3 hours) +**Goal**: Ensure recording button accurately reflects AudioService state + +**TDD Steps**: +1. Write failing test: "Recording button shows correct icon based on AudioService state" +2. Write failing test: "Recording button handles rapid tapping gracefully" +3. Write failing test: "Recording button shows loading state during permission requests" +4. Implement state management fixes +5. Write failing test: "Recording button handles service errors gracefully" +6. Implement error handling + +**Files Modified**: +- `lib/ui/widgets/conversation_tab.dart` (state management) +- `test/widget/conversation_tab_test.dart` (widget tests) + +**Success Criteria**: +- Recording button always shows correct state +- No duplicate recording calls from rapid tapping +- Proper loading states during async operations +- Graceful error handling and user feedback + +--- + +### Chunk 3: Real-Time Timer Integration (2 hours) +**Goal**: Connect timer display to AudioService duration stream + +**TDD Steps**: +1. Write failing test: "Timer displays accurate recording duration from AudioService" +2. Write failing test: "Timer resets correctly when recording stops" +3. Write failing test: "Timer handles stream errors gracefully" +4. Implement timer integration fixes +5. Write failing test: "Timer continues accurately after pause/resume" +6. Implement pause/resume timer handling + +**Files Modified**: +- `lib/ui/widgets/conversation_tab.dart` (timer logic) +- `test/widget/conversation_tab_test.dart` + +**Success Criteria**: +- Timer shows real elapsed recording time +- Timer resets to 00:00 when stopping +- Timer handles stream interruptions gracefully +- Timer works correctly with pause/resume + +--- + +### Chunk 4: Waveform Performance Optimization (4 hours) +**Goal**: Optimize ReactiveWaveform for smooth 30fps real-time updates + +**TDD Steps**: +1. Write failing test: "Waveform renders at target 30fps during recording" +2. Write failing test: "Waveform handles rapid audio level changes without jank" +3. Write failing test: "Waveform maintains history efficiently (no memory leaks)" +4. Implement performance optimizations +5. Write failing test: "Waveform responds to actual voice input accurately" +6. Fine-tune audio level mapping and visualization + +**Files Modified**: +- `lib/ui/widgets/conversation_tab.dart` (ReactiveWaveform) +- `test/widget/waveform_performance_test.dart` (performance tests) + +**Success Criteria**: +- Smooth 30fps waveform animation +- No UI jank during audio level updates +- Efficient memory usage for audio history +- Accurate visual representation of voice input + +--- + +### Chunk 5: Stream Subscription Management (2 hours) +**Goal**: Ensure proper lifecycle management of AudioService streams + +**TDD Steps**: +1. Write failing test: "All AudioService streams are properly subscribed on init" +2. Write failing test: "All stream subscriptions are cancelled on dispose" +3. Write failing test: "Stream subscriptions handle service reinitialization" +4. Implement subscription lifecycle fixes +5. Write failing test: "Stream errors don't crash the UI" +6. Implement robust error handling + +**Files Modified**: +- `lib/ui/widgets/conversation_tab.dart` (subscription management) +- `test/widget/conversation_tab_test.dart` + +**Success Criteria**: +- No memory leaks from uncancelled subscriptions +- Proper error handling for stream failures +- Clean initialization and disposal lifecycle +- Robust handling of service state changes + +--- + +### Chunk 6: Permission Flow Integration (2 hours) +**Goal**: Seamlessly integrate permission requests with recording workflow + +**TDD Steps**: +1. Write failing test: "Permission dialog triggers when microphone access needed" +2. Write failing test: "Recording starts automatically after permission granted" +3. Write failing test: "Proper error handling when permission denied" +4. Implement permission flow improvements +5. Write failing test: "Settings dialog appears for permanently denied permissions" +6. Implement settings dialog integration + +**Files Modified**: +- `lib/ui/widgets/conversation_tab.dart` (permission handling) +- `test/widget/conversation_tab_test.dart` + +**Success Criteria**: +- Smooth permission request flow +- Automatic recording start after permission grant +- Clear error messages for permission failures +- Easy path to app settings for denied permissions + +--- + +### Chunk 7: End-to-End Integration Testing (3 hours) +**Goal**: Comprehensive testing of complete recording workflow + +**TDD Steps**: +1. Write failing test: "Complete recording workflow - start to finish" +2. Write failing test: "Multiple recording sessions work correctly" +3. Write failing test: "Conversation saving includes real audio data" +4. Implement any remaining integration fixes +5. Write failing test: "App handles recording interruptions gracefully" +6. Implement interruption handling + +**Files Modified**: +- `test/integration/complete_recording_workflow_test.dart` +- Any remaining integration fixes + +**Success Criteria**: +- End-to-end recording workflow works perfectly +- Multiple recording sessions don't interfere +- Real audio files are saved correctly +- Graceful handling of interruptions and edge cases + +--- + +### Chunk 8: Performance and Polish (2 hours) +**Goal**: Final optimization and user experience polish + +**TDD Steps**: +1. Write failing test: "UI remains responsive during heavy audio processing" +2. Write failing test: "Memory usage stays within acceptable bounds" +3. Write failing test: "Battery usage is optimized for continuous recording" +4. Implement performance optimizations +5. Write failing test: "All animations are smooth and jank-free" +6. Final UI polish and optimization + +**Files Modified**: +- `lib/ui/widgets/conversation_tab.dart` (optimizations) +- `test/performance/recording_performance_test.dart` + +**Success Criteria**: +- Responsive UI during recording +- Optimized memory and battery usage +- Smooth animations and transitions +- Professional user experience + +--- + +## Code Generation Prompts + +### Prompt 1: Test Infrastructure Setup + +``` +You are implementing Epic 1.2 for the Helix Flutter app. This epic focuses on connecting the ConversationTab UI to the working AudioService implementation. + +CONTEXT: The AudioService implementation is complete and working, but the UI needs better integration testing and some state management fixes. + +YOUR TASK: Set up comprehensive test infrastructure for UI-AudioService integration testing. + +REQUIREMENTS: +1. Create widget tests for ConversationTab that test AudioService integration +2. Create integration tests for complete recording workflow +3. Set up test helpers and mocks for UI testing +4. Establish baseline test coverage + +FILES TO CREATE/MODIFY: +- test/widget/conversation_tab_test.dart (create comprehensive widget tests) +- test/integration/ui_audio_integration_test.dart (create integration tests) +- test/test_helpers.dart (enhance with UI testing utilities) + +FOLLOW TDD: +1. Write failing tests first +2. Make tests pass with minimal code +3. Refactor while keeping tests green +4. Focus on testing the integration between UI and AudioService + +START WITH: Writing failing tests for basic UI-AudioService state synchronization. +``` + +### Prompt 2: Recording Button State Management + +``` +You are continuing Epic 1.2 implementation for the Helix Flutter app. + +CONTEXT: Test infrastructure is set up. Now fix the recording button state management to properly reflect AudioService state. + +YOUR TASK: Implement robust recording button state management using TDD. + +REQUIREMENTS: +1. Recording button shows correct icon based on AudioService state +2. Handle rapid tapping gracefully (prevent duplicate calls) +3. Show loading states during permission requests +4. Graceful error handling with user feedback + +FILES TO MODIFY: +- lib/ui/widgets/conversation_tab.dart (improve _toggleRecording and state management) +- test/widget/conversation_tab_test.dart (add comprehensive button state tests) + +FOLLOW TDD: +1. Write failing test: "Recording button shows correct icon based on AudioService state" +2. Make test pass with minimal implementation +3. Write failing test: "Recording button handles rapid tapping gracefully" +4. Implement protection against rapid tapping +5. Continue with remaining requirements + +CURRENT STATE: The button works but needs better state management and error handling. + +START WITH: Writing a failing test for button icon state accuracy. +``` + +### Prompt 3: Real-Time Timer Integration + +``` +You are continuing Epic 1.2 implementation for the Helix Flutter app. + +CONTEXT: Recording button state management is complete. Now fix the timer integration with AudioService. + +YOUR TASK: Connect timer display to AudioService duration stream using TDD. + +REQUIREMENTS: +1. Timer displays accurate recording duration from AudioService +2. Timer resets correctly when recording stops +3. Timer handles stream errors gracefully +4. Timer continues accurately after pause/resume + +FILES TO MODIFY: +- lib/ui/widgets/conversation_tab.dart (improve timer subscription and display) +- test/widget/conversation_tab_test.dart (add timer integration tests) + +FOLLOW TDD: +1. Write failing test: "Timer displays accurate recording duration from AudioService" +2. Implement proper stream subscription +3. Write failing test: "Timer resets correctly when recording stops" +4. Implement reset logic +5. Continue with error handling and pause/resume + +CURRENT STATE: Timer works but subscription management needs improvement. + +START WITH: Writing a failing test for accurate timer display from AudioService stream. +``` + +### Prompt 4: Waveform Performance Optimization + +``` +You are continuing Epic 1.2 implementation for the Helix Flutter app. + +CONTEXT: Timer integration is complete. Now optimize the ReactiveWaveform for smooth real-time performance. + +YOUR TASK: Optimize ReactiveWaveform for 30fps real-time updates using TDD. + +REQUIREMENTS: +1. Waveform renders at target 30fps during recording +2. Handle rapid audio level changes without UI jank +3. Maintain history efficiently (no memory leaks) +4. Respond to actual voice input accurately + +FILES TO MODIFY: +- lib/ui/widgets/conversation_tab.dart (optimize ReactiveWaveform implementation) +- test/widget/waveform_performance_test.dart (create performance tests) + +FOLLOW TDD: +1. Write failing test: "Waveform renders at target 30fps during recording" +2. Implement performance optimizations +3. Write failing test: "Waveform handles rapid audio level changes without jank" +4. Optimize audio level processing +5. Continue with memory management and accuracy + +CURRENT STATE: Waveform works but may have performance issues during heavy audio processing. + +START WITH: Writing a failing test for 30fps rendering performance. +``` + +### Prompt 5: Stream Subscription Management + +``` +You are continuing Epic 1.2 implementation for the Helix Flutter app. + +CONTEXT: Waveform optimization is complete. Now ensure proper lifecycle management of AudioService streams. + +YOUR TASK: Implement robust stream subscription lifecycle management using TDD. + +REQUIREMENTS: +1. All AudioService streams are properly subscribed on init +2. All stream subscriptions are cancelled on dispose +3. Stream subscriptions handle service reinitialization +4. Stream errors don't crash the UI + +FILES TO MODIFY: +- lib/ui/widgets/conversation_tab.dart (improve subscription lifecycle) +- test/widget/conversation_tab_test.dart (add subscription lifecycle tests) + +FOLLOW TDD: +1. Write failing test: "All AudioService streams are properly subscribed on init" +2. Implement proper subscription setup +3. Write failing test: "All stream subscriptions are cancelled on dispose" +4. Implement proper cleanup +5. Continue with reinitialization and error handling + +CURRENT STATE: Basic subscription management exists but needs robustness improvements. + +START WITH: Writing a failing test for proper stream subscription setup. +``` + +### Prompt 6: Permission Flow Integration + +``` +You are continuing Epic 1.2 implementation for the Helix Flutter app. + +CONTEXT: Stream subscription management is robust. Now improve the permission request integration. + +YOUR TASK: Seamlessly integrate permission requests with recording workflow using TDD. + +REQUIREMENTS: +1. Permission dialog triggers when microphone access needed +2. Recording starts automatically after permission granted +3. Proper error handling when permission denied +4. Settings dialog appears for permanently denied permissions + +FILES TO MODIFY: +- lib/ui/widgets/conversation_tab.dart (improve permission flow in _toggleRecording) +- test/widget/conversation_tab_test.dart (add permission flow tests) + +FOLLOW TDD: +1. Write failing test: "Permission dialog triggers when microphone access needed" +2. Implement permission check integration +3. Write failing test: "Recording starts automatically after permission granted" +4. Implement automatic recording start +5. Continue with error handling and settings dialog + +CURRENT STATE: Permission handling exists but user experience needs improvement. + +START WITH: Writing a failing test for permission dialog triggering. +``` + +### Prompt 7: End-to-End Integration Testing + +``` +You are continuing Epic 1.2 implementation for the Helix Flutter app. + +CONTEXT: Permission flow is seamless. Now create comprehensive end-to-end integration tests. + +YOUR TASK: Implement comprehensive testing of complete recording workflow using TDD. + +REQUIREMENTS: +1. Complete recording workflow - start to finish +2. Multiple recording sessions work correctly +3. Conversation saving includes real audio data +4. App handles recording interruptions gracefully + +FILES TO CREATE/MODIFY: +- test/integration/complete_recording_workflow_test.dart (create comprehensive E2E tests) +- Any remaining integration fixes in conversation_tab.dart + +FOLLOW TDD: +1. Write failing test: "Complete recording workflow - start to finish" +2. Fix any integration issues discovered +3. Write failing test: "Multiple recording sessions work correctly" +4. Implement session management fixes +5. Continue with audio data saving and interruption handling + +CURRENT STATE: Individual components work well, need to verify end-to-end integration. + +START WITH: Writing a failing test for complete recording workflow. +``` + +### Prompt 8: Performance and Polish + +``` +You are completing Epic 1.2 implementation for the Helix Flutter app. + +CONTEXT: End-to-end integration tests pass. Now add final performance optimization and polish. + +YOUR TASK: Final optimization and user experience polish using TDD. + +REQUIREMENTS: +1. UI remains responsive during heavy audio processing +2. Memory usage stays within acceptable bounds +3. Battery usage is optimized for continuous recording +4. All animations are smooth and jank-free + +FILES TO MODIFY: +- lib/ui/widgets/conversation_tab.dart (final optimizations) +- test/performance/recording_performance_test.dart (create performance tests) + +FOLLOW TDD: +1. Write failing test: "UI remains responsive during heavy audio processing" +2. Implement performance optimizations +3. Write failing test: "Memory usage stays within acceptable bounds" +4. Optimize memory management +5. Continue with battery optimization and animation smoothness + +FINAL GOAL: Professional, polished user experience ready for production. + +START WITH: Writing a failing test for UI responsiveness during heavy processing. +``` + +--- + +## Success Metrics + +### Epic 1.2 Definition of Done ✅ +- [ ] Record button triggers actual recording ✅ +- [ ] UI reflects real recording state ✅ +- [ ] Live waveform shows actual voice input ✅ +- [ ] Timer displays real recording duration ✅ +- [ ] Smooth 30fps waveform animation ✅ +- [ ] No UI jank during recording ✅ +- [ ] >80% test coverage on UI-AudioService integration ✅ +- [ ] End-to-end recording workflow works perfectly ✅ + +### Quality Gates +1. **All tests pass** - 100% test success rate +2. **Performance targets met** - 30fps waveform, <100ms button response +3. **Memory efficiency** - No memory leaks, efficient audio history management +4. **User experience** - Smooth animations, clear feedback, graceful error handling + +### Integration Points Verified +- ConversationTab ↔ AudioService communication +- Real-time audio level visualization +- Recording state synchronization +- Permission flow integration +- Error handling and recovery +- Stream lifecycle management + +--- + +## Post-Epic Next Steps + +After Epic 1.2 completion: +1. **Epic 1.3**: Testing & Stability (ART-13) +2. **Epic 2.1**: Speech-to-Text Integration +3. **Epic 2.2**: AI Analysis Integration +4. **Epic 3.1**: Smart Glasses Communication + +This plan ensures a systematic, test-driven approach to connecting the UI to the working AudioService, delivering a polished and robust user experience for the core recording functionality. + +--- + +# Helix Flutter Migration Plan (LEGACY) +## Complete iOS to Cross-Platform Migration Blueprint + +### Executive Summary +Migrate the Helix iOS companion app for Even Realities smart glasses to Flutter for cross-platform deployment (iOS, Android, Web, Desktop). The migration will preserve all existing functionality while leveraging Flutter's cross-platform capabilities and the existing Flutter/Dart infrastructure in the `libs/` directory. + +--- + +## Phase 1: Foundation & Core Architecture (2-3 weeks) + +### Step 1.1: Project Setup & Dependencies +**Goal**: Establish Flutter project structure with all required dependencies + +``` +Set up the Flutter project structure and configure all necessary dependencies for cross-platform development. Create the main Flutter app in a new directory structure that mirrors the existing iOS architecture. + +Key tasks: +1. Create new Flutter project structure under `/flutter_helix/` +2. Configure pubspec.yaml with all required dependencies: + - flutter_blue_plus: ^1.4.4 (Bluetooth for Even Realities) + - flutter_sound: ^9.2.13 (Audio processing) + - provider: ^6.1.1 (State management) + - dio: ^5.4.3+1 (HTTP client for AI APIs) + - permission_handler: ^10.2.0 (Platform permissions) + - audio_session: ^0.1.16 (Audio session management) + - speech_to_text: ^6.6.0 (Local speech recognition) + - shared_preferences: ^2.2.2 (Settings persistence) + - dart_openai: ^5.1.0 (OpenAI integration) + - get_it: ^7.6.4 (Dependency injection) + - freezed: ^2.4.7 (Immutable data classes) + - json_annotation: ^4.8.1 (JSON serialization) + +3. Set up proper folder structure: + lib/ + core/ + audio/ + ai/ + transcription/ + glasses/ + utils/ + ui/ + screens/ + widgets/ + providers/ + services/ + models/ + +4. Configure platform-specific permissions in android/app/src/main/AndroidManifest.xml and ios/Runner/Info.plist +5. Set up build configurations for different platforms +6. Initialize dependency injection container with get_it +``` + +### Step 1.2: Core Service Interfaces +**Goal**: Define Flutter service interfaces that mirror iOS protocols + +``` +Create the core service interfaces and abstract classes that will define the contract for all platform implementations. This step establishes the architectural foundation for dependency injection and testing. + +Key tasks: +1. Create abstract interfaces for all core services: + - AudioService (audio capture, processing, recording) + - TranscriptionService (speech-to-text, both local and remote) + - LLMService (AI analysis, fact-checking, summarization) + - GlassesService (Bluetooth connectivity, HUD rendering) + - SettingsService (app configuration, persistence) + +2. Define data models using Freezed for immutability: + - ConversationModel + - TranscriptionSegment + - AnalysisResult + - GlassesConnectionState + - AudioConfiguration + +3. Create service locator pattern with get_it: + - Register all service interfaces + - Set up dependency resolution + - Configure singleton vs factory patterns + +4. Implement basic error handling and logging infrastructure: + - Custom exception classes + - Logging service with different levels + - Error reporting mechanism + +5. Set up constants and configuration classes: + - API endpoints and keys + - Audio processing parameters + - Bluetooth service UUIDs for Even Realities + - UI constants and themes +``` + +### Step 1.3: Audio Service Implementation +**Goal**: Port iOS AudioManager to Flutter with platform channels + +``` +Implement the core audio processing service that handles real-time audio capture, voice activity detection, and audio format conversion. This is the foundation for all transcription and analysis features. + +Key implementation points: +1. Create AudioServiceImpl class implementing AudioService interface +2. Use flutter_sound for cross-platform audio recording +3. Implement platform channels for native audio processing where needed +4. Port iOS audio configuration (16kHz sample rate, format conversion) +5. Add voice activity detection using native libraries or FFI +6. Implement audio buffering and streaming for real-time processing +7. Create test mode infrastructure for unit testing +8. Add noise reduction preprocessing pipeline +9. Handle platform-specific audio session management +10. Implement recording storage for conversation history + +Core components to implement: +- AudioCaptureEngine (real-time capture) +- AudioProcessor (format conversion, noise reduction) +- VoiceActivityDetector (VAD implementation) +- AudioRecorder (conversation storage) +- AudioConfiguration (settings management) + +Testing requirements: +- Unit tests for audio format conversion +- Mock audio input for testing pipeline +- Integration tests with different audio sources +- Performance tests for real-time processing +``` + +### Step 1.4: State Management Setup +**Goal**: Implement Provider-based state management architecture + +``` +Create the application-wide state management system using Provider pattern that replaces the iOS AppCoordinator functionality. This will handle all cross-service communication and UI state updates. + +Key components: +1. AppProvider - Main application state coordinator + - Manages service initialization and lifecycle + - Coordinates communication between services + - Handles app-wide settings and configuration + - Manages navigation state and deep linking + +2. ConversationProvider - Real-time conversation state + - Current transcription text and segments + - Speaker identification and timing + - Conversation history and persistence + - Real-time updates for UI components + +3. AnalysisProvider - AI analysis results + - Fact-checking results and claims + - Conversation summaries and insights + - Action items and follow-ups + - Analysis history and caching + +4. GlassesProvider - Even Realities connection state + - Bluetooth connection status and device info + - HUD content and rendering state + - Battery level and device health + - Touch gesture handling and commands + +5. SettingsProvider - App configuration + - User preferences and privacy settings + - AI service configuration (providers, models) + - Audio processing parameters + - Theme and display settings + +Implementation approach: +- Use ChangeNotifier pattern for reactive updates +- Implement proper dispose methods for resource cleanup +- Add loading states and error handling for all providers +- Create provider combination for complex state dependencies +- Set up proper testing infrastructure with provider mocking +``` + +--- + +## Phase 2: Core Services Implementation (3-4 weeks) + +### Step 2.1: Bluetooth & Glasses Integration +**Goal**: Port Even Realities Bluetooth connectivity to Flutter + +``` +Implement the complete Bluetooth Low Energy integration with Even Realities smart glasses using flutter_blue_plus. Leverage existing implementations in libs/g1_flutter_blue_plus and libs/EvenDemoApp. + +Core implementation: +1. GlassesServiceImpl class with flutter_blue_plus integration +2. Even Realities protocol implementation: + - Nordic UART Service (6E400001-B5A3-F393-E0A9-E50E24DCCA9E) + - TX/RX characteristics for bidirectional communication + - Command structure and message framing + - Heartbeat and connection management + +3. Device discovery and connection management: + - Scan for Even Realities devices with proper filtering + - Connection state handling and reconnection logic + - Device pairing and authentication if required + - Multiple device support for future expansion + +4. HUD content rendering and display: + - Text rendering with formatting options + - Real-time content updates and streaming + - Display brightness and visibility controls + - Content prioritization and queuing + +5. Touch gesture and input handling: + - Touch event processing from glasses + - Gesture recognition and command mapping + - User interaction feedback and confirmation + +6. Battery and device health monitoring: + - Battery level reporting and alerts + - Connection quality and signal strength + - Device status and error reporting + +Platform considerations: +- Android Bluetooth permissions and location services +- iOS Core Bluetooth background processing +- Platform-specific pairing and connection flows +- Error handling for different Bluetooth stack behaviors + +Testing approach: +- Mock Bluetooth service for unit testing +- Integration tests with actual Even Realities glasses +- Connection reliability and stress testing +- Battery optimization and power management tests +``` + +### Step 2.2: Speech Recognition Services +**Goal**: Implement dual speech recognition (local + Whisper API) + +``` +Create comprehensive speech-to-text functionality with both local on-device recognition and remote OpenAI Whisper API support. Implement backend switching and quality optimization. + +Implementation components: + +1. Local Speech Recognition (speech_to_text plugin): + - Platform-specific configuration for iOS/Android + - Real-time transcription with streaming results + - Language detection and multi-language support + - Confidence scoring and result filtering + - Speaker identification integration + +2. Remote Whisper API Integration: + - Audio chunking and streaming to OpenAI API + - Format conversion and compression for API efficiency + - Batch processing for improved accuracy + - Fallback mechanisms for network issues + - Rate limiting and cost optimization + +3. Hybrid Recognition System: + - Automatic backend selection based on quality/speed needs + - Real-time local processing with periodic Whisper validation + - Quality comparison and accuracy metrics + - User preference and automatic optimization + +4. TranscriptionCoordinator: + - Manages coordination between recognition backends + - Handles result merging and timing synchronization + - Implements speaker diarization and attribution + - Provides unified transcription stream to UI + +5. Advanced Features: + - Punctuation and capitalization enhancement + - Domain-specific vocabulary and customization + - Real-time correction and editing capabilities + - Transcription confidence and quality scoring + +Performance optimization: +- Audio preprocessing for optimal recognition +- Network optimization for API calls +- Caching and result persistence +- Background processing for non-critical tasks + +Testing strategy: +- Audio sample testing with known ground truth +- Network simulation for API reliability testing +- Performance benchmarking across platforms +- Accuracy comparison between local and remote backends +``` + +### Step 2.3: AI/LLM Integration +**Goal**: Port multi-provider AI analysis system to Flutter + +``` +Implement the complete AI analysis pipeline with support for multiple LLM providers (OpenAI, Anthropic). Create comprehensive fact-checking, summarization, and conversation analysis capabilities. + +Core AI Services: + +1. LLMServiceImpl - Multi-provider AI orchestration: + - OpenAI GPT integration with dart_openai package + - Anthropic API integration with custom HTTP client + - Provider fallback and load balancing + - Response caching and optimization + - Rate limiting and cost management + +2. ClaimDetectionService - Real-time fact-checking: + - Extract factual claims from transcribed conversation + - Query LLMs for fact verification and source citation + - Provide confidence scores and supporting evidence + - Handle controversial topics with balanced perspectives + - Cache fact-check results for performance + +3. ConversationAnalyzer - Comprehensive conversation analysis: + - Generate conversation summaries and key insights + - Extract action items and follow-up tasks + - Identify important topics and themes + - Analyze conversation tone and sentiment + - Provide personalized insights and recommendations + +4. PromptManager - Template and persona management: + - Structured prompt templates for different analysis types + - Persona-based prompting for specialized contexts + - Dynamic prompt generation based on conversation context + - A/B testing infrastructure for prompt optimization + - Multi-language prompt support + +5. AnalysisCoordinator - Results aggregation and coordination: + - Coordinate multiple AI analysis requests + - Merge and prioritize analysis results + - Handle real-time vs batch analysis modes + - Manage analysis history and persistence + - Provide unified analysis stream to UI + +Implementation details: +- Dio HTTP client for all API communications +- JSON serialization with freezed and json_annotation +- Error handling and retry logic for API failures +- Background processing for non-urgent analysis +- Result caching with shared_preferences or hive + +Security and privacy: +- API key management and secure storage +- User consent and privacy controls +- Local processing options where possible +- Data retention and deletion policies + +Testing approach: +- Mock AI responses for consistent testing +- Integration tests with actual AI APIs +- Performance benchmarking for analysis speed +- Accuracy validation with known conversation samples +``` + +### Step 2.4: Data Persistence & History +**Goal**: Implement conversation history and settings persistence + +``` +Create comprehensive data persistence layer for conversation history, user settings, and analysis results. Implement local storage with optional cloud synchronization. + +Data Storage Components: + +1. ConversationRepository - Conversation and transcription storage: + - SQLite database with drift package for complex queries + - Conversation metadata (date, duration, participants) + - Transcription segments with timing and speaker attribution + - Audio file references and storage management + - Full-text search capabilities for conversation content + +2. AnalysisRepository - AI analysis results storage: + - Analysis results linked to conversations + - Fact-check results with citations and confidence scores + - Summaries, action items, and insights + - Analysis history and trending topics + - Performance metrics and accuracy tracking + +3. SettingsRepository - User preferences and configuration: + - App settings with shared_preferences + - AI provider preferences and API configurations + - Audio processing parameters and quality settings + - Privacy and consent management + - Backup and restore functionality + +4. CacheManager - Intelligent caching system: + - API response caching for performance + - Offline functionality with local data + - Cache invalidation and cleanup strategies + - Memory management and storage optimization + +Data Models and Serialization: +- Freezed data classes for immutable models +- JSON serialization for API communication +- Database schemas with proper indexing +- Migration strategies for schema updates + +Synchronization and Backup: +- Optional cloud storage integration (Google Drive, iCloud) +- Conflict resolution for multi-device usage +- Data export in standard formats (JSON, CSV) +- Privacy-preserving synchronization options + +Performance Optimization: +- Lazy loading for large conversation histories +- Pagination for UI components +- Background data processing and cleanup +- Database query optimization and indexing + +Testing and Validation: +- Repository unit tests with mock data +- Database migration testing +- Performance testing with large datasets +- Data integrity and backup validation +``` + +--- + +## Phase 3: User Interface Migration (2-3 weeks) + +### Step 3.1: Core UI Components & Navigation +**Goal**: Create Flutter equivalent of SwiftUI views and tab navigation + +``` +Implement the main user interface structure using Flutter widgets that replicate the iOS app's five-tab navigation and core UI components. + +Navigation Structure: + +1. MainApp - Application root with material design: + - MaterialApp configuration with custom theme + - Route management and deep linking support + - Global navigation context and state management + - Error boundary and crash handling UI + +2. MainTabView - Bottom navigation with five tabs: + - Conversation tab (real-time transcription and interaction) + - Analysis tab (AI insights and fact-checking results) + - Glasses tab (Even Realities connection and status) + - History tab (conversation history and search) + - Settings tab (app configuration and preferences) + +3. Core UI Components: + - HelixAppBar - Custom app bar with status indicators + - ConnectionStatusWidget - Bluetooth and service status + - LoadingOverlay - Loading states with proper animations + - ErrorDialog - Consistent error display and recovery + - SettingsCard - Reusable settings UI components + +Theme and Design System: +- Material Design 3 with custom color scheme +- Dark/light theme support with user preference +- Consistent typography and spacing +- Accessibility support with proper semantics +- Responsive design for different screen sizes + +State Integration: +- Provider integration for all tab views +- Proper state preservation during navigation +- Loading and error states for each tab +- Deep linking support for external navigation + +Testing Approach: +- Widget tests for all UI components +- Navigation testing with flutter_test +- Golden file testing for visual consistency +- Accessibility testing with semantics +``` + +--- + +## Implementation Prompts + +### Prompt 1: Project Setup & Core Architecture +``` +Create a new Flutter project for Helix cross-platform app migration. Set up the complete project structure with proper dependencies and folder organization. + +Tasks: +1. Create Flutter project with proper package name and organization +2. Configure pubspec.yaml with all required dependencies: + - flutter_blue_plus: ^1.4.4 + - flutter_sound: ^9.2.13 + - provider: ^6.1.1 + - dio: ^5.4.3+1 + - permission_handler: ^10.2.0 + - audio_session: ^0.1.16 + - speech_to_text: ^6.6.0 + - shared_preferences: ^2.2.2 + - dart_openai: ^5.1.0 + - get_it: ^7.6.4 + - freezed: ^2.4.7 + - json_annotation: ^4.8.1 + - build_runner: ^2.4.7 + - json_serializable: ^6.7.1 + +3. Create folder structure and initialize dependency injection +4. Set up platform permissions and basic error handling +5. Ensure all setup follows Flutter best practices + +This prompt begins the foundation phase with proper project structure and dependencies for cross-platform development. +``` + +### Prompt 2: Core Service Interfaces & Models +``` +Create the core service interfaces and data models that define the architecture for the Helix Flutter app. This establishes the foundation for all service implementations. + +Tasks: +1. Create abstract service interfaces (AudioService, TranscriptionService, LLMService, GlassesService, SettingsService) +2. Define Freezed data models (ConversationModel, TranscriptionSegment, AnalysisResult, etc.) +3. Set up service locator with get_it +4. Create custom exception classes and logging infrastructure +5. Add JSON serialization code generation setup + +This prompt establishes the architectural foundation with clear contracts for all services. +``` + +**Continue with the remaining 13 prompts following the same pattern...** + +--- + +## Success Metrics & Validation + +### Technical Success Criteria +- [ ] Cross-platform deployment on iOS, Android, Web, Desktop +- [ ] Real-time audio processing with <100ms latency +- [ ] 95%+ transcription accuracy with hybrid recognition +- [ ] Stable Bluetooth connectivity with Even Realities glasses +- [ ] AI analysis completion within 30 seconds for 10-minute conversations +- [ ] 90%+ test coverage across all core services +- [ ] App store approval on all target platforms +- [ ] Performance benchmarks meeting or exceeding iOS version + +### User Experience Criteria +- [ ] Intuitive onboarding process (<5 minutes setup) +- [ ] Seamless cross-platform synchronization +- [ ] Accessible design meeting WCAG guidelines +- [ ] Responsive performance on low-end devices +- [ ] Offline functionality for core features +- [ ] Multi-language support for major markets +- [ ] Professional UI/UX matching platform conventions + +### Business Success Criteria +- [ ] Feature parity with existing iOS application +- [ ] Reduced development maintenance overhead +- [ ] Expanded market reach to Android users +- [ ] Web accessibility for broader audience +- [ ] Enterprise deployment capabilities +- [ ] Scalable architecture for future feature additions +- [ ] Cost-effective cross-platform maintenance model + +This comprehensive migration plan provides a structured approach to transforming the Helix iOS app into a full-featured cross-platform Flutter application while maintaining all existing functionality and adding new platform-specific capabilities. \ No newline at end of file diff --git a/README.md b/README.md index 8b70dd7..d90dc76 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Helix +# Helix - Real Time Conversation Prompter for Even Realities G1S App Helix is an iOS companion app for Even Realities smart glasses that provides real-time conversation analysis and AI-powered insights displayed directly on the glasses HUD. The app processes live audio, performs speech-to-text conversion, and sends conversation data to LLM APIs for fact-checking, summarization, and contextual assistance. @@ -81,4 +81,4 @@ libs/ # External libraries and demos - Use Combine publishers for reactive flows ## License -MIT License. See LICENSE for details. \ No newline at end of file +MIT License. See LICENSE for details. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..0caa33f --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.evenrealities.flutter_helix" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.evenrealities.flutter_helix" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..996a9f9 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/evenrealities/flutter_helix/MainActivity.kt b/android/app/src/main/kotlin/com/evenrealities/flutter_helix/MainActivity.kt new file mode 100644 index 0000000..a84bcf1 --- /dev/null +++ b/android/app/src/main/kotlin/com/evenrealities/flutter_helix/MainActivity.kt @@ -0,0 +1,5 @@ +package com.evenrealities.flutter_helix + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..89176ef --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..afa1e8e --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..a439442 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.0" apply false + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/docs/FLUTTER_BEST_PRACTICES.md b/docs/FLUTTER_BEST_PRACTICES.md new file mode 100644 index 0000000..bee3c47 --- /dev/null +++ b/docs/FLUTTER_BEST_PRACTICES.md @@ -0,0 +1,995 @@ +# Flutter Development Best Practices +# Production-Ready Mobile App Development Guide + +## Overview + +This document outlines comprehensive best practices for Flutter development, covering architecture, performance, security, and maintainability. These guidelines are based on industry standards and lessons learned from building production Flutter applications. + +## Table of Contents + +1. [Project Architecture](#project-architecture) +2. [Code Organization](#code-organization) +3. [State Management](#state-management) +4. [Performance Optimization](#performance-optimization) +5. [Security Best Practices](#security-best-practices) +6. [UI/UX Guidelines](#uiux-guidelines) +7. [Error Handling](#error-handling) +8. [Testing Strategy](#testing-strategy) +9. [Build & Deployment](#build--deployment) +10. [Monitoring & Analytics](#monitoring--analytics) + +## Project Architecture + +### Clean Architecture Principles + +``` +lib/ +├── core/ # Core business logic +│ ├── entities/ # Business entities +│ ├── usecases/ # Business use cases +│ ├── errors/ # Error handling +│ └── utils/ # Utilities and extensions +├── data/ # Data layer +│ ├── models/ # Data models +│ ├── repositories/ # Repository implementations +│ ├── datasources/ # Local and remote data sources +│ └── mappers/ # Data mapping logic +├── domain/ # Domain layer +│ ├── entities/ # Domain entities +│ ├── repositories/ # Repository interfaces +│ └── usecases/ # Use case interfaces +├── presentation/ # Presentation layer +│ ├── pages/ # Screen widgets +│ ├── widgets/ # Reusable UI components +│ ├── providers/ # State management +│ └── utils/ # UI utilities +└── injection/ # Dependency injection +``` + +### Dependency Injection Pattern + +```dart +// injection/injection_container.dart +import 'package:get_it/get_it.dart'; + +final GetIt sl = GetIt.instance; + +Future init() async { + // External dependencies + sl.registerLazySingleton(() => http.Client()); + sl.registerLazySingleton(() => SharedPreferences.getInstance()); + + // Data sources + sl.registerLazySingleton( + () => RemoteDataSourceImpl(client: sl()), + ); + + // Repositories + sl.registerLazySingleton( + () => UserRepositoryImpl(remoteDataSource: sl()), + ); + + // Use cases + sl.registerLazySingleton(() => GetUserUseCase(sl())); + + // Providers + sl.registerFactory(() => UserProvider(getUserUseCase: sl())); +} +``` + +## Code Organization + +### File Naming Conventions + +``` +// Good examples +user_repository.dart +conversation_card.dart +audio_service_impl.dart +transcription_model.g.dart + +// Avoid +UserRepository.dart +conversationCard.dart +audioServiceImplementation.dart +``` + +### Import Organization + +```dart +// 1. Dart imports +import 'dart:async'; +import 'dart:io'; + +// 2. Flutter imports +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +// 3. Package imports (alphabetical) +import 'package:dio/dio.dart'; +import 'package:provider/provider.dart'; + +// 4. Local imports (alphabetical) +import '../models/user_model.dart'; +import '../services/auth_service.dart'; +import 'widgets/custom_button.dart'; +``` + +### Documentation Standards + +```dart +/// Service responsible for managing user authentication +/// +/// Handles login, logout, token refresh, and session management. +/// Integrates with Firebase Auth and custom backend APIs. +/// +/// Example usage: +/// ```dart +/// final authService = AuthService(); +/// final user = await authService.signInWithEmail(email, password); +/// ``` +class AuthService { + /// Signs in user with email and password + /// + /// Returns [User] on success, throws [AuthException] on failure. + /// Automatically handles token storage and session initialization. + /// + /// Throws: + /// * [InvalidCredentialsException] - Invalid email/password + /// * [NetworkException] - Network connectivity issues + /// * [ServerException] - Server-side errors + Future signInWithEmail(String email, String password) async { + // Implementation + } +} +``` + +## State Management + +### Provider Pattern Best Practices + +```dart +// Use ChangeNotifier for complex state +class ConversationProvider extends ChangeNotifier { + final List _segments = []; + bool _isRecording = false; + + // Expose immutable views + List get segments => List.unmodifiable(_segments); + bool get isRecording => _isRecording; + + // Single responsibility methods + void startRecording() { + _isRecording = true; + notifyListeners(); + } + + void addSegment(TranscriptionSegment segment) { + _segments.add(segment); + notifyListeners(); + } + + // Dispose resources properly + @override + void dispose() { + _segments.clear(); + super.dispose(); + } +} + +// Use MultiProvider for complex dependencies +class App extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => sl()), + ChangeNotifierProvider(create: (_) => sl()), + ChangeNotifierProxyProvider( + create: (_) => sl(), + update: (_, auth, previous) => previous!..updateAuth(auth), + ), + ], + child: MaterialApp( + home: const HomeScreen(), + ), + ); + } +} +``` + +### Riverpod Alternative (Recommended for Large Apps) + +```dart +// Define providers +final audioServiceProvider = Provider((ref) { + return AudioServiceImpl(); +}); + +final conversationProvider = StateNotifierProvider((ref) { + final audioService = ref.watch(audioServiceProvider); + return ConversationNotifier(audioService); +}); + +// Use in widgets +class ConversationPage extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final conversationState = ref.watch(conversationProvider); + + return Scaffold( + body: conversationState.when( + loading: () => const CircularProgressIndicator(), + error: (error, stack) => ErrorWidget(error.toString()), + data: (conversation) => ConversationView(conversation), + ), + ); + } +} +``` + +## Performance Optimization + +### Widget Performance + +```dart +// Use const constructors whenever possible +class CustomCard extends StatelessWidget { + const CustomCard({ + super.key, + required this.title, + required this.content, + }); + + final String title; + final String content; + + @override + Widget build(BuildContext context) { + return const Card( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Column( + children: [ + Text(title), + Text(content), + ], + ), + ), + ); + } +} + +// Use Builder widgets to limit rebuild scope +class OptimizedWidget extends StatefulWidget { + @override + State createState() => _OptimizedWidgetState(); +} + +class _OptimizedWidgetState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // This part doesn't rebuild when counter changes + const ExpensiveWidget(), + + // Only this Builder rebuilds + Builder( + builder: (context) => Text('Counter: $_counter'), + ), + + ElevatedButton( + onPressed: () => setState(() => _counter++), + child: const Text('Increment'), + ), + ], + ); + } +} +``` + +### Memory Management + +```dart +// Dispose resources properly +class AudioPlayerWidget extends StatefulWidget { + @override + State createState() => _AudioPlayerWidgetState(); +} + +class _AudioPlayerWidgetState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late StreamSubscription _audioSubscription; + Timer? _timer; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 1), + vsync: this, + ); + + _audioSubscription = audioService.stream.listen(_onAudioUpdate); + _timer = Timer.periodic(const Duration(seconds: 1), _updateUI); + } + + @override + void dispose() { + _controller.dispose(); + _audioSubscription.cancel(); + _timer?.cancel(); + super.dispose(); + } + + // Implementation... +} +``` + +### List Performance + +```dart +// Use ListView.builder for large lists +class ConversationList extends StatelessWidget { + final List segments; + + const ConversationList({super.key, required this.segments}); + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: segments.length, + itemBuilder: (context, index) { + final segment = segments[index]; + return ConversationTile( + key: ValueKey(segment.id), // Important for performance + segment: segment, + ); + }, + ); + } +} + +// Use RepaintBoundary for expensive widgets +class ExpensiveVisualization extends StatelessWidget { + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: CustomPaint( + painter: ComplexVisualizationPainter(), + size: const Size(300, 200), + ), + ); + } +} +``` + +## Security Best Practices + +### API Key Management + +```dart +// Use environment variables and secure storage +class ConfigService { + static const String _openaiKeyKey = 'openai_api_key'; + static const String _anthropicKeyKey = 'anthropic_api_key'; + + final FlutterSecureStorage _secureStorage = const FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + ), + iOptions: IOSOptions( + accessibility: IOSAccessibility.first_unlock_this_device, + ), + ); + + Future setOpenAIKey(String key) async { + await _secureStorage.write(key: _openaiKeyKey, value: key); + } + + Future getOpenAIKey() async { + return await _secureStorage.read(key: _openaiKeyKey); + } + + // Validate keys before storage + bool isValidAPIKey(String key, APIProvider provider) { + switch (provider) { + case APIProvider.openai: + return key.startsWith('sk-') && key.length > 20; + case APIProvider.anthropic: + return key.startsWith('sk-ant-') && key.length > 30; + } + } +} +``` + +### Network Security + +```dart +// Use certificate pinning for sensitive APIs +class SecureHttpClient { + static Dio createSecureClient() { + final dio = Dio(); + + // Add certificate pinning + (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { + client.badCertificateCallback = (cert, host, port) { + // Implement certificate validation + return validateCertificate(cert, host); + }; + return client; + }; + + // Add request/response interceptors + dio.interceptors.addAll([ + AuthInterceptor(), + LoggingInterceptor(), + ErrorInterceptor(), + ]); + + return dio; + } +} + +// Sanitize user inputs +class InputValidator { + static String sanitizeText(String input) { + return input + .replaceAll(RegExp(r'<[^>]*>'), '') // Remove HTML tags + .replaceAll(RegExp(r'[^\w\s\.,!?-]'), '') // Allow only safe characters + .trim(); + } + + static bool isValidEmail(String email) { + return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email); + } +} +``` + +### Data Protection + +```dart +// Encrypt sensitive data before storage +class SecureDataService { + final _encryption = Encrypt(AES(Key.fromSecureRandom(32))); + final _iv = IV.fromSecureRandom(16); + + Future storeSecureData(String key, String data) async { + final encrypted = _encryption.encrypt(data, iv: _iv); + await _secureStorage.write(key: key, value: encrypted.base64); + } + + Future getSecureData(String key) async { + final encryptedData = await _secureStorage.read(key: key); + if (encryptedData == null) return null; + + final encrypted = Encrypted.fromBase64(encryptedData); + return _encryption.decrypt(encrypted, iv: _iv); + } +} +``` + +## UI/UX Guidelines + +### Responsive Design + +```dart +// Use responsive design patterns +class ResponsiveLayout extends StatelessWidget { + final Widget mobile; + final Widget tablet; + final Widget desktop; + + const ResponsiveLayout({ + super.key, + required this.mobile, + required this.tablet, + required this.desktop, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 600) { + return mobile; + } else if (constraints.maxWidth < 1200) { + return tablet; + } else { + return desktop; + } + }, + ); + } +} + +// Use MediaQuery for dynamic sizing +class AdaptiveButton extends StatelessWidget { + final String text; + final VoidCallback onPressed; + + const AdaptiveButton({ + super.key, + required this.text, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final screenWidth = MediaQuery.of(context).size.width; + final buttonWidth = screenWidth < 600 ? screenWidth * 0.8 : 300.0; + + return SizedBox( + width: buttonWidth, + height: 48, + child: ElevatedButton( + onPressed: onPressed, + child: Text(text), + ), + ); + } +} +``` + +### Accessibility + +```dart +// Implement proper accessibility +class AccessibleWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Semantics( + label: 'Start recording conversation', + hint: 'Double tap to begin audio recording', + button: true, + child: GestureDetector( + onTap: _startRecording, + child: Container( + width: 72, + height: 72, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.red, + ), + child: const Icon( + Icons.mic, + color: Colors.white, + size: 32, + semanticLabel: 'Microphone', + ), + ), + ), + ); + } +} + +// Support platform conventions +class PlatformAwareWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Platform.isIOS + ? CupertinoButton( + onPressed: _onPressed, + child: const Text('iOS Style Button'), + ) + : ElevatedButton( + onPressed: _onPressed, + child: const Text('Material Style Button'), + ); + } +} +``` + +### Animation Best Practices + +```dart +// Use implicit animations when possible +class AnimatedCard extends StatefulWidget { + final bool isExpanded; + + const AnimatedCard({super.key, required this.isExpanded}); + + @override + State createState() => _AnimatedCardState(); +} + +class _AnimatedCardState extends State { + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + height: widget.isExpanded ? 200 : 100, + child: Card( + child: AnimatedOpacity( + duration: const Duration(milliseconds: 300), + opacity: widget.isExpanded ? 1.0 : 0.5, + child: const Center(child: Text('Content')), + ), + ), + ); + } +} + +// Use explicit animations for complex sequences +class ComplexAnimation extends StatefulWidget { + @override + State createState() => _ComplexAnimationState(); +} + +class _ComplexAnimationState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _rotationAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + ); + + _scaleAnimation = Tween( + begin: 0.5, + end: 1.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeOut), + )); + + _rotationAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.5, 1.0, curve: Curves.easeIn), + )); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Transform.rotate( + angle: _rotationAnimation.value * 2 * 3.14159, + child: child, + ), + ); + }, + child: const Icon(Icons.star, size: 50), + ); + } +} +``` + +## Error Handling + +### Custom Exception Classes + +```dart +// Define specific exception types +abstract class AppException implements Exception { + const AppException(this.message); + final String message; +} + +class NetworkException extends AppException { + const NetworkException(super.message); +} + +class AuthenticationException extends AppException { + const AuthenticationException(super.message); +} + +class ValidationException extends AppException { + const ValidationException(super.message); +} + +// Handle exceptions consistently +class ApiService { + Future handleApiCall(Future apiCall) async { + try { + final response = await apiCall; + + if (response.statusCode == 200) { + return response.data as T; + } else if (response.statusCode == 401) { + throw const AuthenticationException('Authentication failed'); + } else if (response.statusCode >= 500) { + throw const NetworkException('Server error occurred'); + } else { + throw NetworkException('HTTP ${response.statusCode}: ${response.statusMessage}'); + } + } on DioException catch (e) { + switch (e.type) { + case DioExceptionType.connectionTimeout: + case DioExceptionType.receiveTimeout: + throw const NetworkException('Connection timeout'); + case DioExceptionType.connectionError: + throw const NetworkException('No internet connection'); + default: + throw NetworkException('Network error: ${e.message}'); + } + } catch (e) { + throw AppException('Unexpected error: $e'); + } + } +} +``` + +### Global Error Handling + +```dart +// Implement global error boundary +class ErrorBoundary extends StatefulWidget { + final Widget child; + + const ErrorBoundary({super.key, required this.child}); + + @override + State createState() => _ErrorBoundaryState(); +} + +class _ErrorBoundaryState extends State { + Object? error; + StackTrace? stackTrace; + + @override + Widget build(BuildContext context) { + if (error != null) { + return ErrorScreen( + error: error!, + onRetry: () => setState(() { + error = null; + stackTrace = null; + }), + ); + } + + return ErrorWidget.builder = (FlutterErrorDetails details) { + return ErrorScreen( + error: details.exception, + onRetry: () => setState(() { + error = null; + stackTrace = null; + }), + ); + }; + + return widget.child; + } +} + +// Centralized error logging +class ErrorReportingService { + static void reportError(Object error, StackTrace? stackTrace) { + // Log to console in debug mode + if (kDebugMode) { + print('Error: $error'); + print('Stack trace: $stackTrace'); + } + + // Report to crash analytics in production + if (kReleaseMode) { + FirebaseCrashlytics.instance.recordError( + error, + stackTrace, + fatal: false, + ); + } + } +} +``` + +## Build & Deployment + +### Environment Configuration + +```dart +// config/environment.dart +enum Environment { development, staging, production } + +class Config { + static Environment _environment = Environment.development; + + static String get apiBaseUrl { + switch (_environment) { + case Environment.development: + return 'https://dev-api.helix.com'; + case Environment.staging: + return 'https://staging-api.helix.com'; + case Environment.production: + return 'https://api.helix.com'; + } + } + + static bool get enableLogging => _environment != Environment.production; + + static void setEnvironment(Environment environment) { + _environment = environment; + } +} + +// main_development.dart +import 'config/environment.dart'; + +void main() { + Config.setEnvironment(Environment.development); + runApp(const HelixApp()); +} +``` + +### Build Scripts + +```yaml +# scripts/build.yml +name: Build and Deploy + +on: + push: + branches: [main, develop] + +jobs: + build: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.19.0' + channel: 'stable' + + - name: Install dependencies + run: flutter pub get + + - name: Run tests + run: flutter test --coverage + + - name: Build iOS + run: | + flutter build ios --release --no-codesign + cd ios + xcodebuild -workspace Runner.xcworkspace \ + -scheme Runner \ + -configuration Release \ + -archivePath build/Runner.xcarchive \ + archive + + - name: Build Android + run: | + flutter build appbundle --release + flutter build apk --release + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: app-bundles + path: | + build/app/outputs/bundle/release/ + build/app/outputs/apk/release/ +``` + +### Code Signing + +```bash +# iOS code signing setup +security create-keychain -p "" build.keychain +security import certificate.p12 -t agg -k build.keychain -P $CERT_PASSWORD -A +security list-keychains -s build.keychain +security default-keychain -s build.keychain +security unlock-keychain -p "" build.keychain + +# Android signing +echo $ANDROID_KEYSTORE | base64 -d > android/app/key.jks +echo "storeFile=key.jks" >> android/key.properties +echo "storePassword=$KEYSTORE_PASSWORD" >> android/key.properties +echo "keyAlias=$KEY_ALIAS" >> android/key.properties +echo "keyPassword=$KEY_PASSWORD" >> android/key.properties +``` + +## Monitoring & Analytics + +### Performance Monitoring + +```dart +// Performance tracking +class PerformanceMonitor { + static void trackPageLoad(String pageName) { + final stopwatch = Stopwatch()..start(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + stopwatch.stop(); + FirebasePerformance.instance + .newTrace('page_load_$pageName') + .start() + .stop(); + }); + } + + static Future trackAsyncOperation( + String operationName, + Future operation, + ) async { + final trace = FirebasePerformance.instance.newTrace(operationName); + trace.start(); + + try { + final result = await operation; + trace.putAttribute('success', 'true'); + return result; + } catch (e) { + trace.putAttribute('success', 'false'); + trace.putAttribute('error', e.toString()); + rethrow; + } finally { + trace.stop(); + } + } +} + +// Usage tracking +class AnalyticsService { + static void trackEvent(String eventName, Map parameters) { + FirebaseAnalytics.instance.logEvent( + name: eventName, + parameters: parameters, + ); + } + + static void trackUserAction(UserAction action, {Map? metadata}) { + trackEvent('user_action', { + 'action_type': action.name, + 'timestamp': DateTime.now().toIso8601String(), + ...?metadata, + }); + } +} +``` + +### Crash Reporting + +```dart +// main.dart crash handling +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Handle Flutter framework errors + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + FirebaseCrashlytics.instance.recordFlutterFatalError(details); + }; + + // Handle async errors + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); + return true; + }; + + runApp(const HelixApp()); +} +``` + +## Summary + +These best practices provide a solid foundation for building production-ready Flutter applications. Key takeaways: + +1. **Architecture**: Use clean architecture with proper separation of concerns +2. **Performance**: Optimize widgets, manage memory, and monitor performance +3. **Security**: Protect sensitive data and validate all inputs +4. **Testing**: Implement comprehensive testing at all levels +5. **Deployment**: Automate builds and use proper CI/CD practices +6. **Monitoring**: Track performance and user behavior + +Regular review and updates of these practices will help maintain code quality and adapt to new Flutter features and community standards. \ No newline at end of file diff --git a/docs/TESTING_STRATEGY.md b/docs/TESTING_STRATEGY.md new file mode 100644 index 0000000..9a634ff --- /dev/null +++ b/docs/TESTING_STRATEGY.md @@ -0,0 +1,927 @@ +# Flutter Testing Strategy & Best Practices +# Helix AI Conversation Intelligence App + +## Overview + +This document outlines comprehensive testing strategies and best practices for Flutter app development, specifically tailored for the Helix project. Following these guidelines ensures high-quality, maintainable, and reliable Flutter applications. + +## Table of Contents + +1. [Testing Philosophy](#testing-philosophy) +2. [Testing Pyramid](#testing-pyramid) +3. [Unit Testing](#unit-testing) +4. [Widget Testing](#widget-testing) +5. [Integration Testing](#integration-testing) +6. [End-to-End Testing](#end-to-end-testing) +7. [Performance Testing](#performance-testing) +8. [Testing Tools & Dependencies](#testing-tools--dependencies) +9. [Test Organization](#test-organization) +10. [Mocking Strategies](#mocking-strategies) +11. [CI/CD Integration](#cicd-integration) +12. [Best Practices](#best-practices) + +## Testing Philosophy + +### Core Principles + +1. **Test-Driven Development (TDD)**: Write tests before implementation +2. **Fail Fast**: Tests should catch issues early in development +3. **Maintainable Tests**: Tests should be easy to read, update, and debug +4. **Comprehensive Coverage**: Aim for >90% test coverage across all layers +5. **Real-World Scenarios**: Tests should reflect actual user behavior + +### Testing Goals for Helix + +- **Reliability**: Ensure AI analysis features work consistently +- **Performance**: Verify real-time audio processing meets requirements +- **Integration**: Test Bluetooth glasses connectivity thoroughly +- **User Experience**: Validate smooth UI interactions and state management +- **Data Integrity**: Ensure conversation data is handled securely + +## Testing Pyramid + +``` + /\ + / \ E2E Tests (5-10%) + /____\ • Full user workflows + / \ • Critical business scenarios +/________\ • Cross-platform validation + +/ \ Integration Tests (20-30%) +/____________\ • Service interactions +/ \ • API integrations +/________________\ • State management flows + +/ \ Unit Tests (60-70%) +/____________________\ • Business logic +/ \ • Data models +/________________________\ • Service methods +``` + +## Unit Testing + +### What to Test + +#### Core Services +- **AudioService**: Recording, playback, noise reduction +- **TranscriptionService**: Speech-to-text conversion, confidence scoring +- **LLMService**: AI analysis, fact-checking, sentiment analysis +- **GlassesService**: Bluetooth connectivity, HUD rendering +- **SettingsService**: Configuration persistence, validation + +#### Data Models +- **Freezed Models**: Serialization, equality, copyWith methods +- **Validation Logic**: Input sanitization, business rules +- **Transformations**: Data mapping, formatting + +#### Utilities +- **Extensions**: String formatting, date utilities +- **Constants**: Configuration values, validation rules +- **Helper Functions**: Calculations, conversions + +### Unit Testing Structure + +```dart +// test/services/audio_service_test.dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:flutter_helix/services/audio_service.dart'; + +void main() { + group('AudioService', () { + late AudioService audioService; + late MockFlutterSound mockFlutterSound; + + setUp(() { + mockFlutterSound = MockFlutterSound(); + audioService = AudioServiceImpl(mockFlutterSound); + }); + + tearDown(() { + audioService.dispose(); + }); + + group('Recording', () { + test('should start recording with correct configuration', () async { + // Arrange + when(mockFlutterSound.startRecorder()).thenAnswer((_) async => null); + + // Act + await audioService.startRecording(); + + // Assert + verify(mockFlutterSound.startRecorder()).called(1); + expect(audioService.isRecording, isTrue); + }); + + test('should handle recording errors gracefully', () async { + // Arrange + when(mockFlutterSound.startRecorder()) + .thenThrow(Exception('Microphone permission denied')); + + // Act & Assert + expect( + () async => await audioService.startRecording(), + throwsA(isA()), + ); + }); + }); + + group('Audio Processing', () { + test('should apply noise reduction when enabled', () async { + // Arrange + final audioData = generateTestAudioData(); + + // Act + final processedData = await audioService.processAudio( + audioData, + enableNoiseReduction: true, + ); + + // Assert + expect(processedData.length, equals(audioData.length)); + expect(processedData, isNot(equals(audioData))); // Should be modified + }); + }); + }); +} +``` + +### Unit Testing Best Practices + +1. **AAA Pattern**: Arrange, Act, Assert +2. **Single Responsibility**: One test per behavior +3. **Descriptive Names**: Clear test descriptions +4. **Independent Tests**: No dependencies between tests +5. **Mock External Dependencies**: Database, APIs, platform channels + +## Widget Testing + +### What to Test + +#### UI Components +- **Custom Widgets**: FactCheckCard, ConversationCard, SentimentCard +- **State Management**: Provider updates, UI rebuilds +- **User Interactions**: Taps, scrolling, form submissions +- **Animations**: Controller states, transition behaviors + +#### Screen-Level Testing +- **Tab Navigation**: HomeScreen tab switching +- **Form Validation**: Settings forms, API key inputs +- **Error States**: Network failures, permission denials +- **Loading States**: Shimmer effects, progress indicators + +### Widget Testing Structure + +```dart +// test/widgets/conversation_tab_test.dart +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_helix/ui/widgets/conversation_tab.dart'; + +void main() { + group('ConversationTab', () { + Widget createWidgetUnderTest() { + return MaterialApp( + home: MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => MockAudioService(), + ), + ChangeNotifierProvider( + create: (_) => MockTranscriptionService(), + ), + ], + child: const ConversationTab(), + ), + ); + } + + testWidgets('displays empty state when no conversation', (tester) async { + // Arrange + await tester.pumpWidget(createWidgetUnderTest()); + + // Act + await tester.pump(); + + // Assert + expect(find.text('Ready to Record'), findsOneWidget); + expect(find.byIcon(Icons.graphic_eq), findsOneWidget); + }); + + testWidgets('starts recording when microphone button tapped', (tester) async { + // Arrange + await tester.pumpWidget(createWidgetUnderTest()); + + // Act + await tester.tap(find.byIcon(Icons.mic)); + await tester.pump(); + + // Assert + expect(find.byIcon(Icons.stop), findsOneWidget); + // Verify provider state change + final audioService = Provider.of( + tester.element(find.byType(ConversationTab)), + listen: false, + ); + expect(audioService.isRecording, isTrue); + }); + + testWidgets('displays transcription segments correctly', (tester) async { + // Arrange + final mockTranscriptionService = MockTranscriptionService(); + when(mockTranscriptionService.segments).thenReturn([ + TranscriptionSegment( + speaker: 'You', + text: 'Hello world', + timestamp: DateTime.now(), + confidence: 0.95, + ), + ]); + + await tester.pumpWidget(createWidgetUnderTest()); + + // Act + await tester.pump(); + + // Assert + expect(find.text('Hello world'), findsOneWidget); + expect(find.text('95%'), findsOneWidget); // Confidence badge + }); + }); +} +``` + +### Widget Testing Best Practices + +1. **Test Widget Contracts**: Verify expected widgets are present +2. **Interaction Testing**: Simulate user gestures and inputs +3. **State Verification**: Check provider/state changes +4. **Accessibility**: Verify semantic labels and navigation +5. **Visual Regression**: Compare golden files for complex UIs + +## Integration Testing + +### What to Test + +#### Service Integration +- **Audio → Transcription**: Audio data flows to speech recognition +- **Transcription → LLM**: Text analysis pipeline +- **LLM → UI**: Analysis results display correctly +- **Settings → Services**: Configuration changes propagate + +#### Platform Integration +- **Bluetooth**: Glasses connection and communication +- **Permissions**: Microphone, location, Bluetooth access +- **Storage**: SharedPreferences persistence +- **Network**: API calls and error handling + +### Integration Testing Structure + +```dart +// integration_test/app_test.dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:flutter_helix/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Helix App Integration Tests', () { + testWidgets('complete conversation workflow', (tester) async { + // Arrange + app.main(); + await tester.pumpAndSettle(); + + // Navigate to conversation tab + await tester.tap(find.byIcon(Icons.mic)); + await tester.pumpAndSettle(); + + // Start recording + await tester.tap(find.byIcon(Icons.mic)); + await tester.pumpAndSettle(const Duration(seconds: 1)); + + // Verify recording state + expect(find.byIcon(Icons.stop), findsOneWidget); + + // Stop recording + await tester.tap(find.byIcon(Icons.stop)); + await tester.pumpAndSettle(); + + // Verify transcription appears + expect(find.text('Transcribing...'), findsOneWidget); + + // Wait for AI analysis + await tester.pumpAndSettle(const Duration(seconds: 5)); + + // Navigate to analysis tab + await tester.tap(find.text('Analysis')); + await tester.pumpAndSettle(); + + // Verify analysis results + expect(find.text('Facts'), findsOneWidget); + expect(find.text('Summary'), findsOneWidget); + }); + + testWidgets('glasses connection workflow', (tester) async { + // Arrange + app.main(); + await tester.pumpAndSettle(); + + // Navigate to glasses tab + await tester.tap(find.text('Glasses')); + await tester.pumpAndSettle(); + + // Start device scan + await tester.tap(find.text('Scan for Devices')); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + // Verify devices found + expect(find.text('Even Realities G1'), findsOneWidget); + + // Connect to device + await tester.tap(find.text('Connect')); + await tester.pumpAndSettle(const Duration(seconds: 5)); + + // Verify connection success + expect(find.text('Connected'), findsOneWidget); + expect(find.text('85%'), findsOneWidget); // Battery level + }); + }); +} +``` + +### Integration Testing Best Practices + +1. **Real Dependencies**: Use actual services when possible +2. **Environment Setup**: Consistent test data and configuration +3. **Timing Considerations**: Proper waits for async operations +4. **Cleanup**: Reset state between tests +5. **Platform Differences**: Test iOS and Android separately + +## End-to-End Testing + +### What to Test + +#### Critical User Journeys +1. **New User Onboarding**: First-time setup and configuration +2. **Conversation Recording**: Complete audio → analysis workflow +3. **Glasses Setup**: Pairing and HUD configuration +4. **Settings Management**: API keys, preferences, export + +#### Business-Critical Scenarios +- **AI Analysis Accuracy**: Verify fact-checking results +- **Data Persistence**: Settings and conversation history +- **Error Recovery**: Network failures, permission denials +- **Performance**: Real-time transcription latency + +### E2E Testing Structure + +```dart +// test_driver/app_test.dart +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +void main() { + group('Helix E2E Tests', () { + late FlutterDriver driver; + + setUpAll(() async { + driver = await FlutterDriver.connect(); + }); + + tearDownAll(() async { + await driver.close(); + }); + + test('complete user journey from setup to analysis', () async { + // First launch - onboarding + await driver.waitFor(find.text('Welcome to Helix')); + await driver.tap(find.text('Get Started')); + + // API key setup + await driver.waitFor(find.text('Setup')); + await driver.tap(find.byValueKey('openai_key_field')); + await driver.enterText('sk-test-key'); + await driver.tap(find.text('Continue')); + + // Permission requests + await driver.waitFor(find.text('Permissions')); + await driver.tap(find.text('Grant Microphone Access')); + await driver.tap(find.text('Grant Bluetooth Access')); + + // Main app - conversation + await driver.waitFor(find.text('Live Conversation')); + await driver.tap(find.byValueKey('record_button')); + + // Simulate 5 seconds of recording + await Future.delayed(const Duration(seconds: 5)); + await driver.tap(find.byValueKey('stop_button')); + + // Wait for transcription + await driver.waitFor(find.text('Transcription complete')); + + // Check analysis results + await driver.tap(find.text('Analysis')); + await driver.waitFor(find.text('Fact Check')); + + // Verify fact check card appears + await driver.waitFor(find.byType('FactCheckCard')); + + // Export functionality + await driver.tap(find.byValueKey('export_button')); + await driver.tap(find.text('Export as PDF')); + await driver.waitFor(find.text('Export complete')); + }); + }); +} +``` + +## Performance Testing + +### What to Test + +#### Performance Metrics +- **Memory Usage**: Monitor during long recordings +- **CPU Usage**: Real-time audio processing efficiency +- **Battery Impact**: Background processing optimization +- **Network Usage**: API call efficiency + +#### Performance Testing Tools + +```dart +// test/performance/audio_performance_test.dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_helix/services/audio_service.dart'; + +void main() { + group('Audio Performance Tests', () { + test('memory usage stays stable during long recording', () async { + final audioService = AudioServiceImpl(); + final memoryUsage = []; + + await audioService.startRecording(); + + // Monitor memory every second for 5 minutes + for (int i = 0; i < 300; i++) { + await Future.delayed(const Duration(seconds: 1)); + memoryUsage.add(getCurrentMemoryUsage()); + } + + await audioService.stopRecording(); + + // Verify memory growth is within acceptable limits + final maxIncrease = memoryUsage.last - memoryUsage.first; + expect(maxIncrease, lessThan(50 * 1024 * 1024)); // 50MB max increase + }); + + test('transcription latency meets requirements', () async { + final transcriptionService = TranscriptionServiceImpl(); + final audioData = generateTestAudioData(duration: 10); // 10 seconds + + final stopwatch = Stopwatch()..start(); + + await transcriptionService.transcribeAudio(audioData); + + stopwatch.stop(); + + // Transcription should complete within 2x real-time + expect(stopwatch.elapsedMilliseconds, lessThan(20000)); // 20 seconds max + }); + }); +} +``` + +## Testing Tools & Dependencies + +### Essential Testing Packages + +```yaml +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + + # Mocking + mockito: ^5.4.2 + build_runner: ^2.4.7 + + # Widget Testing + golden_toolkit: ^0.15.0 + patrol: ^3.0.0 + + # Performance Testing + flutter_driver: + sdk: flutter + + # Code Coverage + coverage: ^1.6.0 + + # Test Utilities + fake_async: ^1.3.1 + clock: ^1.1.1 +``` + +### Test Configuration + +```dart +// test/test_helpers.dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:flutter_helix/services/services.dart'; + +// Generate mocks +@GenerateMocks([ + AudioService, + TranscriptionService, + LLMService, + GlassesService, + SettingsService, +]) +void main() {} + +// Test utilities +class TestHelpers { + static Widget createApp({List children = const []}) { + return MaterialApp( + home: MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => MockAudioService(), + ), + // ... other providers + ], + child: Scaffold(body: Column(children: children)), + ), + ); + } + + static TranscriptionSegment createTestSegment({ + String text = 'Test text', + double confidence = 0.95, + }) { + return TranscriptionSegment( + speaker: 'Test Speaker', + text: text, + timestamp: DateTime.now(), + confidence: confidence, + ); + } +} +``` + +## Test Organization + +### Directory Structure + +``` +test/ +├── unit/ +│ ├── services/ +│ │ ├── audio_service_test.dart +│ │ ├── transcription_service_test.dart +│ │ ├── llm_service_test.dart +│ │ └── glasses_service_test.dart +│ ├── models/ +│ │ ├── transcription_segment_test.dart +│ │ └── analysis_result_test.dart +│ └── utils/ +│ ├── extensions_test.dart +│ └── validators_test.dart +├── widget/ +│ ├── tabs/ +│ │ ├── conversation_tab_test.dart +│ │ ├── analysis_tab_test.dart +│ │ └── settings_tab_test.dart +│ ├── cards/ +│ │ ├── fact_check_card_test.dart +│ │ └── conversation_card_test.dart +│ └── screens/ +│ └── home_screen_test.dart +├── integration/ +│ ├── audio_pipeline_test.dart +│ ├── ai_analysis_test.dart +│ └── glasses_connection_test.dart +├── e2e/ +│ ├── user_journeys_test.dart +│ └── performance_test.dart +├── mocks/ +│ └── test_mocks.dart +└── test_helpers.dart + +integration_test/ +├── app_test.dart +└── performance_test.dart +``` + +## Mocking Strategies + +### Service Mocking + +```dart +// test/mocks/mock_services.dart +class MockAudioService extends Mock implements AudioService { + @override + Stream get audioLevelStream => Stream.value(AudioLevel(0.5)); + + @override + bool get isRecording => false; + + @override + Future startRecording() async { + // Mock implementation + return Future.value(); + } +} + +class MockLLMService extends Mock implements LLMService { + @override + Future analyzeConversation(String text) async { + return AnalysisResult( + summary: 'Mock summary', + factChecks: [], + sentiment: SentimentType.positive, + confidence: 0.9, + ); + } +} +``` + +### Platform Channel Mocking + +```dart +// test/mocks/platform_mocks.dart +class PlatformMocks { + static void setupAudioSessionMocks() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('com.ryanheise.audio_session'), + (MethodCall methodCall) async { + switch (methodCall.method) { + case 'setActive': + return true; + case 'setCategory': + return null; + default: + return null; + } + }, + ); + } + + static void setupBluetoothMocks() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('flutter_blue_plus'), + (MethodCall methodCall) async { + switch (methodCall.method) { + case 'startScan': + return null; + case 'getAdapterState': + return 'on'; + default: + return null; + } + }, + ); + } +} +``` + +## CI/CD Integration + +### GitHub Actions Configuration + +```yaml +# .github/workflows/test.yml +name: Test + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.19.0' + channel: 'stable' + + - name: Install dependencies + run: flutter pub get + + - name: Run tests + run: flutter test --coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: coverage/lcov.info + + - name: Run integration tests + run: flutter test integration_test/ + + build: + runs-on: macos-latest + needs: test + + steps: + - uses: actions/checkout@v3 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.19.0' + channel: 'stable' + + - name: Build iOS + run: flutter build ios --no-codesign + + - name: Build Android + run: flutter build apk --debug +``` + +### Test Coverage Configuration + +```yaml +# analysis_options.yaml +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "test/**" + +linter: + rules: + - prefer_const_constructors + - avoid_print + - prefer_single_quotes + +coverage: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + - "lib/main.dart" + target: 90 +``` + +## Best Practices + +### General Testing Guidelines + +1. **Test Naming Convention** + ```dart + test('should return valid result when input is correct', () {}); + test('should throw exception when input is null', () {}); + test('should update UI when state changes', () {}); + ``` + +2. **Test Data Management** + ```dart + // Use factories for consistent test data + class TestDataFactory { + static TranscriptionSegment createSegment({ + String? text, + double? confidence, + }) { + return TranscriptionSegment( + speaker: 'Test Speaker', + text: text ?? 'Default test text', + timestamp: DateTime.now(), + confidence: confidence ?? 0.95, + ); + } + } + ``` + +3. **Async Testing** + ```dart + test('should handle async operations correctly', () async { + // Use async/await for Future-based operations + final result = await service.performAsyncOperation(); + expect(result, isNotNull); + + // Use expectAsync for Stream testing + service.dataStream.listen( + expectAsync1((data) { + expect(data, isA()); + }), + ); + }); + ``` + +4. **Error Testing** + ```dart + test('should handle errors gracefully', () async { + // Test expected exceptions + expect( + () async => await service.invalidOperation(), + throwsA(isA()), + ); + + // Test error states + when(mockService.getData()).thenThrow(Exception('Network error')); + final result = await serviceUnderTest.handleDataRetrieval(); + expect(result.hasError, isTrue); + }); + ``` + +### Flutter-Specific Best Practices + +1. **Widget Testing Patterns** + ```dart + testWidgets('should rebuild when provider notifies', (tester) async { + final notifier = ValueNotifier('initial'); + + await tester.pumpWidget( + ValueListenableBuilder( + valueListenable: notifier, + builder: (context, value, child) => Text(value), + ), + ); + + expect(find.text('initial'), findsOneWidget); + + notifier.value = 'updated'; + await tester.pump(); + + expect(find.text('updated'), findsOneWidget); + }); + ``` + +2. **State Management Testing** + ```dart + test('provider notifies listeners when state changes', () { + final provider = ConversationProvider(); + bool wasNotified = false; + + provider.addListener(() { + wasNotified = true; + }); + + provider.addSegment(TestDataFactory.createSegment()); + + expect(wasNotified, isTrue); + expect(provider.segments.length, equals(1)); + }); + ``` + +3. **Performance Testing Guidelines** + ```dart + testWidgets('should not rebuild unnecessarily', (tester) async { + int buildCount = 0; + + await tester.pumpWidget( + Builder( + builder: (context) { + buildCount++; + return const Text('Test'); + }, + ), + ); + + expect(buildCount, equals(1)); + + // Trigger state change that shouldn't affect this widget + await tester.pump(); + + expect(buildCount, equals(1)); // Should not rebuild + }); + ``` + +### Testing Checklist + +#### Before Writing Tests +- [ ] Understand the requirements and expected behavior +- [ ] Identify edge cases and error conditions +- [ ] Plan test data and mock strategies +- [ ] Consider performance implications + +#### During Test Development +- [ ] Write descriptive test names and comments +- [ ] Follow AAA pattern (Arrange, Act, Assert) +- [ ] Test one behavior per test case +- [ ] Mock external dependencies appropriately +- [ ] Include both positive and negative test cases + +#### After Writing Tests +- [ ] Verify tests pass consistently +- [ ] Check code coverage metrics +- [ ] Review test maintainability +- [ ] Document complex test scenarios +- [ ] Integrate with CI/CD pipeline + +## Conclusion + +This comprehensive testing strategy ensures the Helix app maintains high quality standards throughout development. By following these guidelines and implementing the suggested test structure, the team can deliver a reliable, performant, and maintainable Flutter application. + +Regular review and updates of this testing strategy will help adapt to new Flutter features, testing tools, and project requirements as the Helix app evolves. \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Profile.xcconfig b/ios/Flutter/Profile.xcconfig new file mode 100644 index 0000000..d5f6074 --- /dev/null +++ b/ios/Flutter/Profile.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig" +#include "Generated.xcconfig" \ No newline at end of file diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..84a210c --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,62 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + + target.build_configurations.each do |config| + # Permission handler macros + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.microphone + 'PERMISSION_MICROPHONE=1', + + ## dart: PermissionGroup.speech + 'PERMISSION_SPEECH_RECOGNIZER=1', + + ## dart: PermissionGroup.bluetooth + 'PERMISSION_BLUETOOTH=1', + + ## dart: PermissionGroup.location + 'PERMISSION_LOCATION=1', + ] + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..bb51755 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,78 @@ +PODS: + - audio_session (0.0.1): + - Flutter + - Flutter (1.0.0) + - flutter_blue_plus_darwin (0.0.2): + - Flutter + - FlutterMacOS + - flutter_sound (9.28.0): + - Flutter + - flutter_sound_core (= 9.28.0) + - flutter_sound_core (9.28.0) + - integration_test (0.0.1): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.1.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - speech_to_text (0.0.1): + - Flutter + - Try + - Try (2.1.1) + +DEPENDENCIES: + - audio_session (from `.symlinks/plugins/audio_session/ios`) + - Flutter (from `Flutter`) + - flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - flutter_sound (from `.symlinks/plugins/flutter_sound/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - speech_to_text (from `.symlinks/plugins/speech_to_text/ios`) + +SPEC REPOS: + trunk: + - flutter_sound_core + - Try + +EXTERNAL SOURCES: + audio_session: + :path: ".symlinks/plugins/audio_session/ios" + Flutter: + :path: Flutter + flutter_blue_plus_darwin: + :path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin" + flutter_sound: + :path: ".symlinks/plugins/flutter_sound/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + speech_to_text: + :path: ".symlinks/plugins/speech_to_text/ios" + +SPEC CHECKSUMS: + audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 + flutter_sound: b9236a5875299aaa4cef1690afd2f01d52a3f890 + flutter_sound_core: 427465f72d07ab8c3edbe8ffdde709ddacd3763c + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 3787117e48f80715ff04a3830ca039283d6a4f29 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + speech_to_text: ae2abc312e619ff1c53e675a9fc4d785a15c03bb + Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96 + +PODFILE CHECKSUM: 0cd8857e7c5a329325a3692d99cf079dcc94db58 + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c2984eb --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* 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 */; }; + 53F9A0B8243B85618DA557F4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52AC5901D077FEB3BE614CC0 /* Pods_RunnerTests.framework */; }; + 5D0037F9350C546173FAF1C1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E519E142C56944927508E061 /* Pods_Runner.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 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 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0482BD6B23B939FD450E7FF0 /* 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 = ""; }; + 0C14379E580C3D395D4E97D0 /* 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 = ""; }; + 1176BF11AA8CF78DCAECC9FA /* 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 = ""; }; + 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 = ""; }; + 52AC5901D077FEB3BE614CC0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 810BDFEE5F7E427A489D21ED /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 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 = ""; }; + 9B21BA19B9A9D788FFC4C90A /* 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 = ""; }; + E519E142C56944927508E061 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EC3FD102EACFEE047F67C438 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5D0037F9350C546173FAF1C1 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97CEC98A272E4185026F1327 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 53F9A0B8243B85618DA557F4 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 82A203824349BC40257FEF3C /* Frameworks */ = { + isa = PBXGroup; + children = ( + E519E142C56944927508E061 /* Pods_Runner.framework */, + 52AC5901D077FEB3BE614CC0 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + A606CC70B8088153684DFC2B /* Pods */, + 82A203824349BC40257FEF3C /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + A606CC70B8088153684DFC2B /* Pods */ = { + isa = PBXGroup; + children = ( + 1176BF11AA8CF78DCAECC9FA /* Pods-Runner.debug.xcconfig */, + 0C14379E580C3D395D4E97D0 /* Pods-Runner.release.xcconfig */, + EC3FD102EACFEE047F67C438 /* Pods-Runner.profile.xcconfig */, + 0482BD6B23B939FD450E7FF0 /* Pods-RunnerTests.debug.xcconfig */, + 9B21BA19B9A9D788FFC4C90A /* Pods-RunnerTests.release.xcconfig */, + 810BDFEE5F7E427A489D21ED /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 1993F59401010F22913DB33A /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 97CEC98A272E4185026F1327 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 7AA5B2B8E91CB3F0724830E2 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + DCBB0FC873D06E54504A51FB /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1993F59401010F22913DB33A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 7AA5B2B8E91CB3F0724830E2 /* [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; + }; + 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"; + }; + DCBB0FC873D06E54504A51FB /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 8P9N7B6QE8; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0482BD6B23B939FD450E7FF0 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9B21BA19B9A9D788FFC4C90A /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 810BDFEE5F7E427A489D21ED /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 8P9N7B6QE8; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 8P9N7B6QE8; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/Helix.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Helix.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Helix.xcodeproj/xcuserdata/ajiang2.xcuserdatad/xcschemes/xcschememanagement.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 55% rename from Helix.xcodeproj/xcuserdata/ajiang2.xcuserdatad/xcschemes/xcschememanagement.plist rename to ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 47f2ffd..18d9810 100644 --- a/Helix.xcodeproj/xcuserdata/ajiang2.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -2,13 +2,7 @@ - SchemeUserState - - Helix.xcscheme_^#shared#^_ - - orderHint - 0 - - + IDEDidComputeMac32BitWarning + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..15cada4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..6266644 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..80b4909 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/DebugProfile.entitlements b/ios/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..ceb7deb --- /dev/null +++ b/ios/Runner/DebugProfile.entitlements @@ -0,0 +1,27 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + + + com.apple.security.device.microphone + + com.apple.security.device.audio-input + + + + com.apple.security.network.client + + + + com.apple.security.device.bluetooth + + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..aba1b73 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,104 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flutter Helix + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_helix + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + + UISceneStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + + + + NSMicrophoneUsageDescription + Helix needs microphone access to transcribe conversations and provide real-time AI analysis on your Even Realities glasses. + + + NSSpeechRecognitionUsageDescription + Helix uses speech recognition to provide real-time transcription and AI-powered conversation insights. + + + NSBluetoothAlwaysUsageDescription + Helix needs Bluetooth access to connect to your Even Realities smart glasses and display AI insights on the HUD. + NSBluetoothPeripheralUsageDescription + Helix connects to Even Realities smart glasses via Bluetooth to provide real-time conversation analysis and HUD display. + + + UIBackgroundModes + + background-processing + bluetooth-central + audio + + + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSExceptionDomains + + api.openai.com + + NSExceptionRequiresForwardSecrecy + + NSExceptionMinimumTLSVersion + TLSv1.2 + + api.anthropic.com + + NSExceptionRequiresForwardSecrecy + + NSExceptionMinimumTLSVersion + TLSv1.2 + + + + + diff --git a/ios/Runner/Release.entitlements b/ios/Runner/Release.entitlements new file mode 100644 index 0000000..91e869c --- /dev/null +++ b/ios/Runner/Release.entitlements @@ -0,0 +1,21 @@ + + + + + + + + com.apple.security.device.microphone + + com.apple.security.device.audio-input + + + + com.apple.security.network.client + + + + com.apple.security.device.bluetooth + + + \ No newline at end of file diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/ios/build/ios/XCBuildData/PIFCache/project/PROJECT@v11_mod=e586e7129ff5f2e6f341c4426f9dfb34_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1-json b/ios/build/ios/XCBuildData/PIFCache/project/PROJECT@v11_mod=e586e7129ff5f2e6f341c4426f9dfb34_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1-json new file mode 100644 index 0000000..c78060c --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/project/PROJECT@v11_mod=e586e7129ff5f2e6f341c4426f9dfb34_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1-json @@ -0,0 +1 @@ +{"appPreferencesBuildSettings":{},"buildConfigurations":[{"buildSettings":{"ALWAYS_SEARCH_USER_PATHS":"NO","CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED":"YES","CLANG_ANALYZER_NONNULL":"YES","CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION":"YES_AGGRESSIVE","CLANG_CXX_LANGUAGE_STANDARD":"gnu++14","CLANG_CXX_LIBRARY":"libc++","CLANG_ENABLE_MODULES":"YES","CLANG_ENABLE_OBJC_ARC":"YES","CLANG_ENABLE_OBJC_WEAK":"YES","CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING":"YES","CLANG_WARN_BOOL_CONVERSION":"YES","CLANG_WARN_COMMA":"YES","CLANG_WARN_CONSTANT_CONVERSION":"YES","CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS":"YES","CLANG_WARN_DIRECT_OBJC_ISA_USAGE":"YES_ERROR","CLANG_WARN_DOCUMENTATION_COMMENTS":"YES","CLANG_WARN_EMPTY_BODY":"YES","CLANG_WARN_ENUM_CONVERSION":"YES","CLANG_WARN_INFINITE_RECURSION":"YES","CLANG_WARN_INT_CONVERSION":"YES","CLANG_WARN_NON_LITERAL_NULL_CONVERSION":"YES","CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF":"YES","CLANG_WARN_OBJC_LITERAL_CONVERSION":"YES","CLANG_WARN_OBJC_ROOT_CLASS":"YES_ERROR","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"YES","CLANG_WARN_RANGE_LOOP_ANALYSIS":"YES","CLANG_WARN_STRICT_PROTOTYPES":"YES","CLANG_WARN_SUSPICIOUS_MOVE":"YES","CLANG_WARN_UNGUARDED_AVAILABILITY":"YES_AGGRESSIVE","CLANG_WARN_UNREACHABLE_CODE":"YES","CLANG_WARN__DUPLICATE_METHOD_MATCH":"YES","COPY_PHASE_STRIP":"NO","DEBUG_INFORMATION_FORMAT":"dwarf","ENABLE_STRICT_OBJC_MSGSEND":"YES","ENABLE_TESTABILITY":"YES","GCC_C_LANGUAGE_STANDARD":"gnu11","GCC_DYNAMIC_NO_PIC":"NO","GCC_NO_COMMON_BLOCKS":"YES","GCC_OPTIMIZATION_LEVEL":"0","GCC_PREPROCESSOR_DEFINITIONS":"POD_CONFIGURATION_DEBUG=1 DEBUG=1 $(inherited)","GCC_WARN_64_TO_32_BIT_CONVERSION":"YES","GCC_WARN_ABOUT_RETURN_TYPE":"YES_ERROR","GCC_WARN_UNDECLARED_SELECTOR":"YES","GCC_WARN_UNINITIALIZED_AUTOS":"YES_AGGRESSIVE","GCC_WARN_UNUSED_FUNCTION":"YES","GCC_WARN_UNUSED_VARIABLE":"YES","IPHONEOS_DEPLOYMENT_TARGET":"14.0","MTL_ENABLE_DEBUG_INFO":"INCLUDE_SOURCE","MTL_FAST_MATH":"YES","ONLY_ACTIVE_ARCH":"YES","PRODUCT_NAME":"$(TARGET_NAME)","STRIP_INSTALLED_PRODUCT":"NO","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"DEBUG","SWIFT_OPTIMIZATION_LEVEL":"-Onone","SWIFT_VERSION":"5.0","SYMROOT":"${SRCROOT}/../build"},"guid":"bfdfe7dc352907fc980b868725387e98c329620c51892527db69ac984ef9321b","name":"Debug"},{"buildSettings":{"ALWAYS_SEARCH_USER_PATHS":"NO","CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED":"YES","CLANG_ANALYZER_NONNULL":"YES","CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION":"YES_AGGRESSIVE","CLANG_CXX_LANGUAGE_STANDARD":"gnu++14","CLANG_CXX_LIBRARY":"libc++","CLANG_ENABLE_MODULES":"YES","CLANG_ENABLE_OBJC_ARC":"YES","CLANG_ENABLE_OBJC_WEAK":"YES","CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING":"YES","CLANG_WARN_BOOL_CONVERSION":"YES","CLANG_WARN_COMMA":"YES","CLANG_WARN_CONSTANT_CONVERSION":"YES","CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS":"YES","CLANG_WARN_DIRECT_OBJC_ISA_USAGE":"YES_ERROR","CLANG_WARN_DOCUMENTATION_COMMENTS":"YES","CLANG_WARN_EMPTY_BODY":"YES","CLANG_WARN_ENUM_CONVERSION":"YES","CLANG_WARN_INFINITE_RECURSION":"YES","CLANG_WARN_INT_CONVERSION":"YES","CLANG_WARN_NON_LITERAL_NULL_CONVERSION":"YES","CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF":"YES","CLANG_WARN_OBJC_LITERAL_CONVERSION":"YES","CLANG_WARN_OBJC_ROOT_CLASS":"YES_ERROR","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"YES","CLANG_WARN_RANGE_LOOP_ANALYSIS":"YES","CLANG_WARN_STRICT_PROTOTYPES":"YES","CLANG_WARN_SUSPICIOUS_MOVE":"YES","CLANG_WARN_UNGUARDED_AVAILABILITY":"YES_AGGRESSIVE","CLANG_WARN_UNREACHABLE_CODE":"YES","CLANG_WARN__DUPLICATE_METHOD_MATCH":"YES","COPY_PHASE_STRIP":"NO","DEBUG_INFORMATION_FORMAT":"dwarf-with-dsym","ENABLE_NS_ASSERTIONS":"NO","ENABLE_STRICT_OBJC_MSGSEND":"YES","GCC_C_LANGUAGE_STANDARD":"gnu11","GCC_NO_COMMON_BLOCKS":"YES","GCC_PREPROCESSOR_DEFINITIONS":"POD_CONFIGURATION_PROFILE=1 $(inherited)","GCC_WARN_64_TO_32_BIT_CONVERSION":"YES","GCC_WARN_ABOUT_RETURN_TYPE":"YES_ERROR","GCC_WARN_UNDECLARED_SELECTOR":"YES","GCC_WARN_UNINITIALIZED_AUTOS":"YES_AGGRESSIVE","GCC_WARN_UNUSED_FUNCTION":"YES","GCC_WARN_UNUSED_VARIABLE":"YES","IPHONEOS_DEPLOYMENT_TARGET":"14.0","MTL_ENABLE_DEBUG_INFO":"NO","MTL_FAST_MATH":"YES","PRODUCT_NAME":"$(TARGET_NAME)","STRIP_INSTALLED_PRODUCT":"NO","SWIFT_COMPILATION_MODE":"wholemodule","SWIFT_OPTIMIZATION_LEVEL":"-O","SWIFT_VERSION":"5.0","SYMROOT":"${SRCROOT}/../build"},"guid":"bfdfe7dc352907fc980b868725387e986eaba3bbf34fffc52894406988f981b0","name":"Profile"},{"buildSettings":{"ALWAYS_SEARCH_USER_PATHS":"NO","CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED":"YES","CLANG_ANALYZER_NONNULL":"YES","CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION":"YES_AGGRESSIVE","CLANG_CXX_LANGUAGE_STANDARD":"gnu++14","CLANG_CXX_LIBRARY":"libc++","CLANG_ENABLE_MODULES":"YES","CLANG_ENABLE_OBJC_ARC":"YES","CLANG_ENABLE_OBJC_WEAK":"YES","CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING":"YES","CLANG_WARN_BOOL_CONVERSION":"YES","CLANG_WARN_COMMA":"YES","CLANG_WARN_CONSTANT_CONVERSION":"YES","CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS":"YES","CLANG_WARN_DIRECT_OBJC_ISA_USAGE":"YES_ERROR","CLANG_WARN_DOCUMENTATION_COMMENTS":"YES","CLANG_WARN_EMPTY_BODY":"YES","CLANG_WARN_ENUM_CONVERSION":"YES","CLANG_WARN_INFINITE_RECURSION":"YES","CLANG_WARN_INT_CONVERSION":"YES","CLANG_WARN_NON_LITERAL_NULL_CONVERSION":"YES","CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF":"YES","CLANG_WARN_OBJC_LITERAL_CONVERSION":"YES","CLANG_WARN_OBJC_ROOT_CLASS":"YES_ERROR","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"YES","CLANG_WARN_RANGE_LOOP_ANALYSIS":"YES","CLANG_WARN_STRICT_PROTOTYPES":"YES","CLANG_WARN_SUSPICIOUS_MOVE":"YES","CLANG_WARN_UNGUARDED_AVAILABILITY":"YES_AGGRESSIVE","CLANG_WARN_UNREACHABLE_CODE":"YES","CLANG_WARN__DUPLICATE_METHOD_MATCH":"YES","COPY_PHASE_STRIP":"NO","DEBUG_INFORMATION_FORMAT":"dwarf-with-dsym","ENABLE_NS_ASSERTIONS":"NO","ENABLE_STRICT_OBJC_MSGSEND":"YES","GCC_C_LANGUAGE_STANDARD":"gnu11","GCC_NO_COMMON_BLOCKS":"YES","GCC_PREPROCESSOR_DEFINITIONS":"POD_CONFIGURATION_RELEASE=1 $(inherited)","GCC_WARN_64_TO_32_BIT_CONVERSION":"YES","GCC_WARN_ABOUT_RETURN_TYPE":"YES_ERROR","GCC_WARN_UNDECLARED_SELECTOR":"YES","GCC_WARN_UNINITIALIZED_AUTOS":"YES_AGGRESSIVE","GCC_WARN_UNUSED_FUNCTION":"YES","GCC_WARN_UNUSED_VARIABLE":"YES","IPHONEOS_DEPLOYMENT_TARGET":"14.0","MTL_ENABLE_DEBUG_INFO":"NO","MTL_FAST_MATH":"YES","PRODUCT_NAME":"$(TARGET_NAME)","STRIP_INSTALLED_PRODUCT":"NO","SWIFT_COMPILATION_MODE":"wholemodule","SWIFT_OPTIMIZATION_LEVEL":"-O","SWIFT_VERSION":"5.0","SYMROOT":"${SRCROOT}/../build"},"guid":"bfdfe7dc352907fc980b868725387e9804db47a3ceef83edd118018eb43bf272","name":"Release"}],"classPrefix":"","defaultConfigurationName":"Release","developmentRegion":"en","groupTree":{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98d0b25d39b515a574839e998df229c3cb","path":"../Podfile","sourceTree":"SOURCE_ROOT","type":"file"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981e437e97f32342562cc697db30c88ee0","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/audio_session-0.1.25/ios/audio_session/Sources/audio_session/AudioSessionPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98bdbb09bfc18ca7a8115392dd1cd65f0c","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/audio_session-0.1.25/ios/audio_session/Sources/audio_session/DarwinAudioSession.m","sourceTree":"","type":"file"},{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e39bbaf6299ab3c0069356d5feecb338","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/audio_session-0.1.25/ios/audio_session/Sources/audio_session/include/audio_session/AudioSessionPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981bb6171fc9f63ba346ad44c2d834d463","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/audio_session-0.1.25/ios/audio_session/Sources/audio_session/include/audio_session/DarwinAudioSession.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98b8e00215dfd400087f7ce5d3eb337025","name":"audio_session","path":"audio_session","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986fb9bb458393573d39872949a338da82","name":"include","path":"include","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d0ed32f073566a23bee202b4b67c52c7","name":"audio_session","path":"audio_session","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9876227af710c90bab6af48380aa16451c","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d36146b6fca54f65c606e2b798fdb9ad","name":"audio_session","path":"audio_session","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98edb92b22940d9a0f76c1baf75776d3d5","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9848acecdee7881ca16c13c25ea2c0a64a","name":"audio_session","path":"audio_session","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981f14ab50e71919d5c6f2e0986bf93c7a","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e3725db8b03d09b5478a09aedfe092c1","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d4d89d41b422a2b03bdedd451f112693","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f6dfc37e502053e2aca81bd49af2bbc0","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98105d4bdf1b5d6638b771adee120ec4af","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982a1ea5aa7b0dbb311b7231abdc402657","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986ebd88f6e76232a69c5bed6eb6b98726","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987e849f20b4142723966e49dd5db9d400","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984eb8cd27d9e128334d10c481644dc395","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981cdb923b3e5db278cdbedaeedf91ca40","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9814c46f9a29fd62efa2e5d90abc18cd4a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986fecf25cb4b7871e2de81e05ec9296c6","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d68d9b3e00878621b73ecc5bef6d757e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98709673c7e043edd0ae716eb9a17696f5","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98957ae86036e3dd578e4add7835ed3d6d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985e75765b2e59a3ba2c30d82fec97069b","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/audio_session-0.1.25/ios/audio_session/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98b8ef347e3e17336ef80f9880a8eec112","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/audio_session-0.1.25/ios/audio_session.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98aed9018e2afc73992040437b273738e2","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/audio_session-0.1.25/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e983f34f08cefa46b6d59cdacc1fa172268","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98f727fb40a46edf5b6186c79306e14d64","path":"audio_session.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9815355349ea83a29617eaa6a651b7c526","path":"audio_session-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98468fb88e3d3d88eb0d83288036494126","path":"audio_session-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e26c9a59b3e2a49cc0f8df2b2552e31d","path":"audio_session-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9863e909ef67f8e50d1f6b668b9e3471c0","path":"audio_session-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e983c7264ed4a219e1becad19622bc39666","path":"audio_session.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e981afed158e9b2d076136bcfc15512ca52","path":"audio_session.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e980f2e4aaa3c32339c39218ef57d314202","name":"Support Files","path":"../../../../Pods/Target Support Files/audio_session","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9812f698984431f3498d15462a827e87bb","name":"audio_session","path":"../.symlinks/plugins/audio_session/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e982e0a6d7864ca284761826f0be3c20947","path":"Flutter.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e984fa177ca53548dc8175351cf3188fcc5","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9873ed624792dda30c826a3088312775ed","path":"Flutter.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98deb99da4fe35fe63386c3d737157c37f","path":"Flutter.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9892de6ec8f7d63e1df75c84353567d271","name":"Support Files","path":"../Pods/Target Support Files/Flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988222e95ac5c61d67feba12a913cdd140","name":"Flutter","path":"../Flutter","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e97ba4f760a8127267398bdd70d0c239","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_blue_plus_darwin-4.0.1/darwin/flutter_blue_plus_darwin/Sources/flutter_blue_plus_darwin/FlutterBluePlusPlugin.m","sourceTree":"","type":"file"},{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98529da24a3600ab139fc133ec9855f27c","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_blue_plus_darwin-4.0.1/darwin/flutter_blue_plus_darwin/Sources/flutter_blue_plus_darwin/include/flutter_blue_plus_darwin/FlutterBluePlusPlugin.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9818b671f6e8832b9c646671064f5531bf","name":"flutter_blue_plus_darwin","path":"flutter_blue_plus_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987712bab88dab82e83d039c42ec36883f","name":"include","path":"include","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987c6e947b1ac64bae9ff838c6b13f3805","name":"flutter_blue_plus_darwin","path":"flutter_blue_plus_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9813e8359d21f1c3ad007018796e21f75d","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d740a30e1bbc6824fb5a191db318dbfb","name":"flutter_blue_plus_darwin","path":"flutter_blue_plus_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989c5d7cd0e72e8c1abfb94accf5e43670","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98792b5875af5cc8d5cb2f73081cd0b99d","name":"flutter_blue_plus_darwin","path":"flutter_blue_plus_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e2270c0bb830166d8d364c094655c227","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9826bf6c817ffdb4f31401cf2350db516d","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b21741735c9500aaf77907da875de603","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988ad1e232b8705020774cb66f2b0d4cba","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982efaecba031d193251cebaf7b3a4fd14","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98723c60ee0f0b7e74e316c55599ba6ca0","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98364496b22d7964bca15b263af02d5410","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9826f61fb0a378498f3ef121cc147d39c4","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a8df990f07107ee8b5eae74034e189ae","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9872b0b8617ae656ab4e24e106359085b4","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986fcb7037fe5d741d632345de671c9927","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987c7ab4c829e26938f3c7aae9349c2334","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a9ac5a265df4ac172a0fc9af381ec2d9","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980fad0af9c04e64ce0bbb04ffd69d97bb","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987331efebf5abc906f275b96898292070","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98166054b3c8d5b473549f0f6440537c23","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_blue_plus_darwin-4.0.1/darwin/flutter_blue_plus_darwin/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e9856dccdacdcb0fb607d2e286a06e3030d","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_blue_plus_darwin-4.0.1/darwin/flutter_blue_plus_darwin.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98cc7310e2cd97aea061071a076e519d27","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_blue_plus_darwin-4.0.1/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e986ca69f905e05118183639ddf862fd399","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e983d899dad8a7570690097c852a6bd336f","path":"flutter_blue_plus_darwin.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983b433ba26869f5746032c4b4b9d1ac7f","path":"flutter_blue_plus_darwin-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e9881b53c2d03772f5b701c2916c08b3e7f","path":"flutter_blue_plus_darwin-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9886fc09fbd153f8adb3120678d651c488","path":"flutter_blue_plus_darwin-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98224c46c3415999a7e08d26c71434592b","path":"flutter_blue_plus_darwin-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98b9a4a54f9775d27fe806370b1871cf71","path":"flutter_blue_plus_darwin.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98a9302e6415570e7e909a0d0a592b5f53","path":"flutter_blue_plus_darwin.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e982e3ee0a01ad9ee1a3ada4bf300071c0d","name":"Support Files","path":"../../../../Pods/Target Support Files/flutter_blue_plus_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d2dd56d636bb06b556a723805bad3840","name":"flutter_blue_plus_darwin","path":"../.symlinks/plugins/flutter_blue_plus_darwin/darwin","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98449be56911bd1b5ed69eab4a273a27f9","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSound.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e982e9e7b21c8a5834d0107e8e9e0a0c820","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSound.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ca84fd4c6d09392223856e790ced75ce","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9859cc1ff0e1efaa39d4dc07eb74f62de9","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98bf1066473d1662b0642c94695549a22c","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundPlayer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e9897105311a9589a74e9a9add4c60caeb9","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundPlayer.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b394eb96c0adf85e2ae8a34aeeb83e3b","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundPlayerManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e986cf84271f178656b3ecb7e40a6d2a09b","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundPlayerManager.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983946675895f1833af459a1d6a076a55f","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundRecorder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e980a5a5fc6b4c174ede983ad366b1a5ca7","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundRecorder.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982e1ba6a6d30fa544db8e4128cf21ea17","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundRecorderManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98742684df2693fda3da29b96534eaffbe","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/Classes/FlutterSoundRecorderManager.mm","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e983dd0d6d03d4639abeaf1a06f75708a46","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ea9e8ba0d197eaf321bae89971978eeb","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9802813cb55f5d4c1ec12cb03bb63c8eb5","name":"flutter_sound","path":"flutter_sound","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b40661e593d02f72832f3950c9ab0705","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984b6d222d9ca75ae6811bfbaab7d57a3c","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c6b3829124af66b557682890e5a42825","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983c5c17a38c344f02b0df75b19c05255c","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980e00f603299ac22b1e2d983abb9d3a58","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984da796e83515348ed08523671a835e4c","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986f208765e26b8e9eea9124ef32412636","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9807dd25fcf71b809a567457fbc9c25dfb","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c966861e473d0efb7bacd360171b6111","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985bfe53c7a0dcbbb6c454b149577bdbfe","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98676c7e97e58439f4ad38f4db37c03007","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989b41466b690c49f42c3773ad7d7a8e5c","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98dcf7834d1a4927039b59eb0cd9ba4ef3","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980bfeb21c0f40ebf32a7d4b24ad5c3832","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98b60211423187c7d4b80fe81cbf0c9de6","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/ios/flutter_sound.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e9868766dd551996df694c15e254cadc112","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/flutter_sound-9.28.0/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e982ba257e2c88386354bdad9013174455f","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e9808095de57772653ac7c145e685992e68","path":"flutter_sound.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98ef8924cbd3975579f1b16388faf0e29e","path":"flutter_sound-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98f4e7e6d5c1a46c05149fab13d7b6c1e2","path":"flutter_sound-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b72dab5b9efe6ec918914cb62cf3a897","path":"flutter_sound-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989797f772d021291656ea2f5440a1722f","path":"flutter_sound-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98b9b1287bd6938415cc7618feb19c4166","path":"flutter_sound.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98ba9d15bccf639d980c4224aa9d9b8b9c","path":"flutter_sound.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e983b02fe3bf7bcd5eb2d1183f76308cec9","name":"Support Files","path":"../../../../Pods/Target Support Files/flutter_sound","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bb192ce53a6f0cae76623c16bdc07477","name":"flutter_sound","path":"../.symlinks/plugins/flutter_sound/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d7cf642664738ab8cd81b8e8ed5b04dc","path":"../../../../../../../../../../../../../../../opt/homebrew/Caskroom/flutter/3.29.2/flutter/packages/integration_test/ios/integration_test/Sources/integration_test/FLTIntegrationTestRunner.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c54809576d4841b72c6d5eeb0ad72fb1","path":"../../../../../../../../../../../../../../../opt/homebrew/Caskroom/flutter/3.29.2/flutter/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestIosTest.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989c49e041cbb3f061a4073fe8ed08ea6b","path":"../../../../../../../../../../../../../../../opt/homebrew/Caskroom/flutter/3.29.2/flutter/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestPlugin.m","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9849e60a43ced684af8b924d97083fbc62","path":"../../../../../../../../../../../../../../../../opt/homebrew/Caskroom/flutter/3.29.2/flutter/packages/integration_test/ios/integration_test/Sources/integration_test/include/FLTIntegrationTestRunner.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981c21d8e2dd457a139f757dc3c9fe5674","path":"../../../../../../../../../../../../../../../../opt/homebrew/Caskroom/flutter/3.29.2/flutter/packages/integration_test/ios/integration_test/Sources/integration_test/include/IntegrationTestIosTest.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982cf96b2100bac5f4bb84acd3ebc00691","path":"../../../../../../../../../../../../../../../../opt/homebrew/Caskroom/flutter/3.29.2/flutter/packages/integration_test/ios/integration_test/Sources/integration_test/include/IntegrationTestPlugin.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98004ddcd002ac978af306fcde35897c19","name":"include","path":"include","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98da437c5327e399b3fc4d0b54893d3fe0","name":"integration_test","path":"integration_test","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98da74a9446984b8b57b4b902657f3b98c","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a3730cc3c1ef6a36791e4fa0d7b6f44f","name":"integration_test","path":"integration_test","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d53b81847b874516eeff4cc729df6ef8","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98de403322e5f00d78a0065eb0f05e0264","name":"integration_test","path":"integration_test","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e2a8a25925cf8db3f66586346ac04a3e","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c5b602085397744ff9f1018e5a3cca27","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a3d5d882605c3fd16bfe1c918277f165","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98dbe9efade4f981db04a527f49dfb4c0a","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984d73fe9a2c1c37957fe51896bf4d8097","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c0db26060e51189fe0087621a10f2615","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9856be941bcd1288992370a2e87bb2e379","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982deea3999656b14363646c50b51304d6","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9888d068ba3f66c9c9d2f1a37bcb2e1ebb","name":"ajiang2","path":"ajiang2","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98265be29635dccc542bf674a743793f6b","name":"Users","path":"Users","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fb4771a01d7f41cc0814e76d9926eae2","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9830786cea0123e9c02d072a0a1048285b","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cf96355d19fea64fc10eb00ba3fa2d30","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98dd6777b1d9d2a80b9c564b2785990eee","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985f1133571fdad5f0d99cac51e3b85cba","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b6c3a4c46e2800c76915eed7faafd3b4","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e0684c312f4804dd71b1c36d45aeaa41","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985dcb4e18c5694c96727ef4211fe91d19","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9806e6c5ff80d0d58ff54934cae441b739","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983011d7f1c7b64155628d627c12168a4f","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9897a953b03d3a3a89ff3b58713eb6288d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9866218b08cde7687bb77011879090ff09","name":"..","path":"../../../../../../../../../../../../opt/homebrew/Caskroom/flutter/3.29.2/flutter/packages/integration_test/ios/integration_test/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98c36edd171ff7262089d40f27ea1041ea","path":"../../../../../../../../../../../../opt/homebrew/Caskroom/flutter/3.29.2/flutter/packages/integration_test/ios/integration_test.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9805495fd4c215e76789fe81467c63c8b9","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e987532e6c1c4fa433aa028a67320b334c4","path":"integration_test.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98910fb47b442b553bade7ce8ede7f480a","path":"integration_test-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e9809c24f7dfd7735fdf931fb3927096fae","path":"integration_test-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9845772ebdae63b484e729ce3ed5ad5148","path":"integration_test-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b8f419c46e8200fb169e012b14732cef","path":"integration_test-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9852a70497c266c3d2f1246916bc901ca4","path":"integration_test.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98029fa7e50ffe5406e1818f2b55eb7cc2","path":"integration_test.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e982967973f9fb2d6f45c1f9125cd514540","name":"Support Files","path":"../../../../Pods/Target Support Files/integration_test","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988d0c303a4264f0fb8924137045efa85d","name":"integration_test","path":"../.symlinks/plugins/integration_test/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98d66c9a0eca583bf008ee88c953994ed8","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation/Sources/path_provider_foundation/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98348fa7576a5d9f9d315ba8f7503fc057","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980fb8a64878666120814ba6ba67239d3f","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e726860269ec20ad29e7ed01d09b3d5a","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9886b789c4f4273c9abcdeb4fb1e662b87","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b03b513323089fdae2e776c6c2c509de","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989d15f556aa4726f5d1074a1ee82e14c5","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f3ea597f841ba2f31835139ce4df2899","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986ab463085539fa98dfbff87b812e3d66","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987ebef353073a9f0e650c320391c0da8a","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981c1e9ca7a5b3517a368f7eb2105206b3","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e850c6bb7ee0cc37659ebfa12ea82483","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9802b7df71006624d8ee5f13a536f70220","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f3bb774bdf7e971d5375fe795c1f0141","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98810717273fbd3868bd9a6eebd47824c6","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f9832911e8f91900ba729ee8e55358e1","name":"..","path":".","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98b490ba27811b8df8a4daa19187ce372e","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation/Sources/path_provider_foundation/messages.g.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98a72a1cdccce3961f7029e39bc5ef3628","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation/Sources/path_provider_foundation/PathProviderPlugin.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98ee3f436d35162da590cddfbd47b7bc53","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98eaf9012bf84c584f27770511399e451a","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c4656c4763f76ff3487fc50aa0ad35bc","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cceca2dd0672333b83f7745df0841dd9","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989bfd06a782f6ea3818eaed02d8bcca07","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9882b59eb4db87d58824d8f1f584b405bf","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989a38522904c5c7bf4446738902d02702","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f88844de942e49b81da55e9270bfbef0","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9859da2396a2a81f0525df33aa83f33030","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9846198c4fb9959c0caf7f78ee728a3686","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980887629ef64cf6cc0325dfe8442487ea","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988239d6622c58d42e6af1a45d22415281","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c38fc49b93c4bc7fb548f63fc1d41c43","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988afe8afe1032da114334b40ec6e46436","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d762797147ed2ca5b734088473344fb5","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981acdf6df12575a4071381e1b4aa4e74d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981f5410ccfa90cd60cceff85032dfbfa6","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e094dfc032921df7c0441c404c6670e8","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989d8f5e515fef02c6c7f2f34f63b45a90","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9870542f3476d33625075b9f7a48e0a2d7","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985502f77259059e1969fc01ee6ad4753e","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98aa3a81fa53dec0906fe38c85713f2f8b","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e985ded3751831c91fe4f8a6b679e1a7965","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98b0ad5a53814d764e99fd0f29d882b5fc","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e9807c4864884100fd2cdf6413bd08f91fa","path":"path_provider_foundation.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b7ac2fba01f7b5e27013df7c03a25171","path":"path_provider_foundation-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98bcebdfa35f9f7c6a8aaf47bc741ab65e","path":"path_provider_foundation-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a519fdac8af5a1bfa63f038a1b9aad36","path":"path_provider_foundation-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98bd0071a96f2d6e6f55fb2068f6a4f3fc","path":"path_provider_foundation-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e982e28f027f7d56581bc8680624eb50426","path":"path_provider_foundation.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98a0eef8c98746e7ed69f7d625899e19d2","path":"path_provider_foundation.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e980226275449b97ec5fb54303477e560fb","path":"ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e980bb87e0f9b49a47415cef36d3817249c","name":"Support Files","path":"../../../../Pods/Target Support Files/path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980640710241b4750dd85523b742296edc","name":"path_provider_foundation","path":"../.symlinks/plugins/path_provider_foundation/darwin","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98764150597b4b6ba150009052e1ae1a83","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/PermissionHandlerEnums.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a881972e33f903e8ba02952205d4ee65","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/PermissionHandlerPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983d8c7f88e6eb2799d1a05ad59b2c7503","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/PermissionHandlerPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c28fb384571d8d1aabccecb02535284a","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/PermissionManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e082f45c301e538572da89b315ae4ea0","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/PermissionManager.m","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d026c50783a691d70948626b2b786eaa","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/AppTrackingTransparencyPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988b887e1c8d3dcd0152f247269a91e9c8","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/AppTrackingTransparencyPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c703452e02e20da33fb78a7d76714bfa","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/AudioVideoPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e1974f2e1990733512cc1b8a52bc2c12","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/AudioVideoPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9867f0fb56b7a78dfbcf55c67c9bde8371","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/BluetoothPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985cc98e288bc337adee99ea3237c16342","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/BluetoothPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98185074a7563c47ababcc78c609eee4ad","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/ContactPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d3e32716d4c0756ece1470a8b57afa90","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/ContactPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987481cfba29f215c0f2caefcab5b8ad1d","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/CriticalAlertsPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987f34dbbe50686fba6028af12c575a424","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/CriticalAlertsPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ea29fa45ee8af40c59db988047e440c2","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/EventPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983abb904929655d1b7559cb36679cfdd6","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/EventPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c0eb6235e4ac002a45f9a761762f9c88","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/LocationPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982f69903baed716f33a3cdbbf22c580c6","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/LocationPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98eed7a3b81fd72de4bfcbe12f32786c7e","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/MediaLibraryPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d528349dd1e91534d97de92214677020","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/MediaLibraryPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9889de7be23c8c27802d34fae119c63e4b","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/NotificationPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9836c723448053542e92c48aa47c90a78d","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/NotificationPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98853a083978af5c7a1711cd0feae832eb","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/PermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c7a187c2acc20599a580a658eb34906d","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/PhonePermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984c9614e439db528e72ad35d90e2d045c","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/PhonePermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9881a2e788e02e78789972bcb8017ac409","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/PhotoPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b6f005af76d622cb569e2e91c48cc310","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/PhotoPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980875a03066774f59ead8a70cbf26009f","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/SensorPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d7129fb88e10cee83116a5c113f12815","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/SensorPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988a7460408ff3aa54cc4a38db75e32468","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/SpeechPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9870d250d11e910ee003d58b4e31413a00","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/SpeechPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ee9c8df6b27b16d340adf605f8af623b","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/StoragePermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98032376fa5bf3f51bd561b557ecd97906","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/StoragePermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98cfe271c2f11f2011dfd3bef6a393b37d","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/UnknownPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985dc4c9a1e36a7be5549cddf856bd6b46","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/strategies/UnknownPermissionStrategy.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98004e7a3c589ad206eb56aed85cebca01","name":"strategies","path":"strategies","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983b4c31deadbd4729dad472c80aab8249","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/util/Codec.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9868cea9b9d5e2230d90e0c3afa0a32ac4","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/Classes/util/Codec.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9823ad7e5d9f4e067dec3fc44f20e50632","name":"util","path":"util","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98627c7b093afd3379c1ede4ca1c3d92a2","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b422c5273479fd4b6c6bd6761a3473f7","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e14a62cf91877562fb97268b0a689c13","name":"permission_handler_apple","path":"permission_handler_apple","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c7d3e66e629cf85ac3f79bce1ceb0ace","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d4fce1709508a8a4721fe0b1d5613099","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983e9b6946cc7a56fe6574bff378f2154e","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a71473a7812471458dfc24bafae022d2","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984a73502f4bdcca7bc117433489756c98","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9843868a0dbcc8860eed3c1789c5bc7d3b","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9862b529640db3f123983cb5365e07a801","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988a9a71b5825669e034519c7f5a70dbfb","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d1e9097c5ef121bdae6a15d37d6124e7","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fbf36933e5545028468aecd2b446f080","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bc15a67207cba2955907162e24db4bb3","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9880141e47e20e6142bfece5758c023df6","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9856474991dd4cecd36af06bc45ac2d389","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9821b30bb88555de1cc560dd2f8e1582c3","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98dd6d84cd9bb33d1ebea38746678718cc","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e9841f75d2ed7531a1921a4a0acc70f275d","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.1.4/ios/permission_handler_apple.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e980b23a6b11731668b8fc25b8997d8144c","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e9811ecff31631ec065ff4aaf71e7bbf6a7","path":"permission_handler_apple.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b4729fd0b46da590e9865d40b57edb29","path":"permission_handler_apple-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e986546796a89e6c28170ca50d85be697a7","path":"permission_handler_apple-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b35e023afb726adb31069e8801c3cd41","path":"permission_handler_apple-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9871888a285e4b5415b156944e24d21b0a","path":"permission_handler_apple-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98bb902a76c4ca4955d33ed3ba9ad10066","path":"permission_handler_apple.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9826870138bb4d03d3479a06a56fbe0707","path":"permission_handler_apple.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98e0ef4e53239a5df44094d4d4cc7d8f57","name":"Support Files","path":"../../../../Pods/Target Support Files/permission_handler_apple","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9825db32896065153348203edfaed0424b","name":"permission_handler_apple","path":"../.symlinks/plugins/permission_handler_apple/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e9880b44a9a5c2a14c0c2cf09458cdba95e","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation/Sources/shared_preferences_foundation/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98cc6686281fc03495c6963cfa4aca1341","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f26d529e8bfc54f0ce7620dc203fa8c0","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9838d09deeb0070add336e11497117975e","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98005c62ccb21ddb5556714f4f238f3495","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980bd9ae155e3009202cc462bc0506684e","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980751b29c007df7d48218187e78fbc4c9","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98934ab61956b69a560c9b64ebf464ebb6","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980adcf3159a1433724e705b588c098e49","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c52a6d05ee30c4e57af74f1cd50162ff","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c8b706e618a321d31a89844464930137","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d3b94aac95a5eeecc1bd44691ae9323e","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9893a0d3136066006be6eb49c1a3d7e705","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980079d5bf87c68e4a62c47bd9a5245877","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a71577550f332a9e910afe3720a503e7","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98869391a83aca606eeac53ebf83456567","name":"..","path":".","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98702b03cc97ae8c88bec45fb3c9e64aab","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation/Sources/shared_preferences_foundation/messages.g.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98a1bcf16b36f92f148ac66976467a5ed4","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation/Sources/shared_preferences_foundation/SharedPreferencesPlugin.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9805fa80c61c7b6f42320a881ff77e3500","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984fbe0cb8c1d252474304b350d41605f6","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b81cf8e6a776cbab77563ea6d659f7ac","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98808d831773a59ee731939ca43a24828f","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985dd6aba4515cd5b5909c41c836e22c54","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985e78afbb69c0ffec39a0ea107f82d257","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bf0949ea1408551a6c00477930f4259b","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9881e98a3e1b2b5c8bc9ab7948826068f1","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d126bc73a49b7f69547c6646906e3ec0","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983c92940decad9f7b290f97c414889177","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98806f71962f513e170154b94b26e01fb6","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983d75e4c55c22bada843fb6dfd7ebb05d","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986d9cd7969a888153fdee45381294a068","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98778ad51483f58bf2beeaf2f31ea3d6fb","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98040e625a82520ec3ac6d11136aa9c227","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980575b6ff8088f5d6c4dc8ea37f1694b8","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985d62b6dcfcda6edb1397685056d138dc","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983d5716bcb16feba92430b19d1f3834fb","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984c792ca95bc5923dcc6208871bb52d42","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9882268ad9831317f356e0004bdab4b64d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c4f97966f8cdcac7a9e43551643677be","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98e0741ece803abef7aa6459afda93e37b","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e9836ca8e8a0298f1843e247277b5f43d1f","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e988d35c8630ed39f51d6aec23004a3b003","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e989d0a6f780cfa4b7eb13fbb20b9406dbd","path":"ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98c199a9db9f074dd13533faaa651da283","path":"shared_preferences_foundation.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9817b4cbe0cac3815b33710dc4bf3d35c2","path":"shared_preferences_foundation-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e9851a916ea6e2b1832cdf223a6e175b910","path":"shared_preferences_foundation-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9844a52514573c16a9a6767a26df1b662e","path":"shared_preferences_foundation-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983319ef496451d32866fd40181bc59f11","path":"shared_preferences_foundation-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9872edeb19c7aec730cba1f65d9db78214","path":"shared_preferences_foundation.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98c21a4a07e54b4c87e0007412c12d41d9","path":"shared_preferences_foundation.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d450419c63a1efbaca9cc953e58aa9b8","name":"Support Files","path":"../../../../Pods/Target Support Files/shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bb2370fc54c220a2c4dd5765925416db","name":"shared_preferences_foundation","path":"../.symlinks/plugins/shared_preferences_foundation/darwin","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984c3c1aa15953a55f4f7901bae7c671b0","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/speech_to_text-6.6.2/ios/Classes/SpeechToTextPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986adf5cfabe468368f65e9d83acc9eb46","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/speech_to_text-6.6.2/ios/Classes/SpeechToTextPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e980fd17c0e21ffb2cefe7bc24d793880dd","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/speech_to_text-6.6.2/ios/Classes/SwiftSpeechToTextPlugin.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e987276bdf9630e178ce4b7af207e512797","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98072607b1e8e5b30d0acb44e079e32040","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980bac44e401086f0545e2805c19f17be3","name":"speech_to_text","path":"speech_to_text","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985f17c8538668851e4c5b888ec071d4de","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cc14c0a2720ec6218f1f387b712728eb","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98efc777fdafa114b4f66850c368114d1e","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983e6a8b4b657bca99011250dff2b7dda4","name":"audio-service","path":"audio-service","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983783fc7168b7fad1700cc32002f85b37","name":"worktrees","path":"worktrees","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c4f3d065abf7a37424991e78f1d86025","name":"Helix","path":"Helix","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d9ff781817af8318f9a165742c180f14","name":"xcode-projects","path":"xcode-projects","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989fa852b336036b464d419497f1f3fb2a","name":"develop","path":"develop","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ed1db583ff25e0bec3c355eca2b1ff88","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ac0a43799ef7ada2b212bf4cee4cde5d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98890dd3d01bffadb1c65a80d7d1d39580","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986cc72e64d5d8522345ec02def6f4816b","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fbb906adbe9663ee0f12047418f87730","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987682f1e457fdee3178e30fcd4c884e8c","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/speech_to_text-6.6.2/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98521ff92ca78b0f72e0928cd173165396","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/speech_to_text-6.6.2/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98689484d3700e9be1213a5e06d71efca1","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/speech_to_text-6.6.2/ios/speech_to_text.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98fc814c10c8bb6d5384a5b3caed86d40c","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e982f2bba00eb9b50b1afcca44436b32866","path":"speech_to_text.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981d1ff21651613ebb0608d808eca596e0","path":"speech_to_text-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98b03a835e01c8c9fcaa5e88f8a810d355","path":"speech_to_text-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98585b32dcf5f5d8c90bf00abcceed9dc5","path":"speech_to_text-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98de3628f2ddc2d8a80abe7c00305303f9","path":"speech_to_text-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9816b851f75d8f22a4896f859a1b518fa4","path":"speech_to_text.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9896076e833f4e522831fba9e0044a21dd","path":"speech_to_text.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9853cafcd12fe872af11c1687d1d4f81ed","name":"Support Files","path":"../../../../Pods/Target Support Files/speech_to_text","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986a36b76b335b9b29e029ae32506c4235","name":"speech_to_text","path":"../.symlinks/plugins/speech_to_text/ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b766f2389215b7978c51f2fd39b0bc16","name":"Development Pods","path":"","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e9845f00240a9e89e396ca279851b30fabe","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/AVFoundation.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e98886a015712e5147dc288206d0761e2d9","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/CoreBluetooth.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e987cd35d866fc4c4302dc02fe72033285c","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/MediaPlayer.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e9813346ce77eccd8541f866f9742da2351","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/UIKit.framework","sourceTree":"DEVELOPER_DIR","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9862e84377a65e638f44142106010efb54","name":"iOS","path":"","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b9163923ee837c07da085bd144ec1ec3","name":"Frameworks","path":"","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981573e419faf3089f97bdf3c690e693ec","path":"ios/Classes/Flauto.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e980685081bb67df51d3f25df3ebf42cc78","path":"ios/Classes/Flauto.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ed839003e3fd39b928daa2eb9cf90a49","path":"ios/Classes/FlautoPlayer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98829dcc39d62083a3d5c0d426baf6d105","path":"ios/Classes/FlautoPlayer.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989fa19f1cf77b91e4bfa0e758d484774e","path":"ios/Classes/FlautoPlayerEngine.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e9818d4b45bc065a8bea26a912b61c94d69","path":"ios/Classes/FlautoPlayerEngine.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983de0c63a0d3b0d8665c4ebd1180acd0d","path":"ios/Classes/FlautoRecorder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e9801e0d04bc02b987b587207beba2efd66","path":"ios/Classes/FlautoRecorder.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a280167187f3bc2c4fa78c1ce8d505f2","path":"ios/Classes/FlautoRecorderEngine.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98a3a3b79e668136c118ed3683138f61ce","path":"ios/Classes/FlautoRecorderEngine.mm","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98153f2bacc5b6a097bd6bdb96d6c586db","path":"flutter_sound_core.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98bdf4deb813794228c48063766e68b073","path":"flutter_sound_core-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98314e68bc26ef9979ef44a7ffe12ef2bb","path":"flutter_sound_core-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d3bbcaa18bb3a370afc6a2c1a2ce2949","path":"flutter_sound_core-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9879f366ee9c4dd3f68fa31327871d01be","path":"flutter_sound_core-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e982e3a421d0eacab2f0583e0d8f57f11d3","path":"flutter_sound_core.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e987fb8c65a453d7a94dfbcfdaa55f8aede","path":"flutter_sound_core.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98764b94c1c02613009a1cdf90f36ed2f4","name":"Support Files","path":"../Target Support Files/flutter_sound_core","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c78f3dedd9fba2b7f6c409e492501ac6","name":"flutter_sound_core","path":"flutter_sound_core","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98d1aa7c25c99c25d17fa1e72a821e5d6e","path":"Try/trap.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988340fc291ce4022c71f1195488bbb3dc","path":"Try/WBTry.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98796437b3e3041768055dc202475983de","path":"Try/WBTry.m","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e982c75db4ff63620e6890b436ebc643645","path":"Try.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98291a4c0b27555246b0ec894fdea6a342","path":"Try-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98d49947411f0804db05318a0d349eac21","path":"Try-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b1cef1fe3c09a055f63843a4a14dde3c","path":"Try-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989ef4a9ee6be3ac565e7a935d594d2dab","path":"Try-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98d7e84e304ba90d4bbfc82f36a80567e5","path":"Try.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98d1a41b1e0ec8e320d4acb394ecc35fe2","path":"Try.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98531c5015088670da21e643e8899d76bc","name":"Support Files","path":"../Target Support Files/Try","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984934bc92ace5020b92960e70bce7be90","name":"Try","path":"Try","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986c92b8669183d176b958c41d3f2bf2bd","name":"Pods","path":"","sourceTree":"","type":"group"},{"guid":"bfdfe7dc352907fc980b868725387e9846b623d80155f140991fcd4c8c26f94e","name":"Products","path":"","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98d85490c13bb594476aa9be285597497d","path":"Pods-Runner.modulemap","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98c50a2eb9fb28cebb3540daef5d4a8334","path":"Pods-Runner-acknowledgements.markdown","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e987b0bdfb96c434b1bdaf98ff08db5d964","path":"Pods-Runner-acknowledgements.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989c24b47fd1a04f7c3870243d256eb710","path":"Pods-Runner-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.script.sh","guid":"bfdfe7dc352907fc980b868725387e986b56855213c29113cc17d2b495b4605b","path":"Pods-Runner-frameworks.sh","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98850ee204ad70211ee248c6855349a5f9","path":"Pods-Runner-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f67184b23266d4586865ab49f1bd9d8e","path":"Pods-Runner-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e986175ec003efd0925b0e80b27c1a333bb","path":"Pods-Runner.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e981c04a92782605bedf8b5bd015c8dc01a","path":"Pods-Runner.profile.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98ab5e5b11fcf9a2f9f4c2bf61b3a5e465","path":"Pods-Runner.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98dc2407b6e3245e78631c8d5833a16aaa","name":"Pods-Runner","path":"Target Support Files/Pods-Runner","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e983aadeb6c0efc55aa61d4a193c33d1a65","path":"Pods-RunnerTests.modulemap","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98a634232c699d5ed3646d3f024c937ffa","path":"Pods-RunnerTests-acknowledgements.markdown","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e989938b906e3cb2707a2afa9a39150a604","path":"Pods-RunnerTests-acknowledgements.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988d01c1d667722a31ce8e51428338963f","path":"Pods-RunnerTests-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98a85576fdb8cb73c7cd4dd5902a45a27b","path":"Pods-RunnerTests-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f448039d0a832e98acdd3ffd87da1731","path":"Pods-RunnerTests-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98074a9b441078beef16a37aae33ee2900","path":"Pods-RunnerTests.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e987bd2a5c12d5a72dad789e04e26dc5a25","path":"Pods-RunnerTests.profile.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98fc46d149385d28a69f8f8bc860e81763","path":"Pods-RunnerTests.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d5836cf4d97a0c9c99eca09bf2351047","name":"Pods-RunnerTests","path":"Target Support Files/Pods-RunnerTests","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f3d357a58233f32f97cf5aa060ebc8be","name":"Targets Support Files","path":"","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98677e601b37074db53aff90e47c8f96d1","name":"Pods","path":"","sourceTree":"","type":"group"},"guid":"bfdfe7dc352907fc980b868725387e98","path":"/Users/ajiang2/develop/xcode-projects/Helix/worktrees/audio-service/ios/Pods/Pods.xcodeproj","projectDirectory":"/Users/ajiang2/develop/xcode-projects/Helix/worktrees/audio-service/ios/Pods","targets":["TARGET@v11_hash=35821847d896bb5e11dbf7e56f218053","TARGET@v11_hash=ef00dee53b6c04018e669f76e663cf7e","TARGET@v11_hash=b4c7a6a6f6fac140a0bb58992a41be65","TARGET@v11_hash=269ca833703e9c1ecc4799a636a25c46","TARGET@v11_hash=5f9a41f72b9b17bed62972a64b5bcd89","TARGET@v11_hash=0b441df14f4cbf9a8571924f9ce03b03","TARGET@v11_hash=83bb48011e7cbb72ce22a9f7b4ba0c9b","TARGET@v11_hash=7e729c1c163f5dd7877153fb35670149","TARGET@v11_hash=eab96dae2a0065b6b38372c0453d1f07","TARGET@v11_hash=180b65595e3b59eff4dd014e142fe2d0","TARGET@v11_hash=68e2635207846628f8e9c8238abfac79","TARGET@v11_hash=13e73027fcfe07843483de582d954f43","TARGET@v11_hash=1c175e9654d5c06a4fce7296ec983e44","TARGET@v11_hash=f64af7476a3c5da8edff13f61955ad53","TARGET@v11_hash=97b6ace309e306681f0196e5fe3fdad3"]} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=0b441df14f4cbf9a8571924f9ce03b03-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=0b441df14f4cbf9a8571924f9ce03b03-json new file mode 100644 index 0000000..ffec331 --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=0b441df14f4cbf9a8571924f9ce03b03-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9852a70497c266c3d2f1246916bc901ca4","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/integration_test/integration_test-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/integration_test/integration_test-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/integration_test/integration_test.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"integration_test","PRODUCT_NAME":"integration_test","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98e3e4f2c8589c16c2350df7e13df7e1d0","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98029fa7e50ffe5406e1818f2b55eb7cc2","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/integration_test/integration_test-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/integration_test/integration_test-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/integration_test/integration_test.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"integration_test","PRODUCT_NAME":"integration_test","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98004830886de59156a939adebd7a97058","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98029fa7e50ffe5406e1818f2b55eb7cc2","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/integration_test/integration_test-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/integration_test/integration_test-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/integration_test/integration_test.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"integration_test","PRODUCT_NAME":"integration_test","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98f18ee3da4d5ee1b8be785895a101e66d","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e9849e60a43ced684af8b924d97083fbc62","guid":"bfdfe7dc352907fc980b868725387e98d18b48af03d28f0f17b7c956795aeabe","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98b8f419c46e8200fb169e012b14732cef","guid":"bfdfe7dc352907fc980b868725387e98cd4b79f078d7ff3a566e59da9ae5328c","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e981c21d8e2dd457a139f757dc3c9fe5674","guid":"bfdfe7dc352907fc980b868725387e98ee0c4a4caea3d42de9f9c07d6929639e","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e982cf96b2100bac5f4bb84acd3ebc00691","guid":"bfdfe7dc352907fc980b868725387e985b4321158b820b7df555cfbe5060eaeb","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e9862997aa97c710ad60a70d49c58ab3155","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98d7cf642664738ab8cd81b8e8ed5b04dc","guid":"bfdfe7dc352907fc980b868725387e9879ed16c2c0188dfec235b0fa75c8e31e"},{"fileReference":"bfdfe7dc352907fc980b868725387e98910fb47b442b553bade7ce8ede7f480a","guid":"bfdfe7dc352907fc980b868725387e9870fdf761a5e3016e9f53a5c2127f54f5"},{"fileReference":"bfdfe7dc352907fc980b868725387e98c54809576d4841b72c6d5eeb0ad72fb1","guid":"bfdfe7dc352907fc980b868725387e984e18fedae3397ef0e86894158f9d0502"},{"fileReference":"bfdfe7dc352907fc980b868725387e989c49e041cbb3f061a4073fe8ed08ea6b","guid":"bfdfe7dc352907fc980b868725387e98ce4dce39c22e9fd8a570a026355a2de4"}],"guid":"bfdfe7dc352907fc980b868725387e98d687ca8051531872cdfcff63c7941d06","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e9864df96c4baf5d9d52248a5924143d053"},{"fileReference":"bfdfe7dc352907fc980b868725387e9813346ce77eccd8541f866f9742da2351","guid":"bfdfe7dc352907fc980b868725387e98f144d9d0a93da68b66330e0f09ef95c6"}],"guid":"bfdfe7dc352907fc980b868725387e98d1245db48a2b876534b043fd5835fb26","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e980bfed7f0d574e0f434c80641afa9f588","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter"}],"guid":"bfdfe7dc352907fc980b868725387e9809ad3ea68f8eb069e147f62c5d752fe7","name":"integration_test","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C","productReference":{"guid":"bfdfe7dc352907fc980b868725387e980ba8c3e20d4529fa3cbda33b5d3541fa","name":"integration_test.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=13e73027fcfe07843483de582d954f43-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=13e73027fcfe07843483de582d954f43-json new file mode 100644 index 0000000..3d75b33 --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=13e73027fcfe07843483de582d954f43-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9872edeb19c7aec730cba1f65d9db78214","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/shared_preferences_foundation/shared_preferences_foundation-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/shared_preferences_foundation-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","MODULEMAP_FILE":"Target Support Files/shared_preferences_foundation/shared_preferences_foundation.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"shared_preferences_foundation","PRODUCT_NAME":"shared_preferences_foundation","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e986f83bf1d86816a7afe713389f3b0794c","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98c21a4a07e54b4c87e0007412c12d41d9","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/shared_preferences_foundation/shared_preferences_foundation-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/shared_preferences_foundation-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","MODULEMAP_FILE":"Target Support Files/shared_preferences_foundation/shared_preferences_foundation.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"shared_preferences_foundation","PRODUCT_NAME":"shared_preferences_foundation","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98c6ce66678a98cae8c935e06602a448e0","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98c21a4a07e54b4c87e0007412c12d41d9","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/shared_preferences_foundation/shared_preferences_foundation-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/shared_preferences_foundation-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","MODULEMAP_FILE":"Target Support Files/shared_preferences_foundation/shared_preferences_foundation.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"shared_preferences_foundation","PRODUCT_NAME":"shared_preferences_foundation","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e980f59bc0c0df185b08d92e2afa6f35dda","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983319ef496451d32866fd40181bc59f11","guid":"bfdfe7dc352907fc980b868725387e98e82259888cd400660e6ae15b115eb233","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e9845bc282ec8aa7540f3a569c2631d21d5","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98702b03cc97ae8c88bec45fb3c9e64aab","guid":"bfdfe7dc352907fc980b868725387e98c764401149514b2d95620c878e088ca9"},{"fileReference":"bfdfe7dc352907fc980b868725387e9817b4cbe0cac3815b33710dc4bf3d35c2","guid":"bfdfe7dc352907fc980b868725387e98489a95f2019f3ec6e0acd5ba6de8991a"},{"fileReference":"bfdfe7dc352907fc980b868725387e98a1bcf16b36f92f148ac66976467a5ed4","guid":"bfdfe7dc352907fc980b868725387e988fa2861e5294003a4e176171af2095a7"}],"guid":"bfdfe7dc352907fc980b868725387e98f14b5d6b6d6b0c465e2f1e0eaa6bc1cd","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e98475ba5d87573359032e9b06fd466a003"}],"guid":"bfdfe7dc352907fc980b868725387e9859badffc37928e123e98be61f8d11d71","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"guid":"bfdfe7dc352907fc980b868725387e9872e4e537a8c9a8da179493daa4c54b77","targetReference":"bfdfe7dc352907fc980b868725387e98e0be3b0d5ad56f1985578b1f97431765"}],"guid":"bfdfe7dc352907fc980b868725387e9876fd72010a5b056ae41fa1936cd39334","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter"},{"guid":"bfdfe7dc352907fc980b868725387e98e0be3b0d5ad56f1985578b1f97431765","name":"shared_preferences_foundation-shared_preferences_foundation_privacy"}],"guid":"bfdfe7dc352907fc980b868725387e9828cab1f188854e0a973e6ff6905c5ffe","name":"shared_preferences_foundation","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Swift","productReference":{"guid":"bfdfe7dc352907fc980b868725387e9815af7ba71ce93f789a463577fc360420","name":"shared_preferences_foundation.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=180b65595e3b59eff4dd014e142fe2d0-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=180b65595e3b59eff4dd014e142fe2d0-json new file mode 100644 index 0000000..1419c8d --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=180b65595e3b59eff4dd014e142fe2d0-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e986175ec003efd0925b0e80b27c1a333bb","buildSettings":{"ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES":"NO","CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","INFOPLIST_FILE":"Target Support Files/Pods-Runner/Pods-Runner-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/Pods-Runner/Pods-Runner.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","OTHER_LIBTOOLFLAGS":"","PODS_ROOT":"$(SRCROOT)","PRODUCT_BUNDLE_IDENTIFIER":"org.cocoapods.${PRODUCT_NAME:rfc1034identifier}","PRODUCT_NAME":"$(TARGET_NAME:c99extidentifier)","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e9866152b44e640a0f26017e9413fa27e99","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e981c04a92782605bedf8b5bd015c8dc01a","buildSettings":{"ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES":"NO","CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","INFOPLIST_FILE":"Target Support Files/Pods-Runner/Pods-Runner-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/Pods-Runner/Pods-Runner.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","OTHER_LIBTOOLFLAGS":"","PODS_ROOT":"$(SRCROOT)","PRODUCT_BUNDLE_IDENTIFIER":"org.cocoapods.${PRODUCT_NAME:rfc1034identifier}","PRODUCT_NAME":"$(TARGET_NAME:c99extidentifier)","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e989927bdd4a4353a06de2342ef148bdaf5","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98ab5e5b11fcf9a2f9f4c2bf61b3a5e465","buildSettings":{"ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES":"NO","CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","INFOPLIST_FILE":"Target Support Files/Pods-Runner/Pods-Runner-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/Pods-Runner/Pods-Runner.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","OTHER_LIBTOOLFLAGS":"","PODS_ROOT":"$(SRCROOT)","PRODUCT_BUNDLE_IDENTIFIER":"org.cocoapods.${PRODUCT_NAME:rfc1034identifier}","PRODUCT_NAME":"$(TARGET_NAME:c99extidentifier)","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e9838e6c19d4a13c0e5961dd2463b3517c9","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98f67184b23266d4586865ab49f1bd9d8e","guid":"bfdfe7dc352907fc980b868725387e981a6b025139d4cf5ec737c7ba8a8fc6b2","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e98c3cbd0a73225df2cc4aac28ff2ace40b","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e989c24b47fd1a04f7c3870243d256eb710","guid":"bfdfe7dc352907fc980b868725387e9867eb843aa19aafe6c4a762154560b28c"}],"guid":"bfdfe7dc352907fc980b868725387e9815656129653706a754d1fa9618148536","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e982df367331b997b0adf428c7f1edfbd25"}],"guid":"bfdfe7dc352907fc980b868725387e980bd514fb9ba93cceb4b212b18546ae6c","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98638beeb4a3750a9827a3a9205a72d097","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter"},{"guid":"bfdfe7dc352907fc980b868725387e98061ad5753743dc10d394720f4d91af46","name":"Try"},{"guid":"bfdfe7dc352907fc980b868725387e98916834ec4bb54bd12b93f5cff3b46819","name":"audio_session"},{"guid":"bfdfe7dc352907fc980b868725387e98579f270da12a5d081d8785cf82f3dde0","name":"flutter_blue_plus_darwin"},{"guid":"bfdfe7dc352907fc980b868725387e988e2765468126b8189d0a656452a5242d","name":"flutter_sound"},{"guid":"bfdfe7dc352907fc980b868725387e9817d41af66eee3a4c1e145e17163b22b8","name":"flutter_sound_core"},{"guid":"bfdfe7dc352907fc980b868725387e9809ad3ea68f8eb069e147f62c5d752fe7","name":"integration_test"},{"guid":"bfdfe7dc352907fc980b868725387e9830037b09fee48cfce1f8562d753688c8","name":"path_provider_foundation"},{"guid":"bfdfe7dc352907fc980b868725387e98ef10255b706f98e1e88fae00855b0968","name":"permission_handler_apple"},{"guid":"bfdfe7dc352907fc980b868725387e9828cab1f188854e0a973e6ff6905c5ffe","name":"shared_preferences_foundation"},{"guid":"bfdfe7dc352907fc980b868725387e98512dd6ae7d689c296103006db83cb480","name":"speech_to_text"}],"guid":"bfdfe7dc352907fc980b868725387e98312b4bc59bbbe2c06c205bf4da6737f5","name":"Pods-Runner","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98699846e06e93b50cafdb00290784c775","name":"Pods_Runner.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=1c175e9654d5c06a4fce7296ec983e44-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=1c175e9654d5c06a4fce7296ec983e44-json new file mode 100644 index 0000000..93ede6e --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=1c175e9654d5c06a4fce7296ec983e44-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9872edeb19c7aec730cba1f65d9db78214","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/shared_preferences_foundation","DEFINES_MODULE":"YES","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","IBSC_MODULE":"shared_preferences_foundation","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"14.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"shared_preferences_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e982a6a722ffff4d70e1ceda17c5532e642","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98c21a4a07e54b4c87e0007412c12d41d9","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/shared_preferences_foundation","DEFINES_MODULE":"YES","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","IBSC_MODULE":"shared_preferences_foundation","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"14.0","PRODUCT_NAME":"shared_preferences_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98cbbc4615664a834b7948f7fe5bad2dc9","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98c21a4a07e54b4c87e0007412c12d41d9","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/shared_preferences_foundation","DEFINES_MODULE":"YES","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","IBSC_MODULE":"shared_preferences_foundation","INFOPLIST_FILE":"Target Support Files/shared_preferences_foundation/ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"14.0","PRODUCT_NAME":"shared_preferences_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e984f9c4a968db8b9323d7e2b4507ffcd13","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98290dd51f460cbfec286f9cd8b3b9c26b","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e988307c0cf2ce3235628b6afe0e980c689","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e9880b44a9a5c2a14c0c2cf09458cdba95e","guid":"bfdfe7dc352907fc980b868725387e989214dc7db8a196912140283b6b75e71d"}],"guid":"bfdfe7dc352907fc980b868725387e98cc233f5bca903ef319e85ca8430eb17f","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98e0be3b0d5ad56f1985578b1f97431765","name":"shared_preferences_foundation-shared_preferences_foundation_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98ad625504a4c1e61077bbfd33bd1d1785","name":"shared_preferences_foundation_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=269ca833703e9c1ecc4799a636a25c46-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=269ca833703e9c1ecc4799a636a25c46-json new file mode 100644 index 0000000..4356d6e --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=269ca833703e9c1ecc4799a636a25c46-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98b9b1287bd6938415cc7618feb19c4166","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/flutter_sound/flutter_sound-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/flutter_sound/flutter_sound-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/flutter_sound/flutter_sound.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"flutter_sound","PRODUCT_NAME":"flutter_sound","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e980b9b81931b66b864ce056eac4a63bbfc","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98ba9d15bccf639d980c4224aa9d9b8b9c","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/flutter_sound/flutter_sound-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/flutter_sound/flutter_sound-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/flutter_sound/flutter_sound.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"flutter_sound","PRODUCT_NAME":"flutter_sound","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98a5d5b2097e63da9f2dd219c1a01902c8","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98ba9d15bccf639d980c4224aa9d9b8b9c","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/flutter_sound/flutter_sound-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/flutter_sound/flutter_sound-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/flutter_sound/flutter_sound.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"flutter_sound","PRODUCT_NAME":"flutter_sound","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e988b37a8743ec9e375ed207bca0624fd6d","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e989797f772d021291656ea2f5440a1722f","guid":"bfdfe7dc352907fc980b868725387e98a4709156afa699f8d1b51b1df28c99e6","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98449be56911bd1b5ed69eab4a273a27f9","guid":"bfdfe7dc352907fc980b868725387e9839b9744451cf71a5be060a9d0de63629","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98ca84fd4c6d09392223856e790ced75ce","guid":"bfdfe7dc352907fc980b868725387e98a2c3de7fecd01e4b69146435a6eb06b0","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98bf1066473d1662b0642c94695549a22c","guid":"bfdfe7dc352907fc980b868725387e980e0dae7851933ecb35074033b80e88d5","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98b394eb96c0adf85e2ae8a34aeeb83e3b","guid":"bfdfe7dc352907fc980b868725387e98b88fc836bdad1fd8492d089aabd1b743","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e983946675895f1833af459a1d6a076a55f","guid":"bfdfe7dc352907fc980b868725387e98f93a6dcaa9d40cf58c2b832f6cb653b3","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e982e1ba6a6d30fa544db8e4128cf21ea17","guid":"bfdfe7dc352907fc980b868725387e98072500d8ddbfed84fedf110a1d6ecde3","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e98b9aeef718e3c6fb834ee80458e83a644","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98ef8924cbd3975579f1b16388faf0e29e","guid":"bfdfe7dc352907fc980b868725387e9824c29edd0de71ef70b6981a2936e1306"},{"fileReference":"bfdfe7dc352907fc980b868725387e982e9e7b21c8a5834d0107e8e9e0a0c820","guid":"bfdfe7dc352907fc980b868725387e98912a1db6c6c5a78e5ce10697ac7d49f7"},{"fileReference":"bfdfe7dc352907fc980b868725387e9859cc1ff0e1efaa39d4dc07eb74f62de9","guid":"bfdfe7dc352907fc980b868725387e981c588125f03454a3a7452a3d546fb865"},{"fileReference":"bfdfe7dc352907fc980b868725387e9897105311a9589a74e9a9add4c60caeb9","guid":"bfdfe7dc352907fc980b868725387e986adb9a5e480e0bbce45467129de8c0ab"},{"fileReference":"bfdfe7dc352907fc980b868725387e986cf84271f178656b3ecb7e40a6d2a09b","guid":"bfdfe7dc352907fc980b868725387e980e0cf1ec32cd75c87e16e6f2152236a5"},{"fileReference":"bfdfe7dc352907fc980b868725387e980a5a5fc6b4c174ede983ad366b1a5ca7","guid":"bfdfe7dc352907fc980b868725387e982c19ee95504eb682d7202a3748659afd"},{"fileReference":"bfdfe7dc352907fc980b868725387e98742684df2693fda3da29b96534eaffbe","guid":"bfdfe7dc352907fc980b868725387e98f6836f2d6a4447b684dab4a037e58ebc"}],"guid":"bfdfe7dc352907fc980b868725387e984033afda0da5bac1f211c6105dad6a37","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e985126fee2528077bb1e3e71371f705b75"}],"guid":"bfdfe7dc352907fc980b868725387e989cb9d4962ca46483a74e755bd7837e55","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9817e24f9e354470314dfab56b635e96f4","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter"},{"guid":"bfdfe7dc352907fc980b868725387e9817d41af66eee3a4c1e145e17163b22b8","name":"flutter_sound_core"}],"guid":"bfdfe7dc352907fc980b868725387e988e2765468126b8189d0a656452a5242d","name":"flutter_sound","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C-Plus-Plus","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98a792d892ce1319f30820f36c4757210b","name":"flutter_sound.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=35821847d896bb5e11dbf7e56f218053-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=35821847d896bb5e11dbf7e56f218053-json new file mode 100644 index 0000000..0571378 --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=35821847d896bb5e11dbf7e56f218053-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e983c7264ed4a219e1becad19622bc39666","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/audio_session/audio_session-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/audio_session/audio_session-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/audio_session/audio_session.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"audio_session","PRODUCT_NAME":"audio_session","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e9804ab3e3c1518d3ee1f63eff826024a43","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e981afed158e9b2d076136bcfc15512ca52","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/audio_session/audio_session-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/audio_session/audio_session-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/audio_session/audio_session.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"audio_session","PRODUCT_NAME":"audio_session","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e9829daa51899aa8440a3f7a2b2f3ef7e1c","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e981afed158e9b2d076136bcfc15512ca52","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/audio_session/audio_session-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/audio_session/audio_session-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/audio_session/audio_session.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"audio_session","PRODUCT_NAME":"audio_session","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98ceeb7532f66fdfbbc8c7a3ef99616674","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e9863e909ef67f8e50d1f6b668b9e3471c0","guid":"bfdfe7dc352907fc980b868725387e984eb37f6ecc5a727dcf752955a8bb5401","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98e39bbaf6299ab3c0069356d5feecb338","guid":"bfdfe7dc352907fc980b868725387e981ecd71bc602cf25521f482b681652885","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e981bb6171fc9f63ba346ad44c2d834d463","guid":"bfdfe7dc352907fc980b868725387e98d3695f24dea5cd1388fa31dcf74e5992","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e986c68af2dac61084d93ff4a1fb0eaeac1","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e9815355349ea83a29617eaa6a651b7c526","guid":"bfdfe7dc352907fc980b868725387e9834f7d11d0181045e2ac5c9ab5b9914e3"},{"fileReference":"bfdfe7dc352907fc980b868725387e981e437e97f32342562cc697db30c88ee0","guid":"bfdfe7dc352907fc980b868725387e98683ee0d226a17c54f4762a21e0c56527"},{"fileReference":"bfdfe7dc352907fc980b868725387e98bdbb09bfc18ca7a8115392dd1cd65f0c","guid":"bfdfe7dc352907fc980b868725387e981e32c84c34f02a46feacafb90243af0b"}],"guid":"bfdfe7dc352907fc980b868725387e98f3c418d77204fa741d82eadc0cb5246d","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e98c02433c592ad6348a3daf6efac9caf3e"}],"guid":"bfdfe7dc352907fc980b868725387e9888011c687b46efa26f08adaad3446b26","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e984b08d7696144333ef265a4320bf53720","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter"}],"guid":"bfdfe7dc352907fc980b868725387e98916834ec4bb54bd12b93f5cff3b46819","name":"audio_session","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98278fe681dcc7822e5484043e844a6dd3","name":"audio_session.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=5f9a41f72b9b17bed62972a64b5bcd89-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=5f9a41f72b9b17bed62972a64b5bcd89-json new file mode 100644 index 0000000..a9dcb2a --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=5f9a41f72b9b17bed62972a64b5bcd89-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e982e3a421d0eacab2f0583e0d8f57f11d3","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","GCC_PREFIX_HEADER":"Target Support Files/flutter_sound_core/flutter_sound_core-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/flutter_sound_core/flutter_sound_core-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/flutter_sound_core/flutter_sound_core.modulemap","ONLY_ACTIVE_ARCH":"NO","PRODUCT_MODULE_NAME":"flutter_sound_core","PRODUCT_NAME":"flutter_sound_core","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e980df546ed1cf14445289cbf59e747cbcb","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e987fb8c65a453d7a94dfbcfdaa55f8aede","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","GCC_PREFIX_HEADER":"Target Support Files/flutter_sound_core/flutter_sound_core-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/flutter_sound_core/flutter_sound_core-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/flutter_sound_core/flutter_sound_core.modulemap","PRODUCT_MODULE_NAME":"flutter_sound_core","PRODUCT_NAME":"flutter_sound_core","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98cccf2e1366675bb879e1375e44b3a34a","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e987fb8c65a453d7a94dfbcfdaa55f8aede","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","GCC_PREFIX_HEADER":"Target Support Files/flutter_sound_core/flutter_sound_core-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/flutter_sound_core/flutter_sound_core-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/flutter_sound_core/flutter_sound_core.modulemap","PRODUCT_MODULE_NAME":"flutter_sound_core","PRODUCT_NAME":"flutter_sound_core","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e9846b8706dc42470071d8d2d095bdf24c8","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e981573e419faf3089f97bdf3c690e693ec","guid":"bfdfe7dc352907fc980b868725387e9803a867892a5946a69f6554a7315a1c21","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98ed839003e3fd39b928daa2eb9cf90a49","guid":"bfdfe7dc352907fc980b868725387e98a3e561ed16abcaa751bad86cff0c4f4a","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e989fa19f1cf77b91e4bfa0e758d484774e","guid":"bfdfe7dc352907fc980b868725387e98db08099a6c5e7cf60a2acb6d841a9696","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e983de0c63a0d3b0d8665c4ebd1180acd0d","guid":"bfdfe7dc352907fc980b868725387e98d16a37c88cf614718b1cce754891df79","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98a280167187f3bc2c4fa78c1ce8d505f2","guid":"bfdfe7dc352907fc980b868725387e983c37982a6b3615177cae6282cdfe2f9f","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e9879f366ee9c4dd3f68fa31327871d01be","guid":"bfdfe7dc352907fc980b868725387e982224a3ac87d09932c0496b866f01c43a","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e98c78a6c68b5abe7b0b28ceda8c1c25601","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e980685081bb67df51d3f25df3ebf42cc78","guid":"bfdfe7dc352907fc980b868725387e98b9f6325ed53161a2591baed1e0b98656"},{"fileReference":"bfdfe7dc352907fc980b868725387e98829dcc39d62083a3d5c0d426baf6d105","guid":"bfdfe7dc352907fc980b868725387e981034a4c03749a08d618c527969450c3d"},{"fileReference":"bfdfe7dc352907fc980b868725387e9818d4b45bc065a8bea26a912b61c94d69","guid":"bfdfe7dc352907fc980b868725387e981c4b0e689c0fd63a2b2ba9d9bd99e7fe"},{"fileReference":"bfdfe7dc352907fc980b868725387e9801e0d04bc02b987b587207beba2efd66","guid":"bfdfe7dc352907fc980b868725387e98a1cb26b4da7e2e83b0df59a8463aa443"},{"fileReference":"bfdfe7dc352907fc980b868725387e98a3a3b79e668136c118ed3683138f61ce","guid":"bfdfe7dc352907fc980b868725387e98e2a191d7469de4f4878d79000f1ff366"},{"fileReference":"bfdfe7dc352907fc980b868725387e98bdf4deb813794228c48063766e68b073","guid":"bfdfe7dc352907fc980b868725387e9815b5c4a3965a55403f0dc4d990a8261e"}],"guid":"bfdfe7dc352907fc980b868725387e988bd94027e8877178a9446b459987f60c","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e9845f00240a9e89e396ca279851b30fabe","guid":"bfdfe7dc352907fc980b868725387e98f1a4bf294deaaf77c3dc4af58ffd1fff"},{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e981a0c96830b6ab57f69e3b18a80c50c4d"},{"fileReference":"bfdfe7dc352907fc980b868725387e987cd35d866fc4c4302dc02fe72033285c","guid":"bfdfe7dc352907fc980b868725387e98629b76356c26e61884b35a11d3dbb091"}],"guid":"bfdfe7dc352907fc980b868725387e9867f0006171aa4d0a7c9823ab222295a4","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9899e2109b83f1578f308d25e24a90d59a","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e9817d41af66eee3a4c1e145e17163b22b8","name":"flutter_sound_core","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C-Plus-Plus","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98ed846bc5edbcc85d935ace19b53742e0","name":"flutter_sound_core.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=68e2635207846628f8e9c8238abfac79-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=68e2635207846628f8e9c8238abfac79-json new file mode 100644 index 0000000..ecacf8b --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=68e2635207846628f8e9c8238abfac79-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98074a9b441078beef16a37aae33ee2900","buildSettings":{"ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES":"NO","CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","INFOPLIST_FILE":"Target Support Files/Pods-RunnerTests/Pods-RunnerTests-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","OTHER_LIBTOOLFLAGS":"","PODS_ROOT":"$(SRCROOT)","PRODUCT_BUNDLE_IDENTIFIER":"org.cocoapods.${PRODUCT_NAME:rfc1034identifier}","PRODUCT_NAME":"$(TARGET_NAME:c99extidentifier)","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98b6b1707d1b4770ead9ccc06d5a8078bd","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e987bd2a5c12d5a72dad789e04e26dc5a25","buildSettings":{"ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES":"NO","CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","INFOPLIST_FILE":"Target Support Files/Pods-RunnerTests/Pods-RunnerTests-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","OTHER_LIBTOOLFLAGS":"","PODS_ROOT":"$(SRCROOT)","PRODUCT_BUNDLE_IDENTIFIER":"org.cocoapods.${PRODUCT_NAME:rfc1034identifier}","PRODUCT_NAME":"$(TARGET_NAME:c99extidentifier)","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e989849b932ff04bd6de9b5577c96056df1","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98fc46d149385d28a69f8f8bc860e81763","buildSettings":{"ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES":"NO","CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","INFOPLIST_FILE":"Target Support Files/Pods-RunnerTests/Pods-RunnerTests-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/Pods-RunnerTests/Pods-RunnerTests.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","OTHER_LIBTOOLFLAGS":"","PODS_ROOT":"$(SRCROOT)","PRODUCT_BUNDLE_IDENTIFIER":"org.cocoapods.${PRODUCT_NAME:rfc1034identifier}","PRODUCT_NAME":"$(TARGET_NAME:c99extidentifier)","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98b57ad5f90ad8f9246793763e8dbc46bc","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98f448039d0a832e98acdd3ffd87da1731","guid":"bfdfe7dc352907fc980b868725387e98ca9af5e2c54f437f9ebb0c203883ccae","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e986e6b8bd91d07f2fb082ccd84c7dcacb1","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e988d01c1d667722a31ce8e51428338963f","guid":"bfdfe7dc352907fc980b868725387e9881f185e1672aa83b98d6e30b47f8f468"}],"guid":"bfdfe7dc352907fc980b868725387e98de09b1176c796343f1f9bcd422c73402","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e98e7ac2b91ee49764a75561cf994247683"}],"guid":"bfdfe7dc352907fc980b868725387e983bb5c38e7891bdb262f8e050f7d97030","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e987fddc24c35656402341de288e0688015","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e98312b4bc59bbbe2c06c205bf4da6737f5","name":"Pods-Runner"}],"guid":"bfdfe7dc352907fc980b868725387e98483832d3c820398e9d40e1a6904b03fe","name":"Pods-RunnerTests","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C","productReference":{"guid":"bfdfe7dc352907fc980b868725387e984f9f39caeddf64cc331db2b69d62aa63","name":"Pods_RunnerTests.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=7e729c1c163f5dd7877153fb35670149-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=7e729c1c163f5dd7877153fb35670149-json new file mode 100644 index 0000000..e8d3d6d --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=7e729c1c163f5dd7877153fb35670149-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e982e28f027f7d56581bc8680624eb50426","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","DEFINES_MODULE":"YES","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"14.0","ONLY_ACTIVE_ARCH":"NO","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e983c1970a55bccff26dedbcf8d87e5b569","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a0eef8c98746e7ed69f7d625899e19d2","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","DEFINES_MODULE":"YES","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"14.0","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e9856ca22969be5a10f49f68114c25ebd6f","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a0eef8c98746e7ed69f7d625899e19d2","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CODE_SIGNING_ALLOWED":"NO","CODE_SIGNING_IDENTITY":"-","CODE_SIGNING_REQUIRED":"NO","CONFIGURATION_BUILD_DIR":"$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/path_provider_foundation","DEFINES_MODULE":"YES","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","EXPANDED_CODE_SIGN_IDENTITY":"-","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","IBSC_MODULE":"path_provider_foundation","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","IPHONEOS_DEPLOYMENT_TARGET":"14.0","PRODUCT_NAME":"path_provider_foundation_privacy","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","TARGETED_DEVICE_FAMILY":"1,2","WRAPPER_EXTENSION":"bundle"},"guid":"bfdfe7dc352907fc980b868725387e98d0f6058ad6ebcb6322df2f8eb79f6f12","name":"Release"}],"buildPhases":[{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9880c825f08eea5b8134920297423a99c0","type":"com.apple.buildphase.sources"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e9858b8879aaa238fd47827e9aa6cc737e7","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98d66c9a0eca583bf008ee88c953994ed8","guid":"bfdfe7dc352907fc980b868725387e982d7c2aecb2bbfab95e1b2237641ac6f3"}],"guid":"bfdfe7dc352907fc980b868725387e98d094997f536e209b649009defaf82df1","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e987ea64ee8d53085bf9edd1a57aaf8cbb5","name":"path_provider_foundation-path_provider_foundation_privacy","productReference":{"guid":"bfdfe7dc352907fc980b868725387e986e649604f74c414a7c2dbe5ef4cc4e75","name":"path_provider_foundation_privacy.bundle","type":"product"},"productTypeIdentifier":"com.apple.product-type.bundle","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":0}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=83bb48011e7cbb72ce22a9f7b4ba0c9b-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=83bb48011e7cbb72ce22a9f7b4ba0c9b-json new file mode 100644 index 0000000..c8319fa --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=83bb48011e7cbb72ce22a9f7b4ba0c9b-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e982e28f027f7d56581bc8680624eb50426","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/path_provider_foundation/path_provider_foundation-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/path_provider_foundation-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","MODULEMAP_FILE":"Target Support Files/path_provider_foundation/path_provider_foundation.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"path_provider_foundation","PRODUCT_NAME":"path_provider_foundation","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98ab88586633079f928287f370e8b6f07b","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a0eef8c98746e7ed69f7d625899e19d2","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/path_provider_foundation/path_provider_foundation-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/path_provider_foundation-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","MODULEMAP_FILE":"Target Support Files/path_provider_foundation/path_provider_foundation.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"path_provider_foundation","PRODUCT_NAME":"path_provider_foundation","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e9880f884b2537bd891ed54ff6e3ab7d0ee","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a0eef8c98746e7ed69f7d625899e19d2","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/path_provider_foundation/path_provider_foundation-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/path_provider_foundation/path_provider_foundation-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","MODULEMAP_FILE":"Target Support Files/path_provider_foundation/path_provider_foundation.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"path_provider_foundation","PRODUCT_NAME":"path_provider_foundation","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e9858b9d941e76db42d349048c14af0e16e","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98bd0071a96f2d6e6f55fb2068f6a4f3fc","guid":"bfdfe7dc352907fc980b868725387e98e40234757d04478dc54a213f59e845fa","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e98450b40315711083d32b0ed949174ff28","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98b490ba27811b8df8a4daa19187ce372e","guid":"bfdfe7dc352907fc980b868725387e9890d8fdf4ce74cd896fd77e7f9f14678a"},{"fileReference":"bfdfe7dc352907fc980b868725387e98b7ac2fba01f7b5e27013df7c03a25171","guid":"bfdfe7dc352907fc980b868725387e986dfc1b5ca512f6383be32a7124385963"},{"fileReference":"bfdfe7dc352907fc980b868725387e98a72a1cdccce3961f7029e39bc5ef3628","guid":"bfdfe7dc352907fc980b868725387e98fd5d58737bf8fec5e887599c877da4ba"}],"guid":"bfdfe7dc352907fc980b868725387e98f5d455158bacea210fd45e1a8f3245fc","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e9829f34398048903731961241124ac546e"}],"guid":"bfdfe7dc352907fc980b868725387e987ebedde198dc993f3ca38aec4ed08768","type":"com.apple.buildphase.frameworks"},{"buildFiles":[{"guid":"bfdfe7dc352907fc980b868725387e98234997a2811e55e2dfc23faf0b9d3093","targetReference":"bfdfe7dc352907fc980b868725387e987ea64ee8d53085bf9edd1a57aaf8cbb5"}],"guid":"bfdfe7dc352907fc980b868725387e98ac45f7d09c5ae0c1d8f7eb8e8ff004ab","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter"},{"guid":"bfdfe7dc352907fc980b868725387e987ea64ee8d53085bf9edd1a57aaf8cbb5","name":"path_provider_foundation-path_provider_foundation_privacy"}],"guid":"bfdfe7dc352907fc980b868725387e9830037b09fee48cfce1f8562d753688c8","name":"path_provider_foundation","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Swift","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98177b75fe6f519d73b22b382cca137f1c","name":"path_provider_foundation.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=97b6ace309e306681f0196e5fe3fdad3-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=97b6ace309e306681f0196e5fe3fdad3-json new file mode 100644 index 0000000..f864579 --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=97b6ace309e306681f0196e5fe3fdad3-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98d7e84e304ba90d4bbfc82f36a80567e5","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","GCC_PREFIX_HEADER":"Target Support Files/Try/Try-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/Try/Try-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/Try/Try.modulemap","ONLY_ACTIVE_ARCH":"NO","PRODUCT_MODULE_NAME":"Try","PRODUCT_NAME":"Try","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e9845126a788bd0ca7aeb2bcafed5439941","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98d1a41b1e0ec8e320d4acb394ecc35fe2","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","GCC_PREFIX_HEADER":"Target Support Files/Try/Try-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/Try/Try-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/Try/Try.modulemap","PRODUCT_MODULE_NAME":"Try","PRODUCT_NAME":"Try","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98aa397035dd8512b6a701975222e30fa4","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98d1a41b1e0ec8e320d4acb394ecc35fe2","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","GCC_PREFIX_HEADER":"Target Support Files/Try/Try-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/Try/Try-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/Try/Try.modulemap","PRODUCT_MODULE_NAME":"Try","PRODUCT_NAME":"Try","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e984f6b5d62861eb0530927fc30802afdd7","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e989ef4a9ee6be3ac565e7a935d594d2dab","guid":"bfdfe7dc352907fc980b868725387e9852d7db4b69a1f42cf46e73436e907a55","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e988340fc291ce4022c71f1195488bbb3dc","guid":"bfdfe7dc352907fc980b868725387e9855299f08c98216a068cc066f63307019","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e98f44bdc038d6a259283467c9f9ce2e50a","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98d1aa7c25c99c25d17fa1e72a821e5d6e","guid":"bfdfe7dc352907fc980b868725387e98bc49b5a0321a0b07ba2f6ab03d18f745"},{"fileReference":"bfdfe7dc352907fc980b868725387e98291a4c0b27555246b0ec894fdea6a342","guid":"bfdfe7dc352907fc980b868725387e985c1e3532fecf618528feb8422b7f590f"},{"fileReference":"bfdfe7dc352907fc980b868725387e98796437b3e3041768055dc202475983de","guid":"bfdfe7dc352907fc980b868725387e9857a0712626fc2df87f090b246c931304"}],"guid":"bfdfe7dc352907fc980b868725387e9859f1e8f65fc9469925afa9e7e22982ff","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e98f1af07d7a60ad7eaaa15b3ba1b4d67fa"}],"guid":"bfdfe7dc352907fc980b868725387e9859747322a8148d1d1b4f883b14432dac","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98d119a0b85f39c8e670105545288ae6f3","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e98061ad5753743dc10d394720f4d91af46","name":"Try","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98c3ede7ee9aea10b830df70533ecdf5ee","name":"Try.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=b4c7a6a6f6fac140a0bb58992a41be65-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=b4c7a6a6f6fac140a0bb58992a41be65-json new file mode 100644 index 0000000..10b0686 --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=b4c7a6a6f6fac140a0bb58992a41be65-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98b9a4a54f9775d27fe806370b1871cf71","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/flutter_blue_plus_darwin/flutter_blue_plus_darwin-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/flutter_blue_plus_darwin/flutter_blue_plus_darwin-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/flutter_blue_plus_darwin/flutter_blue_plus_darwin.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"flutter_blue_plus_darwin","PRODUCT_NAME":"flutter_blue_plus_darwin","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98388992d907aebf5fac508e3bdd610c52","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a9302e6415570e7e909a0d0a592b5f53","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/flutter_blue_plus_darwin/flutter_blue_plus_darwin-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/flutter_blue_plus_darwin/flutter_blue_plus_darwin-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/flutter_blue_plus_darwin/flutter_blue_plus_darwin.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"flutter_blue_plus_darwin","PRODUCT_NAME":"flutter_blue_plus_darwin","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e984be5f8804d355aea6b4ae7ad8c2a684c","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98a9302e6415570e7e909a0d0a592b5f53","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/flutter_blue_plus_darwin/flutter_blue_plus_darwin-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/flutter_blue_plus_darwin/flutter_blue_plus_darwin-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/flutter_blue_plus_darwin/flutter_blue_plus_darwin.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"flutter_blue_plus_darwin","PRODUCT_NAME":"flutter_blue_plus_darwin","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98f217e6602a962b57036712e8828db99b","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98224c46c3415999a7e08d26c71434592b","guid":"bfdfe7dc352907fc980b868725387e98401de0ace44363447ab435f270753175","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98529da24a3600ab139fc133ec9855f27c","guid":"bfdfe7dc352907fc980b868725387e98d5e4b6d5b210ec5c8ee905b3d0dec88a","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e986674cd9adf5ba6517021df2a59cb6f52","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983b433ba26869f5746032c4b4b9d1ac7f","guid":"bfdfe7dc352907fc980b868725387e984c79f8109c9552c85c66b6086e718bfe"},{"fileReference":"bfdfe7dc352907fc980b868725387e98e97ba4f760a8127267398bdd70d0c239","guid":"bfdfe7dc352907fc980b868725387e984bc7080c6ec38b37a5f17f9b63b8c787"}],"guid":"bfdfe7dc352907fc980b868725387e987732a34704cb4caff004c54c87f78b12","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98886a015712e5147dc288206d0761e2d9","guid":"bfdfe7dc352907fc980b868725387e98cfba1bf486f961196412b4f1454f8961"},{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e98d160607b1fd480249051d3b082d04314"}],"guid":"bfdfe7dc352907fc980b868725387e98ecec11c59dba26c686c31c21809d4f4f","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e981fafdd6caa78471145910050b586faf0","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter"}],"guid":"bfdfe7dc352907fc980b868725387e98579f270da12a5d081d8785cf82f3dde0","name":"flutter_blue_plus_darwin","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98a28931127ba3f5f47ee022a478a28879","name":"flutter_blue_plus_darwin.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=eab96dae2a0065b6b38372c0453d1f07-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=eab96dae2a0065b6b38372c0453d1f07-json new file mode 100644 index 0000000..b352537 --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=eab96dae2a0065b6b38372c0453d1f07-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98bb902a76c4ca4955d33ed3ba9ad10066","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/permission_handler_apple/permission_handler_apple-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/permission_handler_apple-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/permission_handler_apple/permission_handler_apple.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"permission_handler_apple","PRODUCT_NAME":"permission_handler_apple","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e986f60ad41630d9e7ebc6257f2b7c9771a","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9826870138bb4d03d3479a06a56fbe0707","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/permission_handler_apple/permission_handler_apple-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/permission_handler_apple-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/permission_handler_apple/permission_handler_apple.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"permission_handler_apple","PRODUCT_NAME":"permission_handler_apple","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e9808ebb61cc9b6bf2730a4627e98ee10ff","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9826870138bb4d03d3479a06a56fbe0707","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/permission_handler_apple/permission_handler_apple-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/permission_handler_apple/permission_handler_apple-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MACH_O_TYPE":"staticlib","MODULEMAP_FILE":"Target Support Files/permission_handler_apple/permission_handler_apple.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"permission_handler_apple","PRODUCT_NAME":"permission_handler_apple","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e984072357f32a9f8fc95b3c02424bde0a8","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98d026c50783a691d70948626b2b786eaa","guid":"bfdfe7dc352907fc980b868725387e987ef754e44ea5fdf454a84291c7399b87","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98c703452e02e20da33fb78a7d76714bfa","guid":"bfdfe7dc352907fc980b868725387e98a6869e7a3c7ea5b2fe388564adbe7ecf","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e9867f0fb56b7a78dfbcf55c67c9bde8371","guid":"bfdfe7dc352907fc980b868725387e98ba5bdc1ec47c93d507cf2cee7f019ac2","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e983b4c31deadbd4729dad472c80aab8249","guid":"bfdfe7dc352907fc980b868725387e982849d3eb488df59e839a311a25c58a25","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98185074a7563c47ababcc78c609eee4ad","guid":"bfdfe7dc352907fc980b868725387e982975174bf57dd85aed09f514fffc3786","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e987481cfba29f215c0f2caefcab5b8ad1d","guid":"bfdfe7dc352907fc980b868725387e9842b0e9db3c9b8f9e4780cc0b05dee74b","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98ea29fa45ee8af40c59db988047e440c2","guid":"bfdfe7dc352907fc980b868725387e98fcf87b01e21affa1d1edcc22696c53fb","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98c0eb6235e4ac002a45f9a761762f9c88","guid":"bfdfe7dc352907fc980b868725387e9899ed3841024f3f2cd0a04665dfe4c73d","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98eed7a3b81fd72de4bfcbe12f32786c7e","guid":"bfdfe7dc352907fc980b868725387e980081db0ff29b019a9b1ac28eb42d35da","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e9889de7be23c8c27802d34fae119c63e4b","guid":"bfdfe7dc352907fc980b868725387e98a0820c9b865bdfae25a8c7fcb5b43729","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e9871888a285e4b5415b156944e24d21b0a","guid":"bfdfe7dc352907fc980b868725387e98b42e42472dfa234449e2b3d79e02ba09","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98764150597b4b6ba150009052e1ae1a83","guid":"bfdfe7dc352907fc980b868725387e982f282828ce1787e2a5d3b28f517b304c","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98a881972e33f903e8ba02952205d4ee65","guid":"bfdfe7dc352907fc980b868725387e980fb4090d405f6012da693e27f5bba086","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98c28fb384571d8d1aabccecb02535284a","guid":"bfdfe7dc352907fc980b868725387e9808660f7651d2e44a95bd7e799c7889df","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98853a083978af5c7a1711cd0feae832eb","guid":"bfdfe7dc352907fc980b868725387e98e4166a664d0a073fb65afe3f1d35888c","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98c7a187c2acc20599a580a658eb34906d","guid":"bfdfe7dc352907fc980b868725387e981fc2fb4752e9b59b0cfe8cf0c2cf6e47","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e9881a2e788e02e78789972bcb8017ac409","guid":"bfdfe7dc352907fc980b868725387e98e3a9b3f9fbdd76014f2452b643be5d23","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e980875a03066774f59ead8a70cbf26009f","guid":"bfdfe7dc352907fc980b868725387e980d7bdbdc2ac5ef050e71207e896efd4e","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e988a7460408ff3aa54cc4a38db75e32468","guid":"bfdfe7dc352907fc980b868725387e98a80270a32f588baa4abe9f628cb68358","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98ee9c8df6b27b16d340adf605f8af623b","guid":"bfdfe7dc352907fc980b868725387e98dd1caca88b98d558b086156b003979d3","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e98cfe271c2f11f2011dfd3bef6a393b37d","guid":"bfdfe7dc352907fc980b868725387e98bd1bc85e10a4166d2679f4016eae77f0","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e98893830191ea338f02e2c1dc91b910130","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e988b887e1c8d3dcd0152f247269a91e9c8","guid":"bfdfe7dc352907fc980b868725387e988b3a898688874271a6a49e42866df3e7"},{"fileReference":"bfdfe7dc352907fc980b868725387e98e1974f2e1990733512cc1b8a52bc2c12","guid":"bfdfe7dc352907fc980b868725387e98c21d56a236b2ba742192ad8251ea467a"},{"fileReference":"bfdfe7dc352907fc980b868725387e985cc98e288bc337adee99ea3237c16342","guid":"bfdfe7dc352907fc980b868725387e986fc168c2c38e1a36b871d1b4fdabf392"},{"fileReference":"bfdfe7dc352907fc980b868725387e9868cea9b9d5e2230d90e0c3afa0a32ac4","guid":"bfdfe7dc352907fc980b868725387e9881071c4d8963ae203d70b0e688f6d8e9"},{"fileReference":"bfdfe7dc352907fc980b868725387e98d3e32716d4c0756ece1470a8b57afa90","guid":"bfdfe7dc352907fc980b868725387e98b41db1d870408e47c2449e51f5e17d07"},{"fileReference":"bfdfe7dc352907fc980b868725387e987f34dbbe50686fba6028af12c575a424","guid":"bfdfe7dc352907fc980b868725387e986ccc883e7abcf19ca41df28be62e70f7"},{"fileReference":"bfdfe7dc352907fc980b868725387e983abb904929655d1b7559cb36679cfdd6","guid":"bfdfe7dc352907fc980b868725387e98fb5981ecc8d00feb2b848a6e67c42775"},{"fileReference":"bfdfe7dc352907fc980b868725387e982f69903baed716f33a3cdbbf22c580c6","guid":"bfdfe7dc352907fc980b868725387e98b0c2b90c00ea8f1abbde577d9f12bd11"},{"fileReference":"bfdfe7dc352907fc980b868725387e98d528349dd1e91534d97de92214677020","guid":"bfdfe7dc352907fc980b868725387e98bb400f2c4cd2bb65dfdcbdac1b82d962"},{"fileReference":"bfdfe7dc352907fc980b868725387e9836c723448053542e92c48aa47c90a78d","guid":"bfdfe7dc352907fc980b868725387e981080a07162411a23d613f0d50b76f071"},{"fileReference":"bfdfe7dc352907fc980b868725387e98b4729fd0b46da590e9865d40b57edb29","guid":"bfdfe7dc352907fc980b868725387e98f7f3d8af76f1642f368a6513747712f8"},{"fileReference":"bfdfe7dc352907fc980b868725387e983d8c7f88e6eb2799d1a05ad59b2c7503","guid":"bfdfe7dc352907fc980b868725387e984e0cb19d857fd64f47bedd78593e9f65"},{"fileReference":"bfdfe7dc352907fc980b868725387e98e082f45c301e538572da89b315ae4ea0","guid":"bfdfe7dc352907fc980b868725387e9869bdb4cac000506710775930543bc530"},{"fileReference":"bfdfe7dc352907fc980b868725387e984c9614e439db528e72ad35d90e2d045c","guid":"bfdfe7dc352907fc980b868725387e98c3fa42840a80a3dc9b1cdb986b68c876"},{"fileReference":"bfdfe7dc352907fc980b868725387e98b6f005af76d622cb569e2e91c48cc310","guid":"bfdfe7dc352907fc980b868725387e98276acd98f00c42a84568828f3f91330c"},{"fileReference":"bfdfe7dc352907fc980b868725387e98d7129fb88e10cee83116a5c113f12815","guid":"bfdfe7dc352907fc980b868725387e9860693432728c6e1144c7940361956271"},{"fileReference":"bfdfe7dc352907fc980b868725387e9870d250d11e910ee003d58b4e31413a00","guid":"bfdfe7dc352907fc980b868725387e985e564e9894fc8827cb2ca2161ccaf30f"},{"fileReference":"bfdfe7dc352907fc980b868725387e98032376fa5bf3f51bd561b557ecd97906","guid":"bfdfe7dc352907fc980b868725387e980d170b64987b6b3957621008a450858e"},{"fileReference":"bfdfe7dc352907fc980b868725387e985dc4c9a1e36a7be5549cddf856bd6b46","guid":"bfdfe7dc352907fc980b868725387e9807af277cc8ae3a6444190f302f51da9e"}],"guid":"bfdfe7dc352907fc980b868725387e988f2159a3fa518201e99f45201240c014","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e98c55b2fd41ec59641b36bba517fd96ffa"}],"guid":"bfdfe7dc352907fc980b868725387e98f59d14b41d6065eb13a4af8fcfae4a69","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e983e9e224ef10dec5e1925539f36c732b7","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter"}],"guid":"bfdfe7dc352907fc980b868725387e98ef10255b706f98e1e88fae00855b0968","name":"permission_handler_apple","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C-Plus-Plus","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98f8f53f8ba4165e76c7481b24262177ed","name":"permission_handler_apple.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=ef00dee53b6c04018e669f76e663cf7e-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=ef00dee53b6c04018e669f76e663cf7e-json new file mode 100644 index 0000000..dd989e8 --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=ef00dee53b6c04018e669f76e663cf7e-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9873ed624792dda30c826a3088312775ed","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","DEFINES_MODULE":"YES","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","ONLY_ACTIVE_ARCH":"NO","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2"},"guid":"bfdfe7dc352907fc980b868725387e982cf0da236cf10d087750aa1434da9227","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98deb99da4fe35fe63386c3d737157c37f","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","DEFINES_MODULE":"YES","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e98cc28f154213fd8181aa70d4c188a8335","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e98deb99da4fe35fe63386c3d737157c37f","buildSettings":{"ASSETCATALOG_COMPILER_APPICON_NAME":"AppIcon","ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME":"AccentColor","CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_ENABLE_OBJC_WEAK":"NO","DEFINES_MODULE":"YES","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks","SDKROOT":"iphoneos","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES"},"guid":"bfdfe7dc352907fc980b868725387e981f19fefc6e52ad9e4e005a2248234387","name":"Release"}],"buildPhases":[],"buildRules":[],"dependencies":[],"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"","configurationName":"Debug","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Profile","provisioningStyle":0},{"bundleIdentifierFromInfoPlist":"","configurationName":"Release","provisioningStyle":0}],"type":"aggregate"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=f64af7476a3c5da8edff13f61955ad53-json b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=f64af7476a3c5da8edff13f61955ad53-json new file mode 100644 index 0000000..efbfadd --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/target/TARGET@v11_hash=f64af7476a3c5da8edff13f61955ad53-json @@ -0,0 +1 @@ +{"buildConfigurations":[{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9816b851f75d8f22a4896f859a1b518fa4","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/speech_to_text/speech_to_text-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/speech_to_text/speech_to_text-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/speech_to_text/speech_to_text.modulemap","ONLY_ACTIVE_ARCH":"NO","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"speech_to_text","PRODUCT_NAME":"speech_to_text","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e98e48002d89212ca775bbbc3f491d82d5a","name":"Debug"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9896076e833f4e522831fba9e0044a21dd","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/speech_to_text/speech_to_text-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/speech_to_text/speech_to_text-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/speech_to_text/speech_to_text.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"speech_to_text","PRODUCT_NAME":"speech_to_text","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e989f6dd62ad98b9401eea78d45ed69300b","name":"Profile"},{"baseConfigurationFileReference":"bfdfe7dc352907fc980b868725387e9896076e833f4e522831fba9e0044a21dd","buildSettings":{"CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES":"YES","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"NO","CODE_SIGN_IDENTITY[sdk=appletvos*]":"","CODE_SIGN_IDENTITY[sdk=iphoneos*]":"","CODE_SIGN_IDENTITY[sdk=watchos*]":"","CURRENT_PROJECT_VERSION":"1","DEFINES_MODULE":"YES","DYLIB_COMPATIBILITY_VERSION":"1","DYLIB_CURRENT_VERSION":"1","DYLIB_INSTALL_NAME_BASE":"@rpath","ENABLE_BITCODE":"NO","ENABLE_MODULE_VERIFIER":"NO","ENABLE_USER_SCRIPT_SANDBOXING":"NO","EXCLUDED_ARCHS[sdk=iphoneos*]":"$(inherited) armv7","EXCLUDED_ARCHS[sdk=iphonesimulator*]":"i386","FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64\" $(inherited)","FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]":"\"/opt/homebrew/Caskroom/flutter/3.29.2/flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-arm64_x86_64-simulator\" $(inherited)","GCC_PREFIX_HEADER":"Target Support Files/speech_to_text/speech_to_text-prefix.pch","GCC_PREPROCESSOR_DEFINITIONS":"$(inherited) PERMISSION_MICROPHONE=1 PERMISSION_SPEECH_RECOGNIZER=1 PERMISSION_BLUETOOTH=1 PERMISSION_LOCATION=1","GENERATE_INFOPLIST_FILE":"NO","INFOPLIST_FILE":"Target Support Files/speech_to_text/speech_to_text-Info.plist","INSTALL_PATH":"$(LOCAL_LIBRARY_DIR)/Frameworks","IPHONEOS_DEPLOYMENT_TARGET":"14.0","LD_RUNPATH_SEARCH_PATHS":"$(inherited) @executable_path/Frameworks @loader_path/Frameworks","MODULEMAP_FILE":"Target Support Files/speech_to_text/speech_to_text.modulemap","OTHER_LDFLAGS":"$(inherited) -framework Flutter","PRODUCT_MODULE_NAME":"speech_to_text","PRODUCT_NAME":"speech_to_text","SDKROOT":"iphoneos","SKIP_INSTALL":"YES","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"$(inherited) ","SWIFT_INSTALL_OBJC_HEADER":"YES","SWIFT_VERSION":"5.0","TARGETED_DEVICE_FAMILY":"1,2","VALIDATE_PRODUCT":"YES","VALID_ARCHS[sdk=iphonesimulator*]":"$(ARCHS_STANDARD)","VERSIONING_SYSTEM":"apple-generic","VERSION_INFO_PREFIX":""},"guid":"bfdfe7dc352907fc980b868725387e989354401e5c894668a7b60be4bc271cf4","name":"Release"}],"buildPhases":[{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e98de3628f2ddc2d8a80abe7c00305303f9","guid":"bfdfe7dc352907fc980b868725387e98a41af878a4dfa31528abf5cd8e6a30d9","headerVisibility":"public"},{"fileReference":"bfdfe7dc352907fc980b868725387e984c3c1aa15953a55f4f7901bae7c671b0","guid":"bfdfe7dc352907fc980b868725387e98eb44536af92ba287b1f778b6459b29f8","headerVisibility":"public"}],"guid":"bfdfe7dc352907fc980b868725387e985c6361e4c5950fd6aa40d824fe17b216","type":"com.apple.buildphase.headers"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e981d1ff21651613ebb0608d808eca596e0","guid":"bfdfe7dc352907fc980b868725387e98c2e82498f8ab9a6190b0bd6d3f744bb6"},{"fileReference":"bfdfe7dc352907fc980b868725387e986adf5cfabe468368f65e9d83acc9eb46","guid":"bfdfe7dc352907fc980b868725387e98de0b437e39736fa8a73852eac277afc2"},{"fileReference":"bfdfe7dc352907fc980b868725387e980fd17c0e21ffb2cefe7bc24d793880dd","guid":"bfdfe7dc352907fc980b868725387e987bca93dc342bb6288f0ac520afb0d770"}],"guid":"bfdfe7dc352907fc980b868725387e980f8d7e2da91942266ff646e078c904e7","type":"com.apple.buildphase.sources"},{"buildFiles":[{"fileReference":"bfdfe7dc352907fc980b868725387e983a950a7eceac03130fccc59f1d7f1090","guid":"bfdfe7dc352907fc980b868725387e9872083154ef26a25deb1273a00e9bdb6f"}],"guid":"bfdfe7dc352907fc980b868725387e9807367dfbfea4a268287e293fd446b7c9","type":"com.apple.buildphase.frameworks"},{"buildFiles":[],"guid":"bfdfe7dc352907fc980b868725387e98124b51724861d509176591ea77a0604c","type":"com.apple.buildphase.resources"}],"buildRules":[],"dependencies":[{"guid":"bfdfe7dc352907fc980b868725387e989da425bb6d6d5d8dbb95e4afffb82217","name":"Flutter"},{"guid":"bfdfe7dc352907fc980b868725387e98061ad5753743dc10d394720f4d91af46","name":"Try"}],"guid":"bfdfe7dc352907fc980b868725387e98512dd6ae7d689c296103006db83cb480","name":"speech_to_text","predominantSourceCodeLanguage":"Xcode.SourceCodeLanguage.Objective-C","productReference":{"guid":"bfdfe7dc352907fc980b868725387e98ac3159d15ec00980f6f3edeacb71520d","name":"speech_to_text.framework","type":"product"},"productTypeIdentifier":"com.apple.product-type.framework","provisioningSourceData":[{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Debug","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Profile","provisioningStyle":1},{"bundleIdentifierFromInfoPlist":"${PRODUCT_BUNDLE_IDENTIFIER}","configurationName":"Release","provisioningStyle":1}],"type":"standard"} \ No newline at end of file diff --git a/ios/build/ios/XCBuildData/PIFCache/workspace/WORKSPACE@v11_hash=(null)_subobjects=2a13d76b8b47d1d4aaa29515bf78de2b-json b/ios/build/ios/XCBuildData/PIFCache/workspace/WORKSPACE@v11_hash=(null)_subobjects=2a13d76b8b47d1d4aaa29515bf78de2b-json new file mode 100644 index 0000000..516a582 --- /dev/null +++ b/ios/build/ios/XCBuildData/PIFCache/workspace/WORKSPACE@v11_hash=(null)_subobjects=2a13d76b8b47d1d4aaa29515bf78de2b-json @@ -0,0 +1 @@ +{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/ajiang2/develop/xcode-projects/Helix/worktrees/audio-service/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=e586e7129ff5f2e6f341c4426f9dfb34_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]} \ No newline at end of file diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..c804af8 --- /dev/null +++ b/lib/app.dart @@ -0,0 +1,78 @@ +// ABOUTME: Main Flutter app widget with provider setup and routing +// ABOUTME: Configures theme, navigation, and dependency injection for the Helix app + +import 'package:flutter/material.dart'; + +import 'ui/screens/home_screen.dart'; +import 'ui/theme/app_theme.dart'; + +class HelixApp extends StatelessWidget { + const HelixApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Helix', + theme: AppTheme.lightTheme, + darkTheme: AppTheme.darkTheme, + themeMode: ThemeMode.system, + home: const HomeScreen(), + debugShowCheckedModeBanner: false, + ); + } +} + +class ErrorScreen extends StatelessWidget { + final String error; + final VoidCallback onRetry; + + const ErrorScreen({ + super.key, + required this.error, + required this.onRetry, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 64, + color: Colors.red, + ), + const SizedBox(height: 16), + const Text( + 'Oops! Something went wrong', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + error, + style: const TextStyle( + fontSize: 16, + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: onRetry, + child: const Text('Try Again'), + ), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/core/utils/constants.dart b/lib/core/utils/constants.dart new file mode 100644 index 0000000..dac25a7 --- /dev/null +++ b/lib/core/utils/constants.dart @@ -0,0 +1,190 @@ +// ABOUTME: App-wide constants for configuration, UUIDs, and settings +// ABOUTME: Centralized location for all hardcoded values and configuration parameters + +/// API Endpoints and Configuration +class APIConstants { + // OpenAI Configuration + static const String openAIBaseURL = 'https://api.openai.com/v1'; + static const String whisperEndpoint = '/audio/transcriptions'; + static const String chatCompletionsEndpoint = '/chat/completions'; + static const String defaultOpenAIModel = 'gpt-3.5-turbo'; + + // Anthropic Configuration + static const String anthropicBaseURL = 'https://api.anthropic.com/v1'; + static const String anthropicMessagesEndpoint = '/messages'; + static const String defaultAnthropicModel = 'anthropic-3-sonnet-20240229'; + + // Request Configuration + static const Duration apiTimeout = Duration(seconds: 30); + static const int maxRetries = 3; + static const Duration retryDelay = Duration(seconds: 2); +} + +/// Bluetooth Service UUIDs for Even Realities Glasses +class BluetoothConstants { + // Nordic UART Service (NUS) UUIDs + static const String nordicUARTServiceUUID = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E'; + static const String nordicUARTTXCharacteristicUUID = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E'; + static const String nordicUARTRXCharacteristicUUID = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E'; + + // Device Identification + static const String evenRealitiesManufacturerName = 'Even Realities'; + static const List targetDeviceNames = ['G1', 'Even G1', 'Even Realities G1']; + + // Connection Configuration + static const Duration scanTimeout = Duration(seconds: 30); + static const Duration connectionTimeout = Duration(seconds: 10); + static const Duration heartbeatInterval = Duration(seconds: 5); + static const int maxReconnectionAttempts = 3; +} + +/// Audio Processing Configuration +class AudioConstants { + // Recording Configuration + static const int sampleRate = 16000; // 16kHz for optimal speech recognition + static const int bitRate = 64000; // 64kbps for good quality + static const int numChannels = 1; // Mono recording + + // Voice Activity Detection + static const double voiceActivityThreshold = 0.01; + static const Duration silenceTimeout = Duration(milliseconds: 1500); + static const Duration minimumSpeechDuration = Duration(milliseconds: 500); + + // Audio Processing + static const Duration audioChunkDuration = Duration(seconds: 30); // For Whisper API + static const int bufferSizeFrames = 4096; + + // File Storage + static const String audioFileExtension = '.wav'; + static const String recordingsDirectory = 'recordings'; +} + +/// UI Constants and Themes +class UIConstants { + // App Branding + static const String appName = 'Helix'; + static const String appTagline = 'AI-Powered Conversation Intelligence'; + + // Navigation + static const int tabCount = 5; + static const List tabLabels = [ + 'Conversation', + 'Analysis', + 'Glasses', + 'History', + 'Settings' + ]; + + // Animation Durations + static const Duration defaultAnimationDuration = Duration(milliseconds: 300); + static const Duration fastAnimationDuration = Duration(milliseconds: 150); + static const Duration slowAnimationDuration = Duration(milliseconds: 500); + + // UI Spacing + static const double defaultPadding = 16.0; + static const double smallPadding = 8.0; + static const double largePadding = 24.0; + static const double borderRadius = 12.0; + + // Real-time Updates + static const Duration transcriptionUpdateInterval = Duration(milliseconds: 100); + static const Duration statusUpdateInterval = Duration(milliseconds: 500); +} + +/// Data Storage and Persistence +class StorageConstants { + // SharedPreferences Keys + static const String userSettingsKey = 'user_settings'; + static const String apiKeysKey = 'api_keys'; + static const String devicePreferencesKey = 'device_preferences'; + static const String lastConnectedGlassesKey = 'last_connected_glasses'; + + // Database Configuration + static const String databaseName = 'helix_conversations.db'; + static const int databaseVersion = 1; + + // Cache Configuration + static const Duration cacheExpiration = Duration(hours: 24); + static const int maxCacheSize = 100; // MB + static const int maxConversationHistory = 1000; +} + +/// AI Analysis Configuration +class AnalysisConstants { + // Fact-checking + static const int maxClaimsPerAnalysis = 10; + static const double minimumConfidenceThreshold = 0.7; + static const Duration analysisTimeout = Duration(minutes: 2); + + // Conversation Analysis + static const int minimumWordsForAnalysis = 50; + static const Duration batchAnalysisDelay = Duration(seconds: 5); + + // Prompt Templates + static const String factCheckPromptTemplate = ''' +Analyze the following conversation segment for factual claims that can be verified: + +{conversation_text} + +Please identify any specific factual claims and provide verification with sources. +Format your response as JSON with the following structure: +{ + "claims": [ + { + "claim": "statement to verify", + "verification": "verified/disputed/uncertain", + "confidence": 0.0-1.0, + "sources": ["source1", "source2"] + } + ] +} +'''; + + static const String summaryPromptTemplate = ''' +Provide a concise summary of the following conversation: + +{conversation_text} + +Include: +- Key topics discussed +- Main points and decisions +- Action items (if any) +- Overall tone and sentiment + +Keep the summary under 200 words. +'''; +} + +/// Error Messages and User Feedback +class MessageConstants { + // Audio Errors + static const String microphonePermissionRequired = + 'Microphone access is required for conversation transcription. Please enable it in Settings.'; + static const String audioRecordingFailed = + 'Failed to start recording. Please check your microphone and try again.'; + + // Bluetooth Errors + static const String bluetoothPermissionRequired = + 'Bluetooth access is required to connect to your Even Realities glasses.'; + static const String glassesNotFound = + 'No Even Realities glasses found. Make sure they are powered on and nearby.'; + static const String connectionLost = + 'Connection to glasses lost. Attempting to reconnect...'; + + // AI Service Errors + static const String apiKeyRequired = + 'API key is required for AI analysis. Please configure it in Settings.'; + static const String analysisUnavailable = + 'AI analysis is temporarily unavailable. Please try again later.'; + + // Network Errors + static const String noInternetConnection = + 'No internet connection. Some features may be limited.'; + static const String requestTimeout = + 'Request timed out. Please check your connection and try again.'; + + // Success Messages + static const String glassesConnected = 'Successfully connected to Even Realities glasses!'; + static const String recordingStarted = 'Recording started. Speak naturally for best results.'; + static const String analysisComplete = 'Conversation analysis complete.'; +} \ No newline at end of file diff --git a/lib/core/utils/exceptions.dart b/lib/core/utils/exceptions.dart new file mode 100644 index 0000000..c9f2042 --- /dev/null +++ b/lib/core/utils/exceptions.dart @@ -0,0 +1,181 @@ +// ABOUTME: Custom exception classes for different service types +// ABOUTME: Provides specific error types for better error handling and debugging + +/// Base exception class for all Helix app exceptions +abstract class HelixException implements Exception { + final String message; + final Object? originalError; + final StackTrace? stackTrace; + + const HelixException( + this.message, { + this.originalError, + this.stackTrace, + }); + + @override + String toString() { + return '$runtimeType: $message'; + } +} + +/// Audio service related exceptions +class AudioException extends HelixException { + const AudioException( + super.message, { + super.originalError, + super.stackTrace, + }); +} + +class AudioPermissionDeniedException extends AudioException { + const AudioPermissionDeniedException() + : super('Microphone permission was denied. Please enable microphone access in settings.'); +} + +class AudioDeviceNotFoundException extends AudioException { + const AudioDeviceNotFoundException() + : super('No audio input device found. Please check your microphone connection.'); +} + +class AudioRecordingException extends AudioException { + const AudioRecordingException(super.message, {super.originalError}); +} + +/// Transcription service related exceptions +class TranscriptionException extends HelixException { + const TranscriptionException( + super.message, { + super.originalError, + super.stackTrace, + }); +} + +class SpeechRecognitionUnavailableException extends TranscriptionException { + const SpeechRecognitionUnavailableException() + : super('Speech recognition is not available on this device.'); +} + +class WhisperAPIException extends TranscriptionException { + final int? statusCode; + + const WhisperAPIException( + super.message, { + this.statusCode, + super.originalError, + }); +} + +/// AI/LLM service related exceptions +class AIException extends HelixException { + const AIException( + super.message, { + super.originalError, + super.stackTrace, + }); +} + +class APIKeyMissingException extends AIException { + const APIKeyMissingException(String provider) + : super('API key for $provider is missing. Please configure it in settings.'); +} + +class AIProviderException extends AIException { + final String provider; + final int? statusCode; + + const AIProviderException( + this.provider, + super.message, { + this.statusCode, + super.originalError, + }); +} + +class RateLimitExceededException extends AIException { + final Duration retryAfter; + + const RateLimitExceededException(this.retryAfter) + : super('API rate limit exceeded. Please try again later.'); +} + +/// Bluetooth and glasses service related exceptions +class BluetoothException extends HelixException { + const BluetoothException( + super.message, { + super.originalError, + super.stackTrace, + }); +} + +class BluetoothUnavailableException extends BluetoothException { + const BluetoothUnavailableException() + : super('Bluetooth is not available on this device.'); +} + +class BluetoothPermissionDeniedException extends BluetoothException { + const BluetoothPermissionDeniedException() + : super('Bluetooth permission was denied. Please enable Bluetooth access in settings.'); +} + +class GlassesConnectionException extends BluetoothException { + const GlassesConnectionException(String message) + : super('Failed to connect to Even Realities glasses: $message'); +} + +class GlassesNotFoundException extends BluetoothException { + const GlassesNotFoundException() + : super('No Even Realities glasses found. Please make sure they are powered on and nearby.'); +} + +/// Network related exceptions +class NetworkException extends HelixException { + const NetworkException( + super.message, { + super.originalError, + super.stackTrace, + }); +} + +class NoInternetConnectionException extends NetworkException { + const NoInternetConnectionException() + : super('No internet connection available. Please check your network settings.'); +} + +class TimeoutException extends NetworkException { + const TimeoutException(String operation) + : super('$operation timed out. Please check your connection and try again.'); +} + +/// Settings and configuration related exceptions +class SettingsException extends HelixException { + const SettingsException( + super.message, { + super.originalError, + super.stackTrace, + }); +} + +class ConfigurationException extends SettingsException { + const ConfigurationException(String setting) + : super('Invalid configuration for $setting. Please check your settings.'); +} + +/// Data persistence related exceptions +class DataException extends HelixException { + const DataException( + super.message, { + super.originalError, + super.stackTrace, + }); +} + +class DatabaseException extends DataException { + const DatabaseException(String operation, {Object? originalError}) + : super('Database error during $operation', originalError: originalError); +} + +class SerializationException extends DataException { + const SerializationException(String type, {Object? originalError}) + : super('Failed to serialize/deserialize $type', originalError: originalError); +} \ No newline at end of file diff --git a/lib/core/utils/logging_service.dart b/lib/core/utils/logging_service.dart new file mode 100644 index 0000000..36e3be1 --- /dev/null +++ b/lib/core/utils/logging_service.dart @@ -0,0 +1,407 @@ +// ABOUTME: Enhanced logging service with debugging features and file output +// ABOUTME: Provides consistent logging across all app components with filtering and debug tools + +import 'dart:developer' as developer; +import 'dart:io'; +import 'dart:convert'; + +enum LogLevel { + debug, + info, + warning, + error, + critical, +} + +class LoggingService { + static LoggingService? _instance; + static LoggingService get instance => _instance ??= LoggingService._(); + + LoggingService._(); + + LogLevel _currentLevel = LogLevel.debug; + final List _logs = []; + final int _maxLogEntries = 1000; + + // Debug features + bool _fileLoggingEnabled = false; + String? _logFilePath; + bool _performanceLoggingEnabled = false; + final Map _performanceMarkers = {}; + + // Filtering and search + Set _tagFilters = {}; + String? _messageFilter; + + /// Set the minimum log level that will be output + void setLogLevel(LogLevel level) { + _currentLevel = level; + log('LoggingService', 'Log level set to ${level.name}', LogLevel.info); + } + + /// Log a message with specified level + void log(String tag, String message, LogLevel level) { + if (level.index < _currentLevel.index) return; + + final entry = LogEntry( + timestamp: DateTime.now(), + tag: tag, + message: message, + level: level, + ); + + _addLogEntry(entry); + _outputLog(entry); + } + + /// Convenience methods for different log levels + void debug(String tag, String message) => log(tag, message, LogLevel.debug); + void info(String tag, String message) => log(tag, message, LogLevel.info); + void warning(String tag, String message) => log(tag, message, LogLevel.warning); + void error(String tag, String message, [Object? error, StackTrace? stackTrace]) { + String fullMessage = message; + if (error != null) { + fullMessage += '\nError: $error'; + } + if (stackTrace != null) { + fullMessage += '\nStack trace:\n$stackTrace'; + } + log(tag, fullMessage, LogLevel.error); + } + void critical(String tag, String message, [Object? error, StackTrace? stackTrace]) { + String fullMessage = message; + if (error != null) { + fullMessage += '\nError: $error'; + } + if (stackTrace != null) { + fullMessage += '\nStack trace:\n$stackTrace'; + } + log(tag, fullMessage, LogLevel.critical); + } + + /// Get recent log entries + List getRecentLogs([int? limit]) { + if (limit == null) return List.unmodifiable(_logs); + return List.unmodifiable(_logs.take(limit)); + } + + /// Clear all stored logs + void clearLogs() { + _logs.clear(); + log('LoggingService', 'Log history cleared', LogLevel.info); + } + + // ========================================================================== + // Debug and Advanced Features + // ========================================================================== + + /// Enable file logging to a specified path + Future enableFileLogging(String filePath) async { + try { + _logFilePath = filePath; + final file = File(filePath); + await file.create(recursive: true); + _fileLoggingEnabled = true; + log('LoggingService', 'File logging enabled: $filePath', LogLevel.info); + } catch (e) { + log('LoggingService', 'Failed to enable file logging: $e', LogLevel.error); + } + } + + /// Disable file logging + void disableFileLogging() { + _fileLoggingEnabled = false; + _logFilePath = null; + log('LoggingService', 'File logging disabled', LogLevel.info); + } + + /// Enable performance logging for timing operations + void enablePerformanceLogging() { + _performanceLoggingEnabled = true; + log('LoggingService', 'Performance logging enabled', LogLevel.info); + } + + /// Disable performance logging + void disablePerformanceLogging() { + _performanceLoggingEnabled = false; + _performanceMarkers.clear(); + log('LoggingService', 'Performance logging disabled', LogLevel.info); + } + + /// Start a performance timing marker + void startPerformanceTimer(String markerId) { + if (!_performanceLoggingEnabled) return; + _performanceMarkers[markerId] = DateTime.now(); + log('Performance', 'Started timer: $markerId', LogLevel.debug); + } + + /// End a performance timing marker and log the duration + void endPerformanceTimer(String markerId, [String? operation]) { + if (!_performanceLoggingEnabled) return; + + final startTime = _performanceMarkers.remove(markerId); + if (startTime == null) { + log('Performance', 'Timer not found: $markerId', LogLevel.warning); + return; + } + + final duration = DateTime.now().difference(startTime); + final op = operation ?? markerId; + log('Performance', '$op completed in ${duration.inMilliseconds}ms', LogLevel.info); + } + + /// Add tag filters - only logs from these tags will be shown + void addTagFilter(String tag) { + _tagFilters.add(tag); + log('LoggingService', 'Added tag filter: $tag', LogLevel.debug); + } + + /// Remove a tag filter + void removeTagFilter(String tag) { + _tagFilters.remove(tag); + log('LoggingService', 'Removed tag filter: $tag', LogLevel.debug); + } + + /// Clear all tag filters + void clearTagFilters() { + _tagFilters.clear(); + log('LoggingService', 'Cleared all tag filters', LogLevel.debug); + } + + /// Set message filter - only logs containing this text will be shown + void setMessageFilter(String? filter) { + _messageFilter = filter; + log('LoggingService', filter != null ? 'Set message filter: $filter' : 'Cleared message filter', LogLevel.debug); + } + + /// Get filtered logs based on current filters + List getFilteredLogs({ + LogLevel? minLevel, + String? tag, + DateTime? since, + int? limit, + }) { + var filtered = _logs.where((entry) { + // Level filter + if (minLevel != null && entry.level.index < minLevel.index) return false; + + // Tag filter + if (tag != null && entry.tag != tag) return false; + if (_tagFilters.isNotEmpty && !_tagFilters.contains(entry.tag)) return false; + + // Message filter + if (_messageFilter != null && !entry.message.toLowerCase().contains(_messageFilter!.toLowerCase())) return false; + + // Time filter + if (since != null && entry.timestamp.isBefore(since)) return false; + + return true; + }).toList(); + + if (limit != null && filtered.length > limit) { + filtered = filtered.take(limit).toList(); + } + + return filtered; + } + + /// Export logs to JSON format + String exportLogsAsJson({ + LogLevel? minLevel, + String? tag, + DateTime? since, + }) { + final filtered = getFilteredLogs(minLevel: minLevel, tag: tag, since: since); + final jsonData = filtered.map((entry) => { + 'timestamp': entry.timestamp.toIso8601String(), + 'level': entry.level.name, + 'tag': entry.tag, + 'message': entry.message, + }).toList(); + + return jsonEncode(jsonData); + } + + /// Export logs to plain text format + String exportLogsAsText({ + LogLevel? minLevel, + String? tag, + DateTime? since, + }) { + final filtered = getFilteredLogs(minLevel: minLevel, tag: tag, since: since); + return filtered.map((entry) => entry.toString()).join('\n'); + } + + /// Get logging statistics + Map getLoggingStats() { + final now = DateTime.now(); + final oneHourAgo = now.subtract(const Duration(hours: 1)); + final oneDayAgo = now.subtract(const Duration(days: 1)); + + final recentLogs = _logs.where((log) => log.timestamp.isAfter(oneHourAgo)).toList(); + final dailyLogs = _logs.where((log) => log.timestamp.isAfter(oneDayAgo)).toList(); + + final levelCounts = {}; + final tagCounts = {}; + + for (final log in _logs) { + levelCounts[log.level.name] = (levelCounts[log.level.name] ?? 0) + 1; + tagCounts[log.tag] = (tagCounts[log.tag] ?? 0) + 1; + } + + return { + 'totalLogs': _logs.length, + 'recentLogs': recentLogs.length, + 'dailyLogs': dailyLogs.length, + 'levelCounts': levelCounts, + 'topTags': tagCounts.entries.toList()..sort((a, b) => b.value.compareTo(a.value)), + 'fileLoggingEnabled': _fileLoggingEnabled, + 'performanceLoggingEnabled': _performanceLoggingEnabled, + 'activeFilters': { + 'tagFilters': _tagFilters.toList(), + 'messageFilter': _messageFilter, + }, + }; + } + + void _addLogEntry(LogEntry entry) { + _logs.insert(0, entry); // Add to beginning for most recent first + + // Maintain max log entries + if (_logs.length > _maxLogEntries) { + _logs.removeRange(_maxLogEntries, _logs.length); + } + } + + void _outputLog(LogEntry entry) { + final formattedMessage = '[${entry.level.name.toUpperCase()}] ${entry.tag}: ${entry.message}'; + + // Output to developer console + developer.log( + formattedMessage, + time: entry.timestamp, + level: _getDeveloperLogLevel(entry.level), + name: entry.tag, + ); + + // Output to file if enabled + if (_fileLoggingEnabled && _logFilePath != null) { + _writeToFile(entry); + } + } + + void _writeToFile(LogEntry entry) async { + try { + final file = File(_logFilePath!); + final logLine = '${entry.toString()}\n'; + await file.writeAsString(logLine, mode: FileMode.append); + } catch (e) { + // Avoid infinite recursion by not logging this error + developer.log('Failed to write to log file: $e', name: 'LoggingService'); + } + } + + int _getDeveloperLogLevel(LogLevel level) { + switch (level) { + case LogLevel.debug: + return 500; + case LogLevel.info: + return 800; + case LogLevel.warning: + return 900; + case LogLevel.error: + return 1000; + case LogLevel.critical: + return 1200; + } + } +} + +class LogEntry { + final DateTime timestamp; + final String tag; + final String message; + final LogLevel level; + + LogEntry({ + required this.timestamp, + required this.tag, + required this.message, + required this.level, + }); + + @override + String toString() { + return '${timestamp.toIso8601String()} [${level.name.toUpperCase()}] $tag: $message'; + } +} + +/// Global logger instance for convenience +final logger = LoggingService.instance; + +// ========================================================================== +// Debug Helper Functions +// ========================================================================== + +/// Debug helper to log function entry with parameters +void logFunctionEntry(String className, String functionName, [Map? params]) { + final paramStr = params?.entries.map((e) => '${e.key}=${e.value}').join(', ') ?? ''; + logger.debug(className, 'ENTER $functionName($paramStr)'); +} + +/// Debug helper to log function exit with return value +void logFunctionExit(String className, String functionName, [dynamic returnValue]) { + final retStr = returnValue != null ? ' -> $returnValue' : ''; + logger.debug(className, 'EXIT $functionName$retStr'); +} + +/// Debug helper to log state changes +void logStateChange(String className, String property, dynamic oldValue, dynamic newValue) { + logger.debug(className, 'STATE CHANGE $property: $oldValue -> $newValue'); +} + +/// Debug helper to log API calls +void logApiCall(String endpoint, String method, [Map? data]) { + final dataStr = data != null ? ' with data: $data' : ''; + logger.info('API', '$method $endpoint$dataStr'); +} + +/// Debug helper to log API responses +void logApiResponse(String endpoint, int statusCode, [dynamic response]) { + final respStr = response != null ? ' response: $response' : ''; + logger.info('API', '$endpoint returned $statusCode$respStr'); +} + +/// Debug helper to log user interactions +void logUserAction(String action, [Map? context]) { + final contextStr = context?.entries.map((e) => '${e.key}=${e.value}').join(', ') ?? ''; + logger.info('USER', 'Action: $action${contextStr.isNotEmpty ? ' ($contextStr)' : ''}'); +} + +/// Debug helper to log memory usage (simplified) +void logMemoryUsage(String tag) { + // Note: Dart doesn't have direct memory introspection, but we can log process info + logger.debug(tag, 'Memory check requested (detailed memory info not available in Dart)'); +} + +/// Debug helper for recording session management +void logRecordingEvent(String event, [Map? details]) { + final detailStr = details?.entries.map((e) => '${e.key}=${e.value}').join(', ') ?? ''; + logger.info('RECORDING', '$event${detailStr.isNotEmpty ? ' ($detailStr)' : ''}'); +} + +/// Debug helper for audio processing +void logAudioEvent(String event, {double? level, Duration? duration, String? details}) { + var message = event; + if (level != null) message += ' level=${level.toStringAsFixed(3)}'; + if (duration != null) message += ' duration=${duration.inMilliseconds}ms'; + if (details != null) message += ' $details'; + logger.debug('AUDIO', message); +} + +/// Debug helper for conversation processing +void logConversationEvent(String event, String conversationId, [String? details]) { + var message = '$event conversationId=$conversationId'; + if (details != null) message += ' $details'; + logger.info('CONVERSATION', message); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..135debe --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,39 @@ +// ABOUTME: Main entry point for the Helix Flutter application +// ABOUTME: Initializes services, sets up dependency injection, and launches the app + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'app.dart'; +import 'services/service_locator.dart'; +import 'core/utils/logging_service.dart'; + +void main() async { + // Ensure Flutter bindings are initialized + WidgetsFlutterBinding.ensureInitialized(); + + // Set up global error handling + FlutterError.onError = (FlutterErrorDetails details) { + logger.error('Flutter', 'Unhandled Flutter error', details.exception, details.stack); + }; + + // Set up dependency injection + try { + await setupServiceLocator(); + logger.info('Main', 'Service locator initialized successfully'); + } catch (error, stackTrace) { + logger.critical('Main', 'Failed to initialize service locator', error, stackTrace); + // Continue with app launch even if some services fail + } + + // Configure system UI + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + ), + ); + + // Launch the app + runApp(const HelixApp()); +} \ No newline at end of file diff --git a/lib/models/analysis_result.dart b/lib/models/analysis_result.dart new file mode 100644 index 0000000..af4ef81 --- /dev/null +++ b/lib/models/analysis_result.dart @@ -0,0 +1,474 @@ +// ABOUTME: AI analysis result data model for conversation insights and intelligence +// ABOUTME: Comprehensive model for fact-checking, summaries, and extracted insights + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'analysis_result.freezed.dart'; +part 'analysis_result.g.dart'; + +/// Type of analysis performed +enum AnalysisType { + factCheck, + summary, + actionItems, + sentiment, + topics, + comprehensive, +} + +/// Confidence level for analysis results +enum ConfidenceLevel { + low, // < 0.5 + medium, // 0.5 - 0.8 + high, // > 0.8 +} + +/// Status of an analysis +enum AnalysisStatus { + pending, + processing, + completed, + failed, + partial, +} + +/// Main analysis result container +@freezed +class AnalysisResult with _$AnalysisResult { + const factory AnalysisResult({ + /// Unique identifier for this analysis + required String id, + + /// ID of the conversation being analyzed + required String conversationId, + + /// Type of analysis performed + required AnalysisType type, + + /// Current status of the analysis + required AnalysisStatus status, + + /// When the analysis started + required DateTime startTime, + + /// When the analysis completed + DateTime? completionTime, + + /// AI provider used for analysis + String? provider, + + /// Overall confidence score + @Default(0.0) double confidence, + + /// Fact-checking results + List? factChecks, + + /// Conversation summary + ConversationSummary? summary, + + /// Extracted action items + List? actionItems, + + /// Sentiment analysis + SentimentAnalysisResult? sentiment, + + /// Identified topics + List? topics, + + /// Key insights and findings + @Default([]) List insights, + + /// Processing errors or warnings + @Default([]) List errors, + + /// Processing time in milliseconds + int? processingTimeMs, + + /// Token usage for AI processing + Map? tokenUsage, + + /// Additional metadata + @Default({}) Map metadata, + }) = _AnalysisResult; + + factory AnalysisResult.fromJson(Map json) => + _$AnalysisResultFromJson(json); + + const AnalysisResult._(); + + /// Whether the analysis completed successfully + bool get isCompleted => status == AnalysisStatus.completed; + + /// Whether the analysis failed + bool get isFailed => status == AnalysisStatus.failed; + + /// Whether the analysis is still in progress + bool get isInProgress => status == AnalysisStatus.processing || status == AnalysisStatus.pending; + + /// Get confidence level category + ConfidenceLevel get confidenceLevel { + if (confidence < 0.5) return ConfidenceLevel.low; + if (confidence < 0.8) return ConfidenceLevel.medium; + return ConfidenceLevel.high; + } + + /// Processing duration + Duration? get processingDuration { + if (completionTime != null) { + return completionTime!.difference(startTime); + } + return null; + } + + /// Count of verified facts + int get verifiedFactsCount { + return factChecks?.where((f) => f.isVerified).length ?? 0; + } + + /// Count of disputed facts + int get disputedFactsCount { + return factChecks?.where((f) => f.isDisputed).length ?? 0; + } + + /// Count of high-priority action items + int get highPriorityActionItemsCount { + return actionItems?.where((a) => a.priority == ActionItemPriority.high).length ?? 0; + } + + /// Whether the analysis has any critical findings + bool get hasCriticalFindings { + return disputedFactsCount > 0 || + highPriorityActionItemsCount > 0 || + (sentiment?.overallSentiment == SentimentType.negative && sentiment!.confidence > 0.8); + } +} + +/// Fact-checking result for individual claims +@freezed +class FactCheckResult with _$FactCheckResult { + const factory FactCheckResult({ + /// Unique identifier + required String id, + + /// The claim being fact-checked + required String claim, + + /// Verification result + required FactCheckStatus status, + + /// Confidence in the verification + required double confidence, + + /// Supporting sources + @Default([]) List sources, + + /// Detailed explanation + String? explanation, + + /// Context within the conversation + String? context, + + /// Timestamp range where claim appears + int? startTimeMs, + int? endTimeMs, + + /// Speaker who made the claim + String? speakerId, + + /// Category of the claim + String? category, + + /// Related claims + @Default([]) List relatedClaims, + }) = _FactCheckResult; + + factory FactCheckResult.fromJson(Map json) => + _$FactCheckResultFromJson(json); + + const FactCheckResult._(); + + bool get isVerified => status == FactCheckStatus.verified; + bool get isDisputed => status == FactCheckStatus.disputed; + bool get isUncertain => status == FactCheckStatus.uncertain; + bool get needsReview => status == FactCheckStatus.needsReview; +} + +/// Status of fact-check verification +enum FactCheckStatus { + verified, // Confirmed as accurate + disputed, // Found to be inaccurate + uncertain, // Cannot be verified + needsReview, // Requires human review +} + +/// Conversation summary with key points +@freezed +class ConversationSummary with _$ConversationSummary { + const factory ConversationSummary({ + /// Main summary text + required String summary, + + /// Key discussion points + @Default([]) List keyPoints, + + /// Important decisions made + @Default([]) List decisions, + + /// Questions raised + @Default([]) List questions, + + /// Overall tone of conversation + String? tone, + + /// Main topics discussed + @Default([]) List topics, + + /// Summary length category + @Default(SummaryLength.medium) SummaryLength length, + + /// Estimated reading time + Duration? estimatedReadTime, + + /// Confidence in summary accuracy + @Default(0.0) double confidence, + }) = _ConversationSummary; + + factory ConversationSummary.fromJson(Map json) => + _$ConversationSummaryFromJson(json); + + const ConversationSummary._(); + + /// Word count of the summary + int get wordCount => summary.split(' ').where((w) => w.isNotEmpty).length; + + /// Whether the summary is comprehensive + bool get isComprehensive => keyPoints.length >= 3 && decisions.isNotEmpty; +} + +/// Length categories for summaries +enum SummaryLength { + brief, // < 100 words + medium, // 100-300 words + detailed, // > 300 words +} + +/// Action item extracted from conversation +@freezed +class ActionItemResult with _$ActionItemResult { + const factory ActionItemResult({ + /// Unique identifier + required String id, + + /// Description of the action + required String description, + + /// Assigned person (if mentioned) + String? assignee, + + /// Due date (if mentioned) + DateTime? dueDate, + + /// Priority level + @Default(ActionItemPriority.medium) ActionItemPriority priority, + + /// Context where it was mentioned + String? context, + + /// Confidence in extraction accuracy + @Default(0.0) double confidence, + + /// Status of the action item + @Default(ActionItemStatus.pending) ActionItemStatus status, + + /// Timestamp where mentioned + int? mentionedAtMs, + + /// Speaker who mentioned it + String? speakerId, + + /// Related action items + @Default([]) List relatedItems, + + /// Categories or tags + @Default([]) List tags, + }) = _ActionItemResult; + + factory ActionItemResult.fromJson(Map json) => + _$ActionItemResultFromJson(json); + + const ActionItemResult._(); + + /// Whether this is a high-priority item + bool get isHighPriority => priority == ActionItemPriority.high; + + /// Whether the item is overdue + bool get isOverdue => dueDate != null && dueDate!.isBefore(DateTime.now()); + + /// Days until due date + int? get daysUntilDue { + if (dueDate == null) return null; + return dueDate!.difference(DateTime.now()).inDays; + } +} + +/// Priority levels for action items +enum ActionItemPriority { + low, + medium, + high, + urgent, +} + +/// Status of action items +enum ActionItemStatus { + pending, + inProgress, + completed, + cancelled, + deferred, +} + +/// Sentiment analysis result +@freezed +class SentimentAnalysisResult with _$SentimentAnalysisResult { + const factory SentimentAnalysisResult({ + /// Overall sentiment + required SentimentType overallSentiment, + + /// Confidence in sentiment analysis + required double confidence, + + /// Detailed emotion breakdown + required Map emotions, + + /// Conversation tone + String? tone, + + /// Sentiment progression over time + @Default([]) List progression, + + /// Participant-specific sentiment + @Default({}) Map participantSentiments, + + /// Key phrases that influenced sentiment + @Default([]) List keyPhrases, + }) = _SentimentAnalysisResult; + + factory SentimentAnalysisResult.fromJson(Map json) => + _$SentimentAnalysisResultFromJson(json); + + const SentimentAnalysisResult._(); + + /// Whether the overall sentiment is positive + bool get isPositive => overallSentiment == SentimentType.positive; + + /// Whether the overall sentiment is negative + bool get isNegative => overallSentiment == SentimentType.negative; + + /// Get the dominant emotion + String? get dominantEmotion { + if (emotions.isEmpty) return null; + + double maxValue = 0.0; + String? dominant; + + emotions.forEach((emotion, value) { + if (value > maxValue) { + maxValue = value; + dominant = emotion; + } + }); + + return dominant; + } +} + +/// Sentiment types +enum SentimentType { + positive, + negative, + neutral, + mixed, +} + +/// Sentiment at a specific point in time +@freezed +class SentimentTimePoint with _$SentimentTimePoint { + const factory SentimentTimePoint({ + required int timeMs, + required SentimentType sentiment, + required double confidence, + }) = _SentimentTimePoint; + + factory SentimentTimePoint.fromJson(Map json) => + _$SentimentTimePointFromJson(json); +} + +/// Topic identified in conversation +@freezed +class TopicResult with _$TopicResult { + const factory TopicResult({ + /// Topic name or title + required String name, + + /// Relevance score (0.0 to 1.0) + required double relevance, + + /// Keywords associated with topic + @Default([]) List keywords, + + /// Category of the topic + String? category, + + /// Description of the topic + String? description, + + /// Time ranges where topic was discussed + @Default([]) List timeRanges, + + /// Participants who discussed this topic + @Default([]) List participants, + + /// Related topics + @Default([]) List relatedTopics, + + /// Confidence in topic identification + @Default(0.0) double confidence, + }) = _TopicResult; + + factory TopicResult.fromJson(Map json) => + _$TopicResultFromJson(json); + + const TopicResult._(); + + /// Total time spent discussing this topic + Duration get totalDiscussionTime { + return timeRanges.fold( + Duration.zero, + (total, range) => total + range.duration, + ); + } + + /// Whether this is a major topic (high relevance) + bool get isMajorTopic => relevance > 0.7; +} + +/// Time range for topic discussion +@freezed +class TimeRange with _$TimeRange { + const factory TimeRange({ + required int startMs, + required int endMs, + }) = _TimeRange; + + factory TimeRange.fromJson(Map json) => + _$TimeRangeFromJson(json); + + const TimeRange._(); + + /// Duration of this time range + Duration get duration => Duration(milliseconds: endMs - startMs); + + /// Whether this range contains a specific time + bool contains(int timeMs) => timeMs >= startMs && timeMs <= endMs; +} \ No newline at end of file diff --git a/lib/models/analysis_result.freezed.dart b/lib/models/analysis_result.freezed.dart new file mode 100644 index 0000000..ca37e76 --- /dev/null +++ b/lib/models/analysis_result.freezed.dart @@ -0,0 +1,3537 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'analysis_result.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AnalysisResult _$AnalysisResultFromJson(Map json) { + return _AnalysisResult.fromJson(json); +} + +/// @nodoc +mixin _$AnalysisResult { + /// Unique identifier for this analysis + String get id => throw _privateConstructorUsedError; + + /// ID of the conversation being analyzed + String get conversationId => throw _privateConstructorUsedError; + + /// Type of analysis performed + AnalysisType get type => throw _privateConstructorUsedError; + + /// Current status of the analysis + AnalysisStatus get status => throw _privateConstructorUsedError; + + /// When the analysis started + DateTime get startTime => throw _privateConstructorUsedError; + + /// When the analysis completed + DateTime? get completionTime => throw _privateConstructorUsedError; + + /// AI provider used for analysis + String? get provider => throw _privateConstructorUsedError; + + /// Overall confidence score + double get confidence => throw _privateConstructorUsedError; + + /// Fact-checking results + List? get factChecks => throw _privateConstructorUsedError; + + /// Conversation summary + ConversationSummary? get summary => throw _privateConstructorUsedError; + + /// Extracted action items + List? get actionItems => throw _privateConstructorUsedError; + + /// Sentiment analysis + SentimentAnalysisResult? get sentiment => throw _privateConstructorUsedError; + + /// Identified topics + List? get topics => throw _privateConstructorUsedError; + + /// Key insights and findings + List get insights => throw _privateConstructorUsedError; + + /// Processing errors or warnings + List get errors => throw _privateConstructorUsedError; + + /// Processing time in milliseconds + int? get processingTimeMs => throw _privateConstructorUsedError; + + /// Token usage for AI processing + Map? get tokenUsage => throw _privateConstructorUsedError; + + /// Additional metadata + Map get metadata => throw _privateConstructorUsedError; + + /// Serializes this AnalysisResult to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AnalysisResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AnalysisResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AnalysisResultCopyWith<$Res> { + factory $AnalysisResultCopyWith( + AnalysisResult value, + $Res Function(AnalysisResult) then, + ) = _$AnalysisResultCopyWithImpl<$Res, AnalysisResult>; + @useResult + $Res call({ + String id, + String conversationId, + AnalysisType type, + AnalysisStatus status, + DateTime startTime, + DateTime? completionTime, + String? provider, + double confidence, + List? factChecks, + ConversationSummary? summary, + List? actionItems, + SentimentAnalysisResult? sentiment, + List? topics, + List insights, + List errors, + int? processingTimeMs, + Map? tokenUsage, + Map metadata, + }); + + $ConversationSummaryCopyWith<$Res>? get summary; + $SentimentAnalysisResultCopyWith<$Res>? get sentiment; +} + +/// @nodoc +class _$AnalysisResultCopyWithImpl<$Res, $Val extends AnalysisResult> + implements $AnalysisResultCopyWith<$Res> { + _$AnalysisResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AnalysisResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? conversationId = null, + Object? type = null, + Object? status = null, + Object? startTime = null, + Object? completionTime = freezed, + Object? provider = freezed, + Object? confidence = null, + Object? factChecks = freezed, + Object? summary = freezed, + Object? actionItems = freezed, + Object? sentiment = freezed, + Object? topics = freezed, + Object? insights = null, + Object? errors = null, + Object? processingTimeMs = freezed, + Object? tokenUsage = freezed, + Object? metadata = null, + }) { + return _then( + _value.copyWith( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + conversationId: + null == conversationId + ? _value.conversationId + : conversationId // ignore: cast_nullable_to_non_nullable + as String, + type: + null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as AnalysisType, + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as AnalysisStatus, + startTime: + null == startTime + ? _value.startTime + : startTime // ignore: cast_nullable_to_non_nullable + as DateTime, + completionTime: + freezed == completionTime + ? _value.completionTime + : completionTime // ignore: cast_nullable_to_non_nullable + as DateTime?, + provider: + freezed == provider + ? _value.provider + : provider // ignore: cast_nullable_to_non_nullable + as String?, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + factChecks: + freezed == factChecks + ? _value.factChecks + : factChecks // ignore: cast_nullable_to_non_nullable + as List?, + summary: + freezed == summary + ? _value.summary + : summary // ignore: cast_nullable_to_non_nullable + as ConversationSummary?, + actionItems: + freezed == actionItems + ? _value.actionItems + : actionItems // ignore: cast_nullable_to_non_nullable + as List?, + sentiment: + freezed == sentiment + ? _value.sentiment + : sentiment // ignore: cast_nullable_to_non_nullable + as SentimentAnalysisResult?, + topics: + freezed == topics + ? _value.topics + : topics // ignore: cast_nullable_to_non_nullable + as List?, + insights: + null == insights + ? _value.insights + : insights // ignore: cast_nullable_to_non_nullable + as List, + errors: + null == errors + ? _value.errors + : errors // ignore: cast_nullable_to_non_nullable + as List, + processingTimeMs: + freezed == processingTimeMs + ? _value.processingTimeMs + : processingTimeMs // ignore: cast_nullable_to_non_nullable + as int?, + tokenUsage: + freezed == tokenUsage + ? _value.tokenUsage + : tokenUsage // ignore: cast_nullable_to_non_nullable + as Map?, + metadata: + null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ) + as $Val, + ); + } + + /// Create a copy of AnalysisResult + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ConversationSummaryCopyWith<$Res>? get summary { + if (_value.summary == null) { + return null; + } + + return $ConversationSummaryCopyWith<$Res>(_value.summary!, (value) { + return _then(_value.copyWith(summary: value) as $Val); + }); + } + + /// Create a copy of AnalysisResult + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SentimentAnalysisResultCopyWith<$Res>? get sentiment { + if (_value.sentiment == null) { + return null; + } + + return $SentimentAnalysisResultCopyWith<$Res>(_value.sentiment!, (value) { + return _then(_value.copyWith(sentiment: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AnalysisResultImplCopyWith<$Res> + implements $AnalysisResultCopyWith<$Res> { + factory _$$AnalysisResultImplCopyWith( + _$AnalysisResultImpl value, + $Res Function(_$AnalysisResultImpl) then, + ) = __$$AnalysisResultImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String conversationId, + AnalysisType type, + AnalysisStatus status, + DateTime startTime, + DateTime? completionTime, + String? provider, + double confidence, + List? factChecks, + ConversationSummary? summary, + List? actionItems, + SentimentAnalysisResult? sentiment, + List? topics, + List insights, + List errors, + int? processingTimeMs, + Map? tokenUsage, + Map metadata, + }); + + @override + $ConversationSummaryCopyWith<$Res>? get summary; + @override + $SentimentAnalysisResultCopyWith<$Res>? get sentiment; +} + +/// @nodoc +class __$$AnalysisResultImplCopyWithImpl<$Res> + extends _$AnalysisResultCopyWithImpl<$Res, _$AnalysisResultImpl> + implements _$$AnalysisResultImplCopyWith<$Res> { + __$$AnalysisResultImplCopyWithImpl( + _$AnalysisResultImpl _value, + $Res Function(_$AnalysisResultImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AnalysisResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? conversationId = null, + Object? type = null, + Object? status = null, + Object? startTime = null, + Object? completionTime = freezed, + Object? provider = freezed, + Object? confidence = null, + Object? factChecks = freezed, + Object? summary = freezed, + Object? actionItems = freezed, + Object? sentiment = freezed, + Object? topics = freezed, + Object? insights = null, + Object? errors = null, + Object? processingTimeMs = freezed, + Object? tokenUsage = freezed, + Object? metadata = null, + }) { + return _then( + _$AnalysisResultImpl( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + conversationId: + null == conversationId + ? _value.conversationId + : conversationId // ignore: cast_nullable_to_non_nullable + as String, + type: + null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as AnalysisType, + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as AnalysisStatus, + startTime: + null == startTime + ? _value.startTime + : startTime // ignore: cast_nullable_to_non_nullable + as DateTime, + completionTime: + freezed == completionTime + ? _value.completionTime + : completionTime // ignore: cast_nullable_to_non_nullable + as DateTime?, + provider: + freezed == provider + ? _value.provider + : provider // ignore: cast_nullable_to_non_nullable + as String?, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + factChecks: + freezed == factChecks + ? _value._factChecks + : factChecks // ignore: cast_nullable_to_non_nullable + as List?, + summary: + freezed == summary + ? _value.summary + : summary // ignore: cast_nullable_to_non_nullable + as ConversationSummary?, + actionItems: + freezed == actionItems + ? _value._actionItems + : actionItems // ignore: cast_nullable_to_non_nullable + as List?, + sentiment: + freezed == sentiment + ? _value.sentiment + : sentiment // ignore: cast_nullable_to_non_nullable + as SentimentAnalysisResult?, + topics: + freezed == topics + ? _value._topics + : topics // ignore: cast_nullable_to_non_nullable + as List?, + insights: + null == insights + ? _value._insights + : insights // ignore: cast_nullable_to_non_nullable + as List, + errors: + null == errors + ? _value._errors + : errors // ignore: cast_nullable_to_non_nullable + as List, + processingTimeMs: + freezed == processingTimeMs + ? _value.processingTimeMs + : processingTimeMs // ignore: cast_nullable_to_non_nullable + as int?, + tokenUsage: + freezed == tokenUsage + ? _value._tokenUsage + : tokenUsage // ignore: cast_nullable_to_non_nullable + as Map?, + metadata: + null == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AnalysisResultImpl extends _AnalysisResult { + const _$AnalysisResultImpl({ + required this.id, + required this.conversationId, + required this.type, + required this.status, + required this.startTime, + this.completionTime, + this.provider, + this.confidence = 0.0, + final List? factChecks, + this.summary, + final List? actionItems, + this.sentiment, + final List? topics, + final List insights = const [], + final List errors = const [], + this.processingTimeMs, + final Map? tokenUsage, + final Map metadata = const {}, + }) : _factChecks = factChecks, + _actionItems = actionItems, + _topics = topics, + _insights = insights, + _errors = errors, + _tokenUsage = tokenUsage, + _metadata = metadata, + super._(); + + factory _$AnalysisResultImpl.fromJson(Map json) => + _$$AnalysisResultImplFromJson(json); + + /// Unique identifier for this analysis + @override + final String id; + + /// ID of the conversation being analyzed + @override + final String conversationId; + + /// Type of analysis performed + @override + final AnalysisType type; + + /// Current status of the analysis + @override + final AnalysisStatus status; + + /// When the analysis started + @override + final DateTime startTime; + + /// When the analysis completed + @override + final DateTime? completionTime; + + /// AI provider used for analysis + @override + final String? provider; + + /// Overall confidence score + @override + @JsonKey() + final double confidence; + + /// Fact-checking results + final List? _factChecks; + + /// Fact-checking results + @override + List? get factChecks { + final value = _factChecks; + if (value == null) return null; + if (_factChecks is EqualUnmodifiableListView) return _factChecks; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + /// Conversation summary + @override + final ConversationSummary? summary; + + /// Extracted action items + final List? _actionItems; + + /// Extracted action items + @override + List? get actionItems { + final value = _actionItems; + if (value == null) return null; + if (_actionItems is EqualUnmodifiableListView) return _actionItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + /// Sentiment analysis + @override + final SentimentAnalysisResult? sentiment; + + /// Identified topics + final List? _topics; + + /// Identified topics + @override + List? get topics { + final value = _topics; + if (value == null) return null; + if (_topics is EqualUnmodifiableListView) return _topics; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + /// Key insights and findings + final List _insights; + + /// Key insights and findings + @override + @JsonKey() + List get insights { + if (_insights is EqualUnmodifiableListView) return _insights; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_insights); + } + + /// Processing errors or warnings + final List _errors; + + /// Processing errors or warnings + @override + @JsonKey() + List get errors { + if (_errors is EqualUnmodifiableListView) return _errors; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_errors); + } + + /// Processing time in milliseconds + @override + final int? processingTimeMs; + + /// Token usage for AI processing + final Map? _tokenUsage; + + /// Token usage for AI processing + @override + Map? get tokenUsage { + final value = _tokenUsage; + if (value == null) return null; + if (_tokenUsage is EqualUnmodifiableMapView) return _tokenUsage; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + /// Additional metadata + final Map _metadata; + + /// Additional metadata + @override + @JsonKey() + Map get metadata { + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_metadata); + } + + @override + String toString() { + return 'AnalysisResult(id: $id, conversationId: $conversationId, type: $type, status: $status, startTime: $startTime, completionTime: $completionTime, provider: $provider, confidence: $confidence, factChecks: $factChecks, summary: $summary, actionItems: $actionItems, sentiment: $sentiment, topics: $topics, insights: $insights, errors: $errors, processingTimeMs: $processingTimeMs, tokenUsage: $tokenUsage, metadata: $metadata)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AnalysisResultImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.conversationId, conversationId) || + other.conversationId == conversationId) && + (identical(other.type, type) || other.type == type) && + (identical(other.status, status) || other.status == status) && + (identical(other.startTime, startTime) || + other.startTime == startTime) && + (identical(other.completionTime, completionTime) || + other.completionTime == completionTime) && + (identical(other.provider, provider) || + other.provider == provider) && + (identical(other.confidence, confidence) || + other.confidence == confidence) && + const DeepCollectionEquality().equals( + other._factChecks, + _factChecks, + ) && + (identical(other.summary, summary) || other.summary == summary) && + const DeepCollectionEquality().equals( + other._actionItems, + _actionItems, + ) && + (identical(other.sentiment, sentiment) || + other.sentiment == sentiment) && + const DeepCollectionEquality().equals(other._topics, _topics) && + const DeepCollectionEquality().equals(other._insights, _insights) && + const DeepCollectionEquality().equals(other._errors, _errors) && + (identical(other.processingTimeMs, processingTimeMs) || + other.processingTimeMs == processingTimeMs) && + const DeepCollectionEquality().equals( + other._tokenUsage, + _tokenUsage, + ) && + const DeepCollectionEquality().equals(other._metadata, _metadata)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + conversationId, + type, + status, + startTime, + completionTime, + provider, + confidence, + const DeepCollectionEquality().hash(_factChecks), + summary, + const DeepCollectionEquality().hash(_actionItems), + sentiment, + const DeepCollectionEquality().hash(_topics), + const DeepCollectionEquality().hash(_insights), + const DeepCollectionEquality().hash(_errors), + processingTimeMs, + const DeepCollectionEquality().hash(_tokenUsage), + const DeepCollectionEquality().hash(_metadata), + ); + + /// Create a copy of AnalysisResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AnalysisResultImplCopyWith<_$AnalysisResultImpl> get copyWith => + __$$AnalysisResultImplCopyWithImpl<_$AnalysisResultImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AnalysisResultImplToJson(this); + } +} + +abstract class _AnalysisResult extends AnalysisResult { + const factory _AnalysisResult({ + required final String id, + required final String conversationId, + required final AnalysisType type, + required final AnalysisStatus status, + required final DateTime startTime, + final DateTime? completionTime, + final String? provider, + final double confidence, + final List? factChecks, + final ConversationSummary? summary, + final List? actionItems, + final SentimentAnalysisResult? sentiment, + final List? topics, + final List insights, + final List errors, + final int? processingTimeMs, + final Map? tokenUsage, + final Map metadata, + }) = _$AnalysisResultImpl; + const _AnalysisResult._() : super._(); + + factory _AnalysisResult.fromJson(Map json) = + _$AnalysisResultImpl.fromJson; + + /// Unique identifier for this analysis + @override + String get id; + + /// ID of the conversation being analyzed + @override + String get conversationId; + + /// Type of analysis performed + @override + AnalysisType get type; + + /// Current status of the analysis + @override + AnalysisStatus get status; + + /// When the analysis started + @override + DateTime get startTime; + + /// When the analysis completed + @override + DateTime? get completionTime; + + /// AI provider used for analysis + @override + String? get provider; + + /// Overall confidence score + @override + double get confidence; + + /// Fact-checking results + @override + List? get factChecks; + + /// Conversation summary + @override + ConversationSummary? get summary; + + /// Extracted action items + @override + List? get actionItems; + + /// Sentiment analysis + @override + SentimentAnalysisResult? get sentiment; + + /// Identified topics + @override + List? get topics; + + /// Key insights and findings + @override + List get insights; + + /// Processing errors or warnings + @override + List get errors; + + /// Processing time in milliseconds + @override + int? get processingTimeMs; + + /// Token usage for AI processing + @override + Map? get tokenUsage; + + /// Additional metadata + @override + Map get metadata; + + /// Create a copy of AnalysisResult + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AnalysisResultImplCopyWith<_$AnalysisResultImpl> get copyWith => + throw _privateConstructorUsedError; +} + +FactCheckResult _$FactCheckResultFromJson(Map json) { + return _FactCheckResult.fromJson(json); +} + +/// @nodoc +mixin _$FactCheckResult { + /// Unique identifier + String get id => throw _privateConstructorUsedError; + + /// The claim being fact-checked + String get claim => throw _privateConstructorUsedError; + + /// Verification result + FactCheckStatus get status => throw _privateConstructorUsedError; + + /// Confidence in the verification + double get confidence => throw _privateConstructorUsedError; + + /// Supporting sources + List get sources => throw _privateConstructorUsedError; + + /// Detailed explanation + String? get explanation => throw _privateConstructorUsedError; + + /// Context within the conversation + String? get context => throw _privateConstructorUsedError; + + /// Timestamp range where claim appears + int? get startTimeMs => throw _privateConstructorUsedError; + int? get endTimeMs => throw _privateConstructorUsedError; + + /// Speaker who made the claim + String? get speakerId => throw _privateConstructorUsedError; + + /// Category of the claim + String? get category => throw _privateConstructorUsedError; + + /// Related claims + List get relatedClaims => throw _privateConstructorUsedError; + + /// Serializes this FactCheckResult to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of FactCheckResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $FactCheckResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FactCheckResultCopyWith<$Res> { + factory $FactCheckResultCopyWith( + FactCheckResult value, + $Res Function(FactCheckResult) then, + ) = _$FactCheckResultCopyWithImpl<$Res, FactCheckResult>; + @useResult + $Res call({ + String id, + String claim, + FactCheckStatus status, + double confidence, + List sources, + String? explanation, + String? context, + int? startTimeMs, + int? endTimeMs, + String? speakerId, + String? category, + List relatedClaims, + }); +} + +/// @nodoc +class _$FactCheckResultCopyWithImpl<$Res, $Val extends FactCheckResult> + implements $FactCheckResultCopyWith<$Res> { + _$FactCheckResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of FactCheckResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? claim = null, + Object? status = null, + Object? confidence = null, + Object? sources = null, + Object? explanation = freezed, + Object? context = freezed, + Object? startTimeMs = freezed, + Object? endTimeMs = freezed, + Object? speakerId = freezed, + Object? category = freezed, + Object? relatedClaims = null, + }) { + return _then( + _value.copyWith( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + claim: + null == claim + ? _value.claim + : claim // ignore: cast_nullable_to_non_nullable + as String, + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as FactCheckStatus, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + sources: + null == sources + ? _value.sources + : sources // ignore: cast_nullable_to_non_nullable + as List, + explanation: + freezed == explanation + ? _value.explanation + : explanation // ignore: cast_nullable_to_non_nullable + as String?, + context: + freezed == context + ? _value.context + : context // ignore: cast_nullable_to_non_nullable + as String?, + startTimeMs: + freezed == startTimeMs + ? _value.startTimeMs + : startTimeMs // ignore: cast_nullable_to_non_nullable + as int?, + endTimeMs: + freezed == endTimeMs + ? _value.endTimeMs + : endTimeMs // ignore: cast_nullable_to_non_nullable + as int?, + speakerId: + freezed == speakerId + ? _value.speakerId + : speakerId // ignore: cast_nullable_to_non_nullable + as String?, + category: + freezed == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as String?, + relatedClaims: + null == relatedClaims + ? _value.relatedClaims + : relatedClaims // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$FactCheckResultImplCopyWith<$Res> + implements $FactCheckResultCopyWith<$Res> { + factory _$$FactCheckResultImplCopyWith( + _$FactCheckResultImpl value, + $Res Function(_$FactCheckResultImpl) then, + ) = __$$FactCheckResultImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String claim, + FactCheckStatus status, + double confidence, + List sources, + String? explanation, + String? context, + int? startTimeMs, + int? endTimeMs, + String? speakerId, + String? category, + List relatedClaims, + }); +} + +/// @nodoc +class __$$FactCheckResultImplCopyWithImpl<$Res> + extends _$FactCheckResultCopyWithImpl<$Res, _$FactCheckResultImpl> + implements _$$FactCheckResultImplCopyWith<$Res> { + __$$FactCheckResultImplCopyWithImpl( + _$FactCheckResultImpl _value, + $Res Function(_$FactCheckResultImpl) _then, + ) : super(_value, _then); + + /// Create a copy of FactCheckResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? claim = null, + Object? status = null, + Object? confidence = null, + Object? sources = null, + Object? explanation = freezed, + Object? context = freezed, + Object? startTimeMs = freezed, + Object? endTimeMs = freezed, + Object? speakerId = freezed, + Object? category = freezed, + Object? relatedClaims = null, + }) { + return _then( + _$FactCheckResultImpl( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + claim: + null == claim + ? _value.claim + : claim // ignore: cast_nullable_to_non_nullable + as String, + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as FactCheckStatus, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + sources: + null == sources + ? _value._sources + : sources // ignore: cast_nullable_to_non_nullable + as List, + explanation: + freezed == explanation + ? _value.explanation + : explanation // ignore: cast_nullable_to_non_nullable + as String?, + context: + freezed == context + ? _value.context + : context // ignore: cast_nullable_to_non_nullable + as String?, + startTimeMs: + freezed == startTimeMs + ? _value.startTimeMs + : startTimeMs // ignore: cast_nullable_to_non_nullable + as int?, + endTimeMs: + freezed == endTimeMs + ? _value.endTimeMs + : endTimeMs // ignore: cast_nullable_to_non_nullable + as int?, + speakerId: + freezed == speakerId + ? _value.speakerId + : speakerId // ignore: cast_nullable_to_non_nullable + as String?, + category: + freezed == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as String?, + relatedClaims: + null == relatedClaims + ? _value._relatedClaims + : relatedClaims // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$FactCheckResultImpl extends _FactCheckResult { + const _$FactCheckResultImpl({ + required this.id, + required this.claim, + required this.status, + required this.confidence, + final List sources = const [], + this.explanation, + this.context, + this.startTimeMs, + this.endTimeMs, + this.speakerId, + this.category, + final List relatedClaims = const [], + }) : _sources = sources, + _relatedClaims = relatedClaims, + super._(); + + factory _$FactCheckResultImpl.fromJson(Map json) => + _$$FactCheckResultImplFromJson(json); + + /// Unique identifier + @override + final String id; + + /// The claim being fact-checked + @override + final String claim; + + /// Verification result + @override + final FactCheckStatus status; + + /// Confidence in the verification + @override + final double confidence; + + /// Supporting sources + final List _sources; + + /// Supporting sources + @override + @JsonKey() + List get sources { + if (_sources is EqualUnmodifiableListView) return _sources; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_sources); + } + + /// Detailed explanation + @override + final String? explanation; + + /// Context within the conversation + @override + final String? context; + + /// Timestamp range where claim appears + @override + final int? startTimeMs; + @override + final int? endTimeMs; + + /// Speaker who made the claim + @override + final String? speakerId; + + /// Category of the claim + @override + final String? category; + + /// Related claims + final List _relatedClaims; + + /// Related claims + @override + @JsonKey() + List get relatedClaims { + if (_relatedClaims is EqualUnmodifiableListView) return _relatedClaims; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_relatedClaims); + } + + @override + String toString() { + return 'FactCheckResult(id: $id, claim: $claim, status: $status, confidence: $confidence, sources: $sources, explanation: $explanation, context: $context, startTimeMs: $startTimeMs, endTimeMs: $endTimeMs, speakerId: $speakerId, category: $category, relatedClaims: $relatedClaims)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FactCheckResultImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.claim, claim) || other.claim == claim) && + (identical(other.status, status) || other.status == status) && + (identical(other.confidence, confidence) || + other.confidence == confidence) && + const DeepCollectionEquality().equals(other._sources, _sources) && + (identical(other.explanation, explanation) || + other.explanation == explanation) && + (identical(other.context, context) || other.context == context) && + (identical(other.startTimeMs, startTimeMs) || + other.startTimeMs == startTimeMs) && + (identical(other.endTimeMs, endTimeMs) || + other.endTimeMs == endTimeMs) && + (identical(other.speakerId, speakerId) || + other.speakerId == speakerId) && + (identical(other.category, category) || + other.category == category) && + const DeepCollectionEquality().equals( + other._relatedClaims, + _relatedClaims, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + claim, + status, + confidence, + const DeepCollectionEquality().hash(_sources), + explanation, + context, + startTimeMs, + endTimeMs, + speakerId, + category, + const DeepCollectionEquality().hash(_relatedClaims), + ); + + /// Create a copy of FactCheckResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$FactCheckResultImplCopyWith<_$FactCheckResultImpl> get copyWith => + __$$FactCheckResultImplCopyWithImpl<_$FactCheckResultImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$FactCheckResultImplToJson(this); + } +} + +abstract class _FactCheckResult extends FactCheckResult { + const factory _FactCheckResult({ + required final String id, + required final String claim, + required final FactCheckStatus status, + required final double confidence, + final List sources, + final String? explanation, + final String? context, + final int? startTimeMs, + final int? endTimeMs, + final String? speakerId, + final String? category, + final List relatedClaims, + }) = _$FactCheckResultImpl; + const _FactCheckResult._() : super._(); + + factory _FactCheckResult.fromJson(Map json) = + _$FactCheckResultImpl.fromJson; + + /// Unique identifier + @override + String get id; + + /// The claim being fact-checked + @override + String get claim; + + /// Verification result + @override + FactCheckStatus get status; + + /// Confidence in the verification + @override + double get confidence; + + /// Supporting sources + @override + List get sources; + + /// Detailed explanation + @override + String? get explanation; + + /// Context within the conversation + @override + String? get context; + + /// Timestamp range where claim appears + @override + int? get startTimeMs; + @override + int? get endTimeMs; + + /// Speaker who made the claim + @override + String? get speakerId; + + /// Category of the claim + @override + String? get category; + + /// Related claims + @override + List get relatedClaims; + + /// Create a copy of FactCheckResult + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FactCheckResultImplCopyWith<_$FactCheckResultImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ConversationSummary _$ConversationSummaryFromJson(Map json) { + return _ConversationSummary.fromJson(json); +} + +/// @nodoc +mixin _$ConversationSummary { + /// Main summary text + String get summary => throw _privateConstructorUsedError; + + /// Key discussion points + List get keyPoints => throw _privateConstructorUsedError; + + /// Important decisions made + List get decisions => throw _privateConstructorUsedError; + + /// Questions raised + List get questions => throw _privateConstructorUsedError; + + /// Overall tone of conversation + String? get tone => throw _privateConstructorUsedError; + + /// Main topics discussed + List get topics => throw _privateConstructorUsedError; + + /// Summary length category + SummaryLength get length => throw _privateConstructorUsedError; + + /// Estimated reading time + Duration? get estimatedReadTime => throw _privateConstructorUsedError; + + /// Confidence in summary accuracy + double get confidence => throw _privateConstructorUsedError; + + /// Serializes this ConversationSummary to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ConversationSummary + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ConversationSummaryCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ConversationSummaryCopyWith<$Res> { + factory $ConversationSummaryCopyWith( + ConversationSummary value, + $Res Function(ConversationSummary) then, + ) = _$ConversationSummaryCopyWithImpl<$Res, ConversationSummary>; + @useResult + $Res call({ + String summary, + List keyPoints, + List decisions, + List questions, + String? tone, + List topics, + SummaryLength length, + Duration? estimatedReadTime, + double confidence, + }); +} + +/// @nodoc +class _$ConversationSummaryCopyWithImpl<$Res, $Val extends ConversationSummary> + implements $ConversationSummaryCopyWith<$Res> { + _$ConversationSummaryCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ConversationSummary + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? summary = null, + Object? keyPoints = null, + Object? decisions = null, + Object? questions = null, + Object? tone = freezed, + Object? topics = null, + Object? length = null, + Object? estimatedReadTime = freezed, + Object? confidence = null, + }) { + return _then( + _value.copyWith( + summary: + null == summary + ? _value.summary + : summary // ignore: cast_nullable_to_non_nullable + as String, + keyPoints: + null == keyPoints + ? _value.keyPoints + : keyPoints // ignore: cast_nullable_to_non_nullable + as List, + decisions: + null == decisions + ? _value.decisions + : decisions // ignore: cast_nullable_to_non_nullable + as List, + questions: + null == questions + ? _value.questions + : questions // ignore: cast_nullable_to_non_nullable + as List, + tone: + freezed == tone + ? _value.tone + : tone // ignore: cast_nullable_to_non_nullable + as String?, + topics: + null == topics + ? _value.topics + : topics // ignore: cast_nullable_to_non_nullable + as List, + length: + null == length + ? _value.length + : length // ignore: cast_nullable_to_non_nullable + as SummaryLength, + estimatedReadTime: + freezed == estimatedReadTime + ? _value.estimatedReadTime + : estimatedReadTime // ignore: cast_nullable_to_non_nullable + as Duration?, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ConversationSummaryImplCopyWith<$Res> + implements $ConversationSummaryCopyWith<$Res> { + factory _$$ConversationSummaryImplCopyWith( + _$ConversationSummaryImpl value, + $Res Function(_$ConversationSummaryImpl) then, + ) = __$$ConversationSummaryImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String summary, + List keyPoints, + List decisions, + List questions, + String? tone, + List topics, + SummaryLength length, + Duration? estimatedReadTime, + double confidence, + }); +} + +/// @nodoc +class __$$ConversationSummaryImplCopyWithImpl<$Res> + extends _$ConversationSummaryCopyWithImpl<$Res, _$ConversationSummaryImpl> + implements _$$ConversationSummaryImplCopyWith<$Res> { + __$$ConversationSummaryImplCopyWithImpl( + _$ConversationSummaryImpl _value, + $Res Function(_$ConversationSummaryImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ConversationSummary + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? summary = null, + Object? keyPoints = null, + Object? decisions = null, + Object? questions = null, + Object? tone = freezed, + Object? topics = null, + Object? length = null, + Object? estimatedReadTime = freezed, + Object? confidence = null, + }) { + return _then( + _$ConversationSummaryImpl( + summary: + null == summary + ? _value.summary + : summary // ignore: cast_nullable_to_non_nullable + as String, + keyPoints: + null == keyPoints + ? _value._keyPoints + : keyPoints // ignore: cast_nullable_to_non_nullable + as List, + decisions: + null == decisions + ? _value._decisions + : decisions // ignore: cast_nullable_to_non_nullable + as List, + questions: + null == questions + ? _value._questions + : questions // ignore: cast_nullable_to_non_nullable + as List, + tone: + freezed == tone + ? _value.tone + : tone // ignore: cast_nullable_to_non_nullable + as String?, + topics: + null == topics + ? _value._topics + : topics // ignore: cast_nullable_to_non_nullable + as List, + length: + null == length + ? _value.length + : length // ignore: cast_nullable_to_non_nullable + as SummaryLength, + estimatedReadTime: + freezed == estimatedReadTime + ? _value.estimatedReadTime + : estimatedReadTime // ignore: cast_nullable_to_non_nullable + as Duration?, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ConversationSummaryImpl extends _ConversationSummary { + const _$ConversationSummaryImpl({ + required this.summary, + final List keyPoints = const [], + final List decisions = const [], + final List questions = const [], + this.tone, + final List topics = const [], + this.length = SummaryLength.medium, + this.estimatedReadTime, + this.confidence = 0.0, + }) : _keyPoints = keyPoints, + _decisions = decisions, + _questions = questions, + _topics = topics, + super._(); + + factory _$ConversationSummaryImpl.fromJson(Map json) => + _$$ConversationSummaryImplFromJson(json); + + /// Main summary text + @override + final String summary; + + /// Key discussion points + final List _keyPoints; + + /// Key discussion points + @override + @JsonKey() + List get keyPoints { + if (_keyPoints is EqualUnmodifiableListView) return _keyPoints; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_keyPoints); + } + + /// Important decisions made + final List _decisions; + + /// Important decisions made + @override + @JsonKey() + List get decisions { + if (_decisions is EqualUnmodifiableListView) return _decisions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_decisions); + } + + /// Questions raised + final List _questions; + + /// Questions raised + @override + @JsonKey() + List get questions { + if (_questions is EqualUnmodifiableListView) return _questions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_questions); + } + + /// Overall tone of conversation + @override + final String? tone; + + /// Main topics discussed + final List _topics; + + /// Main topics discussed + @override + @JsonKey() + List get topics { + if (_topics is EqualUnmodifiableListView) return _topics; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_topics); + } + + /// Summary length category + @override + @JsonKey() + final SummaryLength length; + + /// Estimated reading time + @override + final Duration? estimatedReadTime; + + /// Confidence in summary accuracy + @override + @JsonKey() + final double confidence; + + @override + String toString() { + return 'ConversationSummary(summary: $summary, keyPoints: $keyPoints, decisions: $decisions, questions: $questions, tone: $tone, topics: $topics, length: $length, estimatedReadTime: $estimatedReadTime, confidence: $confidence)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ConversationSummaryImpl && + (identical(other.summary, summary) || other.summary == summary) && + const DeepCollectionEquality().equals( + other._keyPoints, + _keyPoints, + ) && + const DeepCollectionEquality().equals( + other._decisions, + _decisions, + ) && + const DeepCollectionEquality().equals( + other._questions, + _questions, + ) && + (identical(other.tone, tone) || other.tone == tone) && + const DeepCollectionEquality().equals(other._topics, _topics) && + (identical(other.length, length) || other.length == length) && + (identical(other.estimatedReadTime, estimatedReadTime) || + other.estimatedReadTime == estimatedReadTime) && + (identical(other.confidence, confidence) || + other.confidence == confidence)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + summary, + const DeepCollectionEquality().hash(_keyPoints), + const DeepCollectionEquality().hash(_decisions), + const DeepCollectionEquality().hash(_questions), + tone, + const DeepCollectionEquality().hash(_topics), + length, + estimatedReadTime, + confidence, + ); + + /// Create a copy of ConversationSummary + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ConversationSummaryImplCopyWith<_$ConversationSummaryImpl> get copyWith => + __$$ConversationSummaryImplCopyWithImpl<_$ConversationSummaryImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ConversationSummaryImplToJson(this); + } +} + +abstract class _ConversationSummary extends ConversationSummary { + const factory _ConversationSummary({ + required final String summary, + final List keyPoints, + final List decisions, + final List questions, + final String? tone, + final List topics, + final SummaryLength length, + final Duration? estimatedReadTime, + final double confidence, + }) = _$ConversationSummaryImpl; + const _ConversationSummary._() : super._(); + + factory _ConversationSummary.fromJson(Map json) = + _$ConversationSummaryImpl.fromJson; + + /// Main summary text + @override + String get summary; + + /// Key discussion points + @override + List get keyPoints; + + /// Important decisions made + @override + List get decisions; + + /// Questions raised + @override + List get questions; + + /// Overall tone of conversation + @override + String? get tone; + + /// Main topics discussed + @override + List get topics; + + /// Summary length category + @override + SummaryLength get length; + + /// Estimated reading time + @override + Duration? get estimatedReadTime; + + /// Confidence in summary accuracy + @override + double get confidence; + + /// Create a copy of ConversationSummary + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ConversationSummaryImplCopyWith<_$ConversationSummaryImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ActionItemResult _$ActionItemResultFromJson(Map json) { + return _ActionItemResult.fromJson(json); +} + +/// @nodoc +mixin _$ActionItemResult { + /// Unique identifier + String get id => throw _privateConstructorUsedError; + + /// Description of the action + String get description => throw _privateConstructorUsedError; + + /// Assigned person (if mentioned) + String? get assignee => throw _privateConstructorUsedError; + + /// Due date (if mentioned) + DateTime? get dueDate => throw _privateConstructorUsedError; + + /// Priority level + ActionItemPriority get priority => throw _privateConstructorUsedError; + + /// Context where it was mentioned + String? get context => throw _privateConstructorUsedError; + + /// Confidence in extraction accuracy + double get confidence => throw _privateConstructorUsedError; + + /// Status of the action item + ActionItemStatus get status => throw _privateConstructorUsedError; + + /// Timestamp where mentioned + int? get mentionedAtMs => throw _privateConstructorUsedError; + + /// Speaker who mentioned it + String? get speakerId => throw _privateConstructorUsedError; + + /// Related action items + List get relatedItems => throw _privateConstructorUsedError; + + /// Categories or tags + List get tags => throw _privateConstructorUsedError; + + /// Serializes this ActionItemResult to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ActionItemResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ActionItemResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ActionItemResultCopyWith<$Res> { + factory $ActionItemResultCopyWith( + ActionItemResult value, + $Res Function(ActionItemResult) then, + ) = _$ActionItemResultCopyWithImpl<$Res, ActionItemResult>; + @useResult + $Res call({ + String id, + String description, + String? assignee, + DateTime? dueDate, + ActionItemPriority priority, + String? context, + double confidence, + ActionItemStatus status, + int? mentionedAtMs, + String? speakerId, + List relatedItems, + List tags, + }); +} + +/// @nodoc +class _$ActionItemResultCopyWithImpl<$Res, $Val extends ActionItemResult> + implements $ActionItemResultCopyWith<$Res> { + _$ActionItemResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ActionItemResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? description = null, + Object? assignee = freezed, + Object? dueDate = freezed, + Object? priority = null, + Object? context = freezed, + Object? confidence = null, + Object? status = null, + Object? mentionedAtMs = freezed, + Object? speakerId = freezed, + Object? relatedItems = null, + Object? tags = null, + }) { + return _then( + _value.copyWith( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + description: + null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + assignee: + freezed == assignee + ? _value.assignee + : assignee // ignore: cast_nullable_to_non_nullable + as String?, + dueDate: + freezed == dueDate + ? _value.dueDate + : dueDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + priority: + null == priority + ? _value.priority + : priority // ignore: cast_nullable_to_non_nullable + as ActionItemPriority, + context: + freezed == context + ? _value.context + : context // ignore: cast_nullable_to_non_nullable + as String?, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as ActionItemStatus, + mentionedAtMs: + freezed == mentionedAtMs + ? _value.mentionedAtMs + : mentionedAtMs // ignore: cast_nullable_to_non_nullable + as int?, + speakerId: + freezed == speakerId + ? _value.speakerId + : speakerId // ignore: cast_nullable_to_non_nullable + as String?, + relatedItems: + null == relatedItems + ? _value.relatedItems + : relatedItems // ignore: cast_nullable_to_non_nullable + as List, + tags: + null == tags + ? _value.tags + : tags // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ActionItemResultImplCopyWith<$Res> + implements $ActionItemResultCopyWith<$Res> { + factory _$$ActionItemResultImplCopyWith( + _$ActionItemResultImpl value, + $Res Function(_$ActionItemResultImpl) then, + ) = __$$ActionItemResultImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String description, + String? assignee, + DateTime? dueDate, + ActionItemPriority priority, + String? context, + double confidence, + ActionItemStatus status, + int? mentionedAtMs, + String? speakerId, + List relatedItems, + List tags, + }); +} + +/// @nodoc +class __$$ActionItemResultImplCopyWithImpl<$Res> + extends _$ActionItemResultCopyWithImpl<$Res, _$ActionItemResultImpl> + implements _$$ActionItemResultImplCopyWith<$Res> { + __$$ActionItemResultImplCopyWithImpl( + _$ActionItemResultImpl _value, + $Res Function(_$ActionItemResultImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ActionItemResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? description = null, + Object? assignee = freezed, + Object? dueDate = freezed, + Object? priority = null, + Object? context = freezed, + Object? confidence = null, + Object? status = null, + Object? mentionedAtMs = freezed, + Object? speakerId = freezed, + Object? relatedItems = null, + Object? tags = null, + }) { + return _then( + _$ActionItemResultImpl( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + description: + null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + assignee: + freezed == assignee + ? _value.assignee + : assignee // ignore: cast_nullable_to_non_nullable + as String?, + dueDate: + freezed == dueDate + ? _value.dueDate + : dueDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + priority: + null == priority + ? _value.priority + : priority // ignore: cast_nullable_to_non_nullable + as ActionItemPriority, + context: + freezed == context + ? _value.context + : context // ignore: cast_nullable_to_non_nullable + as String?, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as ActionItemStatus, + mentionedAtMs: + freezed == mentionedAtMs + ? _value.mentionedAtMs + : mentionedAtMs // ignore: cast_nullable_to_non_nullable + as int?, + speakerId: + freezed == speakerId + ? _value.speakerId + : speakerId // ignore: cast_nullable_to_non_nullable + as String?, + relatedItems: + null == relatedItems + ? _value._relatedItems + : relatedItems // ignore: cast_nullable_to_non_nullable + as List, + tags: + null == tags + ? _value._tags + : tags // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ActionItemResultImpl extends _ActionItemResult { + const _$ActionItemResultImpl({ + required this.id, + required this.description, + this.assignee, + this.dueDate, + this.priority = ActionItemPriority.medium, + this.context, + this.confidence = 0.0, + this.status = ActionItemStatus.pending, + this.mentionedAtMs, + this.speakerId, + final List relatedItems = const [], + final List tags = const [], + }) : _relatedItems = relatedItems, + _tags = tags, + super._(); + + factory _$ActionItemResultImpl.fromJson(Map json) => + _$$ActionItemResultImplFromJson(json); + + /// Unique identifier + @override + final String id; + + /// Description of the action + @override + final String description; + + /// Assigned person (if mentioned) + @override + final String? assignee; + + /// Due date (if mentioned) + @override + final DateTime? dueDate; + + /// Priority level + @override + @JsonKey() + final ActionItemPriority priority; + + /// Context where it was mentioned + @override + final String? context; + + /// Confidence in extraction accuracy + @override + @JsonKey() + final double confidence; + + /// Status of the action item + @override + @JsonKey() + final ActionItemStatus status; + + /// Timestamp where mentioned + @override + final int? mentionedAtMs; + + /// Speaker who mentioned it + @override + final String? speakerId; + + /// Related action items + final List _relatedItems; + + /// Related action items + @override + @JsonKey() + List get relatedItems { + if (_relatedItems is EqualUnmodifiableListView) return _relatedItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_relatedItems); + } + + /// Categories or tags + final List _tags; + + /// Categories or tags + @override + @JsonKey() + List get tags { + if (_tags is EqualUnmodifiableListView) return _tags; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_tags); + } + + @override + String toString() { + return 'ActionItemResult(id: $id, description: $description, assignee: $assignee, dueDate: $dueDate, priority: $priority, context: $context, confidence: $confidence, status: $status, mentionedAtMs: $mentionedAtMs, speakerId: $speakerId, relatedItems: $relatedItems, tags: $tags)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ActionItemResultImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.description, description) || + other.description == description) && + (identical(other.assignee, assignee) || + other.assignee == assignee) && + (identical(other.dueDate, dueDate) || other.dueDate == dueDate) && + (identical(other.priority, priority) || + other.priority == priority) && + (identical(other.context, context) || other.context == context) && + (identical(other.confidence, confidence) || + other.confidence == confidence) && + (identical(other.status, status) || other.status == status) && + (identical(other.mentionedAtMs, mentionedAtMs) || + other.mentionedAtMs == mentionedAtMs) && + (identical(other.speakerId, speakerId) || + other.speakerId == speakerId) && + const DeepCollectionEquality().equals( + other._relatedItems, + _relatedItems, + ) && + const DeepCollectionEquality().equals(other._tags, _tags)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + description, + assignee, + dueDate, + priority, + context, + confidence, + status, + mentionedAtMs, + speakerId, + const DeepCollectionEquality().hash(_relatedItems), + const DeepCollectionEquality().hash(_tags), + ); + + /// Create a copy of ActionItemResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ActionItemResultImplCopyWith<_$ActionItemResultImpl> get copyWith => + __$$ActionItemResultImplCopyWithImpl<_$ActionItemResultImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ActionItemResultImplToJson(this); + } +} + +abstract class _ActionItemResult extends ActionItemResult { + const factory _ActionItemResult({ + required final String id, + required final String description, + final String? assignee, + final DateTime? dueDate, + final ActionItemPriority priority, + final String? context, + final double confidence, + final ActionItemStatus status, + final int? mentionedAtMs, + final String? speakerId, + final List relatedItems, + final List tags, + }) = _$ActionItemResultImpl; + const _ActionItemResult._() : super._(); + + factory _ActionItemResult.fromJson(Map json) = + _$ActionItemResultImpl.fromJson; + + /// Unique identifier + @override + String get id; + + /// Description of the action + @override + String get description; + + /// Assigned person (if mentioned) + @override + String? get assignee; + + /// Due date (if mentioned) + @override + DateTime? get dueDate; + + /// Priority level + @override + ActionItemPriority get priority; + + /// Context where it was mentioned + @override + String? get context; + + /// Confidence in extraction accuracy + @override + double get confidence; + + /// Status of the action item + @override + ActionItemStatus get status; + + /// Timestamp where mentioned + @override + int? get mentionedAtMs; + + /// Speaker who mentioned it + @override + String? get speakerId; + + /// Related action items + @override + List get relatedItems; + + /// Categories or tags + @override + List get tags; + + /// Create a copy of ActionItemResult + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ActionItemResultImplCopyWith<_$ActionItemResultImpl> get copyWith => + throw _privateConstructorUsedError; +} + +SentimentAnalysisResult _$SentimentAnalysisResultFromJson( + Map json, +) { + return _SentimentAnalysisResult.fromJson(json); +} + +/// @nodoc +mixin _$SentimentAnalysisResult { + /// Overall sentiment + SentimentType get overallSentiment => throw _privateConstructorUsedError; + + /// Confidence in sentiment analysis + double get confidence => throw _privateConstructorUsedError; + + /// Detailed emotion breakdown + Map get emotions => throw _privateConstructorUsedError; + + /// Conversation tone + String? get tone => throw _privateConstructorUsedError; + + /// Sentiment progression over time + List get progression => + throw _privateConstructorUsedError; + + /// Participant-specific sentiment + Map get participantSentiments => + throw _privateConstructorUsedError; + + /// Key phrases that influenced sentiment + List get keyPhrases => throw _privateConstructorUsedError; + + /// Serializes this SentimentAnalysisResult to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SentimentAnalysisResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SentimentAnalysisResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SentimentAnalysisResultCopyWith<$Res> { + factory $SentimentAnalysisResultCopyWith( + SentimentAnalysisResult value, + $Res Function(SentimentAnalysisResult) then, + ) = _$SentimentAnalysisResultCopyWithImpl<$Res, SentimentAnalysisResult>; + @useResult + $Res call({ + SentimentType overallSentiment, + double confidence, + Map emotions, + String? tone, + List progression, + Map participantSentiments, + List keyPhrases, + }); +} + +/// @nodoc +class _$SentimentAnalysisResultCopyWithImpl< + $Res, + $Val extends SentimentAnalysisResult +> + implements $SentimentAnalysisResultCopyWith<$Res> { + _$SentimentAnalysisResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SentimentAnalysisResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? overallSentiment = null, + Object? confidence = null, + Object? emotions = null, + Object? tone = freezed, + Object? progression = null, + Object? participantSentiments = null, + Object? keyPhrases = null, + }) { + return _then( + _value.copyWith( + overallSentiment: + null == overallSentiment + ? _value.overallSentiment + : overallSentiment // ignore: cast_nullable_to_non_nullable + as SentimentType, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + emotions: + null == emotions + ? _value.emotions + : emotions // ignore: cast_nullable_to_non_nullable + as Map, + tone: + freezed == tone + ? _value.tone + : tone // ignore: cast_nullable_to_non_nullable + as String?, + progression: + null == progression + ? _value.progression + : progression // ignore: cast_nullable_to_non_nullable + as List, + participantSentiments: + null == participantSentiments + ? _value.participantSentiments + : participantSentiments // ignore: cast_nullable_to_non_nullable + as Map, + keyPhrases: + null == keyPhrases + ? _value.keyPhrases + : keyPhrases // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$SentimentAnalysisResultImplCopyWith<$Res> + implements $SentimentAnalysisResultCopyWith<$Res> { + factory _$$SentimentAnalysisResultImplCopyWith( + _$SentimentAnalysisResultImpl value, + $Res Function(_$SentimentAnalysisResultImpl) then, + ) = __$$SentimentAnalysisResultImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + SentimentType overallSentiment, + double confidence, + Map emotions, + String? tone, + List progression, + Map participantSentiments, + List keyPhrases, + }); +} + +/// @nodoc +class __$$SentimentAnalysisResultImplCopyWithImpl<$Res> + extends + _$SentimentAnalysisResultCopyWithImpl< + $Res, + _$SentimentAnalysisResultImpl + > + implements _$$SentimentAnalysisResultImplCopyWith<$Res> { + __$$SentimentAnalysisResultImplCopyWithImpl( + _$SentimentAnalysisResultImpl _value, + $Res Function(_$SentimentAnalysisResultImpl) _then, + ) : super(_value, _then); + + /// Create a copy of SentimentAnalysisResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? overallSentiment = null, + Object? confidence = null, + Object? emotions = null, + Object? tone = freezed, + Object? progression = null, + Object? participantSentiments = null, + Object? keyPhrases = null, + }) { + return _then( + _$SentimentAnalysisResultImpl( + overallSentiment: + null == overallSentiment + ? _value.overallSentiment + : overallSentiment // ignore: cast_nullable_to_non_nullable + as SentimentType, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + emotions: + null == emotions + ? _value._emotions + : emotions // ignore: cast_nullable_to_non_nullable + as Map, + tone: + freezed == tone + ? _value.tone + : tone // ignore: cast_nullable_to_non_nullable + as String?, + progression: + null == progression + ? _value._progression + : progression // ignore: cast_nullable_to_non_nullable + as List, + participantSentiments: + null == participantSentiments + ? _value._participantSentiments + : participantSentiments // ignore: cast_nullable_to_non_nullable + as Map, + keyPhrases: + null == keyPhrases + ? _value._keyPhrases + : keyPhrases // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$SentimentAnalysisResultImpl extends _SentimentAnalysisResult { + const _$SentimentAnalysisResultImpl({ + required this.overallSentiment, + required this.confidence, + required final Map emotions, + this.tone, + final List progression = const [], + final Map participantSentiments = const {}, + final List keyPhrases = const [], + }) : _emotions = emotions, + _progression = progression, + _participantSentiments = participantSentiments, + _keyPhrases = keyPhrases, + super._(); + + factory _$SentimentAnalysisResultImpl.fromJson(Map json) => + _$$SentimentAnalysisResultImplFromJson(json); + + /// Overall sentiment + @override + final SentimentType overallSentiment; + + /// Confidence in sentiment analysis + @override + final double confidence; + + /// Detailed emotion breakdown + final Map _emotions; + + /// Detailed emotion breakdown + @override + Map get emotions { + if (_emotions is EqualUnmodifiableMapView) return _emotions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_emotions); + } + + /// Conversation tone + @override + final String? tone; + + /// Sentiment progression over time + final List _progression; + + /// Sentiment progression over time + @override + @JsonKey() + List get progression { + if (_progression is EqualUnmodifiableListView) return _progression; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_progression); + } + + /// Participant-specific sentiment + final Map _participantSentiments; + + /// Participant-specific sentiment + @override + @JsonKey() + Map get participantSentiments { + if (_participantSentiments is EqualUnmodifiableMapView) + return _participantSentiments; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_participantSentiments); + } + + /// Key phrases that influenced sentiment + final List _keyPhrases; + + /// Key phrases that influenced sentiment + @override + @JsonKey() + List get keyPhrases { + if (_keyPhrases is EqualUnmodifiableListView) return _keyPhrases; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_keyPhrases); + } + + @override + String toString() { + return 'SentimentAnalysisResult(overallSentiment: $overallSentiment, confidence: $confidence, emotions: $emotions, tone: $tone, progression: $progression, participantSentiments: $participantSentiments, keyPhrases: $keyPhrases)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SentimentAnalysisResultImpl && + (identical(other.overallSentiment, overallSentiment) || + other.overallSentiment == overallSentiment) && + (identical(other.confidence, confidence) || + other.confidence == confidence) && + const DeepCollectionEquality().equals(other._emotions, _emotions) && + (identical(other.tone, tone) || other.tone == tone) && + const DeepCollectionEquality().equals( + other._progression, + _progression, + ) && + const DeepCollectionEquality().equals( + other._participantSentiments, + _participantSentiments, + ) && + const DeepCollectionEquality().equals( + other._keyPhrases, + _keyPhrases, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + overallSentiment, + confidence, + const DeepCollectionEquality().hash(_emotions), + tone, + const DeepCollectionEquality().hash(_progression), + const DeepCollectionEquality().hash(_participantSentiments), + const DeepCollectionEquality().hash(_keyPhrases), + ); + + /// Create a copy of SentimentAnalysisResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SentimentAnalysisResultImplCopyWith<_$SentimentAnalysisResultImpl> + get copyWith => __$$SentimentAnalysisResultImplCopyWithImpl< + _$SentimentAnalysisResultImpl + >(this, _$identity); + + @override + Map toJson() { + return _$$SentimentAnalysisResultImplToJson(this); + } +} + +abstract class _SentimentAnalysisResult extends SentimentAnalysisResult { + const factory _SentimentAnalysisResult({ + required final SentimentType overallSentiment, + required final double confidence, + required final Map emotions, + final String? tone, + final List progression, + final Map participantSentiments, + final List keyPhrases, + }) = _$SentimentAnalysisResultImpl; + const _SentimentAnalysisResult._() : super._(); + + factory _SentimentAnalysisResult.fromJson(Map json) = + _$SentimentAnalysisResultImpl.fromJson; + + /// Overall sentiment + @override + SentimentType get overallSentiment; + + /// Confidence in sentiment analysis + @override + double get confidence; + + /// Detailed emotion breakdown + @override + Map get emotions; + + /// Conversation tone + @override + String? get tone; + + /// Sentiment progression over time + @override + List get progression; + + /// Participant-specific sentiment + @override + Map get participantSentiments; + + /// Key phrases that influenced sentiment + @override + List get keyPhrases; + + /// Create a copy of SentimentAnalysisResult + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SentimentAnalysisResultImplCopyWith<_$SentimentAnalysisResultImpl> + get copyWith => throw _privateConstructorUsedError; +} + +SentimentTimePoint _$SentimentTimePointFromJson(Map json) { + return _SentimentTimePoint.fromJson(json); +} + +/// @nodoc +mixin _$SentimentTimePoint { + int get timeMs => throw _privateConstructorUsedError; + SentimentType get sentiment => throw _privateConstructorUsedError; + double get confidence => throw _privateConstructorUsedError; + + /// Serializes this SentimentTimePoint to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SentimentTimePoint + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SentimentTimePointCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SentimentTimePointCopyWith<$Res> { + factory $SentimentTimePointCopyWith( + SentimentTimePoint value, + $Res Function(SentimentTimePoint) then, + ) = _$SentimentTimePointCopyWithImpl<$Res, SentimentTimePoint>; + @useResult + $Res call({int timeMs, SentimentType sentiment, double confidence}); +} + +/// @nodoc +class _$SentimentTimePointCopyWithImpl<$Res, $Val extends SentimentTimePoint> + implements $SentimentTimePointCopyWith<$Res> { + _$SentimentTimePointCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SentimentTimePoint + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? timeMs = null, + Object? sentiment = null, + Object? confidence = null, + }) { + return _then( + _value.copyWith( + timeMs: + null == timeMs + ? _value.timeMs + : timeMs // ignore: cast_nullable_to_non_nullable + as int, + sentiment: + null == sentiment + ? _value.sentiment + : sentiment // ignore: cast_nullable_to_non_nullable + as SentimentType, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$SentimentTimePointImplCopyWith<$Res> + implements $SentimentTimePointCopyWith<$Res> { + factory _$$SentimentTimePointImplCopyWith( + _$SentimentTimePointImpl value, + $Res Function(_$SentimentTimePointImpl) then, + ) = __$$SentimentTimePointImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int timeMs, SentimentType sentiment, double confidence}); +} + +/// @nodoc +class __$$SentimentTimePointImplCopyWithImpl<$Res> + extends _$SentimentTimePointCopyWithImpl<$Res, _$SentimentTimePointImpl> + implements _$$SentimentTimePointImplCopyWith<$Res> { + __$$SentimentTimePointImplCopyWithImpl( + _$SentimentTimePointImpl _value, + $Res Function(_$SentimentTimePointImpl) _then, + ) : super(_value, _then); + + /// Create a copy of SentimentTimePoint + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? timeMs = null, + Object? sentiment = null, + Object? confidence = null, + }) { + return _then( + _$SentimentTimePointImpl( + timeMs: + null == timeMs + ? _value.timeMs + : timeMs // ignore: cast_nullable_to_non_nullable + as int, + sentiment: + null == sentiment + ? _value.sentiment + : sentiment // ignore: cast_nullable_to_non_nullable + as SentimentType, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$SentimentTimePointImpl implements _SentimentTimePoint { + const _$SentimentTimePointImpl({ + required this.timeMs, + required this.sentiment, + required this.confidence, + }); + + factory _$SentimentTimePointImpl.fromJson(Map json) => + _$$SentimentTimePointImplFromJson(json); + + @override + final int timeMs; + @override + final SentimentType sentiment; + @override + final double confidence; + + @override + String toString() { + return 'SentimentTimePoint(timeMs: $timeMs, sentiment: $sentiment, confidence: $confidence)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SentimentTimePointImpl && + (identical(other.timeMs, timeMs) || other.timeMs == timeMs) && + (identical(other.sentiment, sentiment) || + other.sentiment == sentiment) && + (identical(other.confidence, confidence) || + other.confidence == confidence)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, timeMs, sentiment, confidence); + + /// Create a copy of SentimentTimePoint + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SentimentTimePointImplCopyWith<_$SentimentTimePointImpl> get copyWith => + __$$SentimentTimePointImplCopyWithImpl<_$SentimentTimePointImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$SentimentTimePointImplToJson(this); + } +} + +abstract class _SentimentTimePoint implements SentimentTimePoint { + const factory _SentimentTimePoint({ + required final int timeMs, + required final SentimentType sentiment, + required final double confidence, + }) = _$SentimentTimePointImpl; + + factory _SentimentTimePoint.fromJson(Map json) = + _$SentimentTimePointImpl.fromJson; + + @override + int get timeMs; + @override + SentimentType get sentiment; + @override + double get confidence; + + /// Create a copy of SentimentTimePoint + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SentimentTimePointImplCopyWith<_$SentimentTimePointImpl> get copyWith => + throw _privateConstructorUsedError; +} + +TopicResult _$TopicResultFromJson(Map json) { + return _TopicResult.fromJson(json); +} + +/// @nodoc +mixin _$TopicResult { + /// Topic name or title + String get name => throw _privateConstructorUsedError; + + /// Relevance score (0.0 to 1.0) + double get relevance => throw _privateConstructorUsedError; + + /// Keywords associated with topic + List get keywords => throw _privateConstructorUsedError; + + /// Category of the topic + String? get category => throw _privateConstructorUsedError; + + /// Description of the topic + String? get description => throw _privateConstructorUsedError; + + /// Time ranges where topic was discussed + List get timeRanges => throw _privateConstructorUsedError; + + /// Participants who discussed this topic + List get participants => throw _privateConstructorUsedError; + + /// Related topics + List get relatedTopics => throw _privateConstructorUsedError; + + /// Confidence in topic identification + double get confidence => throw _privateConstructorUsedError; + + /// Serializes this TopicResult to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of TopicResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TopicResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TopicResultCopyWith<$Res> { + factory $TopicResultCopyWith( + TopicResult value, + $Res Function(TopicResult) then, + ) = _$TopicResultCopyWithImpl<$Res, TopicResult>; + @useResult + $Res call({ + String name, + double relevance, + List keywords, + String? category, + String? description, + List timeRanges, + List participants, + List relatedTopics, + double confidence, + }); +} + +/// @nodoc +class _$TopicResultCopyWithImpl<$Res, $Val extends TopicResult> + implements $TopicResultCopyWith<$Res> { + _$TopicResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TopicResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? relevance = null, + Object? keywords = null, + Object? category = freezed, + Object? description = freezed, + Object? timeRanges = null, + Object? participants = null, + Object? relatedTopics = null, + Object? confidence = null, + }) { + return _then( + _value.copyWith( + name: + null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + relevance: + null == relevance + ? _value.relevance + : relevance // ignore: cast_nullable_to_non_nullable + as double, + keywords: + null == keywords + ? _value.keywords + : keywords // ignore: cast_nullable_to_non_nullable + as List, + category: + freezed == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as String?, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + timeRanges: + null == timeRanges + ? _value.timeRanges + : timeRanges // ignore: cast_nullable_to_non_nullable + as List, + participants: + null == participants + ? _value.participants + : participants // ignore: cast_nullable_to_non_nullable + as List, + relatedTopics: + null == relatedTopics + ? _value.relatedTopics + : relatedTopics // ignore: cast_nullable_to_non_nullable + as List, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$TopicResultImplCopyWith<$Res> + implements $TopicResultCopyWith<$Res> { + factory _$$TopicResultImplCopyWith( + _$TopicResultImpl value, + $Res Function(_$TopicResultImpl) then, + ) = __$$TopicResultImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String name, + double relevance, + List keywords, + String? category, + String? description, + List timeRanges, + List participants, + List relatedTopics, + double confidence, + }); +} + +/// @nodoc +class __$$TopicResultImplCopyWithImpl<$Res> + extends _$TopicResultCopyWithImpl<$Res, _$TopicResultImpl> + implements _$$TopicResultImplCopyWith<$Res> { + __$$TopicResultImplCopyWithImpl( + _$TopicResultImpl _value, + $Res Function(_$TopicResultImpl) _then, + ) : super(_value, _then); + + /// Create a copy of TopicResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? relevance = null, + Object? keywords = null, + Object? category = freezed, + Object? description = freezed, + Object? timeRanges = null, + Object? participants = null, + Object? relatedTopics = null, + Object? confidence = null, + }) { + return _then( + _$TopicResultImpl( + name: + null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + relevance: + null == relevance + ? _value.relevance + : relevance // ignore: cast_nullable_to_non_nullable + as double, + keywords: + null == keywords + ? _value._keywords + : keywords // ignore: cast_nullable_to_non_nullable + as List, + category: + freezed == category + ? _value.category + : category // ignore: cast_nullable_to_non_nullable + as String?, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + timeRanges: + null == timeRanges + ? _value._timeRanges + : timeRanges // ignore: cast_nullable_to_non_nullable + as List, + participants: + null == participants + ? _value._participants + : participants // ignore: cast_nullable_to_non_nullable + as List, + relatedTopics: + null == relatedTopics + ? _value._relatedTopics + : relatedTopics // ignore: cast_nullable_to_non_nullable + as List, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$TopicResultImpl extends _TopicResult { + const _$TopicResultImpl({ + required this.name, + required this.relevance, + final List keywords = const [], + this.category, + this.description, + final List timeRanges = const [], + final List participants = const [], + final List relatedTopics = const [], + this.confidence = 0.0, + }) : _keywords = keywords, + _timeRanges = timeRanges, + _participants = participants, + _relatedTopics = relatedTopics, + super._(); + + factory _$TopicResultImpl.fromJson(Map json) => + _$$TopicResultImplFromJson(json); + + /// Topic name or title + @override + final String name; + + /// Relevance score (0.0 to 1.0) + @override + final double relevance; + + /// Keywords associated with topic + final List _keywords; + + /// Keywords associated with topic + @override + @JsonKey() + List get keywords { + if (_keywords is EqualUnmodifiableListView) return _keywords; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_keywords); + } + + /// Category of the topic + @override + final String? category; + + /// Description of the topic + @override + final String? description; + + /// Time ranges where topic was discussed + final List _timeRanges; + + /// Time ranges where topic was discussed + @override + @JsonKey() + List get timeRanges { + if (_timeRanges is EqualUnmodifiableListView) return _timeRanges; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_timeRanges); + } + + /// Participants who discussed this topic + final List _participants; + + /// Participants who discussed this topic + @override + @JsonKey() + List get participants { + if (_participants is EqualUnmodifiableListView) return _participants; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_participants); + } + + /// Related topics + final List _relatedTopics; + + /// Related topics + @override + @JsonKey() + List get relatedTopics { + if (_relatedTopics is EqualUnmodifiableListView) return _relatedTopics; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_relatedTopics); + } + + /// Confidence in topic identification + @override + @JsonKey() + final double confidence; + + @override + String toString() { + return 'TopicResult(name: $name, relevance: $relevance, keywords: $keywords, category: $category, description: $description, timeRanges: $timeRanges, participants: $participants, relatedTopics: $relatedTopics, confidence: $confidence)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TopicResultImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.relevance, relevance) || + other.relevance == relevance) && + const DeepCollectionEquality().equals(other._keywords, _keywords) && + (identical(other.category, category) || + other.category == category) && + (identical(other.description, description) || + other.description == description) && + const DeepCollectionEquality().equals( + other._timeRanges, + _timeRanges, + ) && + const DeepCollectionEquality().equals( + other._participants, + _participants, + ) && + const DeepCollectionEquality().equals( + other._relatedTopics, + _relatedTopics, + ) && + (identical(other.confidence, confidence) || + other.confidence == confidence)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + name, + relevance, + const DeepCollectionEquality().hash(_keywords), + category, + description, + const DeepCollectionEquality().hash(_timeRanges), + const DeepCollectionEquality().hash(_participants), + const DeepCollectionEquality().hash(_relatedTopics), + confidence, + ); + + /// Create a copy of TopicResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TopicResultImplCopyWith<_$TopicResultImpl> get copyWith => + __$$TopicResultImplCopyWithImpl<_$TopicResultImpl>(this, _$identity); + + @override + Map toJson() { + return _$$TopicResultImplToJson(this); + } +} + +abstract class _TopicResult extends TopicResult { + const factory _TopicResult({ + required final String name, + required final double relevance, + final List keywords, + final String? category, + final String? description, + final List timeRanges, + final List participants, + final List relatedTopics, + final double confidence, + }) = _$TopicResultImpl; + const _TopicResult._() : super._(); + + factory _TopicResult.fromJson(Map json) = + _$TopicResultImpl.fromJson; + + /// Topic name or title + @override + String get name; + + /// Relevance score (0.0 to 1.0) + @override + double get relevance; + + /// Keywords associated with topic + @override + List get keywords; + + /// Category of the topic + @override + String? get category; + + /// Description of the topic + @override + String? get description; + + /// Time ranges where topic was discussed + @override + List get timeRanges; + + /// Participants who discussed this topic + @override + List get participants; + + /// Related topics + @override + List get relatedTopics; + + /// Confidence in topic identification + @override + double get confidence; + + /// Create a copy of TopicResult + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TopicResultImplCopyWith<_$TopicResultImpl> get copyWith => + throw _privateConstructorUsedError; +} + +TimeRange _$TimeRangeFromJson(Map json) { + return _TimeRange.fromJson(json); +} + +/// @nodoc +mixin _$TimeRange { + int get startMs => throw _privateConstructorUsedError; + int get endMs => throw _privateConstructorUsedError; + + /// Serializes this TimeRange to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of TimeRange + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TimeRangeCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TimeRangeCopyWith<$Res> { + factory $TimeRangeCopyWith(TimeRange value, $Res Function(TimeRange) then) = + _$TimeRangeCopyWithImpl<$Res, TimeRange>; + @useResult + $Res call({int startMs, int endMs}); +} + +/// @nodoc +class _$TimeRangeCopyWithImpl<$Res, $Val extends TimeRange> + implements $TimeRangeCopyWith<$Res> { + _$TimeRangeCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TimeRange + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? startMs = null, Object? endMs = null}) { + return _then( + _value.copyWith( + startMs: + null == startMs + ? _value.startMs + : startMs // ignore: cast_nullable_to_non_nullable + as int, + endMs: + null == endMs + ? _value.endMs + : endMs // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$TimeRangeImplCopyWith<$Res> + implements $TimeRangeCopyWith<$Res> { + factory _$$TimeRangeImplCopyWith( + _$TimeRangeImpl value, + $Res Function(_$TimeRangeImpl) then, + ) = __$$TimeRangeImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int startMs, int endMs}); +} + +/// @nodoc +class __$$TimeRangeImplCopyWithImpl<$Res> + extends _$TimeRangeCopyWithImpl<$Res, _$TimeRangeImpl> + implements _$$TimeRangeImplCopyWith<$Res> { + __$$TimeRangeImplCopyWithImpl( + _$TimeRangeImpl _value, + $Res Function(_$TimeRangeImpl) _then, + ) : super(_value, _then); + + /// Create a copy of TimeRange + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? startMs = null, Object? endMs = null}) { + return _then( + _$TimeRangeImpl( + startMs: + null == startMs + ? _value.startMs + : startMs // ignore: cast_nullable_to_non_nullable + as int, + endMs: + null == endMs + ? _value.endMs + : endMs // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$TimeRangeImpl extends _TimeRange { + const _$TimeRangeImpl({required this.startMs, required this.endMs}) + : super._(); + + factory _$TimeRangeImpl.fromJson(Map json) => + _$$TimeRangeImplFromJson(json); + + @override + final int startMs; + @override + final int endMs; + + @override + String toString() { + return 'TimeRange(startMs: $startMs, endMs: $endMs)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TimeRangeImpl && + (identical(other.startMs, startMs) || other.startMs == startMs) && + (identical(other.endMs, endMs) || other.endMs == endMs)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, startMs, endMs); + + /// Create a copy of TimeRange + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TimeRangeImplCopyWith<_$TimeRangeImpl> get copyWith => + __$$TimeRangeImplCopyWithImpl<_$TimeRangeImpl>(this, _$identity); + + @override + Map toJson() { + return _$$TimeRangeImplToJson(this); + } +} + +abstract class _TimeRange extends TimeRange { + const factory _TimeRange({ + required final int startMs, + required final int endMs, + }) = _$TimeRangeImpl; + const _TimeRange._() : super._(); + + factory _TimeRange.fromJson(Map json) = + _$TimeRangeImpl.fromJson; + + @override + int get startMs; + @override + int get endMs; + + /// Create a copy of TimeRange + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TimeRangeImplCopyWith<_$TimeRangeImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/analysis_result.g.dart b/lib/models/analysis_result.g.dart new file mode 100644 index 0000000..63247b0 --- /dev/null +++ b/lib/models/analysis_result.g.dart @@ -0,0 +1,371 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'analysis_result.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AnalysisResultImpl _$$AnalysisResultImplFromJson( + Map json, +) => _$AnalysisResultImpl( + id: json['id'] as String, + conversationId: json['conversationId'] as String, + type: $enumDecode(_$AnalysisTypeEnumMap, json['type']), + status: $enumDecode(_$AnalysisStatusEnumMap, json['status']), + startTime: DateTime.parse(json['startTime'] as String), + completionTime: + json['completionTime'] == null + ? null + : DateTime.parse(json['completionTime'] as String), + provider: json['provider'] as String?, + confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0, + factChecks: + (json['factChecks'] as List?) + ?.map((e) => FactCheckResult.fromJson(e as Map)) + .toList(), + summary: + json['summary'] == null + ? null + : ConversationSummary.fromJson( + json['summary'] as Map, + ), + actionItems: + (json['actionItems'] as List?) + ?.map((e) => ActionItemResult.fromJson(e as Map)) + .toList(), + sentiment: + json['sentiment'] == null + ? null + : SentimentAnalysisResult.fromJson( + json['sentiment'] as Map, + ), + topics: + (json['topics'] as List?) + ?.map((e) => TopicResult.fromJson(e as Map)) + .toList(), + insights: + (json['insights'] as List?)?.map((e) => e as String).toList() ?? + const [], + errors: + (json['errors'] as List?)?.map((e) => e as String).toList() ?? + const [], + processingTimeMs: (json['processingTimeMs'] as num?)?.toInt(), + tokenUsage: (json['tokenUsage'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toInt()), + ), + metadata: json['metadata'] as Map? ?? const {}, +); + +Map _$$AnalysisResultImplToJson( + _$AnalysisResultImpl instance, +) => { + 'id': instance.id, + 'conversationId': instance.conversationId, + 'type': _$AnalysisTypeEnumMap[instance.type]!, + 'status': _$AnalysisStatusEnumMap[instance.status]!, + 'startTime': instance.startTime.toIso8601String(), + 'completionTime': instance.completionTime?.toIso8601String(), + 'provider': instance.provider, + 'confidence': instance.confidence, + 'factChecks': instance.factChecks, + 'summary': instance.summary, + 'actionItems': instance.actionItems, + 'sentiment': instance.sentiment, + 'topics': instance.topics, + 'insights': instance.insights, + 'errors': instance.errors, + 'processingTimeMs': instance.processingTimeMs, + 'tokenUsage': instance.tokenUsage, + 'metadata': instance.metadata, +}; + +const _$AnalysisTypeEnumMap = { + AnalysisType.factCheck: 'factCheck', + AnalysisType.summary: 'summary', + AnalysisType.actionItems: 'actionItems', + AnalysisType.sentiment: 'sentiment', + AnalysisType.topics: 'topics', + AnalysisType.comprehensive: 'comprehensive', +}; + +const _$AnalysisStatusEnumMap = { + AnalysisStatus.pending: 'pending', + AnalysisStatus.processing: 'processing', + AnalysisStatus.completed: 'completed', + AnalysisStatus.failed: 'failed', + AnalysisStatus.partial: 'partial', +}; + +_$FactCheckResultImpl _$$FactCheckResultImplFromJson( + Map json, +) => _$FactCheckResultImpl( + id: json['id'] as String, + claim: json['claim'] as String, + status: $enumDecode(_$FactCheckStatusEnumMap, json['status']), + confidence: (json['confidence'] as num).toDouble(), + sources: + (json['sources'] as List?)?.map((e) => e as String).toList() ?? + const [], + explanation: json['explanation'] as String?, + context: json['context'] as String?, + startTimeMs: (json['startTimeMs'] as num?)?.toInt(), + endTimeMs: (json['endTimeMs'] as num?)?.toInt(), + speakerId: json['speakerId'] as String?, + category: json['category'] as String?, + relatedClaims: + (json['relatedClaims'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], +); + +Map _$$FactCheckResultImplToJson( + _$FactCheckResultImpl instance, +) => { + 'id': instance.id, + 'claim': instance.claim, + 'status': _$FactCheckStatusEnumMap[instance.status]!, + 'confidence': instance.confidence, + 'sources': instance.sources, + 'explanation': instance.explanation, + 'context': instance.context, + 'startTimeMs': instance.startTimeMs, + 'endTimeMs': instance.endTimeMs, + 'speakerId': instance.speakerId, + 'category': instance.category, + 'relatedClaims': instance.relatedClaims, +}; + +const _$FactCheckStatusEnumMap = { + FactCheckStatus.verified: 'verified', + FactCheckStatus.disputed: 'disputed', + FactCheckStatus.uncertain: 'uncertain', + FactCheckStatus.needsReview: 'needsReview', +}; + +_$ConversationSummaryImpl _$$ConversationSummaryImplFromJson( + Map json, +) => _$ConversationSummaryImpl( + summary: json['summary'] as String, + keyPoints: + (json['keyPoints'] as List?)?.map((e) => e as String).toList() ?? + const [], + decisions: + (json['decisions'] as List?)?.map((e) => e as String).toList() ?? + const [], + questions: + (json['questions'] as List?)?.map((e) => e as String).toList() ?? + const [], + tone: json['tone'] as String?, + topics: + (json['topics'] as List?)?.map((e) => e as String).toList() ?? + const [], + length: + $enumDecodeNullable(_$SummaryLengthEnumMap, json['length']) ?? + SummaryLength.medium, + estimatedReadTime: + json['estimatedReadTime'] == null + ? null + : Duration(microseconds: (json['estimatedReadTime'] as num).toInt()), + confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0, +); + +Map _$$ConversationSummaryImplToJson( + _$ConversationSummaryImpl instance, +) => { + 'summary': instance.summary, + 'keyPoints': instance.keyPoints, + 'decisions': instance.decisions, + 'questions': instance.questions, + 'tone': instance.tone, + 'topics': instance.topics, + 'length': _$SummaryLengthEnumMap[instance.length]!, + 'estimatedReadTime': instance.estimatedReadTime?.inMicroseconds, + 'confidence': instance.confidence, +}; + +const _$SummaryLengthEnumMap = { + SummaryLength.brief: 'brief', + SummaryLength.medium: 'medium', + SummaryLength.detailed: 'detailed', +}; + +_$ActionItemResultImpl _$$ActionItemResultImplFromJson( + Map json, +) => _$ActionItemResultImpl( + id: json['id'] as String, + description: json['description'] as String, + assignee: json['assignee'] as String?, + dueDate: + json['dueDate'] == null + ? null + : DateTime.parse(json['dueDate'] as String), + priority: + $enumDecodeNullable(_$ActionItemPriorityEnumMap, json['priority']) ?? + ActionItemPriority.medium, + context: json['context'] as String?, + confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0, + status: + $enumDecodeNullable(_$ActionItemStatusEnumMap, json['status']) ?? + ActionItemStatus.pending, + mentionedAtMs: (json['mentionedAtMs'] as num?)?.toInt(), + speakerId: json['speakerId'] as String?, + relatedItems: + (json['relatedItems'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + tags: + (json['tags'] as List?)?.map((e) => e as String).toList() ?? + const [], +); + +Map _$$ActionItemResultImplToJson( + _$ActionItemResultImpl instance, +) => { + 'id': instance.id, + 'description': instance.description, + 'assignee': instance.assignee, + 'dueDate': instance.dueDate?.toIso8601String(), + 'priority': _$ActionItemPriorityEnumMap[instance.priority]!, + 'context': instance.context, + 'confidence': instance.confidence, + 'status': _$ActionItemStatusEnumMap[instance.status]!, + 'mentionedAtMs': instance.mentionedAtMs, + 'speakerId': instance.speakerId, + 'relatedItems': instance.relatedItems, + 'tags': instance.tags, +}; + +const _$ActionItemPriorityEnumMap = { + ActionItemPriority.low: 'low', + ActionItemPriority.medium: 'medium', + ActionItemPriority.high: 'high', + ActionItemPriority.urgent: 'urgent', +}; + +const _$ActionItemStatusEnumMap = { + ActionItemStatus.pending: 'pending', + ActionItemStatus.inProgress: 'inProgress', + ActionItemStatus.completed: 'completed', + ActionItemStatus.cancelled: 'cancelled', + ActionItemStatus.deferred: 'deferred', +}; + +_$SentimentAnalysisResultImpl _$$SentimentAnalysisResultImplFromJson( + Map json, +) => _$SentimentAnalysisResultImpl( + overallSentiment: $enumDecode( + _$SentimentTypeEnumMap, + json['overallSentiment'], + ), + confidence: (json['confidence'] as num).toDouble(), + emotions: (json['emotions'] as Map).map( + (k, e) => MapEntry(k, (e as num).toDouble()), + ), + tone: json['tone'] as String?, + progression: + (json['progression'] as List?) + ?.map((e) => SentimentTimePoint.fromJson(e as Map)) + .toList() ?? + const [], + participantSentiments: + (json['participantSentiments'] as Map?)?.map( + (k, e) => MapEntry(k, $enumDecode(_$SentimentTypeEnumMap, e)), + ) ?? + const {}, + keyPhrases: + (json['keyPhrases'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], +); + +Map _$$SentimentAnalysisResultImplToJson( + _$SentimentAnalysisResultImpl instance, +) => { + 'overallSentiment': _$SentimentTypeEnumMap[instance.overallSentiment]!, + 'confidence': instance.confidence, + 'emotions': instance.emotions, + 'tone': instance.tone, + 'progression': instance.progression, + 'participantSentiments': instance.participantSentiments.map( + (k, e) => MapEntry(k, _$SentimentTypeEnumMap[e]!), + ), + 'keyPhrases': instance.keyPhrases, +}; + +const _$SentimentTypeEnumMap = { + SentimentType.positive: 'positive', + SentimentType.negative: 'negative', + SentimentType.neutral: 'neutral', + SentimentType.mixed: 'mixed', +}; + +_$SentimentTimePointImpl _$$SentimentTimePointImplFromJson( + Map json, +) => _$SentimentTimePointImpl( + timeMs: (json['timeMs'] as num).toInt(), + sentiment: $enumDecode(_$SentimentTypeEnumMap, json['sentiment']), + confidence: (json['confidence'] as num).toDouble(), +); + +Map _$$SentimentTimePointImplToJson( + _$SentimentTimePointImpl instance, +) => { + 'timeMs': instance.timeMs, + 'sentiment': _$SentimentTypeEnumMap[instance.sentiment]!, + 'confidence': instance.confidence, +}; + +_$TopicResultImpl _$$TopicResultImplFromJson(Map json) => + _$TopicResultImpl( + name: json['name'] as String, + relevance: (json['relevance'] as num).toDouble(), + keywords: + (json['keywords'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + category: json['category'] as String?, + description: json['description'] as String?, + timeRanges: + (json['timeRanges'] as List?) + ?.map((e) => TimeRange.fromJson(e as Map)) + .toList() ?? + const [], + participants: + (json['participants'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + relatedTopics: + (json['relatedTopics'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + confidence: (json['confidence'] as num?)?.toDouble() ?? 0.0, + ); + +Map _$$TopicResultImplToJson(_$TopicResultImpl instance) => + { + 'name': instance.name, + 'relevance': instance.relevance, + 'keywords': instance.keywords, + 'category': instance.category, + 'description': instance.description, + 'timeRanges': instance.timeRanges, + 'participants': instance.participants, + 'relatedTopics': instance.relatedTopics, + 'confidence': instance.confidence, + }; + +_$TimeRangeImpl _$$TimeRangeImplFromJson(Map json) => + _$TimeRangeImpl( + startMs: (json['startMs'] as num).toInt(), + endMs: (json['endMs'] as num).toInt(), + ); + +Map _$$TimeRangeImplToJson(_$TimeRangeImpl instance) => + {'startMs': instance.startMs, 'endMs': instance.endMs}; diff --git a/lib/models/audio_configuration.dart b/lib/models/audio_configuration.dart new file mode 100644 index 0000000..5a22d1f --- /dev/null +++ b/lib/models/audio_configuration.dart @@ -0,0 +1,154 @@ +// ABOUTME: Audio configuration data model for audio processing settings +// ABOUTME: Immutable configuration object using Freezed for type safety + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'audio_configuration.freezed.dart'; +part 'audio_configuration.g.dart'; + +/// Audio quality levels +enum AudioQuality { + low, // 8kHz, lower quality for bandwidth savings + medium, // 16kHz, standard quality for speech + high, // 44.1kHz, high quality for music/recording +} + +/// Audio format types +enum AudioFormat { + wav, + mp3, + aac, + flac, +} + +/// Audio configuration for recording and processing +@freezed +class AudioConfiguration with _$AudioConfiguration { + const factory AudioConfiguration({ + /// Sample rate in Hz (e.g., 16000 for 16kHz) + @Default(16000) int sampleRate, + + /// Number of audio channels (1 for mono, 2 for stereo) + @Default(1) int channels, + + /// Bit rate for encoding (in bits per second) + @Default(64000) int bitRate, + + /// Audio quality level + @Default(AudioQuality.medium) AudioQuality quality, + + /// Audio format for recording + @Default(AudioFormat.wav) AudioFormat format, + + /// Enable noise reduction + @Default(true) bool enableNoiseReduction, + + /// Enable echo cancellation + @Default(true) bool enableEchoCancellation, + + /// Enable automatic gain control + @Default(true) bool enableAutomaticGainControl, + + /// Audio gain level (0.0 to 2.0, 1.0 is normal) + @Default(1.0) double gainLevel, + + /// Enable voice activity detection + @Default(true) bool enableVoiceActivityDetection, + + /// Voice activity detection threshold (0.0 to 1.0) + @Default(0.01) double vadThreshold, + + /// Buffer size in frames for audio processing + @Default(4096) int bufferSize, + + /// Selected audio input device ID + String? selectedDeviceId, + + /// Enable real-time audio streaming + @Default(true) bool enableRealTimeStreaming, + + /// Audio chunk duration for processing (in milliseconds) + @Default(100) int chunkDurationMs, + }) = _AudioConfiguration; + + factory AudioConfiguration.fromJson(Map json) => + _$AudioConfigurationFromJson(json); + + /// Create configuration optimized for speech recognition + factory AudioConfiguration.speechRecognition() { + return const AudioConfiguration( + sampleRate: 16000, + channels: 1, + quality: AudioQuality.medium, + format: AudioFormat.wav, + enableNoiseReduction: true, + enableVoiceActivityDetection: true, + vadThreshold: 0.01, + ); + } + + /// Create configuration optimized for high-quality recording + factory AudioConfiguration.highQualityRecording() { + return const AudioConfiguration( + sampleRate: 44100, + channels: 2, + quality: AudioQuality.high, + format: AudioFormat.flac, + bitRate: 128000, + enableNoiseReduction: false, + enableAutomaticGainControl: false, + ); + } + + /// Create configuration optimized for low bandwidth + factory AudioConfiguration.lowBandwidth() { + return const AudioConfiguration( + sampleRate: 8000, + channels: 1, + quality: AudioQuality.low, + format: AudioFormat.mp3, + bitRate: 32000, + enableNoiseReduction: true, + vadThreshold: 0.05, + ); + } +} + +/// Audio processing capabilities of the device +@freezed +class AudioCapabilities with _$AudioCapabilities { + const factory AudioCapabilities({ + /// Supported sample rates + required List supportedSampleRates, + + /// Supported channel counts + required List supportedChannels, + + /// Supported audio formats + required List supportedFormats, + + /// Whether noise reduction is supported + @Default(false) bool supportsNoiseReduction, + + /// Whether echo cancellation is supported + @Default(false) bool supportsEchoCancellation, + + /// Whether automatic gain control is supported + @Default(false) bool supportsAutomaticGainControl, + + /// Whether voice activity detection is supported + @Default(false) bool supportsVoiceActivityDetection, + + /// Maximum supported gain level + @Default(2.0) double maxGainLevel, + + /// Minimum supported gain level + @Default(0.0) double minGainLevel, + + /// Available buffer sizes + required List availableBufferSizes, + }) = _AudioCapabilities; + + factory AudioCapabilities.fromJson(Map json) => + _$AudioCapabilitiesFromJson(json); +} \ No newline at end of file diff --git a/lib/models/audio_configuration.freezed.dart b/lib/models/audio_configuration.freezed.dart new file mode 100644 index 0000000..bcb6efa --- /dev/null +++ b/lib/models/audio_configuration.freezed.dart @@ -0,0 +1,1138 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'audio_configuration.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +AudioConfiguration _$AudioConfigurationFromJson(Map json) { + return _AudioConfiguration.fromJson(json); +} + +/// @nodoc +mixin _$AudioConfiguration { + /// Sample rate in Hz (e.g., 16000 for 16kHz) + int get sampleRate => throw _privateConstructorUsedError; + + /// Number of audio channels (1 for mono, 2 for stereo) + int get channels => throw _privateConstructorUsedError; + + /// Bit rate for encoding (in bits per second) + int get bitRate => throw _privateConstructorUsedError; + + /// Audio quality level + AudioQuality get quality => throw _privateConstructorUsedError; + + /// Audio format for recording + AudioFormat get format => throw _privateConstructorUsedError; + + /// Enable noise reduction + bool get enableNoiseReduction => throw _privateConstructorUsedError; + + /// Enable echo cancellation + bool get enableEchoCancellation => throw _privateConstructorUsedError; + + /// Enable automatic gain control + bool get enableAutomaticGainControl => throw _privateConstructorUsedError; + + /// Audio gain level (0.0 to 2.0, 1.0 is normal) + double get gainLevel => throw _privateConstructorUsedError; + + /// Enable voice activity detection + bool get enableVoiceActivityDetection => throw _privateConstructorUsedError; + + /// Voice activity detection threshold (0.0 to 1.0) + double get vadThreshold => throw _privateConstructorUsedError; + + /// Buffer size in frames for audio processing + int get bufferSize => throw _privateConstructorUsedError; + + /// Selected audio input device ID + String? get selectedDeviceId => throw _privateConstructorUsedError; + + /// Enable real-time audio streaming + bool get enableRealTimeStreaming => throw _privateConstructorUsedError; + + /// Audio chunk duration for processing (in milliseconds) + int get chunkDurationMs => throw _privateConstructorUsedError; + + /// Serializes this AudioConfiguration to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AudioConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AudioConfigurationCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AudioConfigurationCopyWith<$Res> { + factory $AudioConfigurationCopyWith( + AudioConfiguration value, + $Res Function(AudioConfiguration) then, + ) = _$AudioConfigurationCopyWithImpl<$Res, AudioConfiguration>; + @useResult + $Res call({ + int sampleRate, + int channels, + int bitRate, + AudioQuality quality, + AudioFormat format, + bool enableNoiseReduction, + bool enableEchoCancellation, + bool enableAutomaticGainControl, + double gainLevel, + bool enableVoiceActivityDetection, + double vadThreshold, + int bufferSize, + String? selectedDeviceId, + bool enableRealTimeStreaming, + int chunkDurationMs, + }); +} + +/// @nodoc +class _$AudioConfigurationCopyWithImpl<$Res, $Val extends AudioConfiguration> + implements $AudioConfigurationCopyWith<$Res> { + _$AudioConfigurationCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AudioConfiguration + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? sampleRate = null, + Object? channels = null, + Object? bitRate = null, + Object? quality = null, + Object? format = null, + Object? enableNoiseReduction = null, + Object? enableEchoCancellation = null, + Object? enableAutomaticGainControl = null, + Object? gainLevel = null, + Object? enableVoiceActivityDetection = null, + Object? vadThreshold = null, + Object? bufferSize = null, + Object? selectedDeviceId = freezed, + Object? enableRealTimeStreaming = null, + Object? chunkDurationMs = null, + }) { + return _then( + _value.copyWith( + sampleRate: + null == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as int, + channels: + null == channels + ? _value.channels + : channels // ignore: cast_nullable_to_non_nullable + as int, + bitRate: + null == bitRate + ? _value.bitRate + : bitRate // ignore: cast_nullable_to_non_nullable + as int, + quality: + null == quality + ? _value.quality + : quality // ignore: cast_nullable_to_non_nullable + as AudioQuality, + format: + null == format + ? _value.format + : format // ignore: cast_nullable_to_non_nullable + as AudioFormat, + enableNoiseReduction: + null == enableNoiseReduction + ? _value.enableNoiseReduction + : enableNoiseReduction // ignore: cast_nullable_to_non_nullable + as bool, + enableEchoCancellation: + null == enableEchoCancellation + ? _value.enableEchoCancellation + : enableEchoCancellation // ignore: cast_nullable_to_non_nullable + as bool, + enableAutomaticGainControl: + null == enableAutomaticGainControl + ? _value.enableAutomaticGainControl + : enableAutomaticGainControl // ignore: cast_nullable_to_non_nullable + as bool, + gainLevel: + null == gainLevel + ? _value.gainLevel + : gainLevel // ignore: cast_nullable_to_non_nullable + as double, + enableVoiceActivityDetection: + null == enableVoiceActivityDetection + ? _value.enableVoiceActivityDetection + : enableVoiceActivityDetection // ignore: cast_nullable_to_non_nullable + as bool, + vadThreshold: + null == vadThreshold + ? _value.vadThreshold + : vadThreshold // ignore: cast_nullable_to_non_nullable + as double, + bufferSize: + null == bufferSize + ? _value.bufferSize + : bufferSize // ignore: cast_nullable_to_non_nullable + as int, + selectedDeviceId: + freezed == selectedDeviceId + ? _value.selectedDeviceId + : selectedDeviceId // ignore: cast_nullable_to_non_nullable + as String?, + enableRealTimeStreaming: + null == enableRealTimeStreaming + ? _value.enableRealTimeStreaming + : enableRealTimeStreaming // ignore: cast_nullable_to_non_nullable + as bool, + chunkDurationMs: + null == chunkDurationMs + ? _value.chunkDurationMs + : chunkDurationMs // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AudioConfigurationImplCopyWith<$Res> + implements $AudioConfigurationCopyWith<$Res> { + factory _$$AudioConfigurationImplCopyWith( + _$AudioConfigurationImpl value, + $Res Function(_$AudioConfigurationImpl) then, + ) = __$$AudioConfigurationImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int sampleRate, + int channels, + int bitRate, + AudioQuality quality, + AudioFormat format, + bool enableNoiseReduction, + bool enableEchoCancellation, + bool enableAutomaticGainControl, + double gainLevel, + bool enableVoiceActivityDetection, + double vadThreshold, + int bufferSize, + String? selectedDeviceId, + bool enableRealTimeStreaming, + int chunkDurationMs, + }); +} + +/// @nodoc +class __$$AudioConfigurationImplCopyWithImpl<$Res> + extends _$AudioConfigurationCopyWithImpl<$Res, _$AudioConfigurationImpl> + implements _$$AudioConfigurationImplCopyWith<$Res> { + __$$AudioConfigurationImplCopyWithImpl( + _$AudioConfigurationImpl _value, + $Res Function(_$AudioConfigurationImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AudioConfiguration + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? sampleRate = null, + Object? channels = null, + Object? bitRate = null, + Object? quality = null, + Object? format = null, + Object? enableNoiseReduction = null, + Object? enableEchoCancellation = null, + Object? enableAutomaticGainControl = null, + Object? gainLevel = null, + Object? enableVoiceActivityDetection = null, + Object? vadThreshold = null, + Object? bufferSize = null, + Object? selectedDeviceId = freezed, + Object? enableRealTimeStreaming = null, + Object? chunkDurationMs = null, + }) { + return _then( + _$AudioConfigurationImpl( + sampleRate: + null == sampleRate + ? _value.sampleRate + : sampleRate // ignore: cast_nullable_to_non_nullable + as int, + channels: + null == channels + ? _value.channels + : channels // ignore: cast_nullable_to_non_nullable + as int, + bitRate: + null == bitRate + ? _value.bitRate + : bitRate // ignore: cast_nullable_to_non_nullable + as int, + quality: + null == quality + ? _value.quality + : quality // ignore: cast_nullable_to_non_nullable + as AudioQuality, + format: + null == format + ? _value.format + : format // ignore: cast_nullable_to_non_nullable + as AudioFormat, + enableNoiseReduction: + null == enableNoiseReduction + ? _value.enableNoiseReduction + : enableNoiseReduction // ignore: cast_nullable_to_non_nullable + as bool, + enableEchoCancellation: + null == enableEchoCancellation + ? _value.enableEchoCancellation + : enableEchoCancellation // ignore: cast_nullable_to_non_nullable + as bool, + enableAutomaticGainControl: + null == enableAutomaticGainControl + ? _value.enableAutomaticGainControl + : enableAutomaticGainControl // ignore: cast_nullable_to_non_nullable + as bool, + gainLevel: + null == gainLevel + ? _value.gainLevel + : gainLevel // ignore: cast_nullable_to_non_nullable + as double, + enableVoiceActivityDetection: + null == enableVoiceActivityDetection + ? _value.enableVoiceActivityDetection + : enableVoiceActivityDetection // ignore: cast_nullable_to_non_nullable + as bool, + vadThreshold: + null == vadThreshold + ? _value.vadThreshold + : vadThreshold // ignore: cast_nullable_to_non_nullable + as double, + bufferSize: + null == bufferSize + ? _value.bufferSize + : bufferSize // ignore: cast_nullable_to_non_nullable + as int, + selectedDeviceId: + freezed == selectedDeviceId + ? _value.selectedDeviceId + : selectedDeviceId // ignore: cast_nullable_to_non_nullable + as String?, + enableRealTimeStreaming: + null == enableRealTimeStreaming + ? _value.enableRealTimeStreaming + : enableRealTimeStreaming // ignore: cast_nullable_to_non_nullable + as bool, + chunkDurationMs: + null == chunkDurationMs + ? _value.chunkDurationMs + : chunkDurationMs // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AudioConfigurationImpl implements _AudioConfiguration { + const _$AudioConfigurationImpl({ + this.sampleRate = 16000, + this.channels = 1, + this.bitRate = 64000, + this.quality = AudioQuality.medium, + this.format = AudioFormat.wav, + this.enableNoiseReduction = true, + this.enableEchoCancellation = true, + this.enableAutomaticGainControl = true, + this.gainLevel = 1.0, + this.enableVoiceActivityDetection = true, + this.vadThreshold = 0.01, + this.bufferSize = 4096, + this.selectedDeviceId, + this.enableRealTimeStreaming = true, + this.chunkDurationMs = 100, + }); + + factory _$AudioConfigurationImpl.fromJson(Map json) => + _$$AudioConfigurationImplFromJson(json); + + /// Sample rate in Hz (e.g., 16000 for 16kHz) + @override + @JsonKey() + final int sampleRate; + + /// Number of audio channels (1 for mono, 2 for stereo) + @override + @JsonKey() + final int channels; + + /// Bit rate for encoding (in bits per second) + @override + @JsonKey() + final int bitRate; + + /// Audio quality level + @override + @JsonKey() + final AudioQuality quality; + + /// Audio format for recording + @override + @JsonKey() + final AudioFormat format; + + /// Enable noise reduction + @override + @JsonKey() + final bool enableNoiseReduction; + + /// Enable echo cancellation + @override + @JsonKey() + final bool enableEchoCancellation; + + /// Enable automatic gain control + @override + @JsonKey() + final bool enableAutomaticGainControl; + + /// Audio gain level (0.0 to 2.0, 1.0 is normal) + @override + @JsonKey() + final double gainLevel; + + /// Enable voice activity detection + @override + @JsonKey() + final bool enableVoiceActivityDetection; + + /// Voice activity detection threshold (0.0 to 1.0) + @override + @JsonKey() + final double vadThreshold; + + /// Buffer size in frames for audio processing + @override + @JsonKey() + final int bufferSize; + + /// Selected audio input device ID + @override + final String? selectedDeviceId; + + /// Enable real-time audio streaming + @override + @JsonKey() + final bool enableRealTimeStreaming; + + /// Audio chunk duration for processing (in milliseconds) + @override + @JsonKey() + final int chunkDurationMs; + + @override + String toString() { + return 'AudioConfiguration(sampleRate: $sampleRate, channels: $channels, bitRate: $bitRate, quality: $quality, format: $format, enableNoiseReduction: $enableNoiseReduction, enableEchoCancellation: $enableEchoCancellation, enableAutomaticGainControl: $enableAutomaticGainControl, gainLevel: $gainLevel, enableVoiceActivityDetection: $enableVoiceActivityDetection, vadThreshold: $vadThreshold, bufferSize: $bufferSize, selectedDeviceId: $selectedDeviceId, enableRealTimeStreaming: $enableRealTimeStreaming, chunkDurationMs: $chunkDurationMs)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AudioConfigurationImpl && + (identical(other.sampleRate, sampleRate) || + other.sampleRate == sampleRate) && + (identical(other.channels, channels) || + other.channels == channels) && + (identical(other.bitRate, bitRate) || other.bitRate == bitRate) && + (identical(other.quality, quality) || other.quality == quality) && + (identical(other.format, format) || other.format == format) && + (identical(other.enableNoiseReduction, enableNoiseReduction) || + other.enableNoiseReduction == enableNoiseReduction) && + (identical(other.enableEchoCancellation, enableEchoCancellation) || + other.enableEchoCancellation == enableEchoCancellation) && + (identical( + other.enableAutomaticGainControl, + enableAutomaticGainControl, + ) || + other.enableAutomaticGainControl == + enableAutomaticGainControl) && + (identical(other.gainLevel, gainLevel) || + other.gainLevel == gainLevel) && + (identical( + other.enableVoiceActivityDetection, + enableVoiceActivityDetection, + ) || + other.enableVoiceActivityDetection == + enableVoiceActivityDetection) && + (identical(other.vadThreshold, vadThreshold) || + other.vadThreshold == vadThreshold) && + (identical(other.bufferSize, bufferSize) || + other.bufferSize == bufferSize) && + (identical(other.selectedDeviceId, selectedDeviceId) || + other.selectedDeviceId == selectedDeviceId) && + (identical( + other.enableRealTimeStreaming, + enableRealTimeStreaming, + ) || + other.enableRealTimeStreaming == enableRealTimeStreaming) && + (identical(other.chunkDurationMs, chunkDurationMs) || + other.chunkDurationMs == chunkDurationMs)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + sampleRate, + channels, + bitRate, + quality, + format, + enableNoiseReduction, + enableEchoCancellation, + enableAutomaticGainControl, + gainLevel, + enableVoiceActivityDetection, + vadThreshold, + bufferSize, + selectedDeviceId, + enableRealTimeStreaming, + chunkDurationMs, + ); + + /// Create a copy of AudioConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AudioConfigurationImplCopyWith<_$AudioConfigurationImpl> get copyWith => + __$$AudioConfigurationImplCopyWithImpl<_$AudioConfigurationImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AudioConfigurationImplToJson(this); + } +} + +abstract class _AudioConfiguration implements AudioConfiguration { + const factory _AudioConfiguration({ + final int sampleRate, + final int channels, + final int bitRate, + final AudioQuality quality, + final AudioFormat format, + final bool enableNoiseReduction, + final bool enableEchoCancellation, + final bool enableAutomaticGainControl, + final double gainLevel, + final bool enableVoiceActivityDetection, + final double vadThreshold, + final int bufferSize, + final String? selectedDeviceId, + final bool enableRealTimeStreaming, + final int chunkDurationMs, + }) = _$AudioConfigurationImpl; + + factory _AudioConfiguration.fromJson(Map json) = + _$AudioConfigurationImpl.fromJson; + + /// Sample rate in Hz (e.g., 16000 for 16kHz) + @override + int get sampleRate; + + /// Number of audio channels (1 for mono, 2 for stereo) + @override + int get channels; + + /// Bit rate for encoding (in bits per second) + @override + int get bitRate; + + /// Audio quality level + @override + AudioQuality get quality; + + /// Audio format for recording + @override + AudioFormat get format; + + /// Enable noise reduction + @override + bool get enableNoiseReduction; + + /// Enable echo cancellation + @override + bool get enableEchoCancellation; + + /// Enable automatic gain control + @override + bool get enableAutomaticGainControl; + + /// Audio gain level (0.0 to 2.0, 1.0 is normal) + @override + double get gainLevel; + + /// Enable voice activity detection + @override + bool get enableVoiceActivityDetection; + + /// Voice activity detection threshold (0.0 to 1.0) + @override + double get vadThreshold; + + /// Buffer size in frames for audio processing + @override + int get bufferSize; + + /// Selected audio input device ID + @override + String? get selectedDeviceId; + + /// Enable real-time audio streaming + @override + bool get enableRealTimeStreaming; + + /// Audio chunk duration for processing (in milliseconds) + @override + int get chunkDurationMs; + + /// Create a copy of AudioConfiguration + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AudioConfigurationImplCopyWith<_$AudioConfigurationImpl> get copyWith => + throw _privateConstructorUsedError; +} + +AudioCapabilities _$AudioCapabilitiesFromJson(Map json) { + return _AudioCapabilities.fromJson(json); +} + +/// @nodoc +mixin _$AudioCapabilities { + /// Supported sample rates + List get supportedSampleRates => throw _privateConstructorUsedError; + + /// Supported channel counts + List get supportedChannels => throw _privateConstructorUsedError; + + /// Supported audio formats + List get supportedFormats => throw _privateConstructorUsedError; + + /// Whether noise reduction is supported + bool get supportsNoiseReduction => throw _privateConstructorUsedError; + + /// Whether echo cancellation is supported + bool get supportsEchoCancellation => throw _privateConstructorUsedError; + + /// Whether automatic gain control is supported + bool get supportsAutomaticGainControl => throw _privateConstructorUsedError; + + /// Whether voice activity detection is supported + bool get supportsVoiceActivityDetection => throw _privateConstructorUsedError; + + /// Maximum supported gain level + double get maxGainLevel => throw _privateConstructorUsedError; + + /// Minimum supported gain level + double get minGainLevel => throw _privateConstructorUsedError; + + /// Available buffer sizes + List get availableBufferSizes => throw _privateConstructorUsedError; + + /// Serializes this AudioCapabilities to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AudioCapabilities + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AudioCapabilitiesCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AudioCapabilitiesCopyWith<$Res> { + factory $AudioCapabilitiesCopyWith( + AudioCapabilities value, + $Res Function(AudioCapabilities) then, + ) = _$AudioCapabilitiesCopyWithImpl<$Res, AudioCapabilities>; + @useResult + $Res call({ + List supportedSampleRates, + List supportedChannels, + List supportedFormats, + bool supportsNoiseReduction, + bool supportsEchoCancellation, + bool supportsAutomaticGainControl, + bool supportsVoiceActivityDetection, + double maxGainLevel, + double minGainLevel, + List availableBufferSizes, + }); +} + +/// @nodoc +class _$AudioCapabilitiesCopyWithImpl<$Res, $Val extends AudioCapabilities> + implements $AudioCapabilitiesCopyWith<$Res> { + _$AudioCapabilitiesCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AudioCapabilities + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? supportedSampleRates = null, + Object? supportedChannels = null, + Object? supportedFormats = null, + Object? supportsNoiseReduction = null, + Object? supportsEchoCancellation = null, + Object? supportsAutomaticGainControl = null, + Object? supportsVoiceActivityDetection = null, + Object? maxGainLevel = null, + Object? minGainLevel = null, + Object? availableBufferSizes = null, + }) { + return _then( + _value.copyWith( + supportedSampleRates: + null == supportedSampleRates + ? _value.supportedSampleRates + : supportedSampleRates // ignore: cast_nullable_to_non_nullable + as List, + supportedChannels: + null == supportedChannels + ? _value.supportedChannels + : supportedChannels // ignore: cast_nullable_to_non_nullable + as List, + supportedFormats: + null == supportedFormats + ? _value.supportedFormats + : supportedFormats // ignore: cast_nullable_to_non_nullable + as List, + supportsNoiseReduction: + null == supportsNoiseReduction + ? _value.supportsNoiseReduction + : supportsNoiseReduction // ignore: cast_nullable_to_non_nullable + as bool, + supportsEchoCancellation: + null == supportsEchoCancellation + ? _value.supportsEchoCancellation + : supportsEchoCancellation // ignore: cast_nullable_to_non_nullable + as bool, + supportsAutomaticGainControl: + null == supportsAutomaticGainControl + ? _value.supportsAutomaticGainControl + : supportsAutomaticGainControl // ignore: cast_nullable_to_non_nullable + as bool, + supportsVoiceActivityDetection: + null == supportsVoiceActivityDetection + ? _value.supportsVoiceActivityDetection + : supportsVoiceActivityDetection // ignore: cast_nullable_to_non_nullable + as bool, + maxGainLevel: + null == maxGainLevel + ? _value.maxGainLevel + : maxGainLevel // ignore: cast_nullable_to_non_nullable + as double, + minGainLevel: + null == minGainLevel + ? _value.minGainLevel + : minGainLevel // ignore: cast_nullable_to_non_nullable + as double, + availableBufferSizes: + null == availableBufferSizes + ? _value.availableBufferSizes + : availableBufferSizes // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AudioCapabilitiesImplCopyWith<$Res> + implements $AudioCapabilitiesCopyWith<$Res> { + factory _$$AudioCapabilitiesImplCopyWith( + _$AudioCapabilitiesImpl value, + $Res Function(_$AudioCapabilitiesImpl) then, + ) = __$$AudioCapabilitiesImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + List supportedSampleRates, + List supportedChannels, + List supportedFormats, + bool supportsNoiseReduction, + bool supportsEchoCancellation, + bool supportsAutomaticGainControl, + bool supportsVoiceActivityDetection, + double maxGainLevel, + double minGainLevel, + List availableBufferSizes, + }); +} + +/// @nodoc +class __$$AudioCapabilitiesImplCopyWithImpl<$Res> + extends _$AudioCapabilitiesCopyWithImpl<$Res, _$AudioCapabilitiesImpl> + implements _$$AudioCapabilitiesImplCopyWith<$Res> { + __$$AudioCapabilitiesImplCopyWithImpl( + _$AudioCapabilitiesImpl _value, + $Res Function(_$AudioCapabilitiesImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AudioCapabilities + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? supportedSampleRates = null, + Object? supportedChannels = null, + Object? supportedFormats = null, + Object? supportsNoiseReduction = null, + Object? supportsEchoCancellation = null, + Object? supportsAutomaticGainControl = null, + Object? supportsVoiceActivityDetection = null, + Object? maxGainLevel = null, + Object? minGainLevel = null, + Object? availableBufferSizes = null, + }) { + return _then( + _$AudioCapabilitiesImpl( + supportedSampleRates: + null == supportedSampleRates + ? _value._supportedSampleRates + : supportedSampleRates // ignore: cast_nullable_to_non_nullable + as List, + supportedChannels: + null == supportedChannels + ? _value._supportedChannels + : supportedChannels // ignore: cast_nullable_to_non_nullable + as List, + supportedFormats: + null == supportedFormats + ? _value._supportedFormats + : supportedFormats // ignore: cast_nullable_to_non_nullable + as List, + supportsNoiseReduction: + null == supportsNoiseReduction + ? _value.supportsNoiseReduction + : supportsNoiseReduction // ignore: cast_nullable_to_non_nullable + as bool, + supportsEchoCancellation: + null == supportsEchoCancellation + ? _value.supportsEchoCancellation + : supportsEchoCancellation // ignore: cast_nullable_to_non_nullable + as bool, + supportsAutomaticGainControl: + null == supportsAutomaticGainControl + ? _value.supportsAutomaticGainControl + : supportsAutomaticGainControl // ignore: cast_nullable_to_non_nullable + as bool, + supportsVoiceActivityDetection: + null == supportsVoiceActivityDetection + ? _value.supportsVoiceActivityDetection + : supportsVoiceActivityDetection // ignore: cast_nullable_to_non_nullable + as bool, + maxGainLevel: + null == maxGainLevel + ? _value.maxGainLevel + : maxGainLevel // ignore: cast_nullable_to_non_nullable + as double, + minGainLevel: + null == minGainLevel + ? _value.minGainLevel + : minGainLevel // ignore: cast_nullable_to_non_nullable + as double, + availableBufferSizes: + null == availableBufferSizes + ? _value._availableBufferSizes + : availableBufferSizes // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AudioCapabilitiesImpl implements _AudioCapabilities { + const _$AudioCapabilitiesImpl({ + required final List supportedSampleRates, + required final List supportedChannels, + required final List supportedFormats, + this.supportsNoiseReduction = false, + this.supportsEchoCancellation = false, + this.supportsAutomaticGainControl = false, + this.supportsVoiceActivityDetection = false, + this.maxGainLevel = 2.0, + this.minGainLevel = 0.0, + required final List availableBufferSizes, + }) : _supportedSampleRates = supportedSampleRates, + _supportedChannels = supportedChannels, + _supportedFormats = supportedFormats, + _availableBufferSizes = availableBufferSizes; + + factory _$AudioCapabilitiesImpl.fromJson(Map json) => + _$$AudioCapabilitiesImplFromJson(json); + + /// Supported sample rates + final List _supportedSampleRates; + + /// Supported sample rates + @override + List get supportedSampleRates { + if (_supportedSampleRates is EqualUnmodifiableListView) + return _supportedSampleRates; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_supportedSampleRates); + } + + /// Supported channel counts + final List _supportedChannels; + + /// Supported channel counts + @override + List get supportedChannels { + if (_supportedChannels is EqualUnmodifiableListView) + return _supportedChannels; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_supportedChannels); + } + + /// Supported audio formats + final List _supportedFormats; + + /// Supported audio formats + @override + List get supportedFormats { + if (_supportedFormats is EqualUnmodifiableListView) + return _supportedFormats; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_supportedFormats); + } + + /// Whether noise reduction is supported + @override + @JsonKey() + final bool supportsNoiseReduction; + + /// Whether echo cancellation is supported + @override + @JsonKey() + final bool supportsEchoCancellation; + + /// Whether automatic gain control is supported + @override + @JsonKey() + final bool supportsAutomaticGainControl; + + /// Whether voice activity detection is supported + @override + @JsonKey() + final bool supportsVoiceActivityDetection; + + /// Maximum supported gain level + @override + @JsonKey() + final double maxGainLevel; + + /// Minimum supported gain level + @override + @JsonKey() + final double minGainLevel; + + /// Available buffer sizes + final List _availableBufferSizes; + + /// Available buffer sizes + @override + List get availableBufferSizes { + if (_availableBufferSizes is EqualUnmodifiableListView) + return _availableBufferSizes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_availableBufferSizes); + } + + @override + String toString() { + return 'AudioCapabilities(supportedSampleRates: $supportedSampleRates, supportedChannels: $supportedChannels, supportedFormats: $supportedFormats, supportsNoiseReduction: $supportsNoiseReduction, supportsEchoCancellation: $supportsEchoCancellation, supportsAutomaticGainControl: $supportsAutomaticGainControl, supportsVoiceActivityDetection: $supportsVoiceActivityDetection, maxGainLevel: $maxGainLevel, minGainLevel: $minGainLevel, availableBufferSizes: $availableBufferSizes)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AudioCapabilitiesImpl && + const DeepCollectionEquality().equals( + other._supportedSampleRates, + _supportedSampleRates, + ) && + const DeepCollectionEquality().equals( + other._supportedChannels, + _supportedChannels, + ) && + const DeepCollectionEquality().equals( + other._supportedFormats, + _supportedFormats, + ) && + (identical(other.supportsNoiseReduction, supportsNoiseReduction) || + other.supportsNoiseReduction == supportsNoiseReduction) && + (identical( + other.supportsEchoCancellation, + supportsEchoCancellation, + ) || + other.supportsEchoCancellation == supportsEchoCancellation) && + (identical( + other.supportsAutomaticGainControl, + supportsAutomaticGainControl, + ) || + other.supportsAutomaticGainControl == + supportsAutomaticGainControl) && + (identical( + other.supportsVoiceActivityDetection, + supportsVoiceActivityDetection, + ) || + other.supportsVoiceActivityDetection == + supportsVoiceActivityDetection) && + (identical(other.maxGainLevel, maxGainLevel) || + other.maxGainLevel == maxGainLevel) && + (identical(other.minGainLevel, minGainLevel) || + other.minGainLevel == minGainLevel) && + const DeepCollectionEquality().equals( + other._availableBufferSizes, + _availableBufferSizes, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_supportedSampleRates), + const DeepCollectionEquality().hash(_supportedChannels), + const DeepCollectionEquality().hash(_supportedFormats), + supportsNoiseReduction, + supportsEchoCancellation, + supportsAutomaticGainControl, + supportsVoiceActivityDetection, + maxGainLevel, + minGainLevel, + const DeepCollectionEquality().hash(_availableBufferSizes), + ); + + /// Create a copy of AudioCapabilities + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AudioCapabilitiesImplCopyWith<_$AudioCapabilitiesImpl> get copyWith => + __$$AudioCapabilitiesImplCopyWithImpl<_$AudioCapabilitiesImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AudioCapabilitiesImplToJson(this); + } +} + +abstract class _AudioCapabilities implements AudioCapabilities { + const factory _AudioCapabilities({ + required final List supportedSampleRates, + required final List supportedChannels, + required final List supportedFormats, + final bool supportsNoiseReduction, + final bool supportsEchoCancellation, + final bool supportsAutomaticGainControl, + final bool supportsVoiceActivityDetection, + final double maxGainLevel, + final double minGainLevel, + required final List availableBufferSizes, + }) = _$AudioCapabilitiesImpl; + + factory _AudioCapabilities.fromJson(Map json) = + _$AudioCapabilitiesImpl.fromJson; + + /// Supported sample rates + @override + List get supportedSampleRates; + + /// Supported channel counts + @override + List get supportedChannels; + + /// Supported audio formats + @override + List get supportedFormats; + + /// Whether noise reduction is supported + @override + bool get supportsNoiseReduction; + + /// Whether echo cancellation is supported + @override + bool get supportsEchoCancellation; + + /// Whether automatic gain control is supported + @override + bool get supportsAutomaticGainControl; + + /// Whether voice activity detection is supported + @override + bool get supportsVoiceActivityDetection; + + /// Maximum supported gain level + @override + double get maxGainLevel; + + /// Minimum supported gain level + @override + double get minGainLevel; + + /// Available buffer sizes + @override + List get availableBufferSizes; + + /// Create a copy of AudioCapabilities + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AudioCapabilitiesImplCopyWith<_$AudioCapabilitiesImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/audio_configuration.g.dart b/lib/models/audio_configuration.g.dart new file mode 100644 index 0000000..e3cf39a --- /dev/null +++ b/lib/models/audio_configuration.g.dart @@ -0,0 +1,111 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'audio_configuration.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AudioConfigurationImpl _$$AudioConfigurationImplFromJson( + Map json, +) => _$AudioConfigurationImpl( + sampleRate: (json['sampleRate'] as num?)?.toInt() ?? 16000, + channels: (json['channels'] as num?)?.toInt() ?? 1, + bitRate: (json['bitRate'] as num?)?.toInt() ?? 64000, + quality: + $enumDecodeNullable(_$AudioQualityEnumMap, json['quality']) ?? + AudioQuality.medium, + format: + $enumDecodeNullable(_$AudioFormatEnumMap, json['format']) ?? + AudioFormat.wav, + enableNoiseReduction: json['enableNoiseReduction'] as bool? ?? true, + enableEchoCancellation: json['enableEchoCancellation'] as bool? ?? true, + enableAutomaticGainControl: + json['enableAutomaticGainControl'] as bool? ?? true, + gainLevel: (json['gainLevel'] as num?)?.toDouble() ?? 1.0, + enableVoiceActivityDetection: + json['enableVoiceActivityDetection'] as bool? ?? true, + vadThreshold: (json['vadThreshold'] as num?)?.toDouble() ?? 0.01, + bufferSize: (json['bufferSize'] as num?)?.toInt() ?? 4096, + selectedDeviceId: json['selectedDeviceId'] as String?, + enableRealTimeStreaming: json['enableRealTimeStreaming'] as bool? ?? true, + chunkDurationMs: (json['chunkDurationMs'] as num?)?.toInt() ?? 100, +); + +Map _$$AudioConfigurationImplToJson( + _$AudioConfigurationImpl instance, +) => { + 'sampleRate': instance.sampleRate, + 'channels': instance.channels, + 'bitRate': instance.bitRate, + 'quality': _$AudioQualityEnumMap[instance.quality]!, + 'format': _$AudioFormatEnumMap[instance.format]!, + 'enableNoiseReduction': instance.enableNoiseReduction, + 'enableEchoCancellation': instance.enableEchoCancellation, + 'enableAutomaticGainControl': instance.enableAutomaticGainControl, + 'gainLevel': instance.gainLevel, + 'enableVoiceActivityDetection': instance.enableVoiceActivityDetection, + 'vadThreshold': instance.vadThreshold, + 'bufferSize': instance.bufferSize, + 'selectedDeviceId': instance.selectedDeviceId, + 'enableRealTimeStreaming': instance.enableRealTimeStreaming, + 'chunkDurationMs': instance.chunkDurationMs, +}; + +const _$AudioQualityEnumMap = { + AudioQuality.low: 'low', + AudioQuality.medium: 'medium', + AudioQuality.high: 'high', +}; + +const _$AudioFormatEnumMap = { + AudioFormat.wav: 'wav', + AudioFormat.mp3: 'mp3', + AudioFormat.aac: 'aac', + AudioFormat.flac: 'flac', +}; + +_$AudioCapabilitiesImpl _$$AudioCapabilitiesImplFromJson( + Map json, +) => _$AudioCapabilitiesImpl( + supportedSampleRates: + (json['supportedSampleRates'] as List) + .map((e) => (e as num).toInt()) + .toList(), + supportedChannels: + (json['supportedChannels'] as List) + .map((e) => (e as num).toInt()) + .toList(), + supportedFormats: + (json['supportedFormats'] as List) + .map((e) => $enumDecode(_$AudioFormatEnumMap, e)) + .toList(), + supportsNoiseReduction: json['supportsNoiseReduction'] as bool? ?? false, + supportsEchoCancellation: json['supportsEchoCancellation'] as bool? ?? false, + supportsAutomaticGainControl: + json['supportsAutomaticGainControl'] as bool? ?? false, + supportsVoiceActivityDetection: + json['supportsVoiceActivityDetection'] as bool? ?? false, + maxGainLevel: (json['maxGainLevel'] as num?)?.toDouble() ?? 2.0, + minGainLevel: (json['minGainLevel'] as num?)?.toDouble() ?? 0.0, + availableBufferSizes: + (json['availableBufferSizes'] as List) + .map((e) => (e as num).toInt()) + .toList(), +); + +Map _$$AudioCapabilitiesImplToJson( + _$AudioCapabilitiesImpl instance, +) => { + 'supportedSampleRates': instance.supportedSampleRates, + 'supportedChannels': instance.supportedChannels, + 'supportedFormats': + instance.supportedFormats.map((e) => _$AudioFormatEnumMap[e]!).toList(), + 'supportsNoiseReduction': instance.supportsNoiseReduction, + 'supportsEchoCancellation': instance.supportsEchoCancellation, + 'supportsAutomaticGainControl': instance.supportsAutomaticGainControl, + 'supportsVoiceActivityDetection': instance.supportsVoiceActivityDetection, + 'maxGainLevel': instance.maxGainLevel, + 'minGainLevel': instance.minGainLevel, + 'availableBufferSizes': instance.availableBufferSizes, +}; diff --git a/lib/models/conversation_model.dart b/lib/models/conversation_model.dart new file mode 100644 index 0000000..f57bd83 --- /dev/null +++ b/lib/models/conversation_model.dart @@ -0,0 +1,339 @@ +// ABOUTME: Conversation data model for managing conversation sessions and history +// ABOUTME: Represents complete conversation threads with participants and metadata + +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'transcription_segment.dart'; + +part 'conversation_model.freezed.dart'; +part 'conversation_model.g.dart'; + +/// Participant in a conversation +@freezed +class ConversationParticipant with _$ConversationParticipant { + const factory ConversationParticipant({ + /// Unique identifier for the participant + required String id, + + /// Display name of the participant + required String name, + + /// Color code for UI display + @Default('#007AFF') String color, + + /// Avatar URL or initials + String? avatar, + + /// Whether this is the device owner + @Default(false) bool isOwner, + + /// Total speaking time in this conversation + @Default(Duration.zero) Duration totalSpeakingTime, + + /// Number of segments spoken + @Default(0) int segmentCount, + + /// Additional metadata + @Default({}) Map metadata, + }) = _ConversationParticipant; + + factory ConversationParticipant.fromJson(Map json) => + _$ConversationParticipantFromJson(json); + + const ConversationParticipant._(); + + /// Get initials for display + String get initials { + final parts = name.split(' '); + if (parts.length >= 2) { + return '${parts[0][0]}${parts[1][0]}'.toUpperCase(); + } + return name.isNotEmpty ? name[0].toUpperCase() : '?'; + } + + /// Average segment duration + Duration get averageSegmentDuration { + return segmentCount > 0 + ? Duration(milliseconds: totalSpeakingTime.inMilliseconds ~/ segmentCount) + : Duration.zero; + } +} + +/// Status of a conversation +enum ConversationStatus { + active, // Currently ongoing + paused, // Temporarily paused + completed, // Finished conversation + archived, // Archived for storage + deleted, // Marked for deletion +} + +/// Priority level for conversation +enum ConversationPriority { + low, + normal, + high, + urgent, +} + +/// Main conversation model +@freezed +class ConversationModel with _$ConversationModel { + const factory ConversationModel({ + /// Unique identifier for the conversation + required String id, + + /// Human-readable title + required String title, + + /// Conversation description or notes + String? description, + + /// Current status + @Default(ConversationStatus.active) ConversationStatus status, + + /// Priority level + @Default(ConversationPriority.normal) ConversationPriority priority, + + /// List of participants + required List participants, + + /// Transcription segments + required List segments, + + /// When the conversation started + required DateTime startTime, + + /// When the conversation ended (if completed) + DateTime? endTime, + + /// Last time the conversation was updated + required DateTime lastUpdated, + + /// Location where conversation took place + String? location, + + /// Tags for categorization + @Default([]) List tags, + + /// Language of the conversation + @Default('en-US') String language, + + /// Whether the conversation has been analyzed by AI + @Default(false) bool hasAIAnalysis, + + /// Whether the conversation is pinned + @Default(false) bool isPinned, + + /// Whether the conversation is private + @Default(false) bool isPrivate, + + /// Audio quality score (0.0 to 1.0) + double? audioQuality, + + /// Transcription confidence score (0.0 to 1.0) + double? transcriptionConfidence, + + /// Path to the audio recording file + String? audioFilePath, + + /// Audio file format (wav, mp3, etc.) + String? audioFormat, + + /// Audio file size in bytes + int? audioFileSize, + + /// Additional metadata + @Default({}) Map metadata, + }) = _ConversationModel; + + factory ConversationModel.fromJson(Map json) => + _$ConversationModelFromJson(json); + + const ConversationModel._(); + + /// Total duration of the conversation + Duration get duration { + if (endTime != null) { + return endTime!.difference(startTime); + } + if (segments.isNotEmpty) { + final lastSegment = segments.last; + return lastSegment.endTime.difference(startTime); + } + return DateTime.now().difference(startTime); + } + + /// Whether the conversation is currently active + bool get isActive => status == ConversationStatus.active; + + /// Whether the conversation is completed + bool get isCompleted => status == ConversationStatus.completed; + + /// Get the full transcribed text + String get fullTranscript => segments.map((s) => s.text).join(' '); + + /// Get word count + int get wordCount => fullTranscript.split(' ').where((w) => w.isNotEmpty).length; + + /// Get speaking time for a specific participant + Duration getSpeakingTimeForParticipant(String participantId) { + return segments + .where((s) => s.speakerId == participantId) + .fold(Duration.zero, (total, segment) => total + segment.duration); + } + + /// Get segments for a specific participant + List getSegmentsForParticipant(String participantId) { + return segments.where((s) => s.speakerId == participantId).toList(); + } + + /// Get participant by ID + ConversationParticipant? getParticipant(String participantId) { + try { + return participants.firstWhere((p) => p.id == participantId); + } catch (e) { + return null; + } + } + + /// Get most active participant (by speaking time) + ConversationParticipant? get mostActiveParticipant { + if (participants.isEmpty) return null; + + ConversationParticipant? mostActive; + Duration longestTime = Duration.zero; + + for (final participant in participants) { + final speakingTime = getSpeakingTimeForParticipant(participant.id); + if (speakingTime > longestTime) { + longestTime = speakingTime; + mostActive = participant; + } + } + + return mostActive; + } + + /// Get segments within a time range + List getSegmentsInTimeRange( + Duration start, + Duration end, + ) { + final startTime = this.startTime.add(start); + final endTime = this.startTime.add(end); + + return segments + .where((s) => s.startTime.isAfter(startTime) && s.endTime.isBefore(endTime)) + .toList(); + } + + /// Get high-confidence segments only + List get highConfidenceSegments { + return segments.where((s) => s.isHighConfidence).toList(); + } + + /// Get average transcription confidence + double get averageConfidence { + if (segments.isEmpty) return 0.0; + + final totalConfidence = segments + .map((s) => s.confidence) + .reduce((a, b) => a + b); + + return totalConfidence / segments.length; + } + + /// Get speaking distribution as percentages + Map get speakingDistribution { + if (participants.isEmpty || duration.inMilliseconds == 0) { + return {}; + } + + final totalMs = duration.inMilliseconds; + final distribution = {}; + + for (final participant in participants) { + final speakingTime = getSpeakingTimeForParticipant(participant.id); + final percentage = (speakingTime.inMilliseconds / totalMs) * 100; + distribution[participant.name] = percentage; + } + + return distribution; + } + + /// Generate a summary title based on content + String generateAutoTitle() { + if (fullTranscript.isEmpty) { + return 'Conversation ${startTime.toString().substring(0, 16)}'; + } + + final words = fullTranscript.split(' ').take(5).join(' '); + return words.length > 30 ? '${words.substring(0, 30)}...' : words; + } + + /// Check if conversation needs attention (low confidence, etc.) + bool get needsAttention { + return averageConfidence < 0.7 || + segments.any((s) => s.isLowConfidence) || + audioQuality != null && audioQuality! < 0.6; + } + + /// Format duration as human readable string + String get formattedDuration { + final hours = duration.inHours; + final minutes = duration.inMinutes % 60; + final seconds = duration.inSeconds % 60; + + if (hours > 0) { + return '${hours}h ${minutes}m ${seconds}s'; + } else if (minutes > 0) { + return '${minutes}m ${seconds}s'; + } else { + return '${seconds}s'; + } + } +} + +/// Conversation search and filter criteria +@freezed +class ConversationFilter with _$ConversationFilter { + const factory ConversationFilter({ + /// Search query for title/content + String? query, + + /// Filter by status + List? statuses, + + /// Filter by priority + List? priorities, + + /// Filter by tags + List? tags, + + /// Filter by participants + List? participantIds, + + /// Date range filter + DateTime? startDate, + DateTime? endDate, + + /// Minimum duration filter + Duration? minDuration, + + /// Maximum duration filter + Duration? maxDuration, + + /// Filter by AI analysis availability + bool? hasAIAnalysis, + + /// Filter by privacy setting + bool? isPrivate, + + /// Minimum confidence threshold + double? minConfidence, + }) = _ConversationFilter; + + factory ConversationFilter.fromJson(Map json) => + _$ConversationFilterFromJson(json); +} \ No newline at end of file diff --git a/lib/models/conversation_model.freezed.dart b/lib/models/conversation_model.freezed.dart new file mode 100644 index 0000000..ff4cc5a --- /dev/null +++ b/lib/models/conversation_model.freezed.dart @@ -0,0 +1,1801 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'conversation_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +ConversationParticipant _$ConversationParticipantFromJson( + Map json, +) { + return _ConversationParticipant.fromJson(json); +} + +/// @nodoc +mixin _$ConversationParticipant { + /// Unique identifier for the participant + String get id => throw _privateConstructorUsedError; + + /// Display name of the participant + String get name => throw _privateConstructorUsedError; + + /// Color code for UI display + String get color => throw _privateConstructorUsedError; + + /// Avatar URL or initials + String? get avatar => throw _privateConstructorUsedError; + + /// Whether this is the device owner + bool get isOwner => throw _privateConstructorUsedError; + + /// Total speaking time in this conversation + Duration get totalSpeakingTime => throw _privateConstructorUsedError; + + /// Number of segments spoken + int get segmentCount => throw _privateConstructorUsedError; + + /// Additional metadata + Map get metadata => throw _privateConstructorUsedError; + + /// Serializes this ConversationParticipant to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ConversationParticipant + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ConversationParticipantCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ConversationParticipantCopyWith<$Res> { + factory $ConversationParticipantCopyWith( + ConversationParticipant value, + $Res Function(ConversationParticipant) then, + ) = _$ConversationParticipantCopyWithImpl<$Res, ConversationParticipant>; + @useResult + $Res call({ + String id, + String name, + String color, + String? avatar, + bool isOwner, + Duration totalSpeakingTime, + int segmentCount, + Map metadata, + }); +} + +/// @nodoc +class _$ConversationParticipantCopyWithImpl< + $Res, + $Val extends ConversationParticipant +> + implements $ConversationParticipantCopyWith<$Res> { + _$ConversationParticipantCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ConversationParticipant + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? color = null, + Object? avatar = freezed, + Object? isOwner = null, + Object? totalSpeakingTime = null, + Object? segmentCount = null, + Object? metadata = null, + }) { + return _then( + _value.copyWith( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: + null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + color: + null == color + ? _value.color + : color // ignore: cast_nullable_to_non_nullable + as String, + avatar: + freezed == avatar + ? _value.avatar + : avatar // ignore: cast_nullable_to_non_nullable + as String?, + isOwner: + null == isOwner + ? _value.isOwner + : isOwner // ignore: cast_nullable_to_non_nullable + as bool, + totalSpeakingTime: + null == totalSpeakingTime + ? _value.totalSpeakingTime + : totalSpeakingTime // ignore: cast_nullable_to_non_nullable + as Duration, + segmentCount: + null == segmentCount + ? _value.segmentCount + : segmentCount // ignore: cast_nullable_to_non_nullable + as int, + metadata: + null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ConversationParticipantImplCopyWith<$Res> + implements $ConversationParticipantCopyWith<$Res> { + factory _$$ConversationParticipantImplCopyWith( + _$ConversationParticipantImpl value, + $Res Function(_$ConversationParticipantImpl) then, + ) = __$$ConversationParticipantImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String name, + String color, + String? avatar, + bool isOwner, + Duration totalSpeakingTime, + int segmentCount, + Map metadata, + }); +} + +/// @nodoc +class __$$ConversationParticipantImplCopyWithImpl<$Res> + extends + _$ConversationParticipantCopyWithImpl< + $Res, + _$ConversationParticipantImpl + > + implements _$$ConversationParticipantImplCopyWith<$Res> { + __$$ConversationParticipantImplCopyWithImpl( + _$ConversationParticipantImpl _value, + $Res Function(_$ConversationParticipantImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ConversationParticipant + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? color = null, + Object? avatar = freezed, + Object? isOwner = null, + Object? totalSpeakingTime = null, + Object? segmentCount = null, + Object? metadata = null, + }) { + return _then( + _$ConversationParticipantImpl( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: + null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + color: + null == color + ? _value.color + : color // ignore: cast_nullable_to_non_nullable + as String, + avatar: + freezed == avatar + ? _value.avatar + : avatar // ignore: cast_nullable_to_non_nullable + as String?, + isOwner: + null == isOwner + ? _value.isOwner + : isOwner // ignore: cast_nullable_to_non_nullable + as bool, + totalSpeakingTime: + null == totalSpeakingTime + ? _value.totalSpeakingTime + : totalSpeakingTime // ignore: cast_nullable_to_non_nullable + as Duration, + segmentCount: + null == segmentCount + ? _value.segmentCount + : segmentCount // ignore: cast_nullable_to_non_nullable + as int, + metadata: + null == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ConversationParticipantImpl extends _ConversationParticipant { + const _$ConversationParticipantImpl({ + required this.id, + required this.name, + this.color = '#007AFF', + this.avatar, + this.isOwner = false, + this.totalSpeakingTime = Duration.zero, + this.segmentCount = 0, + final Map metadata = const {}, + }) : _metadata = metadata, + super._(); + + factory _$ConversationParticipantImpl.fromJson(Map json) => + _$$ConversationParticipantImplFromJson(json); + + /// Unique identifier for the participant + @override + final String id; + + /// Display name of the participant + @override + final String name; + + /// Color code for UI display + @override + @JsonKey() + final String color; + + /// Avatar URL or initials + @override + final String? avatar; + + /// Whether this is the device owner + @override + @JsonKey() + final bool isOwner; + + /// Total speaking time in this conversation + @override + @JsonKey() + final Duration totalSpeakingTime; + + /// Number of segments spoken + @override + @JsonKey() + final int segmentCount; + + /// Additional metadata + final Map _metadata; + + /// Additional metadata + @override + @JsonKey() + Map get metadata { + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_metadata); + } + + @override + String toString() { + return 'ConversationParticipant(id: $id, name: $name, color: $color, avatar: $avatar, isOwner: $isOwner, totalSpeakingTime: $totalSpeakingTime, segmentCount: $segmentCount, metadata: $metadata)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ConversationParticipantImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.color, color) || other.color == color) && + (identical(other.avatar, avatar) || other.avatar == avatar) && + (identical(other.isOwner, isOwner) || other.isOwner == isOwner) && + (identical(other.totalSpeakingTime, totalSpeakingTime) || + other.totalSpeakingTime == totalSpeakingTime) && + (identical(other.segmentCount, segmentCount) || + other.segmentCount == segmentCount) && + const DeepCollectionEquality().equals(other._metadata, _metadata)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + color, + avatar, + isOwner, + totalSpeakingTime, + segmentCount, + const DeepCollectionEquality().hash(_metadata), + ); + + /// Create a copy of ConversationParticipant + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ConversationParticipantImplCopyWith<_$ConversationParticipantImpl> + get copyWith => __$$ConversationParticipantImplCopyWithImpl< + _$ConversationParticipantImpl + >(this, _$identity); + + @override + Map toJson() { + return _$$ConversationParticipantImplToJson(this); + } +} + +abstract class _ConversationParticipant extends ConversationParticipant { + const factory _ConversationParticipant({ + required final String id, + required final String name, + final String color, + final String? avatar, + final bool isOwner, + final Duration totalSpeakingTime, + final int segmentCount, + final Map metadata, + }) = _$ConversationParticipantImpl; + const _ConversationParticipant._() : super._(); + + factory _ConversationParticipant.fromJson(Map json) = + _$ConversationParticipantImpl.fromJson; + + /// Unique identifier for the participant + @override + String get id; + + /// Display name of the participant + @override + String get name; + + /// Color code for UI display + @override + String get color; + + /// Avatar URL or initials + @override + String? get avatar; + + /// Whether this is the device owner + @override + bool get isOwner; + + /// Total speaking time in this conversation + @override + Duration get totalSpeakingTime; + + /// Number of segments spoken + @override + int get segmentCount; + + /// Additional metadata + @override + Map get metadata; + + /// Create a copy of ConversationParticipant + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ConversationParticipantImplCopyWith<_$ConversationParticipantImpl> + get copyWith => throw _privateConstructorUsedError; +} + +ConversationModel _$ConversationModelFromJson(Map json) { + return _ConversationModel.fromJson(json); +} + +/// @nodoc +mixin _$ConversationModel { + /// Unique identifier for the conversation + String get id => throw _privateConstructorUsedError; + + /// Human-readable title + String get title => throw _privateConstructorUsedError; + + /// Conversation description or notes + String? get description => throw _privateConstructorUsedError; + + /// Current status + ConversationStatus get status => throw _privateConstructorUsedError; + + /// Priority level + ConversationPriority get priority => throw _privateConstructorUsedError; + + /// List of participants + List get participants => + throw _privateConstructorUsedError; + + /// Transcription segments + List get segments => throw _privateConstructorUsedError; + + /// When the conversation started + DateTime get startTime => throw _privateConstructorUsedError; + + /// When the conversation ended (if completed) + DateTime? get endTime => throw _privateConstructorUsedError; + + /// Last time the conversation was updated + DateTime get lastUpdated => throw _privateConstructorUsedError; + + /// Location where conversation took place + String? get location => throw _privateConstructorUsedError; + + /// Tags for categorization + List get tags => throw _privateConstructorUsedError; + + /// Language of the conversation + String get language => throw _privateConstructorUsedError; + + /// Whether the conversation has been analyzed by AI + bool get hasAIAnalysis => throw _privateConstructorUsedError; + + /// Whether the conversation is pinned + bool get isPinned => throw _privateConstructorUsedError; + + /// Whether the conversation is private + bool get isPrivate => throw _privateConstructorUsedError; + + /// Audio quality score (0.0 to 1.0) + double? get audioQuality => throw _privateConstructorUsedError; + + /// Transcription confidence score (0.0 to 1.0) + double? get transcriptionConfidence => throw _privateConstructorUsedError; + + /// Path to the audio recording file + String? get audioFilePath => throw _privateConstructorUsedError; + + /// Audio file format (wav, mp3, etc.) + String? get audioFormat => throw _privateConstructorUsedError; + + /// Audio file size in bytes + int? get audioFileSize => throw _privateConstructorUsedError; + + /// Additional metadata + Map get metadata => throw _privateConstructorUsedError; + + /// Serializes this ConversationModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ConversationModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ConversationModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ConversationModelCopyWith<$Res> { + factory $ConversationModelCopyWith( + ConversationModel value, + $Res Function(ConversationModel) then, + ) = _$ConversationModelCopyWithImpl<$Res, ConversationModel>; + @useResult + $Res call({ + String id, + String title, + String? description, + ConversationStatus status, + ConversationPriority priority, + List participants, + List segments, + DateTime startTime, + DateTime? endTime, + DateTime lastUpdated, + String? location, + List tags, + String language, + bool hasAIAnalysis, + bool isPinned, + bool isPrivate, + double? audioQuality, + double? transcriptionConfidence, + String? audioFilePath, + String? audioFormat, + int? audioFileSize, + Map metadata, + }); +} + +/// @nodoc +class _$ConversationModelCopyWithImpl<$Res, $Val extends ConversationModel> + implements $ConversationModelCopyWith<$Res> { + _$ConversationModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ConversationModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? description = freezed, + Object? status = null, + Object? priority = null, + Object? participants = null, + Object? segments = null, + Object? startTime = null, + Object? endTime = freezed, + Object? lastUpdated = null, + Object? location = freezed, + Object? tags = null, + Object? language = null, + Object? hasAIAnalysis = null, + Object? isPinned = null, + Object? isPrivate = null, + Object? audioQuality = freezed, + Object? transcriptionConfidence = freezed, + Object? audioFilePath = freezed, + Object? audioFormat = freezed, + Object? audioFileSize = freezed, + Object? metadata = null, + }) { + return _then( + _value.copyWith( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: + null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as ConversationStatus, + priority: + null == priority + ? _value.priority + : priority // ignore: cast_nullable_to_non_nullable + as ConversationPriority, + participants: + null == participants + ? _value.participants + : participants // ignore: cast_nullable_to_non_nullable + as List, + segments: + null == segments + ? _value.segments + : segments // ignore: cast_nullable_to_non_nullable + as List, + startTime: + null == startTime + ? _value.startTime + : startTime // ignore: cast_nullable_to_non_nullable + as DateTime, + endTime: + freezed == endTime + ? _value.endTime + : endTime // ignore: cast_nullable_to_non_nullable + as DateTime?, + lastUpdated: + null == lastUpdated + ? _value.lastUpdated + : lastUpdated // ignore: cast_nullable_to_non_nullable + as DateTime, + location: + freezed == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String?, + tags: + null == tags + ? _value.tags + : tags // ignore: cast_nullable_to_non_nullable + as List, + language: + null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, + hasAIAnalysis: + null == hasAIAnalysis + ? _value.hasAIAnalysis + : hasAIAnalysis // ignore: cast_nullable_to_non_nullable + as bool, + isPinned: + null == isPinned + ? _value.isPinned + : isPinned // ignore: cast_nullable_to_non_nullable + as bool, + isPrivate: + null == isPrivate + ? _value.isPrivate + : isPrivate // ignore: cast_nullable_to_non_nullable + as bool, + audioQuality: + freezed == audioQuality + ? _value.audioQuality + : audioQuality // ignore: cast_nullable_to_non_nullable + as double?, + transcriptionConfidence: + freezed == transcriptionConfidence + ? _value.transcriptionConfidence + : transcriptionConfidence // ignore: cast_nullable_to_non_nullable + as double?, + audioFilePath: + freezed == audioFilePath + ? _value.audioFilePath + : audioFilePath // ignore: cast_nullable_to_non_nullable + as String?, + audioFormat: + freezed == audioFormat + ? _value.audioFormat + : audioFormat // ignore: cast_nullable_to_non_nullable + as String?, + audioFileSize: + freezed == audioFileSize + ? _value.audioFileSize + : audioFileSize // ignore: cast_nullable_to_non_nullable + as int?, + metadata: + null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ConversationModelImplCopyWith<$Res> + implements $ConversationModelCopyWith<$Res> { + factory _$$ConversationModelImplCopyWith( + _$ConversationModelImpl value, + $Res Function(_$ConversationModelImpl) then, + ) = __$$ConversationModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String title, + String? description, + ConversationStatus status, + ConversationPriority priority, + List participants, + List segments, + DateTime startTime, + DateTime? endTime, + DateTime lastUpdated, + String? location, + List tags, + String language, + bool hasAIAnalysis, + bool isPinned, + bool isPrivate, + double? audioQuality, + double? transcriptionConfidence, + String? audioFilePath, + String? audioFormat, + int? audioFileSize, + Map metadata, + }); +} + +/// @nodoc +class __$$ConversationModelImplCopyWithImpl<$Res> + extends _$ConversationModelCopyWithImpl<$Res, _$ConversationModelImpl> + implements _$$ConversationModelImplCopyWith<$Res> { + __$$ConversationModelImplCopyWithImpl( + _$ConversationModelImpl _value, + $Res Function(_$ConversationModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ConversationModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = null, + Object? description = freezed, + Object? status = null, + Object? priority = null, + Object? participants = null, + Object? segments = null, + Object? startTime = null, + Object? endTime = freezed, + Object? lastUpdated = null, + Object? location = freezed, + Object? tags = null, + Object? language = null, + Object? hasAIAnalysis = null, + Object? isPinned = null, + Object? isPrivate = null, + Object? audioQuality = freezed, + Object? transcriptionConfidence = freezed, + Object? audioFilePath = freezed, + Object? audioFormat = freezed, + Object? audioFileSize = freezed, + Object? metadata = null, + }) { + return _then( + _$ConversationModelImpl( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + title: + null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + description: + freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as ConversationStatus, + priority: + null == priority + ? _value.priority + : priority // ignore: cast_nullable_to_non_nullable + as ConversationPriority, + participants: + null == participants + ? _value._participants + : participants // ignore: cast_nullable_to_non_nullable + as List, + segments: + null == segments + ? _value._segments + : segments // ignore: cast_nullable_to_non_nullable + as List, + startTime: + null == startTime + ? _value.startTime + : startTime // ignore: cast_nullable_to_non_nullable + as DateTime, + endTime: + freezed == endTime + ? _value.endTime + : endTime // ignore: cast_nullable_to_non_nullable + as DateTime?, + lastUpdated: + null == lastUpdated + ? _value.lastUpdated + : lastUpdated // ignore: cast_nullable_to_non_nullable + as DateTime, + location: + freezed == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String?, + tags: + null == tags + ? _value._tags + : tags // ignore: cast_nullable_to_non_nullable + as List, + language: + null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, + hasAIAnalysis: + null == hasAIAnalysis + ? _value.hasAIAnalysis + : hasAIAnalysis // ignore: cast_nullable_to_non_nullable + as bool, + isPinned: + null == isPinned + ? _value.isPinned + : isPinned // ignore: cast_nullable_to_non_nullable + as bool, + isPrivate: + null == isPrivate + ? _value.isPrivate + : isPrivate // ignore: cast_nullable_to_non_nullable + as bool, + audioQuality: + freezed == audioQuality + ? _value.audioQuality + : audioQuality // ignore: cast_nullable_to_non_nullable + as double?, + transcriptionConfidence: + freezed == transcriptionConfidence + ? _value.transcriptionConfidence + : transcriptionConfidence // ignore: cast_nullable_to_non_nullable + as double?, + audioFilePath: + freezed == audioFilePath + ? _value.audioFilePath + : audioFilePath // ignore: cast_nullable_to_non_nullable + as String?, + audioFormat: + freezed == audioFormat + ? _value.audioFormat + : audioFormat // ignore: cast_nullable_to_non_nullable + as String?, + audioFileSize: + freezed == audioFileSize + ? _value.audioFileSize + : audioFileSize // ignore: cast_nullable_to_non_nullable + as int?, + metadata: + null == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ConversationModelImpl extends _ConversationModel { + const _$ConversationModelImpl({ + required this.id, + required this.title, + this.description, + this.status = ConversationStatus.active, + this.priority = ConversationPriority.normal, + required final List participants, + required final List segments, + required this.startTime, + this.endTime, + required this.lastUpdated, + this.location, + final List tags = const [], + this.language = 'en-US', + this.hasAIAnalysis = false, + this.isPinned = false, + this.isPrivate = false, + this.audioQuality, + this.transcriptionConfidence, + this.audioFilePath, + this.audioFormat, + this.audioFileSize, + final Map metadata = const {}, + }) : _participants = participants, + _segments = segments, + _tags = tags, + _metadata = metadata, + super._(); + + factory _$ConversationModelImpl.fromJson(Map json) => + _$$ConversationModelImplFromJson(json); + + /// Unique identifier for the conversation + @override + final String id; + + /// Human-readable title + @override + final String title; + + /// Conversation description or notes + @override + final String? description; + + /// Current status + @override + @JsonKey() + final ConversationStatus status; + + /// Priority level + @override + @JsonKey() + final ConversationPriority priority; + + /// List of participants + final List _participants; + + /// List of participants + @override + List get participants { + if (_participants is EqualUnmodifiableListView) return _participants; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_participants); + } + + /// Transcription segments + final List _segments; + + /// Transcription segments + @override + List get segments { + if (_segments is EqualUnmodifiableListView) return _segments; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_segments); + } + + /// When the conversation started + @override + final DateTime startTime; + + /// When the conversation ended (if completed) + @override + final DateTime? endTime; + + /// Last time the conversation was updated + @override + final DateTime lastUpdated; + + /// Location where conversation took place + @override + final String? location; + + /// Tags for categorization + final List _tags; + + /// Tags for categorization + @override + @JsonKey() + List get tags { + if (_tags is EqualUnmodifiableListView) return _tags; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_tags); + } + + /// Language of the conversation + @override + @JsonKey() + final String language; + + /// Whether the conversation has been analyzed by AI + @override + @JsonKey() + final bool hasAIAnalysis; + + /// Whether the conversation is pinned + @override + @JsonKey() + final bool isPinned; + + /// Whether the conversation is private + @override + @JsonKey() + final bool isPrivate; + + /// Audio quality score (0.0 to 1.0) + @override + final double? audioQuality; + + /// Transcription confidence score (0.0 to 1.0) + @override + final double? transcriptionConfidence; + + /// Path to the audio recording file + @override + final String? audioFilePath; + + /// Audio file format (wav, mp3, etc.) + @override + final String? audioFormat; + + /// Audio file size in bytes + @override + final int? audioFileSize; + + /// Additional metadata + final Map _metadata; + + /// Additional metadata + @override + @JsonKey() + Map get metadata { + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_metadata); + } + + @override + String toString() { + return 'ConversationModel(id: $id, title: $title, description: $description, status: $status, priority: $priority, participants: $participants, segments: $segments, startTime: $startTime, endTime: $endTime, lastUpdated: $lastUpdated, location: $location, tags: $tags, language: $language, hasAIAnalysis: $hasAIAnalysis, isPinned: $isPinned, isPrivate: $isPrivate, audioQuality: $audioQuality, transcriptionConfidence: $transcriptionConfidence, audioFilePath: $audioFilePath, audioFormat: $audioFormat, audioFileSize: $audioFileSize, metadata: $metadata)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ConversationModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.title, title) || other.title == title) && + (identical(other.description, description) || + other.description == description) && + (identical(other.status, status) || other.status == status) && + (identical(other.priority, priority) || + other.priority == priority) && + const DeepCollectionEquality().equals( + other._participants, + _participants, + ) && + const DeepCollectionEquality().equals(other._segments, _segments) && + (identical(other.startTime, startTime) || + other.startTime == startTime) && + (identical(other.endTime, endTime) || other.endTime == endTime) && + (identical(other.lastUpdated, lastUpdated) || + other.lastUpdated == lastUpdated) && + (identical(other.location, location) || + other.location == location) && + const DeepCollectionEquality().equals(other._tags, _tags) && + (identical(other.language, language) || + other.language == language) && + (identical(other.hasAIAnalysis, hasAIAnalysis) || + other.hasAIAnalysis == hasAIAnalysis) && + (identical(other.isPinned, isPinned) || + other.isPinned == isPinned) && + (identical(other.isPrivate, isPrivate) || + other.isPrivate == isPrivate) && + (identical(other.audioQuality, audioQuality) || + other.audioQuality == audioQuality) && + (identical( + other.transcriptionConfidence, + transcriptionConfidence, + ) || + other.transcriptionConfidence == transcriptionConfidence) && + (identical(other.audioFilePath, audioFilePath) || + other.audioFilePath == audioFilePath) && + (identical(other.audioFormat, audioFormat) || + other.audioFormat == audioFormat) && + (identical(other.audioFileSize, audioFileSize) || + other.audioFileSize == audioFileSize) && + const DeepCollectionEquality().equals(other._metadata, _metadata)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hashAll([ + runtimeType, + id, + title, + description, + status, + priority, + const DeepCollectionEquality().hash(_participants), + const DeepCollectionEquality().hash(_segments), + startTime, + endTime, + lastUpdated, + location, + const DeepCollectionEquality().hash(_tags), + language, + hasAIAnalysis, + isPinned, + isPrivate, + audioQuality, + transcriptionConfidence, + audioFilePath, + audioFormat, + audioFileSize, + const DeepCollectionEquality().hash(_metadata), + ]); + + /// Create a copy of ConversationModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ConversationModelImplCopyWith<_$ConversationModelImpl> get copyWith => + __$$ConversationModelImplCopyWithImpl<_$ConversationModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ConversationModelImplToJson(this); + } +} + +abstract class _ConversationModel extends ConversationModel { + const factory _ConversationModel({ + required final String id, + required final String title, + final String? description, + final ConversationStatus status, + final ConversationPriority priority, + required final List participants, + required final List segments, + required final DateTime startTime, + final DateTime? endTime, + required final DateTime lastUpdated, + final String? location, + final List tags, + final String language, + final bool hasAIAnalysis, + final bool isPinned, + final bool isPrivate, + final double? audioQuality, + final double? transcriptionConfidence, + final String? audioFilePath, + final String? audioFormat, + final int? audioFileSize, + final Map metadata, + }) = _$ConversationModelImpl; + const _ConversationModel._() : super._(); + + factory _ConversationModel.fromJson(Map json) = + _$ConversationModelImpl.fromJson; + + /// Unique identifier for the conversation + @override + String get id; + + /// Human-readable title + @override + String get title; + + /// Conversation description or notes + @override + String? get description; + + /// Current status + @override + ConversationStatus get status; + + /// Priority level + @override + ConversationPriority get priority; + + /// List of participants + @override + List get participants; + + /// Transcription segments + @override + List get segments; + + /// When the conversation started + @override + DateTime get startTime; + + /// When the conversation ended (if completed) + @override + DateTime? get endTime; + + /// Last time the conversation was updated + @override + DateTime get lastUpdated; + + /// Location where conversation took place + @override + String? get location; + + /// Tags for categorization + @override + List get tags; + + /// Language of the conversation + @override + String get language; + + /// Whether the conversation has been analyzed by AI + @override + bool get hasAIAnalysis; + + /// Whether the conversation is pinned + @override + bool get isPinned; + + /// Whether the conversation is private + @override + bool get isPrivate; + + /// Audio quality score (0.0 to 1.0) + @override + double? get audioQuality; + + /// Transcription confidence score (0.0 to 1.0) + @override + double? get transcriptionConfidence; + + /// Path to the audio recording file + @override + String? get audioFilePath; + + /// Audio file format (wav, mp3, etc.) + @override + String? get audioFormat; + + /// Audio file size in bytes + @override + int? get audioFileSize; + + /// Additional metadata + @override + Map get metadata; + + /// Create a copy of ConversationModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ConversationModelImplCopyWith<_$ConversationModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ConversationFilter _$ConversationFilterFromJson(Map json) { + return _ConversationFilter.fromJson(json); +} + +/// @nodoc +mixin _$ConversationFilter { + /// Search query for title/content + String? get query => throw _privateConstructorUsedError; + + /// Filter by status + List? get statuses => throw _privateConstructorUsedError; + + /// Filter by priority + List? get priorities => + throw _privateConstructorUsedError; + + /// Filter by tags + List? get tags => throw _privateConstructorUsedError; + + /// Filter by participants + List? get participantIds => throw _privateConstructorUsedError; + + /// Date range filter + DateTime? get startDate => throw _privateConstructorUsedError; + DateTime? get endDate => throw _privateConstructorUsedError; + + /// Minimum duration filter + Duration? get minDuration => throw _privateConstructorUsedError; + + /// Maximum duration filter + Duration? get maxDuration => throw _privateConstructorUsedError; + + /// Filter by AI analysis availability + bool? get hasAIAnalysis => throw _privateConstructorUsedError; + + /// Filter by privacy setting + bool? get isPrivate => throw _privateConstructorUsedError; + + /// Minimum confidence threshold + double? get minConfidence => throw _privateConstructorUsedError; + + /// Serializes this ConversationFilter to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ConversationFilter + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ConversationFilterCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ConversationFilterCopyWith<$Res> { + factory $ConversationFilterCopyWith( + ConversationFilter value, + $Res Function(ConversationFilter) then, + ) = _$ConversationFilterCopyWithImpl<$Res, ConversationFilter>; + @useResult + $Res call({ + String? query, + List? statuses, + List? priorities, + List? tags, + List? participantIds, + DateTime? startDate, + DateTime? endDate, + Duration? minDuration, + Duration? maxDuration, + bool? hasAIAnalysis, + bool? isPrivate, + double? minConfidence, + }); +} + +/// @nodoc +class _$ConversationFilterCopyWithImpl<$Res, $Val extends ConversationFilter> + implements $ConversationFilterCopyWith<$Res> { + _$ConversationFilterCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ConversationFilter + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? query = freezed, + Object? statuses = freezed, + Object? priorities = freezed, + Object? tags = freezed, + Object? participantIds = freezed, + Object? startDate = freezed, + Object? endDate = freezed, + Object? minDuration = freezed, + Object? maxDuration = freezed, + Object? hasAIAnalysis = freezed, + Object? isPrivate = freezed, + Object? minConfidence = freezed, + }) { + return _then( + _value.copyWith( + query: + freezed == query + ? _value.query + : query // ignore: cast_nullable_to_non_nullable + as String?, + statuses: + freezed == statuses + ? _value.statuses + : statuses // ignore: cast_nullable_to_non_nullable + as List?, + priorities: + freezed == priorities + ? _value.priorities + : priorities // ignore: cast_nullable_to_non_nullable + as List?, + tags: + freezed == tags + ? _value.tags + : tags // ignore: cast_nullable_to_non_nullable + as List?, + participantIds: + freezed == participantIds + ? _value.participantIds + : participantIds // ignore: cast_nullable_to_non_nullable + as List?, + startDate: + freezed == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + endDate: + freezed == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + minDuration: + freezed == minDuration + ? _value.minDuration + : minDuration // ignore: cast_nullable_to_non_nullable + as Duration?, + maxDuration: + freezed == maxDuration + ? _value.maxDuration + : maxDuration // ignore: cast_nullable_to_non_nullable + as Duration?, + hasAIAnalysis: + freezed == hasAIAnalysis + ? _value.hasAIAnalysis + : hasAIAnalysis // ignore: cast_nullable_to_non_nullable + as bool?, + isPrivate: + freezed == isPrivate + ? _value.isPrivate + : isPrivate // ignore: cast_nullable_to_non_nullable + as bool?, + minConfidence: + freezed == minConfidence + ? _value.minConfidence + : minConfidence // ignore: cast_nullable_to_non_nullable + as double?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ConversationFilterImplCopyWith<$Res> + implements $ConversationFilterCopyWith<$Res> { + factory _$$ConversationFilterImplCopyWith( + _$ConversationFilterImpl value, + $Res Function(_$ConversationFilterImpl) then, + ) = __$$ConversationFilterImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String? query, + List? statuses, + List? priorities, + List? tags, + List? participantIds, + DateTime? startDate, + DateTime? endDate, + Duration? minDuration, + Duration? maxDuration, + bool? hasAIAnalysis, + bool? isPrivate, + double? minConfidence, + }); +} + +/// @nodoc +class __$$ConversationFilterImplCopyWithImpl<$Res> + extends _$ConversationFilterCopyWithImpl<$Res, _$ConversationFilterImpl> + implements _$$ConversationFilterImplCopyWith<$Res> { + __$$ConversationFilterImplCopyWithImpl( + _$ConversationFilterImpl _value, + $Res Function(_$ConversationFilterImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ConversationFilter + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? query = freezed, + Object? statuses = freezed, + Object? priorities = freezed, + Object? tags = freezed, + Object? participantIds = freezed, + Object? startDate = freezed, + Object? endDate = freezed, + Object? minDuration = freezed, + Object? maxDuration = freezed, + Object? hasAIAnalysis = freezed, + Object? isPrivate = freezed, + Object? minConfidence = freezed, + }) { + return _then( + _$ConversationFilterImpl( + query: + freezed == query + ? _value.query + : query // ignore: cast_nullable_to_non_nullable + as String?, + statuses: + freezed == statuses + ? _value._statuses + : statuses // ignore: cast_nullable_to_non_nullable + as List?, + priorities: + freezed == priorities + ? _value._priorities + : priorities // ignore: cast_nullable_to_non_nullable + as List?, + tags: + freezed == tags + ? _value._tags + : tags // ignore: cast_nullable_to_non_nullable + as List?, + participantIds: + freezed == participantIds + ? _value._participantIds + : participantIds // ignore: cast_nullable_to_non_nullable + as List?, + startDate: + freezed == startDate + ? _value.startDate + : startDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + endDate: + freezed == endDate + ? _value.endDate + : endDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + minDuration: + freezed == minDuration + ? _value.minDuration + : minDuration // ignore: cast_nullable_to_non_nullable + as Duration?, + maxDuration: + freezed == maxDuration + ? _value.maxDuration + : maxDuration // ignore: cast_nullable_to_non_nullable + as Duration?, + hasAIAnalysis: + freezed == hasAIAnalysis + ? _value.hasAIAnalysis + : hasAIAnalysis // ignore: cast_nullable_to_non_nullable + as bool?, + isPrivate: + freezed == isPrivate + ? _value.isPrivate + : isPrivate // ignore: cast_nullable_to_non_nullable + as bool?, + minConfidence: + freezed == minConfidence + ? _value.minConfidence + : minConfidence // ignore: cast_nullable_to_non_nullable + as double?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ConversationFilterImpl implements _ConversationFilter { + const _$ConversationFilterImpl({ + this.query, + final List? statuses, + final List? priorities, + final List? tags, + final List? participantIds, + this.startDate, + this.endDate, + this.minDuration, + this.maxDuration, + this.hasAIAnalysis, + this.isPrivate, + this.minConfidence, + }) : _statuses = statuses, + _priorities = priorities, + _tags = tags, + _participantIds = participantIds; + + factory _$ConversationFilterImpl.fromJson(Map json) => + _$$ConversationFilterImplFromJson(json); + + /// Search query for title/content + @override + final String? query; + + /// Filter by status + final List? _statuses; + + /// Filter by status + @override + List? get statuses { + final value = _statuses; + if (value == null) return null; + if (_statuses is EqualUnmodifiableListView) return _statuses; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + /// Filter by priority + final List? _priorities; + + /// Filter by priority + @override + List? get priorities { + final value = _priorities; + if (value == null) return null; + if (_priorities is EqualUnmodifiableListView) return _priorities; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + /// Filter by tags + final List? _tags; + + /// Filter by tags + @override + List? get tags { + final value = _tags; + if (value == null) return null; + if (_tags is EqualUnmodifiableListView) return _tags; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + /// Filter by participants + final List? _participantIds; + + /// Filter by participants + @override + List? get participantIds { + final value = _participantIds; + if (value == null) return null; + if (_participantIds is EqualUnmodifiableListView) return _participantIds; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + /// Date range filter + @override + final DateTime? startDate; + @override + final DateTime? endDate; + + /// Minimum duration filter + @override + final Duration? minDuration; + + /// Maximum duration filter + @override + final Duration? maxDuration; + + /// Filter by AI analysis availability + @override + final bool? hasAIAnalysis; + + /// Filter by privacy setting + @override + final bool? isPrivate; + + /// Minimum confidence threshold + @override + final double? minConfidence; + + @override + String toString() { + return 'ConversationFilter(query: $query, statuses: $statuses, priorities: $priorities, tags: $tags, participantIds: $participantIds, startDate: $startDate, endDate: $endDate, minDuration: $minDuration, maxDuration: $maxDuration, hasAIAnalysis: $hasAIAnalysis, isPrivate: $isPrivate, minConfidence: $minConfidence)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ConversationFilterImpl && + (identical(other.query, query) || other.query == query) && + const DeepCollectionEquality().equals(other._statuses, _statuses) && + const DeepCollectionEquality().equals( + other._priorities, + _priorities, + ) && + const DeepCollectionEquality().equals(other._tags, _tags) && + const DeepCollectionEquality().equals( + other._participantIds, + _participantIds, + ) && + (identical(other.startDate, startDate) || + other.startDate == startDate) && + (identical(other.endDate, endDate) || other.endDate == endDate) && + (identical(other.minDuration, minDuration) || + other.minDuration == minDuration) && + (identical(other.maxDuration, maxDuration) || + other.maxDuration == maxDuration) && + (identical(other.hasAIAnalysis, hasAIAnalysis) || + other.hasAIAnalysis == hasAIAnalysis) && + (identical(other.isPrivate, isPrivate) || + other.isPrivate == isPrivate) && + (identical(other.minConfidence, minConfidence) || + other.minConfidence == minConfidence)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + query, + const DeepCollectionEquality().hash(_statuses), + const DeepCollectionEquality().hash(_priorities), + const DeepCollectionEquality().hash(_tags), + const DeepCollectionEquality().hash(_participantIds), + startDate, + endDate, + minDuration, + maxDuration, + hasAIAnalysis, + isPrivate, + minConfidence, + ); + + /// Create a copy of ConversationFilter + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ConversationFilterImplCopyWith<_$ConversationFilterImpl> get copyWith => + __$$ConversationFilterImplCopyWithImpl<_$ConversationFilterImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ConversationFilterImplToJson(this); + } +} + +abstract class _ConversationFilter implements ConversationFilter { + const factory _ConversationFilter({ + final String? query, + final List? statuses, + final List? priorities, + final List? tags, + final List? participantIds, + final DateTime? startDate, + final DateTime? endDate, + final Duration? minDuration, + final Duration? maxDuration, + final bool? hasAIAnalysis, + final bool? isPrivate, + final double? minConfidence, + }) = _$ConversationFilterImpl; + + factory _ConversationFilter.fromJson(Map json) = + _$ConversationFilterImpl.fromJson; + + /// Search query for title/content + @override + String? get query; + + /// Filter by status + @override + List? get statuses; + + /// Filter by priority + @override + List? get priorities; + + /// Filter by tags + @override + List? get tags; + + /// Filter by participants + @override + List? get participantIds; + + /// Date range filter + @override + DateTime? get startDate; + @override + DateTime? get endDate; + + /// Minimum duration filter + @override + Duration? get minDuration; + + /// Maximum duration filter + @override + Duration? get maxDuration; + + /// Filter by AI analysis availability + @override + bool? get hasAIAnalysis; + + /// Filter by privacy setting + @override + bool? get isPrivate; + + /// Minimum confidence threshold + @override + double? get minConfidence; + + /// Create a copy of ConversationFilter + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ConversationFilterImplCopyWith<_$ConversationFilterImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/conversation_model.g.dart b/lib/models/conversation_model.g.dart new file mode 100644 index 0000000..902b0cf --- /dev/null +++ b/lib/models/conversation_model.g.dart @@ -0,0 +1,182 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'conversation_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ConversationParticipantImpl _$$ConversationParticipantImplFromJson( + Map json, +) => _$ConversationParticipantImpl( + id: json['id'] as String, + name: json['name'] as String, + color: json['color'] as String? ?? '#007AFF', + avatar: json['avatar'] as String?, + isOwner: json['isOwner'] as bool? ?? false, + totalSpeakingTime: + json['totalSpeakingTime'] == null + ? Duration.zero + : Duration(microseconds: (json['totalSpeakingTime'] as num).toInt()), + segmentCount: (json['segmentCount'] as num?)?.toInt() ?? 0, + metadata: json['metadata'] as Map? ?? const {}, +); + +Map _$$ConversationParticipantImplToJson( + _$ConversationParticipantImpl instance, +) => { + 'id': instance.id, + 'name': instance.name, + 'color': instance.color, + 'avatar': instance.avatar, + 'isOwner': instance.isOwner, + 'totalSpeakingTime': instance.totalSpeakingTime.inMicroseconds, + 'segmentCount': instance.segmentCount, + 'metadata': instance.metadata, +}; + +_$ConversationModelImpl _$$ConversationModelImplFromJson( + Map json, +) => _$ConversationModelImpl( + id: json['id'] as String, + title: json['title'] as String, + description: json['description'] as String?, + status: + $enumDecodeNullable(_$ConversationStatusEnumMap, json['status']) ?? + ConversationStatus.active, + priority: + $enumDecodeNullable(_$ConversationPriorityEnumMap, json['priority']) ?? + ConversationPriority.normal, + participants: + (json['participants'] as List) + .map( + (e) => ConversationParticipant.fromJson(e as Map), + ) + .toList(), + segments: + (json['segments'] as List) + .map((e) => TranscriptionSegment.fromJson(e as Map)) + .toList(), + startTime: DateTime.parse(json['startTime'] as String), + endTime: + json['endTime'] == null + ? null + : DateTime.parse(json['endTime'] as String), + lastUpdated: DateTime.parse(json['lastUpdated'] as String), + location: json['location'] as String?, + tags: + (json['tags'] as List?)?.map((e) => e as String).toList() ?? + const [], + language: json['language'] as String? ?? 'en-US', + hasAIAnalysis: json['hasAIAnalysis'] as bool? ?? false, + isPinned: json['isPinned'] as bool? ?? false, + isPrivate: json['isPrivate'] as bool? ?? false, + audioQuality: (json['audioQuality'] as num?)?.toDouble(), + transcriptionConfidence: + (json['transcriptionConfidence'] as num?)?.toDouble(), + audioFilePath: json['audioFilePath'] as String?, + audioFormat: json['audioFormat'] as String?, + audioFileSize: (json['audioFileSize'] as num?)?.toInt(), + metadata: json['metadata'] as Map? ?? const {}, +); + +Map _$$ConversationModelImplToJson( + _$ConversationModelImpl instance, +) => { + 'id': instance.id, + 'title': instance.title, + 'description': instance.description, + 'status': _$ConversationStatusEnumMap[instance.status]!, + 'priority': _$ConversationPriorityEnumMap[instance.priority]!, + 'participants': instance.participants, + 'segments': instance.segments, + 'startTime': instance.startTime.toIso8601String(), + 'endTime': instance.endTime?.toIso8601String(), + 'lastUpdated': instance.lastUpdated.toIso8601String(), + 'location': instance.location, + 'tags': instance.tags, + 'language': instance.language, + 'hasAIAnalysis': instance.hasAIAnalysis, + 'isPinned': instance.isPinned, + 'isPrivate': instance.isPrivate, + 'audioQuality': instance.audioQuality, + 'transcriptionConfidence': instance.transcriptionConfidence, + 'audioFilePath': instance.audioFilePath, + 'audioFormat': instance.audioFormat, + 'audioFileSize': instance.audioFileSize, + 'metadata': instance.metadata, +}; + +const _$ConversationStatusEnumMap = { + ConversationStatus.active: 'active', + ConversationStatus.paused: 'paused', + ConversationStatus.completed: 'completed', + ConversationStatus.archived: 'archived', + ConversationStatus.deleted: 'deleted', +}; + +const _$ConversationPriorityEnumMap = { + ConversationPriority.low: 'low', + ConversationPriority.normal: 'normal', + ConversationPriority.high: 'high', + ConversationPriority.urgent: 'urgent', +}; + +_$ConversationFilterImpl _$$ConversationFilterImplFromJson( + Map json, +) => _$ConversationFilterImpl( + query: json['query'] as String?, + statuses: + (json['statuses'] as List?) + ?.map((e) => $enumDecode(_$ConversationStatusEnumMap, e)) + .toList(), + priorities: + (json['priorities'] as List?) + ?.map((e) => $enumDecode(_$ConversationPriorityEnumMap, e)) + .toList(), + tags: (json['tags'] as List?)?.map((e) => e as String).toList(), + participantIds: + (json['participantIds'] as List?) + ?.map((e) => e as String) + .toList(), + startDate: + json['startDate'] == null + ? null + : DateTime.parse(json['startDate'] as String), + endDate: + json['endDate'] == null + ? null + : DateTime.parse(json['endDate'] as String), + minDuration: + json['minDuration'] == null + ? null + : Duration(microseconds: (json['minDuration'] as num).toInt()), + maxDuration: + json['maxDuration'] == null + ? null + : Duration(microseconds: (json['maxDuration'] as num).toInt()), + hasAIAnalysis: json['hasAIAnalysis'] as bool?, + isPrivate: json['isPrivate'] as bool?, + minConfidence: (json['minConfidence'] as num?)?.toDouble(), +); + +Map _$$ConversationFilterImplToJson( + _$ConversationFilterImpl instance, +) => { + 'query': instance.query, + 'statuses': + instance.statuses?.map((e) => _$ConversationStatusEnumMap[e]!).toList(), + 'priorities': + instance.priorities + ?.map((e) => _$ConversationPriorityEnumMap[e]!) + .toList(), + 'tags': instance.tags, + 'participantIds': instance.participantIds, + 'startDate': instance.startDate?.toIso8601String(), + 'endDate': instance.endDate?.toIso8601String(), + 'minDuration': instance.minDuration?.inMicroseconds, + 'maxDuration': instance.maxDuration?.inMicroseconds, + 'hasAIAnalysis': instance.hasAIAnalysis, + 'isPrivate': instance.isPrivate, + 'minConfidence': instance.minConfidence, +}; diff --git a/lib/models/glasses_connection_state.dart b/lib/models/glasses_connection_state.dart new file mode 100644 index 0000000..d2565de --- /dev/null +++ b/lib/models/glasses_connection_state.dart @@ -0,0 +1,513 @@ +// ABOUTME: Glasses connection state data model for Even Realities smart glasses +// ABOUTME: Manages connection status, device information, and real-time state + +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'glasses_connection_state.freezed.dart'; +part 'glasses_connection_state.g.dart'; + +/// Connection status for smart glasses +enum ConnectionStatus { + disconnected, // Not connected + scanning, // Searching for devices + connecting, // Attempting to connect + connected, // Successfully connected + disconnecting, // In process of disconnecting + error, // Connection error + unauthorized, // Bluetooth permissions denied +} + +/// Bluetooth signal strength categories +enum SignalStrength { + excellent, // > -40 dBm + good, // -40 to -60 dBm + fair, // -60 to -80 dBm + poor, // < -80 dBm + unknown, // Cannot determine +} + +/// Device health status +enum DeviceHealth { + excellent, // All systems normal + good, // Minor issues + warning, // Some concerns + critical, // Major problems + unknown, // Cannot determine +} + +/// Battery status +enum BatteryStatus { + charging, // Currently charging + full, // 90-100% + high, // 70-89% + medium, // 30-69% + low, // 10-29% + critical, // < 10% + unknown, // Cannot determine +} + +/// Main glasses connection state +@freezed +class GlassesConnectionState with _$GlassesConnectionState { + const factory GlassesConnectionState({ + /// Current connection status + @Default(ConnectionStatus.disconnected) ConnectionStatus status, + + /// Connected device information + GlassesDeviceInfo? connectedDevice, + + /// List of discovered devices + @Default([]) List discoveredDevices, + + /// Last successful connection time + DateTime? lastConnectedTime, + + /// Connection attempt count + @Default(0) int connectionAttempts, + + /// Last error message + String? lastError, + + /// Error timestamp + DateTime? errorTimestamp, + + /// Whether auto-reconnect is enabled + @Default(true) bool autoReconnectEnabled, + + /// Whether scanning is active + @Default(false) bool isScanning, + + /// Scan timeout duration + @Default(Duration(seconds: 30)) Duration scanTimeout, + + /// Connection quality metrics + ConnectionQuality? connectionQuality, + + /// HUD display state + @Default(HUDDisplayState()) HUDDisplayState hudState, + + /// Additional metadata + @Default({}) Map metadata, + }) = _GlassesConnectionState; + + factory GlassesConnectionState.fromJson(Map json) => + _$GlassesConnectionStateFromJson(json); + + const GlassesConnectionState._(); + + /// Whether glasses are currently connected + bool get isConnected => status == ConnectionStatus.connected; + + /// Whether connection is in progress + bool get isConnecting => status == ConnectionStatus.connecting; + + /// Whether there's a connection error + bool get hasError => status == ConnectionStatus.error; + + /// Whether connection is stable + bool get isStable => isConnected && + connectionQuality != null && + connectionQuality!.isStable; + + /// Time since last connection + Duration? get timeSinceLastConnection { + if (lastConnectedTime == null) return null; + return DateTime.now().difference(lastConnectedTime!); + } + + /// Whether device needs attention (errors, low battery, etc.) + bool get needsAttention { + if (!isConnected) return false; + if (connectedDevice == null) return false; + + return connectedDevice!.batteryLevel < 0.2 || + connectedDevice!.health == DeviceHealth.warning || + connectedDevice!.health == DeviceHealth.critical || + (connectionQuality?.signalStrength == SignalStrength.poor); + } + + /// Get device by ID from discovered devices + GlassesDeviceInfo? getDiscoveredDevice(String deviceId) { + try { + return discoveredDevices.firstWhere((d) => d.deviceId == deviceId); + } catch (e) { + return null; + } + } +} + +/// Information about a glasses device +@freezed +class GlassesDeviceInfo with _$GlassesDeviceInfo { + const factory GlassesDeviceInfo({ + /// Unique device identifier + required String deviceId, + + /// Device name as advertised + required String name, + + /// Model number + String? modelNumber, + + /// Manufacturer name + @Default('Even Realities') String manufacturer, + + /// Firmware version + String? firmwareVersion, + + /// Hardware version + String? hardwareVersion, + + /// Serial number + String? serialNumber, + + /// Battery level (0.0 to 1.0) + @Default(0.0) double batteryLevel, + + /// Battery status + @Default(BatteryStatus.unknown) BatteryStatus batteryStatus, + + /// Whether device is charging + @Default(false) bool isCharging, + + /// Signal strength (RSSI) + @Default(-100) int rssi, + + /// Signal strength category + @Default(SignalStrength.unknown) SignalStrength signalStrength, + + /// Device health status + @Default(DeviceHealth.unknown) DeviceHealth health, + + /// Whether device is currently connected + @Default(false) bool isConnected, + + /// Last seen timestamp + DateTime? lastSeen, + + /// Device capabilities + @Default(GlassesCapabilities()) GlassesCapabilities capabilities, + + /// Device configuration + @Default(GlassesConfiguration()) GlassesConfiguration configuration, + + /// Additional device metadata + @Default({}) Map metadata, + }) = _GlassesDeviceInfo; + + factory GlassesDeviceInfo.fromJson(Map json) => + _$GlassesDeviceInfoFromJson(json); + + const GlassesDeviceInfo._(); + + /// Battery percentage (0-100) + int get batteryPercentage => (batteryLevel * 100).round(); + + /// Whether battery is low + bool get isBatteryLow => batteryLevel < 0.2; + + /// Whether battery is critical + bool get isBatteryCritical => batteryLevel < 0.1; + + /// Whether device has good signal + bool get hasGoodSignal => signalStrength == SignalStrength.excellent || + signalStrength == SignalStrength.good; + + /// Signal strength as percentage + int get signalPercentage { + // Convert RSSI to percentage (rough approximation) + if (rssi >= -40) return 100; + if (rssi >= -50) return 90; + if (rssi >= -60) return 70; + if (rssi >= -70) return 50; + if (rssi >= -80) return 30; + if (rssi >= -90) return 10; + return 0; + } + + /// Device display name for UI + String get displayName { + if (name.isNotEmpty) return name; + return 'Even Realities ${modelNumber ?? 'Glasses'}'; + } + + /// Whether device is healthy + bool get isHealthy => health == DeviceHealth.excellent || + health == DeviceHealth.good; + + /// Time since last seen + Duration? get timeSinceLastSeen { + if (lastSeen == null) return null; + return DateTime.now().difference(lastSeen!); + } +} + +/// Connection quality metrics +@freezed +class ConnectionQuality with _$ConnectionQuality { + const factory ConnectionQuality({ + /// Signal strength + @Default(SignalStrength.unknown) SignalStrength signalStrength, + + /// Raw RSSI value + @Default(-100) int rssi, + + /// Connection stability score (0.0 to 1.0) + @Default(0.0) double stabilityScore, + + /// Packet loss percentage + @Default(0.0) double packetLoss, + + /// Average latency in milliseconds + @Default(0) int latencyMs, + + /// Number of disconnections in last hour + @Default(0) int recentDisconnections, + + /// Data transfer rate (bytes/second) + @Default(0) int dataRate, + + /// Quality assessment timestamp + required DateTime timestamp, + }) = _ConnectionQuality; + + factory ConnectionQuality.fromJson(Map json) => + _$ConnectionQualityFromJson(json); + + const ConnectionQuality._(); + + /// Whether connection is stable + bool get isStable => stabilityScore > 0.8 && packetLoss < 5.0; + + /// Whether connection is good quality + bool get isGoodQuality => signalStrength == SignalStrength.excellent || + signalStrength == SignalStrength.good; + + /// Overall quality score (0.0 to 1.0) + double get overallQuality { + double signalScore = signalStrength == SignalStrength.excellent ? 1.0 : + signalStrength == SignalStrength.good ? 0.8 : + signalStrength == SignalStrength.fair ? 0.5 : 0.2; + + double latencyScore = latencyMs < 50 ? 1.0 : + latencyMs < 100 ? 0.8 : + latencyMs < 200 ? 0.5 : 0.2; + + double lossScore = packetLoss < 1.0 ? 1.0 : + packetLoss < 5.0 ? 0.7 : + packetLoss < 10.0 ? 0.4 : 0.1; + + return (signalScore + stabilityScore + latencyScore + lossScore) / 4.0; + } +} + +/// HUD display state +@freezed +class HUDDisplayState with _$HUDDisplayState { + const factory HUDDisplayState({ + /// Whether HUD is currently active + @Default(false) bool isActive, + + /// Current brightness level (0.0 to 1.0) + @Default(0.8) double brightness, + + /// Currently displayed content + String? currentContent, + + /// Content type being displayed + HUDContentType? contentType, + + /// Display position + @Default(HUDPosition.center) HUDPosition position, + + /// Display style settings + @Default(HUDStyleSettings()) HUDStyleSettings style, + + /// Whether display is temporarily paused + @Default(false) bool isPaused, + + /// Last update timestamp + DateTime? lastUpdate, + + /// Display queue for upcoming content + @Default([]) List displayQueue, + }) = _HUDDisplayState; + + factory HUDDisplayState.fromJson(Map json) => + _$HUDDisplayStateFromJson(json); + + const HUDDisplayState._(); + + /// Whether there's content in the display queue + bool get hasQueuedContent => displayQueue.isNotEmpty; + + /// Number of items in display queue + int get queueLength => displayQueue.length; +} + +/// HUD content types +enum HUDContentType { + text, + notification, + menu, + status, + image, + animation, +} + +/// HUD display positions +enum HUDPosition { + topLeft, + topCenter, + topRight, + centerLeft, + center, + centerRight, + bottomLeft, + bottomCenter, + bottomRight, +} + +/// HUD style settings +@freezed +class HUDStyleSettings with _$HUDStyleSettings { + const factory HUDStyleSettings({ + /// Font size + @Default(16.0) double fontSize, + + /// Text color + @Default('#FFFFFF') String textColor, + + /// Background color + @Default('#000000') String backgroundColor, + + /// Font weight + @Default('normal') String fontWeight, + + /// Text alignment + @Default('center') String alignment, + + /// Display duration in seconds + @Default(5) int displayDuration, + + /// Animation type + @Default('fade') String animation, + }) = _HUDStyleSettings; + + factory HUDStyleSettings.fromJson(Map json) => + _$HUDStyleSettingsFromJson(json); +} + +/// Item in HUD display queue +@freezed +class HUDQueueItem with _$HUDQueueItem { + const factory HUDQueueItem({ + /// Content to display + required String content, + + /// Content type + required HUDContentType type, + + /// Display position + @Default(HUDPosition.center) HUDPosition position, + + /// Priority (higher numbers = higher priority) + @Default(1) int priority, + + /// When this item was queued + required DateTime queuedAt, + + /// Display duration + @Default(Duration(seconds: 5)) Duration duration, + + /// Style overrides + HUDStyleSettings? styleOverrides, + }) = _HUDQueueItem; + + factory HUDQueueItem.fromJson(Map json) => + _$HUDQueueItemFromJson(json); +} + +/// Device capabilities +@freezed +class GlassesCapabilities with _$GlassesCapabilities { + const factory GlassesCapabilities({ + /// Supports text display + @Default(true) bool supportsText, + + /// Supports images + @Default(false) bool supportsImages, + + /// Supports animations + @Default(false) bool supportsAnimations, + + /// Supports touch gestures + @Default(true) bool supportsTouchGestures, + + /// Supports voice commands + @Default(false) bool supportsVoiceCommands, + + /// Maximum text length + @Default(256) int maxTextLength, + + /// Supported display positions + @Default([HUDPosition.center]) List supportedPositions, + + /// Battery monitoring capability + @Default(true) bool supportsBatteryMonitoring, + + /// Firmware update capability + @Default(true) bool supportsFirmwareUpdate, + }) = _GlassesCapabilities; + + factory GlassesCapabilities.fromJson(Map json) => + _$GlassesCapabilitiesFromJson(json); +} + +/// Device configuration +@freezed +class GlassesConfiguration with _$GlassesConfiguration { + const factory GlassesConfiguration({ + /// Auto-reconnect setting + @Default(true) bool autoReconnect, + + /// Default brightness + @Default(0.8) double defaultBrightness, + + /// Gesture sensitivity + @Default(0.5) double gestureSensitivity, + + /// Display timeout in seconds + @Default(10) int displayTimeout, + + /// Power save mode enabled + @Default(false) bool powerSaveMode, + + /// Notification settings + @Default(NotificationSettings()) NotificationSettings notifications, + }) = _GlassesConfiguration; + + factory GlassesConfiguration.fromJson(Map json) => + _$GlassesConfigurationFromJson(json); +} + +/// Notification settings +@freezed +class NotificationSettings with _$NotificationSettings { + const factory NotificationSettings({ + /// Enable notifications + @Default(true) bool enabled, + + /// Priority threshold + @Default(1) int priorityThreshold, + + /// Vibration enabled + @Default(false) bool vibrationEnabled, + + /// Sound enabled + @Default(false) bool soundEnabled, + }) = _NotificationSettings; + + factory NotificationSettings.fromJson(Map json) => + _$NotificationSettingsFromJson(json); +} \ No newline at end of file diff --git a/lib/models/glasses_connection_state.freezed.dart b/lib/models/glasses_connection_state.freezed.dart new file mode 100644 index 0000000..2ae529d --- /dev/null +++ b/lib/models/glasses_connection_state.freezed.dart @@ -0,0 +1,3996 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'glasses_connection_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +GlassesConnectionState _$GlassesConnectionStateFromJson( + Map json, +) { + return _GlassesConnectionState.fromJson(json); +} + +/// @nodoc +mixin _$GlassesConnectionState { + /// Current connection status + ConnectionStatus get status => throw _privateConstructorUsedError; + + /// Connected device information + GlassesDeviceInfo? get connectedDevice => throw _privateConstructorUsedError; + + /// List of discovered devices + List get discoveredDevices => + throw _privateConstructorUsedError; + + /// Last successful connection time + DateTime? get lastConnectedTime => throw _privateConstructorUsedError; + + /// Connection attempt count + int get connectionAttempts => throw _privateConstructorUsedError; + + /// Last error message + String? get lastError => throw _privateConstructorUsedError; + + /// Error timestamp + DateTime? get errorTimestamp => throw _privateConstructorUsedError; + + /// Whether auto-reconnect is enabled + bool get autoReconnectEnabled => throw _privateConstructorUsedError; + + /// Whether scanning is active + bool get isScanning => throw _privateConstructorUsedError; + + /// Scan timeout duration + Duration get scanTimeout => throw _privateConstructorUsedError; + + /// Connection quality metrics + ConnectionQuality? get connectionQuality => + throw _privateConstructorUsedError; + + /// HUD display state + HUDDisplayState get hudState => throw _privateConstructorUsedError; + + /// Additional metadata + Map get metadata => throw _privateConstructorUsedError; + + /// Serializes this GlassesConnectionState to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of GlassesConnectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GlassesConnectionStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GlassesConnectionStateCopyWith<$Res> { + factory $GlassesConnectionStateCopyWith( + GlassesConnectionState value, + $Res Function(GlassesConnectionState) then, + ) = _$GlassesConnectionStateCopyWithImpl<$Res, GlassesConnectionState>; + @useResult + $Res call({ + ConnectionStatus status, + GlassesDeviceInfo? connectedDevice, + List discoveredDevices, + DateTime? lastConnectedTime, + int connectionAttempts, + String? lastError, + DateTime? errorTimestamp, + bool autoReconnectEnabled, + bool isScanning, + Duration scanTimeout, + ConnectionQuality? connectionQuality, + HUDDisplayState hudState, + Map metadata, + }); + + $GlassesDeviceInfoCopyWith<$Res>? get connectedDevice; + $ConnectionQualityCopyWith<$Res>? get connectionQuality; + $HUDDisplayStateCopyWith<$Res> get hudState; +} + +/// @nodoc +class _$GlassesConnectionStateCopyWithImpl< + $Res, + $Val extends GlassesConnectionState +> + implements $GlassesConnectionStateCopyWith<$Res> { + _$GlassesConnectionStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GlassesConnectionState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? status = null, + Object? connectedDevice = freezed, + Object? discoveredDevices = null, + Object? lastConnectedTime = freezed, + Object? connectionAttempts = null, + Object? lastError = freezed, + Object? errorTimestamp = freezed, + Object? autoReconnectEnabled = null, + Object? isScanning = null, + Object? scanTimeout = null, + Object? connectionQuality = freezed, + Object? hudState = null, + Object? metadata = null, + }) { + return _then( + _value.copyWith( + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as ConnectionStatus, + connectedDevice: + freezed == connectedDevice + ? _value.connectedDevice + : connectedDevice // ignore: cast_nullable_to_non_nullable + as GlassesDeviceInfo?, + discoveredDevices: + null == discoveredDevices + ? _value.discoveredDevices + : discoveredDevices // ignore: cast_nullable_to_non_nullable + as List, + lastConnectedTime: + freezed == lastConnectedTime + ? _value.lastConnectedTime + : lastConnectedTime // ignore: cast_nullable_to_non_nullable + as DateTime?, + connectionAttempts: + null == connectionAttempts + ? _value.connectionAttempts + : connectionAttempts // ignore: cast_nullable_to_non_nullable + as int, + lastError: + freezed == lastError + ? _value.lastError + : lastError // ignore: cast_nullable_to_non_nullable + as String?, + errorTimestamp: + freezed == errorTimestamp + ? _value.errorTimestamp + : errorTimestamp // ignore: cast_nullable_to_non_nullable + as DateTime?, + autoReconnectEnabled: + null == autoReconnectEnabled + ? _value.autoReconnectEnabled + : autoReconnectEnabled // ignore: cast_nullable_to_non_nullable + as bool, + isScanning: + null == isScanning + ? _value.isScanning + : isScanning // ignore: cast_nullable_to_non_nullable + as bool, + scanTimeout: + null == scanTimeout + ? _value.scanTimeout + : scanTimeout // ignore: cast_nullable_to_non_nullable + as Duration, + connectionQuality: + freezed == connectionQuality + ? _value.connectionQuality + : connectionQuality // ignore: cast_nullable_to_non_nullable + as ConnectionQuality?, + hudState: + null == hudState + ? _value.hudState + : hudState // ignore: cast_nullable_to_non_nullable + as HUDDisplayState, + metadata: + null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ) + as $Val, + ); + } + + /// Create a copy of GlassesConnectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $GlassesDeviceInfoCopyWith<$Res>? get connectedDevice { + if (_value.connectedDevice == null) { + return null; + } + + return $GlassesDeviceInfoCopyWith<$Res>(_value.connectedDevice!, (value) { + return _then(_value.copyWith(connectedDevice: value) as $Val); + }); + } + + /// Create a copy of GlassesConnectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ConnectionQualityCopyWith<$Res>? get connectionQuality { + if (_value.connectionQuality == null) { + return null; + } + + return $ConnectionQualityCopyWith<$Res>(_value.connectionQuality!, (value) { + return _then(_value.copyWith(connectionQuality: value) as $Val); + }); + } + + /// Create a copy of GlassesConnectionState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $HUDDisplayStateCopyWith<$Res> get hudState { + return $HUDDisplayStateCopyWith<$Res>(_value.hudState, (value) { + return _then(_value.copyWith(hudState: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$GlassesConnectionStateImplCopyWith<$Res> + implements $GlassesConnectionStateCopyWith<$Res> { + factory _$$GlassesConnectionStateImplCopyWith( + _$GlassesConnectionStateImpl value, + $Res Function(_$GlassesConnectionStateImpl) then, + ) = __$$GlassesConnectionStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + ConnectionStatus status, + GlassesDeviceInfo? connectedDevice, + List discoveredDevices, + DateTime? lastConnectedTime, + int connectionAttempts, + String? lastError, + DateTime? errorTimestamp, + bool autoReconnectEnabled, + bool isScanning, + Duration scanTimeout, + ConnectionQuality? connectionQuality, + HUDDisplayState hudState, + Map metadata, + }); + + @override + $GlassesDeviceInfoCopyWith<$Res>? get connectedDevice; + @override + $ConnectionQualityCopyWith<$Res>? get connectionQuality; + @override + $HUDDisplayStateCopyWith<$Res> get hudState; +} + +/// @nodoc +class __$$GlassesConnectionStateImplCopyWithImpl<$Res> + extends + _$GlassesConnectionStateCopyWithImpl<$Res, _$GlassesConnectionStateImpl> + implements _$$GlassesConnectionStateImplCopyWith<$Res> { + __$$GlassesConnectionStateImplCopyWithImpl( + _$GlassesConnectionStateImpl _value, + $Res Function(_$GlassesConnectionStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of GlassesConnectionState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? status = null, + Object? connectedDevice = freezed, + Object? discoveredDevices = null, + Object? lastConnectedTime = freezed, + Object? connectionAttempts = null, + Object? lastError = freezed, + Object? errorTimestamp = freezed, + Object? autoReconnectEnabled = null, + Object? isScanning = null, + Object? scanTimeout = null, + Object? connectionQuality = freezed, + Object? hudState = null, + Object? metadata = null, + }) { + return _then( + _$GlassesConnectionStateImpl( + status: + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as ConnectionStatus, + connectedDevice: + freezed == connectedDevice + ? _value.connectedDevice + : connectedDevice // ignore: cast_nullable_to_non_nullable + as GlassesDeviceInfo?, + discoveredDevices: + null == discoveredDevices + ? _value._discoveredDevices + : discoveredDevices // ignore: cast_nullable_to_non_nullable + as List, + lastConnectedTime: + freezed == lastConnectedTime + ? _value.lastConnectedTime + : lastConnectedTime // ignore: cast_nullable_to_non_nullable + as DateTime?, + connectionAttempts: + null == connectionAttempts + ? _value.connectionAttempts + : connectionAttempts // ignore: cast_nullable_to_non_nullable + as int, + lastError: + freezed == lastError + ? _value.lastError + : lastError // ignore: cast_nullable_to_non_nullable + as String?, + errorTimestamp: + freezed == errorTimestamp + ? _value.errorTimestamp + : errorTimestamp // ignore: cast_nullable_to_non_nullable + as DateTime?, + autoReconnectEnabled: + null == autoReconnectEnabled + ? _value.autoReconnectEnabled + : autoReconnectEnabled // ignore: cast_nullable_to_non_nullable + as bool, + isScanning: + null == isScanning + ? _value.isScanning + : isScanning // ignore: cast_nullable_to_non_nullable + as bool, + scanTimeout: + null == scanTimeout + ? _value.scanTimeout + : scanTimeout // ignore: cast_nullable_to_non_nullable + as Duration, + connectionQuality: + freezed == connectionQuality + ? _value.connectionQuality + : connectionQuality // ignore: cast_nullable_to_non_nullable + as ConnectionQuality?, + hudState: + null == hudState + ? _value.hudState + : hudState // ignore: cast_nullable_to_non_nullable + as HUDDisplayState, + metadata: + null == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$GlassesConnectionStateImpl extends _GlassesConnectionState { + const _$GlassesConnectionStateImpl({ + this.status = ConnectionStatus.disconnected, + this.connectedDevice, + final List discoveredDevices = const [], + this.lastConnectedTime, + this.connectionAttempts = 0, + this.lastError, + this.errorTimestamp, + this.autoReconnectEnabled = true, + this.isScanning = false, + this.scanTimeout = const Duration(seconds: 30), + this.connectionQuality, + this.hudState = const HUDDisplayState(), + final Map metadata = const {}, + }) : _discoveredDevices = discoveredDevices, + _metadata = metadata, + super._(); + + factory _$GlassesConnectionStateImpl.fromJson(Map json) => + _$$GlassesConnectionStateImplFromJson(json); + + /// Current connection status + @override + @JsonKey() + final ConnectionStatus status; + + /// Connected device information + @override + final GlassesDeviceInfo? connectedDevice; + + /// List of discovered devices + final List _discoveredDevices; + + /// List of discovered devices + @override + @JsonKey() + List get discoveredDevices { + if (_discoveredDevices is EqualUnmodifiableListView) + return _discoveredDevices; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_discoveredDevices); + } + + /// Last successful connection time + @override + final DateTime? lastConnectedTime; + + /// Connection attempt count + @override + @JsonKey() + final int connectionAttempts; + + /// Last error message + @override + final String? lastError; + + /// Error timestamp + @override + final DateTime? errorTimestamp; + + /// Whether auto-reconnect is enabled + @override + @JsonKey() + final bool autoReconnectEnabled; + + /// Whether scanning is active + @override + @JsonKey() + final bool isScanning; + + /// Scan timeout duration + @override + @JsonKey() + final Duration scanTimeout; + + /// Connection quality metrics + @override + final ConnectionQuality? connectionQuality; + + /// HUD display state + @override + @JsonKey() + final HUDDisplayState hudState; + + /// Additional metadata + final Map _metadata; + + /// Additional metadata + @override + @JsonKey() + Map get metadata { + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_metadata); + } + + @override + String toString() { + return 'GlassesConnectionState(status: $status, connectedDevice: $connectedDevice, discoveredDevices: $discoveredDevices, lastConnectedTime: $lastConnectedTime, connectionAttempts: $connectionAttempts, lastError: $lastError, errorTimestamp: $errorTimestamp, autoReconnectEnabled: $autoReconnectEnabled, isScanning: $isScanning, scanTimeout: $scanTimeout, connectionQuality: $connectionQuality, hudState: $hudState, metadata: $metadata)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GlassesConnectionStateImpl && + (identical(other.status, status) || other.status == status) && + (identical(other.connectedDevice, connectedDevice) || + other.connectedDevice == connectedDevice) && + const DeepCollectionEquality().equals( + other._discoveredDevices, + _discoveredDevices, + ) && + (identical(other.lastConnectedTime, lastConnectedTime) || + other.lastConnectedTime == lastConnectedTime) && + (identical(other.connectionAttempts, connectionAttempts) || + other.connectionAttempts == connectionAttempts) && + (identical(other.lastError, lastError) || + other.lastError == lastError) && + (identical(other.errorTimestamp, errorTimestamp) || + other.errorTimestamp == errorTimestamp) && + (identical(other.autoReconnectEnabled, autoReconnectEnabled) || + other.autoReconnectEnabled == autoReconnectEnabled) && + (identical(other.isScanning, isScanning) || + other.isScanning == isScanning) && + (identical(other.scanTimeout, scanTimeout) || + other.scanTimeout == scanTimeout) && + (identical(other.connectionQuality, connectionQuality) || + other.connectionQuality == connectionQuality) && + (identical(other.hudState, hudState) || + other.hudState == hudState) && + const DeepCollectionEquality().equals(other._metadata, _metadata)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + status, + connectedDevice, + const DeepCollectionEquality().hash(_discoveredDevices), + lastConnectedTime, + connectionAttempts, + lastError, + errorTimestamp, + autoReconnectEnabled, + isScanning, + scanTimeout, + connectionQuality, + hudState, + const DeepCollectionEquality().hash(_metadata), + ); + + /// Create a copy of GlassesConnectionState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GlassesConnectionStateImplCopyWith<_$GlassesConnectionStateImpl> + get copyWith => + __$$GlassesConnectionStateImplCopyWithImpl<_$GlassesConnectionStateImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$GlassesConnectionStateImplToJson(this); + } +} + +abstract class _GlassesConnectionState extends GlassesConnectionState { + const factory _GlassesConnectionState({ + final ConnectionStatus status, + final GlassesDeviceInfo? connectedDevice, + final List discoveredDevices, + final DateTime? lastConnectedTime, + final int connectionAttempts, + final String? lastError, + final DateTime? errorTimestamp, + final bool autoReconnectEnabled, + final bool isScanning, + final Duration scanTimeout, + final ConnectionQuality? connectionQuality, + final HUDDisplayState hudState, + final Map metadata, + }) = _$GlassesConnectionStateImpl; + const _GlassesConnectionState._() : super._(); + + factory _GlassesConnectionState.fromJson(Map json) = + _$GlassesConnectionStateImpl.fromJson; + + /// Current connection status + @override + ConnectionStatus get status; + + /// Connected device information + @override + GlassesDeviceInfo? get connectedDevice; + + /// List of discovered devices + @override + List get discoveredDevices; + + /// Last successful connection time + @override + DateTime? get lastConnectedTime; + + /// Connection attempt count + @override + int get connectionAttempts; + + /// Last error message + @override + String? get lastError; + + /// Error timestamp + @override + DateTime? get errorTimestamp; + + /// Whether auto-reconnect is enabled + @override + bool get autoReconnectEnabled; + + /// Whether scanning is active + @override + bool get isScanning; + + /// Scan timeout duration + @override + Duration get scanTimeout; + + /// Connection quality metrics + @override + ConnectionQuality? get connectionQuality; + + /// HUD display state + @override + HUDDisplayState get hudState; + + /// Additional metadata + @override + Map get metadata; + + /// Create a copy of GlassesConnectionState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GlassesConnectionStateImplCopyWith<_$GlassesConnectionStateImpl> + get copyWith => throw _privateConstructorUsedError; +} + +GlassesDeviceInfo _$GlassesDeviceInfoFromJson(Map json) { + return _GlassesDeviceInfo.fromJson(json); +} + +/// @nodoc +mixin _$GlassesDeviceInfo { + /// Unique device identifier + String get deviceId => throw _privateConstructorUsedError; + + /// Device name as advertised + String get name => throw _privateConstructorUsedError; + + /// Model number + String? get modelNumber => throw _privateConstructorUsedError; + + /// Manufacturer name + String get manufacturer => throw _privateConstructorUsedError; + + /// Firmware version + String? get firmwareVersion => throw _privateConstructorUsedError; + + /// Hardware version + String? get hardwareVersion => throw _privateConstructorUsedError; + + /// Serial number + String? get serialNumber => throw _privateConstructorUsedError; + + /// Battery level (0.0 to 1.0) + double get batteryLevel => throw _privateConstructorUsedError; + + /// Battery status + BatteryStatus get batteryStatus => throw _privateConstructorUsedError; + + /// Whether device is charging + bool get isCharging => throw _privateConstructorUsedError; + + /// Signal strength (RSSI) + int get rssi => throw _privateConstructorUsedError; + + /// Signal strength category + SignalStrength get signalStrength => throw _privateConstructorUsedError; + + /// Device health status + DeviceHealth get health => throw _privateConstructorUsedError; + + /// Whether device is currently connected + bool get isConnected => throw _privateConstructorUsedError; + + /// Last seen timestamp + DateTime? get lastSeen => throw _privateConstructorUsedError; + + /// Device capabilities + GlassesCapabilities get capabilities => throw _privateConstructorUsedError; + + /// Device configuration + GlassesConfiguration get configuration => throw _privateConstructorUsedError; + + /// Additional device metadata + Map get metadata => throw _privateConstructorUsedError; + + /// Serializes this GlassesDeviceInfo to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of GlassesDeviceInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GlassesDeviceInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GlassesDeviceInfoCopyWith<$Res> { + factory $GlassesDeviceInfoCopyWith( + GlassesDeviceInfo value, + $Res Function(GlassesDeviceInfo) then, + ) = _$GlassesDeviceInfoCopyWithImpl<$Res, GlassesDeviceInfo>; + @useResult + $Res call({ + String deviceId, + String name, + String? modelNumber, + String manufacturer, + String? firmwareVersion, + String? hardwareVersion, + String? serialNumber, + double batteryLevel, + BatteryStatus batteryStatus, + bool isCharging, + int rssi, + SignalStrength signalStrength, + DeviceHealth health, + bool isConnected, + DateTime? lastSeen, + GlassesCapabilities capabilities, + GlassesConfiguration configuration, + Map metadata, + }); + + $GlassesCapabilitiesCopyWith<$Res> get capabilities; + $GlassesConfigurationCopyWith<$Res> get configuration; +} + +/// @nodoc +class _$GlassesDeviceInfoCopyWithImpl<$Res, $Val extends GlassesDeviceInfo> + implements $GlassesDeviceInfoCopyWith<$Res> { + _$GlassesDeviceInfoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GlassesDeviceInfo + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? deviceId = null, + Object? name = null, + Object? modelNumber = freezed, + Object? manufacturer = null, + Object? firmwareVersion = freezed, + Object? hardwareVersion = freezed, + Object? serialNumber = freezed, + Object? batteryLevel = null, + Object? batteryStatus = null, + Object? isCharging = null, + Object? rssi = null, + Object? signalStrength = null, + Object? health = null, + Object? isConnected = null, + Object? lastSeen = freezed, + Object? capabilities = null, + Object? configuration = null, + Object? metadata = null, + }) { + return _then( + _value.copyWith( + deviceId: + null == deviceId + ? _value.deviceId + : deviceId // ignore: cast_nullable_to_non_nullable + as String, + name: + null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + modelNumber: + freezed == modelNumber + ? _value.modelNumber + : modelNumber // ignore: cast_nullable_to_non_nullable + as String?, + manufacturer: + null == manufacturer + ? _value.manufacturer + : manufacturer // ignore: cast_nullable_to_non_nullable + as String, + firmwareVersion: + freezed == firmwareVersion + ? _value.firmwareVersion + : firmwareVersion // ignore: cast_nullable_to_non_nullable + as String?, + hardwareVersion: + freezed == hardwareVersion + ? _value.hardwareVersion + : hardwareVersion // ignore: cast_nullable_to_non_nullable + as String?, + serialNumber: + freezed == serialNumber + ? _value.serialNumber + : serialNumber // ignore: cast_nullable_to_non_nullable + as String?, + batteryLevel: + null == batteryLevel + ? _value.batteryLevel + : batteryLevel // ignore: cast_nullable_to_non_nullable + as double, + batteryStatus: + null == batteryStatus + ? _value.batteryStatus + : batteryStatus // ignore: cast_nullable_to_non_nullable + as BatteryStatus, + isCharging: + null == isCharging + ? _value.isCharging + : isCharging // ignore: cast_nullable_to_non_nullable + as bool, + rssi: + null == rssi + ? _value.rssi + : rssi // ignore: cast_nullable_to_non_nullable + as int, + signalStrength: + null == signalStrength + ? _value.signalStrength + : signalStrength // ignore: cast_nullable_to_non_nullable + as SignalStrength, + health: + null == health + ? _value.health + : health // ignore: cast_nullable_to_non_nullable + as DeviceHealth, + isConnected: + null == isConnected + ? _value.isConnected + : isConnected // ignore: cast_nullable_to_non_nullable + as bool, + lastSeen: + freezed == lastSeen + ? _value.lastSeen + : lastSeen // ignore: cast_nullable_to_non_nullable + as DateTime?, + capabilities: + null == capabilities + ? _value.capabilities + : capabilities // ignore: cast_nullable_to_non_nullable + as GlassesCapabilities, + configuration: + null == configuration + ? _value.configuration + : configuration // ignore: cast_nullable_to_non_nullable + as GlassesConfiguration, + metadata: + null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ) + as $Val, + ); + } + + /// Create a copy of GlassesDeviceInfo + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $GlassesCapabilitiesCopyWith<$Res> get capabilities { + return $GlassesCapabilitiesCopyWith<$Res>(_value.capabilities, (value) { + return _then(_value.copyWith(capabilities: value) as $Val); + }); + } + + /// Create a copy of GlassesDeviceInfo + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $GlassesConfigurationCopyWith<$Res> get configuration { + return $GlassesConfigurationCopyWith<$Res>(_value.configuration, (value) { + return _then(_value.copyWith(configuration: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$GlassesDeviceInfoImplCopyWith<$Res> + implements $GlassesDeviceInfoCopyWith<$Res> { + factory _$$GlassesDeviceInfoImplCopyWith( + _$GlassesDeviceInfoImpl value, + $Res Function(_$GlassesDeviceInfoImpl) then, + ) = __$$GlassesDeviceInfoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String deviceId, + String name, + String? modelNumber, + String manufacturer, + String? firmwareVersion, + String? hardwareVersion, + String? serialNumber, + double batteryLevel, + BatteryStatus batteryStatus, + bool isCharging, + int rssi, + SignalStrength signalStrength, + DeviceHealth health, + bool isConnected, + DateTime? lastSeen, + GlassesCapabilities capabilities, + GlassesConfiguration configuration, + Map metadata, + }); + + @override + $GlassesCapabilitiesCopyWith<$Res> get capabilities; + @override + $GlassesConfigurationCopyWith<$Res> get configuration; +} + +/// @nodoc +class __$$GlassesDeviceInfoImplCopyWithImpl<$Res> + extends _$GlassesDeviceInfoCopyWithImpl<$Res, _$GlassesDeviceInfoImpl> + implements _$$GlassesDeviceInfoImplCopyWith<$Res> { + __$$GlassesDeviceInfoImplCopyWithImpl( + _$GlassesDeviceInfoImpl _value, + $Res Function(_$GlassesDeviceInfoImpl) _then, + ) : super(_value, _then); + + /// Create a copy of GlassesDeviceInfo + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? deviceId = null, + Object? name = null, + Object? modelNumber = freezed, + Object? manufacturer = null, + Object? firmwareVersion = freezed, + Object? hardwareVersion = freezed, + Object? serialNumber = freezed, + Object? batteryLevel = null, + Object? batteryStatus = null, + Object? isCharging = null, + Object? rssi = null, + Object? signalStrength = null, + Object? health = null, + Object? isConnected = null, + Object? lastSeen = freezed, + Object? capabilities = null, + Object? configuration = null, + Object? metadata = null, + }) { + return _then( + _$GlassesDeviceInfoImpl( + deviceId: + null == deviceId + ? _value.deviceId + : deviceId // ignore: cast_nullable_to_non_nullable + as String, + name: + null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + modelNumber: + freezed == modelNumber + ? _value.modelNumber + : modelNumber // ignore: cast_nullable_to_non_nullable + as String?, + manufacturer: + null == manufacturer + ? _value.manufacturer + : manufacturer // ignore: cast_nullable_to_non_nullable + as String, + firmwareVersion: + freezed == firmwareVersion + ? _value.firmwareVersion + : firmwareVersion // ignore: cast_nullable_to_non_nullable + as String?, + hardwareVersion: + freezed == hardwareVersion + ? _value.hardwareVersion + : hardwareVersion // ignore: cast_nullable_to_non_nullable + as String?, + serialNumber: + freezed == serialNumber + ? _value.serialNumber + : serialNumber // ignore: cast_nullable_to_non_nullable + as String?, + batteryLevel: + null == batteryLevel + ? _value.batteryLevel + : batteryLevel // ignore: cast_nullable_to_non_nullable + as double, + batteryStatus: + null == batteryStatus + ? _value.batteryStatus + : batteryStatus // ignore: cast_nullable_to_non_nullable + as BatteryStatus, + isCharging: + null == isCharging + ? _value.isCharging + : isCharging // ignore: cast_nullable_to_non_nullable + as bool, + rssi: + null == rssi + ? _value.rssi + : rssi // ignore: cast_nullable_to_non_nullable + as int, + signalStrength: + null == signalStrength + ? _value.signalStrength + : signalStrength // ignore: cast_nullable_to_non_nullable + as SignalStrength, + health: + null == health + ? _value.health + : health // ignore: cast_nullable_to_non_nullable + as DeviceHealth, + isConnected: + null == isConnected + ? _value.isConnected + : isConnected // ignore: cast_nullable_to_non_nullable + as bool, + lastSeen: + freezed == lastSeen + ? _value.lastSeen + : lastSeen // ignore: cast_nullable_to_non_nullable + as DateTime?, + capabilities: + null == capabilities + ? _value.capabilities + : capabilities // ignore: cast_nullable_to_non_nullable + as GlassesCapabilities, + configuration: + null == configuration + ? _value.configuration + : configuration // ignore: cast_nullable_to_non_nullable + as GlassesConfiguration, + metadata: + null == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$GlassesDeviceInfoImpl extends _GlassesDeviceInfo { + const _$GlassesDeviceInfoImpl({ + required this.deviceId, + required this.name, + this.modelNumber, + this.manufacturer = 'Even Realities', + this.firmwareVersion, + this.hardwareVersion, + this.serialNumber, + this.batteryLevel = 0.0, + this.batteryStatus = BatteryStatus.unknown, + this.isCharging = false, + this.rssi = -100, + this.signalStrength = SignalStrength.unknown, + this.health = DeviceHealth.unknown, + this.isConnected = false, + this.lastSeen, + this.capabilities = const GlassesCapabilities(), + this.configuration = const GlassesConfiguration(), + final Map metadata = const {}, + }) : _metadata = metadata, + super._(); + + factory _$GlassesDeviceInfoImpl.fromJson(Map json) => + _$$GlassesDeviceInfoImplFromJson(json); + + /// Unique device identifier + @override + final String deviceId; + + /// Device name as advertised + @override + final String name; + + /// Model number + @override + final String? modelNumber; + + /// Manufacturer name + @override + @JsonKey() + final String manufacturer; + + /// Firmware version + @override + final String? firmwareVersion; + + /// Hardware version + @override + final String? hardwareVersion; + + /// Serial number + @override + final String? serialNumber; + + /// Battery level (0.0 to 1.0) + @override + @JsonKey() + final double batteryLevel; + + /// Battery status + @override + @JsonKey() + final BatteryStatus batteryStatus; + + /// Whether device is charging + @override + @JsonKey() + final bool isCharging; + + /// Signal strength (RSSI) + @override + @JsonKey() + final int rssi; + + /// Signal strength category + @override + @JsonKey() + final SignalStrength signalStrength; + + /// Device health status + @override + @JsonKey() + final DeviceHealth health; + + /// Whether device is currently connected + @override + @JsonKey() + final bool isConnected; + + /// Last seen timestamp + @override + final DateTime? lastSeen; + + /// Device capabilities + @override + @JsonKey() + final GlassesCapabilities capabilities; + + /// Device configuration + @override + @JsonKey() + final GlassesConfiguration configuration; + + /// Additional device metadata + final Map _metadata; + + /// Additional device metadata + @override + @JsonKey() + Map get metadata { + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_metadata); + } + + @override + String toString() { + return 'GlassesDeviceInfo(deviceId: $deviceId, name: $name, modelNumber: $modelNumber, manufacturer: $manufacturer, firmwareVersion: $firmwareVersion, hardwareVersion: $hardwareVersion, serialNumber: $serialNumber, batteryLevel: $batteryLevel, batteryStatus: $batteryStatus, isCharging: $isCharging, rssi: $rssi, signalStrength: $signalStrength, health: $health, isConnected: $isConnected, lastSeen: $lastSeen, capabilities: $capabilities, configuration: $configuration, metadata: $metadata)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GlassesDeviceInfoImpl && + (identical(other.deviceId, deviceId) || + other.deviceId == deviceId) && + (identical(other.name, name) || other.name == name) && + (identical(other.modelNumber, modelNumber) || + other.modelNumber == modelNumber) && + (identical(other.manufacturer, manufacturer) || + other.manufacturer == manufacturer) && + (identical(other.firmwareVersion, firmwareVersion) || + other.firmwareVersion == firmwareVersion) && + (identical(other.hardwareVersion, hardwareVersion) || + other.hardwareVersion == hardwareVersion) && + (identical(other.serialNumber, serialNumber) || + other.serialNumber == serialNumber) && + (identical(other.batteryLevel, batteryLevel) || + other.batteryLevel == batteryLevel) && + (identical(other.batteryStatus, batteryStatus) || + other.batteryStatus == batteryStatus) && + (identical(other.isCharging, isCharging) || + other.isCharging == isCharging) && + (identical(other.rssi, rssi) || other.rssi == rssi) && + (identical(other.signalStrength, signalStrength) || + other.signalStrength == signalStrength) && + (identical(other.health, health) || other.health == health) && + (identical(other.isConnected, isConnected) || + other.isConnected == isConnected) && + (identical(other.lastSeen, lastSeen) || + other.lastSeen == lastSeen) && + (identical(other.capabilities, capabilities) || + other.capabilities == capabilities) && + (identical(other.configuration, configuration) || + other.configuration == configuration) && + const DeepCollectionEquality().equals(other._metadata, _metadata)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + deviceId, + name, + modelNumber, + manufacturer, + firmwareVersion, + hardwareVersion, + serialNumber, + batteryLevel, + batteryStatus, + isCharging, + rssi, + signalStrength, + health, + isConnected, + lastSeen, + capabilities, + configuration, + const DeepCollectionEquality().hash(_metadata), + ); + + /// Create a copy of GlassesDeviceInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GlassesDeviceInfoImplCopyWith<_$GlassesDeviceInfoImpl> get copyWith => + __$$GlassesDeviceInfoImplCopyWithImpl<_$GlassesDeviceInfoImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$GlassesDeviceInfoImplToJson(this); + } +} + +abstract class _GlassesDeviceInfo extends GlassesDeviceInfo { + const factory _GlassesDeviceInfo({ + required final String deviceId, + required final String name, + final String? modelNumber, + final String manufacturer, + final String? firmwareVersion, + final String? hardwareVersion, + final String? serialNumber, + final double batteryLevel, + final BatteryStatus batteryStatus, + final bool isCharging, + final int rssi, + final SignalStrength signalStrength, + final DeviceHealth health, + final bool isConnected, + final DateTime? lastSeen, + final GlassesCapabilities capabilities, + final GlassesConfiguration configuration, + final Map metadata, + }) = _$GlassesDeviceInfoImpl; + const _GlassesDeviceInfo._() : super._(); + + factory _GlassesDeviceInfo.fromJson(Map json) = + _$GlassesDeviceInfoImpl.fromJson; + + /// Unique device identifier + @override + String get deviceId; + + /// Device name as advertised + @override + String get name; + + /// Model number + @override + String? get modelNumber; + + /// Manufacturer name + @override + String get manufacturer; + + /// Firmware version + @override + String? get firmwareVersion; + + /// Hardware version + @override + String? get hardwareVersion; + + /// Serial number + @override + String? get serialNumber; + + /// Battery level (0.0 to 1.0) + @override + double get batteryLevel; + + /// Battery status + @override + BatteryStatus get batteryStatus; + + /// Whether device is charging + @override + bool get isCharging; + + /// Signal strength (RSSI) + @override + int get rssi; + + /// Signal strength category + @override + SignalStrength get signalStrength; + + /// Device health status + @override + DeviceHealth get health; + + /// Whether device is currently connected + @override + bool get isConnected; + + /// Last seen timestamp + @override + DateTime? get lastSeen; + + /// Device capabilities + @override + GlassesCapabilities get capabilities; + + /// Device configuration + @override + GlassesConfiguration get configuration; + + /// Additional device metadata + @override + Map get metadata; + + /// Create a copy of GlassesDeviceInfo + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GlassesDeviceInfoImplCopyWith<_$GlassesDeviceInfoImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ConnectionQuality _$ConnectionQualityFromJson(Map json) { + return _ConnectionQuality.fromJson(json); +} + +/// @nodoc +mixin _$ConnectionQuality { + /// Signal strength + SignalStrength get signalStrength => throw _privateConstructorUsedError; + + /// Raw RSSI value + int get rssi => throw _privateConstructorUsedError; + + /// Connection stability score (0.0 to 1.0) + double get stabilityScore => throw _privateConstructorUsedError; + + /// Packet loss percentage + double get packetLoss => throw _privateConstructorUsedError; + + /// Average latency in milliseconds + int get latencyMs => throw _privateConstructorUsedError; + + /// Number of disconnections in last hour + int get recentDisconnections => throw _privateConstructorUsedError; + + /// Data transfer rate (bytes/second) + int get dataRate => throw _privateConstructorUsedError; + + /// Quality assessment timestamp + DateTime get timestamp => throw _privateConstructorUsedError; + + /// Serializes this ConnectionQuality to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ConnectionQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ConnectionQualityCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ConnectionQualityCopyWith<$Res> { + factory $ConnectionQualityCopyWith( + ConnectionQuality value, + $Res Function(ConnectionQuality) then, + ) = _$ConnectionQualityCopyWithImpl<$Res, ConnectionQuality>; + @useResult + $Res call({ + SignalStrength signalStrength, + int rssi, + double stabilityScore, + double packetLoss, + int latencyMs, + int recentDisconnections, + int dataRate, + DateTime timestamp, + }); +} + +/// @nodoc +class _$ConnectionQualityCopyWithImpl<$Res, $Val extends ConnectionQuality> + implements $ConnectionQualityCopyWith<$Res> { + _$ConnectionQualityCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ConnectionQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? signalStrength = null, + Object? rssi = null, + Object? stabilityScore = null, + Object? packetLoss = null, + Object? latencyMs = null, + Object? recentDisconnections = null, + Object? dataRate = null, + Object? timestamp = null, + }) { + return _then( + _value.copyWith( + signalStrength: + null == signalStrength + ? _value.signalStrength + : signalStrength // ignore: cast_nullable_to_non_nullable + as SignalStrength, + rssi: + null == rssi + ? _value.rssi + : rssi // ignore: cast_nullable_to_non_nullable + as int, + stabilityScore: + null == stabilityScore + ? _value.stabilityScore + : stabilityScore // ignore: cast_nullable_to_non_nullable + as double, + packetLoss: + null == packetLoss + ? _value.packetLoss + : packetLoss // ignore: cast_nullable_to_non_nullable + as double, + latencyMs: + null == latencyMs + ? _value.latencyMs + : latencyMs // ignore: cast_nullable_to_non_nullable + as int, + recentDisconnections: + null == recentDisconnections + ? _value.recentDisconnections + : recentDisconnections // ignore: cast_nullable_to_non_nullable + as int, + dataRate: + null == dataRate + ? _value.dataRate + : dataRate // ignore: cast_nullable_to_non_nullable + as int, + timestamp: + null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as DateTime, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ConnectionQualityImplCopyWith<$Res> + implements $ConnectionQualityCopyWith<$Res> { + factory _$$ConnectionQualityImplCopyWith( + _$ConnectionQualityImpl value, + $Res Function(_$ConnectionQualityImpl) then, + ) = __$$ConnectionQualityImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + SignalStrength signalStrength, + int rssi, + double stabilityScore, + double packetLoss, + int latencyMs, + int recentDisconnections, + int dataRate, + DateTime timestamp, + }); +} + +/// @nodoc +class __$$ConnectionQualityImplCopyWithImpl<$Res> + extends _$ConnectionQualityCopyWithImpl<$Res, _$ConnectionQualityImpl> + implements _$$ConnectionQualityImplCopyWith<$Res> { + __$$ConnectionQualityImplCopyWithImpl( + _$ConnectionQualityImpl _value, + $Res Function(_$ConnectionQualityImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ConnectionQuality + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? signalStrength = null, + Object? rssi = null, + Object? stabilityScore = null, + Object? packetLoss = null, + Object? latencyMs = null, + Object? recentDisconnections = null, + Object? dataRate = null, + Object? timestamp = null, + }) { + return _then( + _$ConnectionQualityImpl( + signalStrength: + null == signalStrength + ? _value.signalStrength + : signalStrength // ignore: cast_nullable_to_non_nullable + as SignalStrength, + rssi: + null == rssi + ? _value.rssi + : rssi // ignore: cast_nullable_to_non_nullable + as int, + stabilityScore: + null == stabilityScore + ? _value.stabilityScore + : stabilityScore // ignore: cast_nullable_to_non_nullable + as double, + packetLoss: + null == packetLoss + ? _value.packetLoss + : packetLoss // ignore: cast_nullable_to_non_nullable + as double, + latencyMs: + null == latencyMs + ? _value.latencyMs + : latencyMs // ignore: cast_nullable_to_non_nullable + as int, + recentDisconnections: + null == recentDisconnections + ? _value.recentDisconnections + : recentDisconnections // ignore: cast_nullable_to_non_nullable + as int, + dataRate: + null == dataRate + ? _value.dataRate + : dataRate // ignore: cast_nullable_to_non_nullable + as int, + timestamp: + null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as DateTime, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ConnectionQualityImpl extends _ConnectionQuality { + const _$ConnectionQualityImpl({ + this.signalStrength = SignalStrength.unknown, + this.rssi = -100, + this.stabilityScore = 0.0, + this.packetLoss = 0.0, + this.latencyMs = 0, + this.recentDisconnections = 0, + this.dataRate = 0, + required this.timestamp, + }) : super._(); + + factory _$ConnectionQualityImpl.fromJson(Map json) => + _$$ConnectionQualityImplFromJson(json); + + /// Signal strength + @override + @JsonKey() + final SignalStrength signalStrength; + + /// Raw RSSI value + @override + @JsonKey() + final int rssi; + + /// Connection stability score (0.0 to 1.0) + @override + @JsonKey() + final double stabilityScore; + + /// Packet loss percentage + @override + @JsonKey() + final double packetLoss; + + /// Average latency in milliseconds + @override + @JsonKey() + final int latencyMs; + + /// Number of disconnections in last hour + @override + @JsonKey() + final int recentDisconnections; + + /// Data transfer rate (bytes/second) + @override + @JsonKey() + final int dataRate; + + /// Quality assessment timestamp + @override + final DateTime timestamp; + + @override + String toString() { + return 'ConnectionQuality(signalStrength: $signalStrength, rssi: $rssi, stabilityScore: $stabilityScore, packetLoss: $packetLoss, latencyMs: $latencyMs, recentDisconnections: $recentDisconnections, dataRate: $dataRate, timestamp: $timestamp)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ConnectionQualityImpl && + (identical(other.signalStrength, signalStrength) || + other.signalStrength == signalStrength) && + (identical(other.rssi, rssi) || other.rssi == rssi) && + (identical(other.stabilityScore, stabilityScore) || + other.stabilityScore == stabilityScore) && + (identical(other.packetLoss, packetLoss) || + other.packetLoss == packetLoss) && + (identical(other.latencyMs, latencyMs) || + other.latencyMs == latencyMs) && + (identical(other.recentDisconnections, recentDisconnections) || + other.recentDisconnections == recentDisconnections) && + (identical(other.dataRate, dataRate) || + other.dataRate == dataRate) && + (identical(other.timestamp, timestamp) || + other.timestamp == timestamp)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + signalStrength, + rssi, + stabilityScore, + packetLoss, + latencyMs, + recentDisconnections, + dataRate, + timestamp, + ); + + /// Create a copy of ConnectionQuality + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ConnectionQualityImplCopyWith<_$ConnectionQualityImpl> get copyWith => + __$$ConnectionQualityImplCopyWithImpl<_$ConnectionQualityImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ConnectionQualityImplToJson(this); + } +} + +abstract class _ConnectionQuality extends ConnectionQuality { + const factory _ConnectionQuality({ + final SignalStrength signalStrength, + final int rssi, + final double stabilityScore, + final double packetLoss, + final int latencyMs, + final int recentDisconnections, + final int dataRate, + required final DateTime timestamp, + }) = _$ConnectionQualityImpl; + const _ConnectionQuality._() : super._(); + + factory _ConnectionQuality.fromJson(Map json) = + _$ConnectionQualityImpl.fromJson; + + /// Signal strength + @override + SignalStrength get signalStrength; + + /// Raw RSSI value + @override + int get rssi; + + /// Connection stability score (0.0 to 1.0) + @override + double get stabilityScore; + + /// Packet loss percentage + @override + double get packetLoss; + + /// Average latency in milliseconds + @override + int get latencyMs; + + /// Number of disconnections in last hour + @override + int get recentDisconnections; + + /// Data transfer rate (bytes/second) + @override + int get dataRate; + + /// Quality assessment timestamp + @override + DateTime get timestamp; + + /// Create a copy of ConnectionQuality + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ConnectionQualityImplCopyWith<_$ConnectionQualityImpl> get copyWith => + throw _privateConstructorUsedError; +} + +HUDDisplayState _$HUDDisplayStateFromJson(Map json) { + return _HUDDisplayState.fromJson(json); +} + +/// @nodoc +mixin _$HUDDisplayState { + /// Whether HUD is currently active + bool get isActive => throw _privateConstructorUsedError; + + /// Current brightness level (0.0 to 1.0) + double get brightness => throw _privateConstructorUsedError; + + /// Currently displayed content + String? get currentContent => throw _privateConstructorUsedError; + + /// Content type being displayed + HUDContentType? get contentType => throw _privateConstructorUsedError; + + /// Display position + HUDPosition get position => throw _privateConstructorUsedError; + + /// Display style settings + HUDStyleSettings get style => throw _privateConstructorUsedError; + + /// Whether display is temporarily paused + bool get isPaused => throw _privateConstructorUsedError; + + /// Last update timestamp + DateTime? get lastUpdate => throw _privateConstructorUsedError; + + /// Display queue for upcoming content + List get displayQueue => throw _privateConstructorUsedError; + + /// Serializes this HUDDisplayState to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of HUDDisplayState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $HUDDisplayStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HUDDisplayStateCopyWith<$Res> { + factory $HUDDisplayStateCopyWith( + HUDDisplayState value, + $Res Function(HUDDisplayState) then, + ) = _$HUDDisplayStateCopyWithImpl<$Res, HUDDisplayState>; + @useResult + $Res call({ + bool isActive, + double brightness, + String? currentContent, + HUDContentType? contentType, + HUDPosition position, + HUDStyleSettings style, + bool isPaused, + DateTime? lastUpdate, + List displayQueue, + }); + + $HUDStyleSettingsCopyWith<$Res> get style; +} + +/// @nodoc +class _$HUDDisplayStateCopyWithImpl<$Res, $Val extends HUDDisplayState> + implements $HUDDisplayStateCopyWith<$Res> { + _$HUDDisplayStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of HUDDisplayState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isActive = null, + Object? brightness = null, + Object? currentContent = freezed, + Object? contentType = freezed, + Object? position = null, + Object? style = null, + Object? isPaused = null, + Object? lastUpdate = freezed, + Object? displayQueue = null, + }) { + return _then( + _value.copyWith( + isActive: + null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + brightness: + null == brightness + ? _value.brightness + : brightness // ignore: cast_nullable_to_non_nullable + as double, + currentContent: + freezed == currentContent + ? _value.currentContent + : currentContent // ignore: cast_nullable_to_non_nullable + as String?, + contentType: + freezed == contentType + ? _value.contentType + : contentType // ignore: cast_nullable_to_non_nullable + as HUDContentType?, + position: + null == position + ? _value.position + : position // ignore: cast_nullable_to_non_nullable + as HUDPosition, + style: + null == style + ? _value.style + : style // ignore: cast_nullable_to_non_nullable + as HUDStyleSettings, + isPaused: + null == isPaused + ? _value.isPaused + : isPaused // ignore: cast_nullable_to_non_nullable + as bool, + lastUpdate: + freezed == lastUpdate + ? _value.lastUpdate + : lastUpdate // ignore: cast_nullable_to_non_nullable + as DateTime?, + displayQueue: + null == displayQueue + ? _value.displayQueue + : displayQueue // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } + + /// Create a copy of HUDDisplayState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $HUDStyleSettingsCopyWith<$Res> get style { + return $HUDStyleSettingsCopyWith<$Res>(_value.style, (value) { + return _then(_value.copyWith(style: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$HUDDisplayStateImplCopyWith<$Res> + implements $HUDDisplayStateCopyWith<$Res> { + factory _$$HUDDisplayStateImplCopyWith( + _$HUDDisplayStateImpl value, + $Res Function(_$HUDDisplayStateImpl) then, + ) = __$$HUDDisplayStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + bool isActive, + double brightness, + String? currentContent, + HUDContentType? contentType, + HUDPosition position, + HUDStyleSettings style, + bool isPaused, + DateTime? lastUpdate, + List displayQueue, + }); + + @override + $HUDStyleSettingsCopyWith<$Res> get style; +} + +/// @nodoc +class __$$HUDDisplayStateImplCopyWithImpl<$Res> + extends _$HUDDisplayStateCopyWithImpl<$Res, _$HUDDisplayStateImpl> + implements _$$HUDDisplayStateImplCopyWith<$Res> { + __$$HUDDisplayStateImplCopyWithImpl( + _$HUDDisplayStateImpl _value, + $Res Function(_$HUDDisplayStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of HUDDisplayState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isActive = null, + Object? brightness = null, + Object? currentContent = freezed, + Object? contentType = freezed, + Object? position = null, + Object? style = null, + Object? isPaused = null, + Object? lastUpdate = freezed, + Object? displayQueue = null, + }) { + return _then( + _$HUDDisplayStateImpl( + isActive: + null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + brightness: + null == brightness + ? _value.brightness + : brightness // ignore: cast_nullable_to_non_nullable + as double, + currentContent: + freezed == currentContent + ? _value.currentContent + : currentContent // ignore: cast_nullable_to_non_nullable + as String?, + contentType: + freezed == contentType + ? _value.contentType + : contentType // ignore: cast_nullable_to_non_nullable + as HUDContentType?, + position: + null == position + ? _value.position + : position // ignore: cast_nullable_to_non_nullable + as HUDPosition, + style: + null == style + ? _value.style + : style // ignore: cast_nullable_to_non_nullable + as HUDStyleSettings, + isPaused: + null == isPaused + ? _value.isPaused + : isPaused // ignore: cast_nullable_to_non_nullable + as bool, + lastUpdate: + freezed == lastUpdate + ? _value.lastUpdate + : lastUpdate // ignore: cast_nullable_to_non_nullable + as DateTime?, + displayQueue: + null == displayQueue + ? _value._displayQueue + : displayQueue // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$HUDDisplayStateImpl extends _HUDDisplayState { + const _$HUDDisplayStateImpl({ + this.isActive = false, + this.brightness = 0.8, + this.currentContent, + this.contentType, + this.position = HUDPosition.center, + this.style = const HUDStyleSettings(), + this.isPaused = false, + this.lastUpdate, + final List displayQueue = const [], + }) : _displayQueue = displayQueue, + super._(); + + factory _$HUDDisplayStateImpl.fromJson(Map json) => + _$$HUDDisplayStateImplFromJson(json); + + /// Whether HUD is currently active + @override + @JsonKey() + final bool isActive; + + /// Current brightness level (0.0 to 1.0) + @override + @JsonKey() + final double brightness; + + /// Currently displayed content + @override + final String? currentContent; + + /// Content type being displayed + @override + final HUDContentType? contentType; + + /// Display position + @override + @JsonKey() + final HUDPosition position; + + /// Display style settings + @override + @JsonKey() + final HUDStyleSettings style; + + /// Whether display is temporarily paused + @override + @JsonKey() + final bool isPaused; + + /// Last update timestamp + @override + final DateTime? lastUpdate; + + /// Display queue for upcoming content + final List _displayQueue; + + /// Display queue for upcoming content + @override + @JsonKey() + List get displayQueue { + if (_displayQueue is EqualUnmodifiableListView) return _displayQueue; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_displayQueue); + } + + @override + String toString() { + return 'HUDDisplayState(isActive: $isActive, brightness: $brightness, currentContent: $currentContent, contentType: $contentType, position: $position, style: $style, isPaused: $isPaused, lastUpdate: $lastUpdate, displayQueue: $displayQueue)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HUDDisplayStateImpl && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + (identical(other.brightness, brightness) || + other.brightness == brightness) && + (identical(other.currentContent, currentContent) || + other.currentContent == currentContent) && + (identical(other.contentType, contentType) || + other.contentType == contentType) && + (identical(other.position, position) || + other.position == position) && + (identical(other.style, style) || other.style == style) && + (identical(other.isPaused, isPaused) || + other.isPaused == isPaused) && + (identical(other.lastUpdate, lastUpdate) || + other.lastUpdate == lastUpdate) && + const DeepCollectionEquality().equals( + other._displayQueue, + _displayQueue, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + isActive, + brightness, + currentContent, + contentType, + position, + style, + isPaused, + lastUpdate, + const DeepCollectionEquality().hash(_displayQueue), + ); + + /// Create a copy of HUDDisplayState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$HUDDisplayStateImplCopyWith<_$HUDDisplayStateImpl> get copyWith => + __$$HUDDisplayStateImplCopyWithImpl<_$HUDDisplayStateImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$HUDDisplayStateImplToJson(this); + } +} + +abstract class _HUDDisplayState extends HUDDisplayState { + const factory _HUDDisplayState({ + final bool isActive, + final double brightness, + final String? currentContent, + final HUDContentType? contentType, + final HUDPosition position, + final HUDStyleSettings style, + final bool isPaused, + final DateTime? lastUpdate, + final List displayQueue, + }) = _$HUDDisplayStateImpl; + const _HUDDisplayState._() : super._(); + + factory _HUDDisplayState.fromJson(Map json) = + _$HUDDisplayStateImpl.fromJson; + + /// Whether HUD is currently active + @override + bool get isActive; + + /// Current brightness level (0.0 to 1.0) + @override + double get brightness; + + /// Currently displayed content + @override + String? get currentContent; + + /// Content type being displayed + @override + HUDContentType? get contentType; + + /// Display position + @override + HUDPosition get position; + + /// Display style settings + @override + HUDStyleSettings get style; + + /// Whether display is temporarily paused + @override + bool get isPaused; + + /// Last update timestamp + @override + DateTime? get lastUpdate; + + /// Display queue for upcoming content + @override + List get displayQueue; + + /// Create a copy of HUDDisplayState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$HUDDisplayStateImplCopyWith<_$HUDDisplayStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +HUDStyleSettings _$HUDStyleSettingsFromJson(Map json) { + return _HUDStyleSettings.fromJson(json); +} + +/// @nodoc +mixin _$HUDStyleSettings { + /// Font size + double get fontSize => throw _privateConstructorUsedError; + + /// Text color + String get textColor => throw _privateConstructorUsedError; + + /// Background color + String get backgroundColor => throw _privateConstructorUsedError; + + /// Font weight + String get fontWeight => throw _privateConstructorUsedError; + + /// Text alignment + String get alignment => throw _privateConstructorUsedError; + + /// Display duration in seconds + int get displayDuration => throw _privateConstructorUsedError; + + /// Animation type + String get animation => throw _privateConstructorUsedError; + + /// Serializes this HUDStyleSettings to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of HUDStyleSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $HUDStyleSettingsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HUDStyleSettingsCopyWith<$Res> { + factory $HUDStyleSettingsCopyWith( + HUDStyleSettings value, + $Res Function(HUDStyleSettings) then, + ) = _$HUDStyleSettingsCopyWithImpl<$Res, HUDStyleSettings>; + @useResult + $Res call({ + double fontSize, + String textColor, + String backgroundColor, + String fontWeight, + String alignment, + int displayDuration, + String animation, + }); +} + +/// @nodoc +class _$HUDStyleSettingsCopyWithImpl<$Res, $Val extends HUDStyleSettings> + implements $HUDStyleSettingsCopyWith<$Res> { + _$HUDStyleSettingsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of HUDStyleSettings + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? fontSize = null, + Object? textColor = null, + Object? backgroundColor = null, + Object? fontWeight = null, + Object? alignment = null, + Object? displayDuration = null, + Object? animation = null, + }) { + return _then( + _value.copyWith( + fontSize: + null == fontSize + ? _value.fontSize + : fontSize // ignore: cast_nullable_to_non_nullable + as double, + textColor: + null == textColor + ? _value.textColor + : textColor // ignore: cast_nullable_to_non_nullable + as String, + backgroundColor: + null == backgroundColor + ? _value.backgroundColor + : backgroundColor // ignore: cast_nullable_to_non_nullable + as String, + fontWeight: + null == fontWeight + ? _value.fontWeight + : fontWeight // ignore: cast_nullable_to_non_nullable + as String, + alignment: + null == alignment + ? _value.alignment + : alignment // ignore: cast_nullable_to_non_nullable + as String, + displayDuration: + null == displayDuration + ? _value.displayDuration + : displayDuration // ignore: cast_nullable_to_non_nullable + as int, + animation: + null == animation + ? _value.animation + : animation // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$HUDStyleSettingsImplCopyWith<$Res> + implements $HUDStyleSettingsCopyWith<$Res> { + factory _$$HUDStyleSettingsImplCopyWith( + _$HUDStyleSettingsImpl value, + $Res Function(_$HUDStyleSettingsImpl) then, + ) = __$$HUDStyleSettingsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + double fontSize, + String textColor, + String backgroundColor, + String fontWeight, + String alignment, + int displayDuration, + String animation, + }); +} + +/// @nodoc +class __$$HUDStyleSettingsImplCopyWithImpl<$Res> + extends _$HUDStyleSettingsCopyWithImpl<$Res, _$HUDStyleSettingsImpl> + implements _$$HUDStyleSettingsImplCopyWith<$Res> { + __$$HUDStyleSettingsImplCopyWithImpl( + _$HUDStyleSettingsImpl _value, + $Res Function(_$HUDStyleSettingsImpl) _then, + ) : super(_value, _then); + + /// Create a copy of HUDStyleSettings + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? fontSize = null, + Object? textColor = null, + Object? backgroundColor = null, + Object? fontWeight = null, + Object? alignment = null, + Object? displayDuration = null, + Object? animation = null, + }) { + return _then( + _$HUDStyleSettingsImpl( + fontSize: + null == fontSize + ? _value.fontSize + : fontSize // ignore: cast_nullable_to_non_nullable + as double, + textColor: + null == textColor + ? _value.textColor + : textColor // ignore: cast_nullable_to_non_nullable + as String, + backgroundColor: + null == backgroundColor + ? _value.backgroundColor + : backgroundColor // ignore: cast_nullable_to_non_nullable + as String, + fontWeight: + null == fontWeight + ? _value.fontWeight + : fontWeight // ignore: cast_nullable_to_non_nullable + as String, + alignment: + null == alignment + ? _value.alignment + : alignment // ignore: cast_nullable_to_non_nullable + as String, + displayDuration: + null == displayDuration + ? _value.displayDuration + : displayDuration // ignore: cast_nullable_to_non_nullable + as int, + animation: + null == animation + ? _value.animation + : animation // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$HUDStyleSettingsImpl implements _HUDStyleSettings { + const _$HUDStyleSettingsImpl({ + this.fontSize = 16.0, + this.textColor = '#FFFFFF', + this.backgroundColor = '#000000', + this.fontWeight = 'normal', + this.alignment = 'center', + this.displayDuration = 5, + this.animation = 'fade', + }); + + factory _$HUDStyleSettingsImpl.fromJson(Map json) => + _$$HUDStyleSettingsImplFromJson(json); + + /// Font size + @override + @JsonKey() + final double fontSize; + + /// Text color + @override + @JsonKey() + final String textColor; + + /// Background color + @override + @JsonKey() + final String backgroundColor; + + /// Font weight + @override + @JsonKey() + final String fontWeight; + + /// Text alignment + @override + @JsonKey() + final String alignment; + + /// Display duration in seconds + @override + @JsonKey() + final int displayDuration; + + /// Animation type + @override + @JsonKey() + final String animation; + + @override + String toString() { + return 'HUDStyleSettings(fontSize: $fontSize, textColor: $textColor, backgroundColor: $backgroundColor, fontWeight: $fontWeight, alignment: $alignment, displayDuration: $displayDuration, animation: $animation)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HUDStyleSettingsImpl && + (identical(other.fontSize, fontSize) || + other.fontSize == fontSize) && + (identical(other.textColor, textColor) || + other.textColor == textColor) && + (identical(other.backgroundColor, backgroundColor) || + other.backgroundColor == backgroundColor) && + (identical(other.fontWeight, fontWeight) || + other.fontWeight == fontWeight) && + (identical(other.alignment, alignment) || + other.alignment == alignment) && + (identical(other.displayDuration, displayDuration) || + other.displayDuration == displayDuration) && + (identical(other.animation, animation) || + other.animation == animation)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + fontSize, + textColor, + backgroundColor, + fontWeight, + alignment, + displayDuration, + animation, + ); + + /// Create a copy of HUDStyleSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$HUDStyleSettingsImplCopyWith<_$HUDStyleSettingsImpl> get copyWith => + __$$HUDStyleSettingsImplCopyWithImpl<_$HUDStyleSettingsImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$HUDStyleSettingsImplToJson(this); + } +} + +abstract class _HUDStyleSettings implements HUDStyleSettings { + const factory _HUDStyleSettings({ + final double fontSize, + final String textColor, + final String backgroundColor, + final String fontWeight, + final String alignment, + final int displayDuration, + final String animation, + }) = _$HUDStyleSettingsImpl; + + factory _HUDStyleSettings.fromJson(Map json) = + _$HUDStyleSettingsImpl.fromJson; + + /// Font size + @override + double get fontSize; + + /// Text color + @override + String get textColor; + + /// Background color + @override + String get backgroundColor; + + /// Font weight + @override + String get fontWeight; + + /// Text alignment + @override + String get alignment; + + /// Display duration in seconds + @override + int get displayDuration; + + /// Animation type + @override + String get animation; + + /// Create a copy of HUDStyleSettings + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$HUDStyleSettingsImplCopyWith<_$HUDStyleSettingsImpl> get copyWith => + throw _privateConstructorUsedError; +} + +HUDQueueItem _$HUDQueueItemFromJson(Map json) { + return _HUDQueueItem.fromJson(json); +} + +/// @nodoc +mixin _$HUDQueueItem { + /// Content to display + String get content => throw _privateConstructorUsedError; + + /// Content type + HUDContentType get type => throw _privateConstructorUsedError; + + /// Display position + HUDPosition get position => throw _privateConstructorUsedError; + + /// Priority (higher numbers = higher priority) + int get priority => throw _privateConstructorUsedError; + + /// When this item was queued + DateTime get queuedAt => throw _privateConstructorUsedError; + + /// Display duration + Duration get duration => throw _privateConstructorUsedError; + + /// Style overrides + HUDStyleSettings? get styleOverrides => throw _privateConstructorUsedError; + + /// Serializes this HUDQueueItem to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of HUDQueueItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $HUDQueueItemCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HUDQueueItemCopyWith<$Res> { + factory $HUDQueueItemCopyWith( + HUDQueueItem value, + $Res Function(HUDQueueItem) then, + ) = _$HUDQueueItemCopyWithImpl<$Res, HUDQueueItem>; + @useResult + $Res call({ + String content, + HUDContentType type, + HUDPosition position, + int priority, + DateTime queuedAt, + Duration duration, + HUDStyleSettings? styleOverrides, + }); + + $HUDStyleSettingsCopyWith<$Res>? get styleOverrides; +} + +/// @nodoc +class _$HUDQueueItemCopyWithImpl<$Res, $Val extends HUDQueueItem> + implements $HUDQueueItemCopyWith<$Res> { + _$HUDQueueItemCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of HUDQueueItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? content = null, + Object? type = null, + Object? position = null, + Object? priority = null, + Object? queuedAt = null, + Object? duration = null, + Object? styleOverrides = freezed, + }) { + return _then( + _value.copyWith( + content: + null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as String, + type: + null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as HUDContentType, + position: + null == position + ? _value.position + : position // ignore: cast_nullable_to_non_nullable + as HUDPosition, + priority: + null == priority + ? _value.priority + : priority // ignore: cast_nullable_to_non_nullable + as int, + queuedAt: + null == queuedAt + ? _value.queuedAt + : queuedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + duration: + null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + styleOverrides: + freezed == styleOverrides + ? _value.styleOverrides + : styleOverrides // ignore: cast_nullable_to_non_nullable + as HUDStyleSettings?, + ) + as $Val, + ); + } + + /// Create a copy of HUDQueueItem + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $HUDStyleSettingsCopyWith<$Res>? get styleOverrides { + if (_value.styleOverrides == null) { + return null; + } + + return $HUDStyleSettingsCopyWith<$Res>(_value.styleOverrides!, (value) { + return _then(_value.copyWith(styleOverrides: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$HUDQueueItemImplCopyWith<$Res> + implements $HUDQueueItemCopyWith<$Res> { + factory _$$HUDQueueItemImplCopyWith( + _$HUDQueueItemImpl value, + $Res Function(_$HUDQueueItemImpl) then, + ) = __$$HUDQueueItemImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String content, + HUDContentType type, + HUDPosition position, + int priority, + DateTime queuedAt, + Duration duration, + HUDStyleSettings? styleOverrides, + }); + + @override + $HUDStyleSettingsCopyWith<$Res>? get styleOverrides; +} + +/// @nodoc +class __$$HUDQueueItemImplCopyWithImpl<$Res> + extends _$HUDQueueItemCopyWithImpl<$Res, _$HUDQueueItemImpl> + implements _$$HUDQueueItemImplCopyWith<$Res> { + __$$HUDQueueItemImplCopyWithImpl( + _$HUDQueueItemImpl _value, + $Res Function(_$HUDQueueItemImpl) _then, + ) : super(_value, _then); + + /// Create a copy of HUDQueueItem + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? content = null, + Object? type = null, + Object? position = null, + Object? priority = null, + Object? queuedAt = null, + Object? duration = null, + Object? styleOverrides = freezed, + }) { + return _then( + _$HUDQueueItemImpl( + content: + null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as String, + type: + null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as HUDContentType, + position: + null == position + ? _value.position + : position // ignore: cast_nullable_to_non_nullable + as HUDPosition, + priority: + null == priority + ? _value.priority + : priority // ignore: cast_nullable_to_non_nullable + as int, + queuedAt: + null == queuedAt + ? _value.queuedAt + : queuedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + duration: + null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + styleOverrides: + freezed == styleOverrides + ? _value.styleOverrides + : styleOverrides // ignore: cast_nullable_to_non_nullable + as HUDStyleSettings?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$HUDQueueItemImpl implements _HUDQueueItem { + const _$HUDQueueItemImpl({ + required this.content, + required this.type, + this.position = HUDPosition.center, + this.priority = 1, + required this.queuedAt, + this.duration = const Duration(seconds: 5), + this.styleOverrides, + }); + + factory _$HUDQueueItemImpl.fromJson(Map json) => + _$$HUDQueueItemImplFromJson(json); + + /// Content to display + @override + final String content; + + /// Content type + @override + final HUDContentType type; + + /// Display position + @override + @JsonKey() + final HUDPosition position; + + /// Priority (higher numbers = higher priority) + @override + @JsonKey() + final int priority; + + /// When this item was queued + @override + final DateTime queuedAt; + + /// Display duration + @override + @JsonKey() + final Duration duration; + + /// Style overrides + @override + final HUDStyleSettings? styleOverrides; + + @override + String toString() { + return 'HUDQueueItem(content: $content, type: $type, position: $position, priority: $priority, queuedAt: $queuedAt, duration: $duration, styleOverrides: $styleOverrides)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HUDQueueItemImpl && + (identical(other.content, content) || other.content == content) && + (identical(other.type, type) || other.type == type) && + (identical(other.position, position) || + other.position == position) && + (identical(other.priority, priority) || + other.priority == priority) && + (identical(other.queuedAt, queuedAt) || + other.queuedAt == queuedAt) && + (identical(other.duration, duration) || + other.duration == duration) && + (identical(other.styleOverrides, styleOverrides) || + other.styleOverrides == styleOverrides)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + content, + type, + position, + priority, + queuedAt, + duration, + styleOverrides, + ); + + /// Create a copy of HUDQueueItem + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$HUDQueueItemImplCopyWith<_$HUDQueueItemImpl> get copyWith => + __$$HUDQueueItemImplCopyWithImpl<_$HUDQueueItemImpl>(this, _$identity); + + @override + Map toJson() { + return _$$HUDQueueItemImplToJson(this); + } +} + +abstract class _HUDQueueItem implements HUDQueueItem { + const factory _HUDQueueItem({ + required final String content, + required final HUDContentType type, + final HUDPosition position, + final int priority, + required final DateTime queuedAt, + final Duration duration, + final HUDStyleSettings? styleOverrides, + }) = _$HUDQueueItemImpl; + + factory _HUDQueueItem.fromJson(Map json) = + _$HUDQueueItemImpl.fromJson; + + /// Content to display + @override + String get content; + + /// Content type + @override + HUDContentType get type; + + /// Display position + @override + HUDPosition get position; + + /// Priority (higher numbers = higher priority) + @override + int get priority; + + /// When this item was queued + @override + DateTime get queuedAt; + + /// Display duration + @override + Duration get duration; + + /// Style overrides + @override + HUDStyleSettings? get styleOverrides; + + /// Create a copy of HUDQueueItem + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$HUDQueueItemImplCopyWith<_$HUDQueueItemImpl> get copyWith => + throw _privateConstructorUsedError; +} + +GlassesCapabilities _$GlassesCapabilitiesFromJson(Map json) { + return _GlassesCapabilities.fromJson(json); +} + +/// @nodoc +mixin _$GlassesCapabilities { + /// Supports text display + bool get supportsText => throw _privateConstructorUsedError; + + /// Supports images + bool get supportsImages => throw _privateConstructorUsedError; + + /// Supports animations + bool get supportsAnimations => throw _privateConstructorUsedError; + + /// Supports touch gestures + bool get supportsTouchGestures => throw _privateConstructorUsedError; + + /// Supports voice commands + bool get supportsVoiceCommands => throw _privateConstructorUsedError; + + /// Maximum text length + int get maxTextLength => throw _privateConstructorUsedError; + + /// Supported display positions + List get supportedPositions => + throw _privateConstructorUsedError; + + /// Battery monitoring capability + bool get supportsBatteryMonitoring => throw _privateConstructorUsedError; + + /// Firmware update capability + bool get supportsFirmwareUpdate => throw _privateConstructorUsedError; + + /// Serializes this GlassesCapabilities to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of GlassesCapabilities + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GlassesCapabilitiesCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GlassesCapabilitiesCopyWith<$Res> { + factory $GlassesCapabilitiesCopyWith( + GlassesCapabilities value, + $Res Function(GlassesCapabilities) then, + ) = _$GlassesCapabilitiesCopyWithImpl<$Res, GlassesCapabilities>; + @useResult + $Res call({ + bool supportsText, + bool supportsImages, + bool supportsAnimations, + bool supportsTouchGestures, + bool supportsVoiceCommands, + int maxTextLength, + List supportedPositions, + bool supportsBatteryMonitoring, + bool supportsFirmwareUpdate, + }); +} + +/// @nodoc +class _$GlassesCapabilitiesCopyWithImpl<$Res, $Val extends GlassesCapabilities> + implements $GlassesCapabilitiesCopyWith<$Res> { + _$GlassesCapabilitiesCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GlassesCapabilities + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? supportsText = null, + Object? supportsImages = null, + Object? supportsAnimations = null, + Object? supportsTouchGestures = null, + Object? supportsVoiceCommands = null, + Object? maxTextLength = null, + Object? supportedPositions = null, + Object? supportsBatteryMonitoring = null, + Object? supportsFirmwareUpdate = null, + }) { + return _then( + _value.copyWith( + supportsText: + null == supportsText + ? _value.supportsText + : supportsText // ignore: cast_nullable_to_non_nullable + as bool, + supportsImages: + null == supportsImages + ? _value.supportsImages + : supportsImages // ignore: cast_nullable_to_non_nullable + as bool, + supportsAnimations: + null == supportsAnimations + ? _value.supportsAnimations + : supportsAnimations // ignore: cast_nullable_to_non_nullable + as bool, + supportsTouchGestures: + null == supportsTouchGestures + ? _value.supportsTouchGestures + : supportsTouchGestures // ignore: cast_nullable_to_non_nullable + as bool, + supportsVoiceCommands: + null == supportsVoiceCommands + ? _value.supportsVoiceCommands + : supportsVoiceCommands // ignore: cast_nullable_to_non_nullable + as bool, + maxTextLength: + null == maxTextLength + ? _value.maxTextLength + : maxTextLength // ignore: cast_nullable_to_non_nullable + as int, + supportedPositions: + null == supportedPositions + ? _value.supportedPositions + : supportedPositions // ignore: cast_nullable_to_non_nullable + as List, + supportsBatteryMonitoring: + null == supportsBatteryMonitoring + ? _value.supportsBatteryMonitoring + : supportsBatteryMonitoring // ignore: cast_nullable_to_non_nullable + as bool, + supportsFirmwareUpdate: + null == supportsFirmwareUpdate + ? _value.supportsFirmwareUpdate + : supportsFirmwareUpdate // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$GlassesCapabilitiesImplCopyWith<$Res> + implements $GlassesCapabilitiesCopyWith<$Res> { + factory _$$GlassesCapabilitiesImplCopyWith( + _$GlassesCapabilitiesImpl value, + $Res Function(_$GlassesCapabilitiesImpl) then, + ) = __$$GlassesCapabilitiesImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + bool supportsText, + bool supportsImages, + bool supportsAnimations, + bool supportsTouchGestures, + bool supportsVoiceCommands, + int maxTextLength, + List supportedPositions, + bool supportsBatteryMonitoring, + bool supportsFirmwareUpdate, + }); +} + +/// @nodoc +class __$$GlassesCapabilitiesImplCopyWithImpl<$Res> + extends _$GlassesCapabilitiesCopyWithImpl<$Res, _$GlassesCapabilitiesImpl> + implements _$$GlassesCapabilitiesImplCopyWith<$Res> { + __$$GlassesCapabilitiesImplCopyWithImpl( + _$GlassesCapabilitiesImpl _value, + $Res Function(_$GlassesCapabilitiesImpl) _then, + ) : super(_value, _then); + + /// Create a copy of GlassesCapabilities + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? supportsText = null, + Object? supportsImages = null, + Object? supportsAnimations = null, + Object? supportsTouchGestures = null, + Object? supportsVoiceCommands = null, + Object? maxTextLength = null, + Object? supportedPositions = null, + Object? supportsBatteryMonitoring = null, + Object? supportsFirmwareUpdate = null, + }) { + return _then( + _$GlassesCapabilitiesImpl( + supportsText: + null == supportsText + ? _value.supportsText + : supportsText // ignore: cast_nullable_to_non_nullable + as bool, + supportsImages: + null == supportsImages + ? _value.supportsImages + : supportsImages // ignore: cast_nullable_to_non_nullable + as bool, + supportsAnimations: + null == supportsAnimations + ? _value.supportsAnimations + : supportsAnimations // ignore: cast_nullable_to_non_nullable + as bool, + supportsTouchGestures: + null == supportsTouchGestures + ? _value.supportsTouchGestures + : supportsTouchGestures // ignore: cast_nullable_to_non_nullable + as bool, + supportsVoiceCommands: + null == supportsVoiceCommands + ? _value.supportsVoiceCommands + : supportsVoiceCommands // ignore: cast_nullable_to_non_nullable + as bool, + maxTextLength: + null == maxTextLength + ? _value.maxTextLength + : maxTextLength // ignore: cast_nullable_to_non_nullable + as int, + supportedPositions: + null == supportedPositions + ? _value._supportedPositions + : supportedPositions // ignore: cast_nullable_to_non_nullable + as List, + supportsBatteryMonitoring: + null == supportsBatteryMonitoring + ? _value.supportsBatteryMonitoring + : supportsBatteryMonitoring // ignore: cast_nullable_to_non_nullable + as bool, + supportsFirmwareUpdate: + null == supportsFirmwareUpdate + ? _value.supportsFirmwareUpdate + : supportsFirmwareUpdate // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$GlassesCapabilitiesImpl implements _GlassesCapabilities { + const _$GlassesCapabilitiesImpl({ + this.supportsText = true, + this.supportsImages = false, + this.supportsAnimations = false, + this.supportsTouchGestures = true, + this.supportsVoiceCommands = false, + this.maxTextLength = 256, + final List supportedPositions = const [HUDPosition.center], + this.supportsBatteryMonitoring = true, + this.supportsFirmwareUpdate = true, + }) : _supportedPositions = supportedPositions; + + factory _$GlassesCapabilitiesImpl.fromJson(Map json) => + _$$GlassesCapabilitiesImplFromJson(json); + + /// Supports text display + @override + @JsonKey() + final bool supportsText; + + /// Supports images + @override + @JsonKey() + final bool supportsImages; + + /// Supports animations + @override + @JsonKey() + final bool supportsAnimations; + + /// Supports touch gestures + @override + @JsonKey() + final bool supportsTouchGestures; + + /// Supports voice commands + @override + @JsonKey() + final bool supportsVoiceCommands; + + /// Maximum text length + @override + @JsonKey() + final int maxTextLength; + + /// Supported display positions + final List _supportedPositions; + + /// Supported display positions + @override + @JsonKey() + List get supportedPositions { + if (_supportedPositions is EqualUnmodifiableListView) + return _supportedPositions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_supportedPositions); + } + + /// Battery monitoring capability + @override + @JsonKey() + final bool supportsBatteryMonitoring; + + /// Firmware update capability + @override + @JsonKey() + final bool supportsFirmwareUpdate; + + @override + String toString() { + return 'GlassesCapabilities(supportsText: $supportsText, supportsImages: $supportsImages, supportsAnimations: $supportsAnimations, supportsTouchGestures: $supportsTouchGestures, supportsVoiceCommands: $supportsVoiceCommands, maxTextLength: $maxTextLength, supportedPositions: $supportedPositions, supportsBatteryMonitoring: $supportsBatteryMonitoring, supportsFirmwareUpdate: $supportsFirmwareUpdate)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GlassesCapabilitiesImpl && + (identical(other.supportsText, supportsText) || + other.supportsText == supportsText) && + (identical(other.supportsImages, supportsImages) || + other.supportsImages == supportsImages) && + (identical(other.supportsAnimations, supportsAnimations) || + other.supportsAnimations == supportsAnimations) && + (identical(other.supportsTouchGestures, supportsTouchGestures) || + other.supportsTouchGestures == supportsTouchGestures) && + (identical(other.supportsVoiceCommands, supportsVoiceCommands) || + other.supportsVoiceCommands == supportsVoiceCommands) && + (identical(other.maxTextLength, maxTextLength) || + other.maxTextLength == maxTextLength) && + const DeepCollectionEquality().equals( + other._supportedPositions, + _supportedPositions, + ) && + (identical( + other.supportsBatteryMonitoring, + supportsBatteryMonitoring, + ) || + other.supportsBatteryMonitoring == supportsBatteryMonitoring) && + (identical(other.supportsFirmwareUpdate, supportsFirmwareUpdate) || + other.supportsFirmwareUpdate == supportsFirmwareUpdate)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + supportsText, + supportsImages, + supportsAnimations, + supportsTouchGestures, + supportsVoiceCommands, + maxTextLength, + const DeepCollectionEquality().hash(_supportedPositions), + supportsBatteryMonitoring, + supportsFirmwareUpdate, + ); + + /// Create a copy of GlassesCapabilities + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GlassesCapabilitiesImplCopyWith<_$GlassesCapabilitiesImpl> get copyWith => + __$$GlassesCapabilitiesImplCopyWithImpl<_$GlassesCapabilitiesImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$GlassesCapabilitiesImplToJson(this); + } +} + +abstract class _GlassesCapabilities implements GlassesCapabilities { + const factory _GlassesCapabilities({ + final bool supportsText, + final bool supportsImages, + final bool supportsAnimations, + final bool supportsTouchGestures, + final bool supportsVoiceCommands, + final int maxTextLength, + final List supportedPositions, + final bool supportsBatteryMonitoring, + final bool supportsFirmwareUpdate, + }) = _$GlassesCapabilitiesImpl; + + factory _GlassesCapabilities.fromJson(Map json) = + _$GlassesCapabilitiesImpl.fromJson; + + /// Supports text display + @override + bool get supportsText; + + /// Supports images + @override + bool get supportsImages; + + /// Supports animations + @override + bool get supportsAnimations; + + /// Supports touch gestures + @override + bool get supportsTouchGestures; + + /// Supports voice commands + @override + bool get supportsVoiceCommands; + + /// Maximum text length + @override + int get maxTextLength; + + /// Supported display positions + @override + List get supportedPositions; + + /// Battery monitoring capability + @override + bool get supportsBatteryMonitoring; + + /// Firmware update capability + @override + bool get supportsFirmwareUpdate; + + /// Create a copy of GlassesCapabilities + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GlassesCapabilitiesImplCopyWith<_$GlassesCapabilitiesImpl> get copyWith => + throw _privateConstructorUsedError; +} + +GlassesConfiguration _$GlassesConfigurationFromJson(Map json) { + return _GlassesConfiguration.fromJson(json); +} + +/// @nodoc +mixin _$GlassesConfiguration { + /// Auto-reconnect setting + bool get autoReconnect => throw _privateConstructorUsedError; + + /// Default brightness + double get defaultBrightness => throw _privateConstructorUsedError; + + /// Gesture sensitivity + double get gestureSensitivity => throw _privateConstructorUsedError; + + /// Display timeout in seconds + int get displayTimeout => throw _privateConstructorUsedError; + + /// Power save mode enabled + bool get powerSaveMode => throw _privateConstructorUsedError; + + /// Notification settings + NotificationSettings get notifications => throw _privateConstructorUsedError; + + /// Serializes this GlassesConfiguration to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of GlassesConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GlassesConfigurationCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GlassesConfigurationCopyWith<$Res> { + factory $GlassesConfigurationCopyWith( + GlassesConfiguration value, + $Res Function(GlassesConfiguration) then, + ) = _$GlassesConfigurationCopyWithImpl<$Res, GlassesConfiguration>; + @useResult + $Res call({ + bool autoReconnect, + double defaultBrightness, + double gestureSensitivity, + int displayTimeout, + bool powerSaveMode, + NotificationSettings notifications, + }); + + $NotificationSettingsCopyWith<$Res> get notifications; +} + +/// @nodoc +class _$GlassesConfigurationCopyWithImpl< + $Res, + $Val extends GlassesConfiguration +> + implements $GlassesConfigurationCopyWith<$Res> { + _$GlassesConfigurationCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GlassesConfiguration + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? autoReconnect = null, + Object? defaultBrightness = null, + Object? gestureSensitivity = null, + Object? displayTimeout = null, + Object? powerSaveMode = null, + Object? notifications = null, + }) { + return _then( + _value.copyWith( + autoReconnect: + null == autoReconnect + ? _value.autoReconnect + : autoReconnect // ignore: cast_nullable_to_non_nullable + as bool, + defaultBrightness: + null == defaultBrightness + ? _value.defaultBrightness + : defaultBrightness // ignore: cast_nullable_to_non_nullable + as double, + gestureSensitivity: + null == gestureSensitivity + ? _value.gestureSensitivity + : gestureSensitivity // ignore: cast_nullable_to_non_nullable + as double, + displayTimeout: + null == displayTimeout + ? _value.displayTimeout + : displayTimeout // ignore: cast_nullable_to_non_nullable + as int, + powerSaveMode: + null == powerSaveMode + ? _value.powerSaveMode + : powerSaveMode // ignore: cast_nullable_to_non_nullable + as bool, + notifications: + null == notifications + ? _value.notifications + : notifications // ignore: cast_nullable_to_non_nullable + as NotificationSettings, + ) + as $Val, + ); + } + + /// Create a copy of GlassesConfiguration + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $NotificationSettingsCopyWith<$Res> get notifications { + return $NotificationSettingsCopyWith<$Res>(_value.notifications, (value) { + return _then(_value.copyWith(notifications: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$GlassesConfigurationImplCopyWith<$Res> + implements $GlassesConfigurationCopyWith<$Res> { + factory _$$GlassesConfigurationImplCopyWith( + _$GlassesConfigurationImpl value, + $Res Function(_$GlassesConfigurationImpl) then, + ) = __$$GlassesConfigurationImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + bool autoReconnect, + double defaultBrightness, + double gestureSensitivity, + int displayTimeout, + bool powerSaveMode, + NotificationSettings notifications, + }); + + @override + $NotificationSettingsCopyWith<$Res> get notifications; +} + +/// @nodoc +class __$$GlassesConfigurationImplCopyWithImpl<$Res> + extends _$GlassesConfigurationCopyWithImpl<$Res, _$GlassesConfigurationImpl> + implements _$$GlassesConfigurationImplCopyWith<$Res> { + __$$GlassesConfigurationImplCopyWithImpl( + _$GlassesConfigurationImpl _value, + $Res Function(_$GlassesConfigurationImpl) _then, + ) : super(_value, _then); + + /// Create a copy of GlassesConfiguration + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? autoReconnect = null, + Object? defaultBrightness = null, + Object? gestureSensitivity = null, + Object? displayTimeout = null, + Object? powerSaveMode = null, + Object? notifications = null, + }) { + return _then( + _$GlassesConfigurationImpl( + autoReconnect: + null == autoReconnect + ? _value.autoReconnect + : autoReconnect // ignore: cast_nullable_to_non_nullable + as bool, + defaultBrightness: + null == defaultBrightness + ? _value.defaultBrightness + : defaultBrightness // ignore: cast_nullable_to_non_nullable + as double, + gestureSensitivity: + null == gestureSensitivity + ? _value.gestureSensitivity + : gestureSensitivity // ignore: cast_nullable_to_non_nullable + as double, + displayTimeout: + null == displayTimeout + ? _value.displayTimeout + : displayTimeout // ignore: cast_nullable_to_non_nullable + as int, + powerSaveMode: + null == powerSaveMode + ? _value.powerSaveMode + : powerSaveMode // ignore: cast_nullable_to_non_nullable + as bool, + notifications: + null == notifications + ? _value.notifications + : notifications // ignore: cast_nullable_to_non_nullable + as NotificationSettings, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$GlassesConfigurationImpl implements _GlassesConfiguration { + const _$GlassesConfigurationImpl({ + this.autoReconnect = true, + this.defaultBrightness = 0.8, + this.gestureSensitivity = 0.5, + this.displayTimeout = 10, + this.powerSaveMode = false, + this.notifications = const NotificationSettings(), + }); + + factory _$GlassesConfigurationImpl.fromJson(Map json) => + _$$GlassesConfigurationImplFromJson(json); + + /// Auto-reconnect setting + @override + @JsonKey() + final bool autoReconnect; + + /// Default brightness + @override + @JsonKey() + final double defaultBrightness; + + /// Gesture sensitivity + @override + @JsonKey() + final double gestureSensitivity; + + /// Display timeout in seconds + @override + @JsonKey() + final int displayTimeout; + + /// Power save mode enabled + @override + @JsonKey() + final bool powerSaveMode; + + /// Notification settings + @override + @JsonKey() + final NotificationSettings notifications; + + @override + String toString() { + return 'GlassesConfiguration(autoReconnect: $autoReconnect, defaultBrightness: $defaultBrightness, gestureSensitivity: $gestureSensitivity, displayTimeout: $displayTimeout, powerSaveMode: $powerSaveMode, notifications: $notifications)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GlassesConfigurationImpl && + (identical(other.autoReconnect, autoReconnect) || + other.autoReconnect == autoReconnect) && + (identical(other.defaultBrightness, defaultBrightness) || + other.defaultBrightness == defaultBrightness) && + (identical(other.gestureSensitivity, gestureSensitivity) || + other.gestureSensitivity == gestureSensitivity) && + (identical(other.displayTimeout, displayTimeout) || + other.displayTimeout == displayTimeout) && + (identical(other.powerSaveMode, powerSaveMode) || + other.powerSaveMode == powerSaveMode) && + (identical(other.notifications, notifications) || + other.notifications == notifications)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + autoReconnect, + defaultBrightness, + gestureSensitivity, + displayTimeout, + powerSaveMode, + notifications, + ); + + /// Create a copy of GlassesConfiguration + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GlassesConfigurationImplCopyWith<_$GlassesConfigurationImpl> + get copyWith => + __$$GlassesConfigurationImplCopyWithImpl<_$GlassesConfigurationImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$GlassesConfigurationImplToJson(this); + } +} + +abstract class _GlassesConfiguration implements GlassesConfiguration { + const factory _GlassesConfiguration({ + final bool autoReconnect, + final double defaultBrightness, + final double gestureSensitivity, + final int displayTimeout, + final bool powerSaveMode, + final NotificationSettings notifications, + }) = _$GlassesConfigurationImpl; + + factory _GlassesConfiguration.fromJson(Map json) = + _$GlassesConfigurationImpl.fromJson; + + /// Auto-reconnect setting + @override + bool get autoReconnect; + + /// Default brightness + @override + double get defaultBrightness; + + /// Gesture sensitivity + @override + double get gestureSensitivity; + + /// Display timeout in seconds + @override + int get displayTimeout; + + /// Power save mode enabled + @override + bool get powerSaveMode; + + /// Notification settings + @override + NotificationSettings get notifications; + + /// Create a copy of GlassesConfiguration + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GlassesConfigurationImplCopyWith<_$GlassesConfigurationImpl> + get copyWith => throw _privateConstructorUsedError; +} + +NotificationSettings _$NotificationSettingsFromJson(Map json) { + return _NotificationSettings.fromJson(json); +} + +/// @nodoc +mixin _$NotificationSettings { + /// Enable notifications + bool get enabled => throw _privateConstructorUsedError; + + /// Priority threshold + int get priorityThreshold => throw _privateConstructorUsedError; + + /// Vibration enabled + bool get vibrationEnabled => throw _privateConstructorUsedError; + + /// Sound enabled + bool get soundEnabled => throw _privateConstructorUsedError; + + /// Serializes this NotificationSettings to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of NotificationSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $NotificationSettingsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NotificationSettingsCopyWith<$Res> { + factory $NotificationSettingsCopyWith( + NotificationSettings value, + $Res Function(NotificationSettings) then, + ) = _$NotificationSettingsCopyWithImpl<$Res, NotificationSettings>; + @useResult + $Res call({ + bool enabled, + int priorityThreshold, + bool vibrationEnabled, + bool soundEnabled, + }); +} + +/// @nodoc +class _$NotificationSettingsCopyWithImpl< + $Res, + $Val extends NotificationSettings +> + implements $NotificationSettingsCopyWith<$Res> { + _$NotificationSettingsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of NotificationSettings + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? enabled = null, + Object? priorityThreshold = null, + Object? vibrationEnabled = null, + Object? soundEnabled = null, + }) { + return _then( + _value.copyWith( + enabled: + null == enabled + ? _value.enabled + : enabled // ignore: cast_nullable_to_non_nullable + as bool, + priorityThreshold: + null == priorityThreshold + ? _value.priorityThreshold + : priorityThreshold // ignore: cast_nullable_to_non_nullable + as int, + vibrationEnabled: + null == vibrationEnabled + ? _value.vibrationEnabled + : vibrationEnabled // ignore: cast_nullable_to_non_nullable + as bool, + soundEnabled: + null == soundEnabled + ? _value.soundEnabled + : soundEnabled // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$NotificationSettingsImplCopyWith<$Res> + implements $NotificationSettingsCopyWith<$Res> { + factory _$$NotificationSettingsImplCopyWith( + _$NotificationSettingsImpl value, + $Res Function(_$NotificationSettingsImpl) then, + ) = __$$NotificationSettingsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + bool enabled, + int priorityThreshold, + bool vibrationEnabled, + bool soundEnabled, + }); +} + +/// @nodoc +class __$$NotificationSettingsImplCopyWithImpl<$Res> + extends _$NotificationSettingsCopyWithImpl<$Res, _$NotificationSettingsImpl> + implements _$$NotificationSettingsImplCopyWith<$Res> { + __$$NotificationSettingsImplCopyWithImpl( + _$NotificationSettingsImpl _value, + $Res Function(_$NotificationSettingsImpl) _then, + ) : super(_value, _then); + + /// Create a copy of NotificationSettings + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? enabled = null, + Object? priorityThreshold = null, + Object? vibrationEnabled = null, + Object? soundEnabled = null, + }) { + return _then( + _$NotificationSettingsImpl( + enabled: + null == enabled + ? _value.enabled + : enabled // ignore: cast_nullable_to_non_nullable + as bool, + priorityThreshold: + null == priorityThreshold + ? _value.priorityThreshold + : priorityThreshold // ignore: cast_nullable_to_non_nullable + as int, + vibrationEnabled: + null == vibrationEnabled + ? _value.vibrationEnabled + : vibrationEnabled // ignore: cast_nullable_to_non_nullable + as bool, + soundEnabled: + null == soundEnabled + ? _value.soundEnabled + : soundEnabled // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$NotificationSettingsImpl implements _NotificationSettings { + const _$NotificationSettingsImpl({ + this.enabled = true, + this.priorityThreshold = 1, + this.vibrationEnabled = false, + this.soundEnabled = false, + }); + + factory _$NotificationSettingsImpl.fromJson(Map json) => + _$$NotificationSettingsImplFromJson(json); + + /// Enable notifications + @override + @JsonKey() + final bool enabled; + + /// Priority threshold + @override + @JsonKey() + final int priorityThreshold; + + /// Vibration enabled + @override + @JsonKey() + final bool vibrationEnabled; + + /// Sound enabled + @override + @JsonKey() + final bool soundEnabled; + + @override + String toString() { + return 'NotificationSettings(enabled: $enabled, priorityThreshold: $priorityThreshold, vibrationEnabled: $vibrationEnabled, soundEnabled: $soundEnabled)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NotificationSettingsImpl && + (identical(other.enabled, enabled) || other.enabled == enabled) && + (identical(other.priorityThreshold, priorityThreshold) || + other.priorityThreshold == priorityThreshold) && + (identical(other.vibrationEnabled, vibrationEnabled) || + other.vibrationEnabled == vibrationEnabled) && + (identical(other.soundEnabled, soundEnabled) || + other.soundEnabled == soundEnabled)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + enabled, + priorityThreshold, + vibrationEnabled, + soundEnabled, + ); + + /// Create a copy of NotificationSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$NotificationSettingsImplCopyWith<_$NotificationSettingsImpl> + get copyWith => + __$$NotificationSettingsImplCopyWithImpl<_$NotificationSettingsImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$NotificationSettingsImplToJson(this); + } +} + +abstract class _NotificationSettings implements NotificationSettings { + const factory _NotificationSettings({ + final bool enabled, + final int priorityThreshold, + final bool vibrationEnabled, + final bool soundEnabled, + }) = _$NotificationSettingsImpl; + + factory _NotificationSettings.fromJson(Map json) = + _$NotificationSettingsImpl.fromJson; + + /// Enable notifications + @override + bool get enabled; + + /// Priority threshold + @override + int get priorityThreshold; + + /// Vibration enabled + @override + bool get vibrationEnabled; + + /// Sound enabled + @override + bool get soundEnabled; + + /// Create a copy of NotificationSettings + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$NotificationSettingsImplCopyWith<_$NotificationSettingsImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/models/glasses_connection_state.g.dart b/lib/models/glasses_connection_state.g.dart new file mode 100644 index 0000000..16e9d8f --- /dev/null +++ b/lib/models/glasses_connection_state.g.dart @@ -0,0 +1,398 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'glasses_connection_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$GlassesConnectionStateImpl _$$GlassesConnectionStateImplFromJson( + Map json, +) => _$GlassesConnectionStateImpl( + status: + $enumDecodeNullable(_$ConnectionStatusEnumMap, json['status']) ?? + ConnectionStatus.disconnected, + connectedDevice: + json['connectedDevice'] == null + ? null + : GlassesDeviceInfo.fromJson( + json['connectedDevice'] as Map, + ), + discoveredDevices: + (json['discoveredDevices'] as List?) + ?.map((e) => GlassesDeviceInfo.fromJson(e as Map)) + .toList() ?? + const [], + lastConnectedTime: + json['lastConnectedTime'] == null + ? null + : DateTime.parse(json['lastConnectedTime'] as String), + connectionAttempts: (json['connectionAttempts'] as num?)?.toInt() ?? 0, + lastError: json['lastError'] as String?, + errorTimestamp: + json['errorTimestamp'] == null + ? null + : DateTime.parse(json['errorTimestamp'] as String), + autoReconnectEnabled: json['autoReconnectEnabled'] as bool? ?? true, + isScanning: json['isScanning'] as bool? ?? false, + scanTimeout: + json['scanTimeout'] == null + ? const Duration(seconds: 30) + : Duration(microseconds: (json['scanTimeout'] as num).toInt()), + connectionQuality: + json['connectionQuality'] == null + ? null + : ConnectionQuality.fromJson( + json['connectionQuality'] as Map, + ), + hudState: + json['hudState'] == null + ? const HUDDisplayState() + : HUDDisplayState.fromJson(json['hudState'] as Map), + metadata: json['metadata'] as Map? ?? const {}, +); + +Map _$$GlassesConnectionStateImplToJson( + _$GlassesConnectionStateImpl instance, +) => { + 'status': _$ConnectionStatusEnumMap[instance.status]!, + 'connectedDevice': instance.connectedDevice, + 'discoveredDevices': instance.discoveredDevices, + 'lastConnectedTime': instance.lastConnectedTime?.toIso8601String(), + 'connectionAttempts': instance.connectionAttempts, + 'lastError': instance.lastError, + 'errorTimestamp': instance.errorTimestamp?.toIso8601String(), + 'autoReconnectEnabled': instance.autoReconnectEnabled, + 'isScanning': instance.isScanning, + 'scanTimeout': instance.scanTimeout.inMicroseconds, + 'connectionQuality': instance.connectionQuality, + 'hudState': instance.hudState, + 'metadata': instance.metadata, +}; + +const _$ConnectionStatusEnumMap = { + ConnectionStatus.disconnected: 'disconnected', + ConnectionStatus.scanning: 'scanning', + ConnectionStatus.connecting: 'connecting', + ConnectionStatus.connected: 'connected', + ConnectionStatus.disconnecting: 'disconnecting', + ConnectionStatus.error: 'error', + ConnectionStatus.unauthorized: 'unauthorized', +}; + +_$GlassesDeviceInfoImpl _$$GlassesDeviceInfoImplFromJson( + Map json, +) => _$GlassesDeviceInfoImpl( + deviceId: json['deviceId'] as String, + name: json['name'] as String, + modelNumber: json['modelNumber'] as String?, + manufacturer: json['manufacturer'] as String? ?? 'Even Realities', + firmwareVersion: json['firmwareVersion'] as String?, + hardwareVersion: json['hardwareVersion'] as String?, + serialNumber: json['serialNumber'] as String?, + batteryLevel: (json['batteryLevel'] as num?)?.toDouble() ?? 0.0, + batteryStatus: + $enumDecodeNullable(_$BatteryStatusEnumMap, json['batteryStatus']) ?? + BatteryStatus.unknown, + isCharging: json['isCharging'] as bool? ?? false, + rssi: (json['rssi'] as num?)?.toInt() ?? -100, + signalStrength: + $enumDecodeNullable(_$SignalStrengthEnumMap, json['signalStrength']) ?? + SignalStrength.unknown, + health: + $enumDecodeNullable(_$DeviceHealthEnumMap, json['health']) ?? + DeviceHealth.unknown, + isConnected: json['isConnected'] as bool? ?? false, + lastSeen: + json['lastSeen'] == null + ? null + : DateTime.parse(json['lastSeen'] as String), + capabilities: + json['capabilities'] == null + ? const GlassesCapabilities() + : GlassesCapabilities.fromJson( + json['capabilities'] as Map, + ), + configuration: + json['configuration'] == null + ? const GlassesConfiguration() + : GlassesConfiguration.fromJson( + json['configuration'] as Map, + ), + metadata: json['metadata'] as Map? ?? const {}, +); + +Map _$$GlassesDeviceInfoImplToJson( + _$GlassesDeviceInfoImpl instance, +) => { + 'deviceId': instance.deviceId, + 'name': instance.name, + 'modelNumber': instance.modelNumber, + 'manufacturer': instance.manufacturer, + 'firmwareVersion': instance.firmwareVersion, + 'hardwareVersion': instance.hardwareVersion, + 'serialNumber': instance.serialNumber, + 'batteryLevel': instance.batteryLevel, + 'batteryStatus': _$BatteryStatusEnumMap[instance.batteryStatus]!, + 'isCharging': instance.isCharging, + 'rssi': instance.rssi, + 'signalStrength': _$SignalStrengthEnumMap[instance.signalStrength]!, + 'health': _$DeviceHealthEnumMap[instance.health]!, + 'isConnected': instance.isConnected, + 'lastSeen': instance.lastSeen?.toIso8601String(), + 'capabilities': instance.capabilities, + 'configuration': instance.configuration, + 'metadata': instance.metadata, +}; + +const _$BatteryStatusEnumMap = { + BatteryStatus.charging: 'charging', + BatteryStatus.full: 'full', + BatteryStatus.high: 'high', + BatteryStatus.medium: 'medium', + BatteryStatus.low: 'low', + BatteryStatus.critical: 'critical', + BatteryStatus.unknown: 'unknown', +}; + +const _$SignalStrengthEnumMap = { + SignalStrength.excellent: 'excellent', + SignalStrength.good: 'good', + SignalStrength.fair: 'fair', + SignalStrength.poor: 'poor', + SignalStrength.unknown: 'unknown', +}; + +const _$DeviceHealthEnumMap = { + DeviceHealth.excellent: 'excellent', + DeviceHealth.good: 'good', + DeviceHealth.warning: 'warning', + DeviceHealth.critical: 'critical', + DeviceHealth.unknown: 'unknown', +}; + +_$ConnectionQualityImpl _$$ConnectionQualityImplFromJson( + Map json, +) => _$ConnectionQualityImpl( + signalStrength: + $enumDecodeNullable(_$SignalStrengthEnumMap, json['signalStrength']) ?? + SignalStrength.unknown, + rssi: (json['rssi'] as num?)?.toInt() ?? -100, + stabilityScore: (json['stabilityScore'] as num?)?.toDouble() ?? 0.0, + packetLoss: (json['packetLoss'] as num?)?.toDouble() ?? 0.0, + latencyMs: (json['latencyMs'] as num?)?.toInt() ?? 0, + recentDisconnections: (json['recentDisconnections'] as num?)?.toInt() ?? 0, + dataRate: (json['dataRate'] as num?)?.toInt() ?? 0, + timestamp: DateTime.parse(json['timestamp'] as String), +); + +Map _$$ConnectionQualityImplToJson( + _$ConnectionQualityImpl instance, +) => { + 'signalStrength': _$SignalStrengthEnumMap[instance.signalStrength]!, + 'rssi': instance.rssi, + 'stabilityScore': instance.stabilityScore, + 'packetLoss': instance.packetLoss, + 'latencyMs': instance.latencyMs, + 'recentDisconnections': instance.recentDisconnections, + 'dataRate': instance.dataRate, + 'timestamp': instance.timestamp.toIso8601String(), +}; + +_$HUDDisplayStateImpl _$$HUDDisplayStateImplFromJson( + Map json, +) => _$HUDDisplayStateImpl( + isActive: json['isActive'] as bool? ?? false, + brightness: (json['brightness'] as num?)?.toDouble() ?? 0.8, + currentContent: json['currentContent'] as String?, + contentType: $enumDecodeNullable( + _$HUDContentTypeEnumMap, + json['contentType'], + ), + position: + $enumDecodeNullable(_$HUDPositionEnumMap, json['position']) ?? + HUDPosition.center, + style: + json['style'] == null + ? const HUDStyleSettings() + : HUDStyleSettings.fromJson(json['style'] as Map), + isPaused: json['isPaused'] as bool? ?? false, + lastUpdate: + json['lastUpdate'] == null + ? null + : DateTime.parse(json['lastUpdate'] as String), + displayQueue: + (json['displayQueue'] as List?) + ?.map((e) => HUDQueueItem.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$$HUDDisplayStateImplToJson( + _$HUDDisplayStateImpl instance, +) => { + 'isActive': instance.isActive, + 'brightness': instance.brightness, + 'currentContent': instance.currentContent, + 'contentType': _$HUDContentTypeEnumMap[instance.contentType], + 'position': _$HUDPositionEnumMap[instance.position]!, + 'style': instance.style, + 'isPaused': instance.isPaused, + 'lastUpdate': instance.lastUpdate?.toIso8601String(), + 'displayQueue': instance.displayQueue, +}; + +const _$HUDContentTypeEnumMap = { + HUDContentType.text: 'text', + HUDContentType.notification: 'notification', + HUDContentType.menu: 'menu', + HUDContentType.status: 'status', + HUDContentType.image: 'image', + HUDContentType.animation: 'animation', +}; + +const _$HUDPositionEnumMap = { + HUDPosition.topLeft: 'topLeft', + HUDPosition.topCenter: 'topCenter', + HUDPosition.topRight: 'topRight', + HUDPosition.centerLeft: 'centerLeft', + HUDPosition.center: 'center', + HUDPosition.centerRight: 'centerRight', + HUDPosition.bottomLeft: 'bottomLeft', + HUDPosition.bottomCenter: 'bottomCenter', + HUDPosition.bottomRight: 'bottomRight', +}; + +_$HUDStyleSettingsImpl _$$HUDStyleSettingsImplFromJson( + Map json, +) => _$HUDStyleSettingsImpl( + fontSize: (json['fontSize'] as num?)?.toDouble() ?? 16.0, + textColor: json['textColor'] as String? ?? '#FFFFFF', + backgroundColor: json['backgroundColor'] as String? ?? '#000000', + fontWeight: json['fontWeight'] as String? ?? 'normal', + alignment: json['alignment'] as String? ?? 'center', + displayDuration: (json['displayDuration'] as num?)?.toInt() ?? 5, + animation: json['animation'] as String? ?? 'fade', +); + +Map _$$HUDStyleSettingsImplToJson( + _$HUDStyleSettingsImpl instance, +) => { + 'fontSize': instance.fontSize, + 'textColor': instance.textColor, + 'backgroundColor': instance.backgroundColor, + 'fontWeight': instance.fontWeight, + 'alignment': instance.alignment, + 'displayDuration': instance.displayDuration, + 'animation': instance.animation, +}; + +_$HUDQueueItemImpl _$$HUDQueueItemImplFromJson(Map json) => + _$HUDQueueItemImpl( + content: json['content'] as String, + type: $enumDecode(_$HUDContentTypeEnumMap, json['type']), + position: + $enumDecodeNullable(_$HUDPositionEnumMap, json['position']) ?? + HUDPosition.center, + priority: (json['priority'] as num?)?.toInt() ?? 1, + queuedAt: DateTime.parse(json['queuedAt'] as String), + duration: + json['duration'] == null + ? const Duration(seconds: 5) + : Duration(microseconds: (json['duration'] as num).toInt()), + styleOverrides: + json['styleOverrides'] == null + ? null + : HUDStyleSettings.fromJson( + json['styleOverrides'] as Map, + ), + ); + +Map _$$HUDQueueItemImplToJson(_$HUDQueueItemImpl instance) => + { + 'content': instance.content, + 'type': _$HUDContentTypeEnumMap[instance.type]!, + 'position': _$HUDPositionEnumMap[instance.position]!, + 'priority': instance.priority, + 'queuedAt': instance.queuedAt.toIso8601String(), + 'duration': instance.duration.inMicroseconds, + 'styleOverrides': instance.styleOverrides, + }; + +_$GlassesCapabilitiesImpl _$$GlassesCapabilitiesImplFromJson( + Map json, +) => _$GlassesCapabilitiesImpl( + supportsText: json['supportsText'] as bool? ?? true, + supportsImages: json['supportsImages'] as bool? ?? false, + supportsAnimations: json['supportsAnimations'] as bool? ?? false, + supportsTouchGestures: json['supportsTouchGestures'] as bool? ?? true, + supportsVoiceCommands: json['supportsVoiceCommands'] as bool? ?? false, + maxTextLength: (json['maxTextLength'] as num?)?.toInt() ?? 256, + supportedPositions: + (json['supportedPositions'] as List?) + ?.map((e) => $enumDecode(_$HUDPositionEnumMap, e)) + .toList() ?? + const [HUDPosition.center], + supportsBatteryMonitoring: json['supportsBatteryMonitoring'] as bool? ?? true, + supportsFirmwareUpdate: json['supportsFirmwareUpdate'] as bool? ?? true, +); + +Map _$$GlassesCapabilitiesImplToJson( + _$GlassesCapabilitiesImpl instance, +) => { + 'supportsText': instance.supportsText, + 'supportsImages': instance.supportsImages, + 'supportsAnimations': instance.supportsAnimations, + 'supportsTouchGestures': instance.supportsTouchGestures, + 'supportsVoiceCommands': instance.supportsVoiceCommands, + 'maxTextLength': instance.maxTextLength, + 'supportedPositions': + instance.supportedPositions.map((e) => _$HUDPositionEnumMap[e]!).toList(), + 'supportsBatteryMonitoring': instance.supportsBatteryMonitoring, + 'supportsFirmwareUpdate': instance.supportsFirmwareUpdate, +}; + +_$GlassesConfigurationImpl _$$GlassesConfigurationImplFromJson( + Map json, +) => _$GlassesConfigurationImpl( + autoReconnect: json['autoReconnect'] as bool? ?? true, + defaultBrightness: (json['defaultBrightness'] as num?)?.toDouble() ?? 0.8, + gestureSensitivity: (json['gestureSensitivity'] as num?)?.toDouble() ?? 0.5, + displayTimeout: (json['displayTimeout'] as num?)?.toInt() ?? 10, + powerSaveMode: json['powerSaveMode'] as bool? ?? false, + notifications: + json['notifications'] == null + ? const NotificationSettings() + : NotificationSettings.fromJson( + json['notifications'] as Map, + ), +); + +Map _$$GlassesConfigurationImplToJson( + _$GlassesConfigurationImpl instance, +) => { + 'autoReconnect': instance.autoReconnect, + 'defaultBrightness': instance.defaultBrightness, + 'gestureSensitivity': instance.gestureSensitivity, + 'displayTimeout': instance.displayTimeout, + 'powerSaveMode': instance.powerSaveMode, + 'notifications': instance.notifications, +}; + +_$NotificationSettingsImpl _$$NotificationSettingsImplFromJson( + Map json, +) => _$NotificationSettingsImpl( + enabled: json['enabled'] as bool? ?? true, + priorityThreshold: (json['priorityThreshold'] as num?)?.toInt() ?? 1, + vibrationEnabled: json['vibrationEnabled'] as bool? ?? false, + soundEnabled: json['soundEnabled'] as bool? ?? false, +); + +Map _$$NotificationSettingsImplToJson( + _$NotificationSettingsImpl instance, +) => { + 'enabled': instance.enabled, + 'priorityThreshold': instance.priorityThreshold, + 'vibrationEnabled': instance.vibrationEnabled, + 'soundEnabled': instance.soundEnabled, +}; diff --git a/lib/models/transcription_segment.dart b/lib/models/transcription_segment.dart new file mode 100644 index 0000000..6e2f77b --- /dev/null +++ b/lib/models/transcription_segment.dart @@ -0,0 +1,185 @@ +// ABOUTME: Transcription segment data model for speech-to-text results +// ABOUTME: Represents individual pieces of transcribed speech with timing and metadata + +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../services/transcription_service.dart'; + +part 'transcription_segment.freezed.dart'; +part 'transcription_segment.g.dart'; + +// JSON converters for TranscriptionBackend enum +TranscriptionBackend? _backendFromJson(String? json) { + if (json == null) return null; + return TranscriptionBackend.values + .where((e) => e.name == json) + .firstOrNull; +} + +String? _backendToJson(TranscriptionBackend? backend) => backend?.name; + +/// Transcription segment representing a piece of spoken text +@freezed +class TranscriptionSegment with _$TranscriptionSegment { + const factory TranscriptionSegment({ + /// Transcribed text content + required String text, + + /// Start time of the segment + required DateTime startTime, + + /// End time of the segment + required DateTime endTime, + + /// Confidence score for the transcription (0.0 to 1.0) + required double confidence, + + /// Speaker information (if available) + String? speakerId, + + /// Speaker name (if known) + String? speakerName, + + /// Language code for the transcribed text + @Default('en-US') String language, + + /// Whether this is a final transcription or interim result + @Default(true) bool isFinal, + + /// Unique identifier for this segment + String? segmentId, + + /// Transcription backend used + @JsonKey(fromJson: _backendFromJson, toJson: _backendToJson) TranscriptionBackend? backend, + + /// Processing time in milliseconds + int? processingTimeMs, + + /// Additional metadata + @Default({}) Map metadata, + }) = _TranscriptionSegment; + + factory TranscriptionSegment.fromJson(Map json) => + _$TranscriptionSegmentFromJson(json); + + /// Create a new segment with updated text (for interim results) + const TranscriptionSegment._(); + + /// Duration of this segment + Duration get duration => endTime.difference(startTime); + + /// Duration of this segment in milliseconds + int get durationMs => duration.inMilliseconds; + + /// Whether this segment has speaker information + bool get hasSpeakerInfo => speakerId != null || speakerName != null; + + /// Display name for the speaker + String get speakerDisplayName { + if (speakerName != null) return speakerName!; + if (speakerId != null) return 'Speaker $speakerId'; + return 'Unknown Speaker'; + } + + /// Whether this is a high-confidence transcription + bool get isHighConfidence => confidence >= 0.8; + + /// Whether this is a low-confidence transcription + bool get isLowConfidence => confidence < 0.5; + + /// Formatted time range string + String get timeRangeString { + return '${_formatDateTime(startTime)} - ${_formatDateTime(endTime)}'; + } + + String _formatDateTime(DateTime dateTime) { + return '${dateTime.hour.toString().padLeft(2, '0')}:' + '${dateTime.minute.toString().padLeft(2, '0')}:' + '${dateTime.second.toString().padLeft(2, '0')}'; + } +} + +/// Collection of transcription segments for a conversation +@freezed +class TranscriptionResult with _$TranscriptionResult { + const factory TranscriptionResult({ + /// Unique identifier for this transcription result + required String id, + + /// List of transcription segments + required List segments, + + /// Overall confidence score for the entire transcription + required double overallConfidence, + + /// Total duration of the transcription + required Duration totalDuration, + + /// Language code for the transcription + @Default('en-US') String language, + + /// Transcription backend used + String? backend, + + /// Total processing time + Duration? processingTime, + + /// Number of speakers detected + @Default(1) int speakerCount, + + /// Whether speaker diarization was performed + @Default(false) bool hasSpeakerDiarization, + + /// Additional metadata for the entire transcription + @Default({}) Map metadata, + + /// Timestamp when this result was created + required DateTime timestamp, + }) = _TranscriptionResult; + + factory TranscriptionResult.fromJson(Map json) => + _$TranscriptionResultFromJson(json); + + const TranscriptionResult._(); + + /// Get the full transcribed text + String get fullText => segments.map((s) => s.text).join(' '); + + /// Get segments for a specific speaker + List getSegmentsForSpeaker(String speakerId) { + return segments.where((s) => s.speakerId == speakerId).toList(); + } + + /// Get all unique speaker IDs + List get speakerIds { + return segments + .where((s) => s.speakerId != null) + .map((s) => s.speakerId!) + .toSet() + .toList(); + } + + /// Get segments within a time range + List getSegmentsInRange(DateTime start, DateTime end) { + return segments + .where((s) => s.startTime.isAfter(start) && s.endTime.isBefore(end)) + .toList(); + } + + /// Get high-confidence segments only + List get highConfidenceSegments { + return segments.where((s) => s.isHighConfidence).toList(); + } + + /// Get low-confidence segments that may need review + List get lowConfidenceSegments { + return segments.where((s) => s.isLowConfidence).toList(); + } + + /// Calculate words per minute + double get wordsPerMinute { + final wordCount = fullText.split(' ').length; + final minutes = totalDuration.inMilliseconds / 60000.0; + return minutes > 0 ? wordCount / minutes : 0.0; + } +} \ No newline at end of file diff --git a/lib/models/transcription_segment.freezed.dart b/lib/models/transcription_segment.freezed.dart new file mode 100644 index 0000000..6a41f20 --- /dev/null +++ b/lib/models/transcription_segment.freezed.dart @@ -0,0 +1,1030 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'transcription_segment.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +TranscriptionSegment _$TranscriptionSegmentFromJson(Map json) { + return _TranscriptionSegment.fromJson(json); +} + +/// @nodoc +mixin _$TranscriptionSegment { + /// Transcribed text content + String get text => throw _privateConstructorUsedError; + + /// Start time of the segment + DateTime get startTime => throw _privateConstructorUsedError; + + /// End time of the segment + DateTime get endTime => throw _privateConstructorUsedError; + + /// Confidence score for the transcription (0.0 to 1.0) + double get confidence => throw _privateConstructorUsedError; + + /// Speaker information (if available) + String? get speakerId => throw _privateConstructorUsedError; + + /// Speaker name (if known) + String? get speakerName => throw _privateConstructorUsedError; + + /// Language code for the transcribed text + String get language => throw _privateConstructorUsedError; + + /// Whether this is a final transcription or interim result + bool get isFinal => throw _privateConstructorUsedError; + + /// Unique identifier for this segment + String? get segmentId => throw _privateConstructorUsedError; + + /// Transcription backend used + @JsonKey(fromJson: _backendFromJson, toJson: _backendToJson) + TranscriptionBackend? get backend => throw _privateConstructorUsedError; + + /// Processing time in milliseconds + int? get processingTimeMs => throw _privateConstructorUsedError; + + /// Additional metadata + Map get metadata => throw _privateConstructorUsedError; + + /// Serializes this TranscriptionSegment to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of TranscriptionSegment + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TranscriptionSegmentCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TranscriptionSegmentCopyWith<$Res> { + factory $TranscriptionSegmentCopyWith( + TranscriptionSegment value, + $Res Function(TranscriptionSegment) then, + ) = _$TranscriptionSegmentCopyWithImpl<$Res, TranscriptionSegment>; + @useResult + $Res call({ + String text, + DateTime startTime, + DateTime endTime, + double confidence, + String? speakerId, + String? speakerName, + String language, + bool isFinal, + String? segmentId, + @JsonKey(fromJson: _backendFromJson, toJson: _backendToJson) + TranscriptionBackend? backend, + int? processingTimeMs, + Map metadata, + }); +} + +/// @nodoc +class _$TranscriptionSegmentCopyWithImpl< + $Res, + $Val extends TranscriptionSegment +> + implements $TranscriptionSegmentCopyWith<$Res> { + _$TranscriptionSegmentCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TranscriptionSegment + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? text = null, + Object? startTime = null, + Object? endTime = null, + Object? confidence = null, + Object? speakerId = freezed, + Object? speakerName = freezed, + Object? language = null, + Object? isFinal = null, + Object? segmentId = freezed, + Object? backend = freezed, + Object? processingTimeMs = freezed, + Object? metadata = null, + }) { + return _then( + _value.copyWith( + text: + null == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + startTime: + null == startTime + ? _value.startTime + : startTime // ignore: cast_nullable_to_non_nullable + as DateTime, + endTime: + null == endTime + ? _value.endTime + : endTime // ignore: cast_nullable_to_non_nullable + as DateTime, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + speakerId: + freezed == speakerId + ? _value.speakerId + : speakerId // ignore: cast_nullable_to_non_nullable + as String?, + speakerName: + freezed == speakerName + ? _value.speakerName + : speakerName // ignore: cast_nullable_to_non_nullable + as String?, + language: + null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, + isFinal: + null == isFinal + ? _value.isFinal + : isFinal // ignore: cast_nullable_to_non_nullable + as bool, + segmentId: + freezed == segmentId + ? _value.segmentId + : segmentId // ignore: cast_nullable_to_non_nullable + as String?, + backend: + freezed == backend + ? _value.backend + : backend // ignore: cast_nullable_to_non_nullable + as TranscriptionBackend?, + processingTimeMs: + freezed == processingTimeMs + ? _value.processingTimeMs + : processingTimeMs // ignore: cast_nullable_to_non_nullable + as int?, + metadata: + null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$TranscriptionSegmentImplCopyWith<$Res> + implements $TranscriptionSegmentCopyWith<$Res> { + factory _$$TranscriptionSegmentImplCopyWith( + _$TranscriptionSegmentImpl value, + $Res Function(_$TranscriptionSegmentImpl) then, + ) = __$$TranscriptionSegmentImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String text, + DateTime startTime, + DateTime endTime, + double confidence, + String? speakerId, + String? speakerName, + String language, + bool isFinal, + String? segmentId, + @JsonKey(fromJson: _backendFromJson, toJson: _backendToJson) + TranscriptionBackend? backend, + int? processingTimeMs, + Map metadata, + }); +} + +/// @nodoc +class __$$TranscriptionSegmentImplCopyWithImpl<$Res> + extends _$TranscriptionSegmentCopyWithImpl<$Res, _$TranscriptionSegmentImpl> + implements _$$TranscriptionSegmentImplCopyWith<$Res> { + __$$TranscriptionSegmentImplCopyWithImpl( + _$TranscriptionSegmentImpl _value, + $Res Function(_$TranscriptionSegmentImpl) _then, + ) : super(_value, _then); + + /// Create a copy of TranscriptionSegment + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? text = null, + Object? startTime = null, + Object? endTime = null, + Object? confidence = null, + Object? speakerId = freezed, + Object? speakerName = freezed, + Object? language = null, + Object? isFinal = null, + Object? segmentId = freezed, + Object? backend = freezed, + Object? processingTimeMs = freezed, + Object? metadata = null, + }) { + return _then( + _$TranscriptionSegmentImpl( + text: + null == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + startTime: + null == startTime + ? _value.startTime + : startTime // ignore: cast_nullable_to_non_nullable + as DateTime, + endTime: + null == endTime + ? _value.endTime + : endTime // ignore: cast_nullable_to_non_nullable + as DateTime, + confidence: + null == confidence + ? _value.confidence + : confidence // ignore: cast_nullable_to_non_nullable + as double, + speakerId: + freezed == speakerId + ? _value.speakerId + : speakerId // ignore: cast_nullable_to_non_nullable + as String?, + speakerName: + freezed == speakerName + ? _value.speakerName + : speakerName // ignore: cast_nullable_to_non_nullable + as String?, + language: + null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, + isFinal: + null == isFinal + ? _value.isFinal + : isFinal // ignore: cast_nullable_to_non_nullable + as bool, + segmentId: + freezed == segmentId + ? _value.segmentId + : segmentId // ignore: cast_nullable_to_non_nullable + as String?, + backend: + freezed == backend + ? _value.backend + : backend // ignore: cast_nullable_to_non_nullable + as TranscriptionBackend?, + processingTimeMs: + freezed == processingTimeMs + ? _value.processingTimeMs + : processingTimeMs // ignore: cast_nullable_to_non_nullable + as int?, + metadata: + null == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$TranscriptionSegmentImpl extends _TranscriptionSegment { + const _$TranscriptionSegmentImpl({ + required this.text, + required this.startTime, + required this.endTime, + required this.confidence, + this.speakerId, + this.speakerName, + this.language = 'en-US', + this.isFinal = true, + this.segmentId, + @JsonKey(fromJson: _backendFromJson, toJson: _backendToJson) this.backend, + this.processingTimeMs, + final Map metadata = const {}, + }) : _metadata = metadata, + super._(); + + factory _$TranscriptionSegmentImpl.fromJson(Map json) => + _$$TranscriptionSegmentImplFromJson(json); + + /// Transcribed text content + @override + final String text; + + /// Start time of the segment + @override + final DateTime startTime; + + /// End time of the segment + @override + final DateTime endTime; + + /// Confidence score for the transcription (0.0 to 1.0) + @override + final double confidence; + + /// Speaker information (if available) + @override + final String? speakerId; + + /// Speaker name (if known) + @override + final String? speakerName; + + /// Language code for the transcribed text + @override + @JsonKey() + final String language; + + /// Whether this is a final transcription or interim result + @override + @JsonKey() + final bool isFinal; + + /// Unique identifier for this segment + @override + final String? segmentId; + + /// Transcription backend used + @override + @JsonKey(fromJson: _backendFromJson, toJson: _backendToJson) + final TranscriptionBackend? backend; + + /// Processing time in milliseconds + @override + final int? processingTimeMs; + + /// Additional metadata + final Map _metadata; + + /// Additional metadata + @override + @JsonKey() + Map get metadata { + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_metadata); + } + + @override + String toString() { + return 'TranscriptionSegment(text: $text, startTime: $startTime, endTime: $endTime, confidence: $confidence, speakerId: $speakerId, speakerName: $speakerName, language: $language, isFinal: $isFinal, segmentId: $segmentId, backend: $backend, processingTimeMs: $processingTimeMs, metadata: $metadata)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TranscriptionSegmentImpl && + (identical(other.text, text) || other.text == text) && + (identical(other.startTime, startTime) || + other.startTime == startTime) && + (identical(other.endTime, endTime) || other.endTime == endTime) && + (identical(other.confidence, confidence) || + other.confidence == confidence) && + (identical(other.speakerId, speakerId) || + other.speakerId == speakerId) && + (identical(other.speakerName, speakerName) || + other.speakerName == speakerName) && + (identical(other.language, language) || + other.language == language) && + (identical(other.isFinal, isFinal) || other.isFinal == isFinal) && + (identical(other.segmentId, segmentId) || + other.segmentId == segmentId) && + (identical(other.backend, backend) || other.backend == backend) && + (identical(other.processingTimeMs, processingTimeMs) || + other.processingTimeMs == processingTimeMs) && + const DeepCollectionEquality().equals(other._metadata, _metadata)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + text, + startTime, + endTime, + confidence, + speakerId, + speakerName, + language, + isFinal, + segmentId, + backend, + processingTimeMs, + const DeepCollectionEquality().hash(_metadata), + ); + + /// Create a copy of TranscriptionSegment + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TranscriptionSegmentImplCopyWith<_$TranscriptionSegmentImpl> + get copyWith => + __$$TranscriptionSegmentImplCopyWithImpl<_$TranscriptionSegmentImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$TranscriptionSegmentImplToJson(this); + } +} + +abstract class _TranscriptionSegment extends TranscriptionSegment { + const factory _TranscriptionSegment({ + required final String text, + required final DateTime startTime, + required final DateTime endTime, + required final double confidence, + final String? speakerId, + final String? speakerName, + final String language, + final bool isFinal, + final String? segmentId, + @JsonKey(fromJson: _backendFromJson, toJson: _backendToJson) + final TranscriptionBackend? backend, + final int? processingTimeMs, + final Map metadata, + }) = _$TranscriptionSegmentImpl; + const _TranscriptionSegment._() : super._(); + + factory _TranscriptionSegment.fromJson(Map json) = + _$TranscriptionSegmentImpl.fromJson; + + /// Transcribed text content + @override + String get text; + + /// Start time of the segment + @override + DateTime get startTime; + + /// End time of the segment + @override + DateTime get endTime; + + /// Confidence score for the transcription (0.0 to 1.0) + @override + double get confidence; + + /// Speaker information (if available) + @override + String? get speakerId; + + /// Speaker name (if known) + @override + String? get speakerName; + + /// Language code for the transcribed text + @override + String get language; + + /// Whether this is a final transcription or interim result + @override + bool get isFinal; + + /// Unique identifier for this segment + @override + String? get segmentId; + + /// Transcription backend used + @override + @JsonKey(fromJson: _backendFromJson, toJson: _backendToJson) + TranscriptionBackend? get backend; + + /// Processing time in milliseconds + @override + int? get processingTimeMs; + + /// Additional metadata + @override + Map get metadata; + + /// Create a copy of TranscriptionSegment + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TranscriptionSegmentImplCopyWith<_$TranscriptionSegmentImpl> + get copyWith => throw _privateConstructorUsedError; +} + +TranscriptionResult _$TranscriptionResultFromJson(Map json) { + return _TranscriptionResult.fromJson(json); +} + +/// @nodoc +mixin _$TranscriptionResult { + /// Unique identifier for this transcription result + String get id => throw _privateConstructorUsedError; + + /// List of transcription segments + List get segments => throw _privateConstructorUsedError; + + /// Overall confidence score for the entire transcription + double get overallConfidence => throw _privateConstructorUsedError; + + /// Total duration of the transcription + Duration get totalDuration => throw _privateConstructorUsedError; + + /// Language code for the transcription + String get language => throw _privateConstructorUsedError; + + /// Transcription backend used + String? get backend => throw _privateConstructorUsedError; + + /// Total processing time + Duration? get processingTime => throw _privateConstructorUsedError; + + /// Number of speakers detected + int get speakerCount => throw _privateConstructorUsedError; + + /// Whether speaker diarization was performed + bool get hasSpeakerDiarization => throw _privateConstructorUsedError; + + /// Additional metadata for the entire transcription + Map get metadata => throw _privateConstructorUsedError; + + /// Timestamp when this result was created + DateTime get timestamp => throw _privateConstructorUsedError; + + /// Serializes this TranscriptionResult to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of TranscriptionResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TranscriptionResultCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TranscriptionResultCopyWith<$Res> { + factory $TranscriptionResultCopyWith( + TranscriptionResult value, + $Res Function(TranscriptionResult) then, + ) = _$TranscriptionResultCopyWithImpl<$Res, TranscriptionResult>; + @useResult + $Res call({ + String id, + List segments, + double overallConfidence, + Duration totalDuration, + String language, + String? backend, + Duration? processingTime, + int speakerCount, + bool hasSpeakerDiarization, + Map metadata, + DateTime timestamp, + }); +} + +/// @nodoc +class _$TranscriptionResultCopyWithImpl<$Res, $Val extends TranscriptionResult> + implements $TranscriptionResultCopyWith<$Res> { + _$TranscriptionResultCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TranscriptionResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? segments = null, + Object? overallConfidence = null, + Object? totalDuration = null, + Object? language = null, + Object? backend = freezed, + Object? processingTime = freezed, + Object? speakerCount = null, + Object? hasSpeakerDiarization = null, + Object? metadata = null, + Object? timestamp = null, + }) { + return _then( + _value.copyWith( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + segments: + null == segments + ? _value.segments + : segments // ignore: cast_nullable_to_non_nullable + as List, + overallConfidence: + null == overallConfidence + ? _value.overallConfidence + : overallConfidence // ignore: cast_nullable_to_non_nullable + as double, + totalDuration: + null == totalDuration + ? _value.totalDuration + : totalDuration // ignore: cast_nullable_to_non_nullable + as Duration, + language: + null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, + backend: + freezed == backend + ? _value.backend + : backend // ignore: cast_nullable_to_non_nullable + as String?, + processingTime: + freezed == processingTime + ? _value.processingTime + : processingTime // ignore: cast_nullable_to_non_nullable + as Duration?, + speakerCount: + null == speakerCount + ? _value.speakerCount + : speakerCount // ignore: cast_nullable_to_non_nullable + as int, + hasSpeakerDiarization: + null == hasSpeakerDiarization + ? _value.hasSpeakerDiarization + : hasSpeakerDiarization // ignore: cast_nullable_to_non_nullable + as bool, + metadata: + null == metadata + ? _value.metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + timestamp: + null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as DateTime, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$TranscriptionResultImplCopyWith<$Res> + implements $TranscriptionResultCopyWith<$Res> { + factory _$$TranscriptionResultImplCopyWith( + _$TranscriptionResultImpl value, + $Res Function(_$TranscriptionResultImpl) then, + ) = __$$TranscriptionResultImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + List segments, + double overallConfidence, + Duration totalDuration, + String language, + String? backend, + Duration? processingTime, + int speakerCount, + bool hasSpeakerDiarization, + Map metadata, + DateTime timestamp, + }); +} + +/// @nodoc +class __$$TranscriptionResultImplCopyWithImpl<$Res> + extends _$TranscriptionResultCopyWithImpl<$Res, _$TranscriptionResultImpl> + implements _$$TranscriptionResultImplCopyWith<$Res> { + __$$TranscriptionResultImplCopyWithImpl( + _$TranscriptionResultImpl _value, + $Res Function(_$TranscriptionResultImpl) _then, + ) : super(_value, _then); + + /// Create a copy of TranscriptionResult + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? segments = null, + Object? overallConfidence = null, + Object? totalDuration = null, + Object? language = null, + Object? backend = freezed, + Object? processingTime = freezed, + Object? speakerCount = null, + Object? hasSpeakerDiarization = null, + Object? metadata = null, + Object? timestamp = null, + }) { + return _then( + _$TranscriptionResultImpl( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + segments: + null == segments + ? _value._segments + : segments // ignore: cast_nullable_to_non_nullable + as List, + overallConfidence: + null == overallConfidence + ? _value.overallConfidence + : overallConfidence // ignore: cast_nullable_to_non_nullable + as double, + totalDuration: + null == totalDuration + ? _value.totalDuration + : totalDuration // ignore: cast_nullable_to_non_nullable + as Duration, + language: + null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as String, + backend: + freezed == backend + ? _value.backend + : backend // ignore: cast_nullable_to_non_nullable + as String?, + processingTime: + freezed == processingTime + ? _value.processingTime + : processingTime // ignore: cast_nullable_to_non_nullable + as Duration?, + speakerCount: + null == speakerCount + ? _value.speakerCount + : speakerCount // ignore: cast_nullable_to_non_nullable + as int, + hasSpeakerDiarization: + null == hasSpeakerDiarization + ? _value.hasSpeakerDiarization + : hasSpeakerDiarization // ignore: cast_nullable_to_non_nullable + as bool, + metadata: + null == metadata + ? _value._metadata + : metadata // ignore: cast_nullable_to_non_nullable + as Map, + timestamp: + null == timestamp + ? _value.timestamp + : timestamp // ignore: cast_nullable_to_non_nullable + as DateTime, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$TranscriptionResultImpl extends _TranscriptionResult { + const _$TranscriptionResultImpl({ + required this.id, + required final List segments, + required this.overallConfidence, + required this.totalDuration, + this.language = 'en-US', + this.backend, + this.processingTime, + this.speakerCount = 1, + this.hasSpeakerDiarization = false, + final Map metadata = const {}, + required this.timestamp, + }) : _segments = segments, + _metadata = metadata, + super._(); + + factory _$TranscriptionResultImpl.fromJson(Map json) => + _$$TranscriptionResultImplFromJson(json); + + /// Unique identifier for this transcription result + @override + final String id; + + /// List of transcription segments + final List _segments; + + /// List of transcription segments + @override + List get segments { + if (_segments is EqualUnmodifiableListView) return _segments; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_segments); + } + + /// Overall confidence score for the entire transcription + @override + final double overallConfidence; + + /// Total duration of the transcription + @override + final Duration totalDuration; + + /// Language code for the transcription + @override + @JsonKey() + final String language; + + /// Transcription backend used + @override + final String? backend; + + /// Total processing time + @override + final Duration? processingTime; + + /// Number of speakers detected + @override + @JsonKey() + final int speakerCount; + + /// Whether speaker diarization was performed + @override + @JsonKey() + final bool hasSpeakerDiarization; + + /// Additional metadata for the entire transcription + final Map _metadata; + + /// Additional metadata for the entire transcription + @override + @JsonKey() + Map get metadata { + if (_metadata is EqualUnmodifiableMapView) return _metadata; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_metadata); + } + + /// Timestamp when this result was created + @override + final DateTime timestamp; + + @override + String toString() { + return 'TranscriptionResult(id: $id, segments: $segments, overallConfidence: $overallConfidence, totalDuration: $totalDuration, language: $language, backend: $backend, processingTime: $processingTime, speakerCount: $speakerCount, hasSpeakerDiarization: $hasSpeakerDiarization, metadata: $metadata, timestamp: $timestamp)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TranscriptionResultImpl && + (identical(other.id, id) || other.id == id) && + const DeepCollectionEquality().equals(other._segments, _segments) && + (identical(other.overallConfidence, overallConfidence) || + other.overallConfidence == overallConfidence) && + (identical(other.totalDuration, totalDuration) || + other.totalDuration == totalDuration) && + (identical(other.language, language) || + other.language == language) && + (identical(other.backend, backend) || other.backend == backend) && + (identical(other.processingTime, processingTime) || + other.processingTime == processingTime) && + (identical(other.speakerCount, speakerCount) || + other.speakerCount == speakerCount) && + (identical(other.hasSpeakerDiarization, hasSpeakerDiarization) || + other.hasSpeakerDiarization == hasSpeakerDiarization) && + const DeepCollectionEquality().equals(other._metadata, _metadata) && + (identical(other.timestamp, timestamp) || + other.timestamp == timestamp)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + const DeepCollectionEquality().hash(_segments), + overallConfidence, + totalDuration, + language, + backend, + processingTime, + speakerCount, + hasSpeakerDiarization, + const DeepCollectionEquality().hash(_metadata), + timestamp, + ); + + /// Create a copy of TranscriptionResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TranscriptionResultImplCopyWith<_$TranscriptionResultImpl> get copyWith => + __$$TranscriptionResultImplCopyWithImpl<_$TranscriptionResultImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$TranscriptionResultImplToJson(this); + } +} + +abstract class _TranscriptionResult extends TranscriptionResult { + const factory _TranscriptionResult({ + required final String id, + required final List segments, + required final double overallConfidence, + required final Duration totalDuration, + final String language, + final String? backend, + final Duration? processingTime, + final int speakerCount, + final bool hasSpeakerDiarization, + final Map metadata, + required final DateTime timestamp, + }) = _$TranscriptionResultImpl; + const _TranscriptionResult._() : super._(); + + factory _TranscriptionResult.fromJson(Map json) = + _$TranscriptionResultImpl.fromJson; + + /// Unique identifier for this transcription result + @override + String get id; + + /// List of transcription segments + @override + List get segments; + + /// Overall confidence score for the entire transcription + @override + double get overallConfidence; + + /// Total duration of the transcription + @override + Duration get totalDuration; + + /// Language code for the transcription + @override + String get language; + + /// Transcription backend used + @override + String? get backend; + + /// Total processing time + @override + Duration? get processingTime; + + /// Number of speakers detected + @override + int get speakerCount; + + /// Whether speaker diarization was performed + @override + bool get hasSpeakerDiarization; + + /// Additional metadata for the entire transcription + @override + Map get metadata; + + /// Timestamp when this result was created + @override + DateTime get timestamp; + + /// Create a copy of TranscriptionResult + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TranscriptionResultImplCopyWith<_$TranscriptionResultImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/transcription_segment.g.dart b/lib/models/transcription_segment.g.dart new file mode 100644 index 0000000..6d03c77 --- /dev/null +++ b/lib/models/transcription_segment.g.dart @@ -0,0 +1,79 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'transcription_segment.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$TranscriptionSegmentImpl _$$TranscriptionSegmentImplFromJson( + Map json, +) => _$TranscriptionSegmentImpl( + text: json['text'] as String, + startTime: DateTime.parse(json['startTime'] as String), + endTime: DateTime.parse(json['endTime'] as String), + confidence: (json['confidence'] as num).toDouble(), + speakerId: json['speakerId'] as String?, + speakerName: json['speakerName'] as String?, + language: json['language'] as String? ?? 'en-US', + isFinal: json['isFinal'] as bool? ?? true, + segmentId: json['segmentId'] as String?, + backend: _backendFromJson(json['backend'] as String?), + processingTimeMs: (json['processingTimeMs'] as num?)?.toInt(), + metadata: json['metadata'] as Map? ?? const {}, +); + +Map _$$TranscriptionSegmentImplToJson( + _$TranscriptionSegmentImpl instance, +) => { + 'text': instance.text, + 'startTime': instance.startTime.toIso8601String(), + 'endTime': instance.endTime.toIso8601String(), + 'confidence': instance.confidence, + 'speakerId': instance.speakerId, + 'speakerName': instance.speakerName, + 'language': instance.language, + 'isFinal': instance.isFinal, + 'segmentId': instance.segmentId, + 'backend': _backendToJson(instance.backend), + 'processingTimeMs': instance.processingTimeMs, + 'metadata': instance.metadata, +}; + +_$TranscriptionResultImpl _$$TranscriptionResultImplFromJson( + Map json, +) => _$TranscriptionResultImpl( + id: json['id'] as String, + segments: + (json['segments'] as List) + .map((e) => TranscriptionSegment.fromJson(e as Map)) + .toList(), + overallConfidence: (json['overallConfidence'] as num).toDouble(), + totalDuration: Duration(microseconds: (json['totalDuration'] as num).toInt()), + language: json['language'] as String? ?? 'en-US', + backend: json['backend'] as String?, + processingTime: + json['processingTime'] == null + ? null + : Duration(microseconds: (json['processingTime'] as num).toInt()), + speakerCount: (json['speakerCount'] as num?)?.toInt() ?? 1, + hasSpeakerDiarization: json['hasSpeakerDiarization'] as bool? ?? false, + metadata: json['metadata'] as Map? ?? const {}, + timestamp: DateTime.parse(json['timestamp'] as String), +); + +Map _$$TranscriptionResultImplToJson( + _$TranscriptionResultImpl instance, +) => { + 'id': instance.id, + 'segments': instance.segments, + 'overallConfidence': instance.overallConfidence, + 'totalDuration': instance.totalDuration.inMicroseconds, + 'language': instance.language, + 'backend': instance.backend, + 'processingTime': instance.processingTime?.inMicroseconds, + 'speakerCount': instance.speakerCount, + 'hasSpeakerDiarization': instance.hasSpeakerDiarization, + 'metadata': instance.metadata, + 'timestamp': instance.timestamp.toIso8601String(), +}; diff --git a/lib/providers/app_state_provider.dart b/lib/providers/app_state_provider.dart new file mode 100644 index 0000000..b6ae019 --- /dev/null +++ b/lib/providers/app_state_provider.dart @@ -0,0 +1,403 @@ +// ABOUTME: Main application state provider managing global app state +// ABOUTME: Coordinates all service states and provides unified state management + +import 'package:flutter/foundation.dart'; + +import '../services/audio_service.dart'; +import '../services/transcription_service.dart'; +import '../services/llm_service.dart'; +import '../services/glasses_service.dart'; +import '../services/settings_service.dart'; +import '../models/conversation_model.dart'; +import '../models/glasses_connection_state.dart' as model; +import '../models/audio_configuration.dart'; +import '../core/utils/logging_service.dart'; + +/// Main application state provider +class AppStateProvider extends ChangeNotifier { + static const String _tag = 'AppStateProvider'; + + final LoggingService _logger; + final AudioService _audioService; + final TranscriptionService _transcriptionService; + final LLMService _llmService; + final GlassesService _glassesService; + final SettingsService _settingsService; + + // Current app state + AppStatus _appStatus = AppStatus.initializing; + String? _currentError; + DateTime? _lastErrorTime; + + // Current conversation + ConversationModel? _currentConversation; + bool _isRecording = false; + final bool _isAnalyzing = false; + + // Service states + bool _audioServiceReady = false; + bool _transcriptionServiceReady = false; + bool _llmServiceReady = false; + bool _glassesServiceReady = false; + bool _settingsServiceReady = false; + + // Connection states + model.GlassesConnectionState _glassesConnectionState = const model.GlassesConnectionState(); + + // Settings + bool _darkMode = false; + String _currentLanguage = 'en-US'; + double _audioSensitivity = 0.5; + + AppStateProvider({ + required LoggingService logger, + required AudioService audioService, + required TranscriptionService transcriptionService, + required LLMService llmService, + required GlassesService glassesService, + required SettingsService settingsService, + }) : _logger = logger, + _audioService = audioService, + _transcriptionService = transcriptionService, + _llmService = llmService, + _glassesService = glassesService, + _settingsService = settingsService; + + // Getters + AppStatus get appStatus => _appStatus; + String? get currentError => _currentError; + DateTime? get lastErrorTime => _lastErrorTime; + + ConversationModel? get currentConversation => _currentConversation; + bool get isRecording => _isRecording; + bool get isAnalyzing => _isAnalyzing; + + bool get audioServiceReady => _audioServiceReady; + bool get transcriptionServiceReady => _transcriptionServiceReady; + bool get llmServiceReady => _llmServiceReady; + bool get glassesServiceReady => _glassesServiceReady; + bool get settingsServiceReady => _settingsServiceReady; + + model.GlassesConnectionState get glassesConnectionState => _glassesConnectionState; + + bool get darkMode => _darkMode; + String get currentLanguage => _currentLanguage; + double get audioSensitivity => _audioSensitivity; + + /// Whether all core services are ready + bool get allServicesReady => + _audioServiceReady && + _transcriptionServiceReady && + _llmServiceReady && + _settingsServiceReady; + + /// Whether the app is ready for conversation + bool get readyForConversation => + allServicesReady && _appStatus == AppStatus.ready; + + /// Whether glasses are connected + bool get glassesConnected => _glassesConnectionState.isConnected; + + /// Initialize the app state and all services + Future initialize() async { + try { + _logger.log(_tag, 'Initializing app state provider', LogLevel.info); + _setAppStatus(AppStatus.initializing); + + // Initialize settings service first + await _initializeSettingsService(); + + // Load initial settings + await _loadSettings(); + + // Initialize other services + await _initializeAudioService(); + await _initializeTranscriptionService(); + await _initializeLLMService(); + await _initializeGlassesService(); + + // Set up service listeners + _setupServiceListeners(); + + _setAppStatus(AppStatus.ready); + _logger.log(_tag, 'App state provider initialized successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to initialize app state: $e', LogLevel.error); + _setError('Failed to initialize app: $e'); + _setAppStatus(AppStatus.error); + } + } + + /// Start a new conversation + Future startConversation({String? title}) async { + try { + if (!readyForConversation) { + throw Exception('App not ready for conversation'); + } + + _logger.log(_tag, 'Starting new conversation', LogLevel.info); + + final conversationId = 'conv_${DateTime.now().millisecondsSinceEpoch}'; + final conversation = ConversationModel( + id: conversationId, + title: title ?? 'Conversation ${DateTime.now().toString().substring(0, 16)}', + participants: [], + segments: [], + startTime: DateTime.now(), + lastUpdated: DateTime.now(), + ); + + _currentConversation = conversation; + + // Start audio recording + await _audioService.startConversationRecording(conversationId); + _isRecording = true; + + // Start transcription + await _transcriptionService.startTranscription(); + + notifyListeners(); + _logger.log(_tag, 'Conversation started: $conversationId', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to start conversation: $e', LogLevel.error); + _setError('Failed to start conversation: $e'); + } + } + + /// Stop the current conversation + Future stopConversation() async { + try { + if (_currentConversation == null) return; + + _logger.log(_tag, 'Stopping conversation: ${_currentConversation!.id}', LogLevel.info); + + // Stop recording and transcription + await _audioService.stopConversationRecording(); + await _transcriptionService.stopTranscription(); + + _isRecording = false; + + // Update conversation end time + _currentConversation = _currentConversation!.copyWith( + endTime: DateTime.now(), + status: ConversationStatus.completed, + lastUpdated: DateTime.now(), + ); + + notifyListeners(); + _logger.log(_tag, 'Conversation stopped', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to stop conversation: $e', LogLevel.error); + _setError('Failed to stop conversation: $e'); + } + } + + /// Toggle conversation recording + Future toggleRecording() async { + if (_isRecording) { + await stopConversation(); + } else { + await startConversation(); + } + } + + /// Connect to glasses + Future connectToGlasses() async { + try { + _logger.log(_tag, 'Connecting to glasses', LogLevel.info); + await _glassesService.startScanning(); + } catch (e) { + _logger.log(_tag, 'Failed to connect to glasses: $e', LogLevel.error); + _setError('Failed to connect to glasses: $e'); + } + } + + /// Disconnect from glasses + Future disconnectFromGlasses() async { + try { + _logger.log(_tag, 'Disconnecting from glasses', LogLevel.info); + await _glassesService.disconnect(); + } catch (e) { + _logger.log(_tag, 'Failed to disconnect from glasses: $e', LogLevel.error); + _setError('Failed to disconnect from glasses: $e'); + } + } + + /// Update app settings + Future updateSettings({ + bool? darkMode, + String? language, + double? audioSensitivity, + }) async { + try { + if (darkMode != null && darkMode != _darkMode) { + await _settingsService.setThemeMode(darkMode ? ThemeMode.dark : ThemeMode.light); + _darkMode = darkMode; + } + + if (language != null && language != _currentLanguage) { + await _settingsService.setLanguage(language); + _currentLanguage = language; + } + + if (audioSensitivity != null && audioSensitivity != _audioSensitivity) { + await _settingsService.setVADSensitivity(audioSensitivity); + _audioSensitivity = audioSensitivity; + } + + notifyListeners(); + _logger.log(_tag, 'Settings updated', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to update settings: $e', LogLevel.error); + _setError('Failed to update settings: $e'); + } + } + + /// Clear current error + void clearError() { + _currentError = null; + _lastErrorTime = null; + notifyListeners(); + } + + /// Retry initialization + Future retryInitialization() async { + _currentError = null; + _lastErrorTime = null; + await initialize(); + } + + @override + void dispose() { + _logger.log(_tag, 'Disposing app state provider', LogLevel.info); + super.dispose(); + } + + // Private methods + + void _setAppStatus(AppStatus status) { + _appStatus = status; + notifyListeners(); + _logger.log(_tag, 'App status changed to: $status', LogLevel.debug); + } + + void _setError(String error) { + _currentError = error; + _lastErrorTime = DateTime.now(); + notifyListeners(); + } + + Future _initializeSettingsService() async { + try { + await _settingsService.initialize(); + _settingsServiceReady = true; + _logger.log(_tag, 'Settings service initialized', LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Settings service initialization failed: $e', LogLevel.error); + rethrow; + } + } + + Future _loadSettings() async { + try { + final themeMode = await _settingsService.getThemeMode(); + _darkMode = themeMode == ThemeMode.dark; + + _currentLanguage = await _settingsService.getLanguage(); + _audioSensitivity = await _settingsService.getVADSensitivity(); + + _logger.log(_tag, 'Settings loaded', LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Failed to load settings: $e', LogLevel.warning); + // Continue with defaults + } + } + + Future _initializeAudioService() async { + try { + final audioConfig = AudioConfiguration.speechRecognition().copyWith( + vadThreshold: _audioSensitivity, + ); + + await _audioService.initialize(audioConfig); + + // Request permissions + final hasPermission = await _audioService.requestPermission(); + if (!hasPermission) { + throw Exception('Microphone permission denied'); + } + + _audioServiceReady = true; + _logger.log(_tag, 'Audio service initialized', LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Audio service initialization failed: $e', LogLevel.error); + rethrow; + } + } + + Future _initializeTranscriptionService() async { + try { + await _transcriptionService.initialize(); + _transcriptionServiceReady = true; + _logger.log(_tag, 'Transcription service initialized', LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Transcription service initialization failed: $e', LogLevel.error); + rethrow; + } + } + + Future _initializeLLMService() async { + try { + // Get API keys from settings + final openAIKey = await _settingsService.getAPIKey('openai'); + final anthropicKey = await _settingsService.getAPIKey('anthropic'); + + await _llmService.initialize( + openAIKey: openAIKey, + anthropicKey: anthropicKey, + ); + + _llmServiceReady = true; + _logger.log(_tag, 'LLM service initialized', LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'LLM service initialization failed: $e', LogLevel.warning); + // LLM service is optional, continue without it + _llmServiceReady = false; + } + } + + Future _initializeGlassesService() async { + try { + await _glassesService.initialize(); + _glassesServiceReady = true; + _logger.log(_tag, 'Glasses service initialized', LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Glasses service initialization failed: $e', LogLevel.warning); + // Glasses service is optional, continue without it + _glassesServiceReady = false; + } + } + + void _setupServiceListeners() { + // Listen to glasses connection state changes + _glassesService.connectionStateStream.listen( + (state) { + _glassesConnectionState = _glassesConnectionState.copyWith(status: state); + notifyListeners(); + }, + onError: (error) { + _logger.log(_tag, 'Glasses connection error: $error', LogLevel.error); + }, + ); + + // Add other service listeners as needed + } +} + +/// Application status enumeration +enum AppStatus { + initializing, + ready, + error, + updating, +} \ No newline at end of file diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart new file mode 100644 index 0000000..f42b0a6 --- /dev/null +++ b/lib/services/audio_service.dart @@ -0,0 +1,112 @@ +// ABOUTME: Audio service interface for audio capture, processing, and recording +// ABOUTME: Abstracts platform-specific audio operations for cross-platform compatibility + +import 'dart:async'; +import 'dart:typed_data'; + +import '../models/audio_configuration.dart'; + +/// Service interface for audio capture, processing, and recording management +abstract class AudioService { + /// Current audio configuration + AudioConfiguration get configuration; + + /// Whether audio recording is currently active + bool get isRecording; + + /// Whether audio permission has been granted + bool get hasPermission; + + /// Stream of real-time audio data for processing + Stream get audioStream; + + /// Stream of audio level updates for UI visualization + Stream get audioLevelStream; + + /// Stream of voice activity detection updates + Stream get voiceActivityStream; + + /// Stream of recording duration updates + Stream get recordingDurationStream; + + /// Initialize the audio service with configuration + Future initialize(AudioConfiguration config); + + /// Request audio permission from the user + Future requestPermission(); + + /// Start audio recording and streaming + Future startRecording(); + + /// Stop audio recording + Future stopRecording(); + + /// Pause audio recording (if supported) + Future pauseRecording(); + + /// Resume audio recording from pause + Future resumeRecording(); + + /// Start a new conversation recording session + /// Returns the file path where the recording will be saved + Future startConversationRecording(String conversationId); + + /// Stop conversation recording and finalize the file + Future stopConversationRecording(); + + /// Get available audio input devices + Future> getInputDevices(); + + /// Select a specific audio input device + Future selectInputDevice(String deviceId); + + /// Configure audio processing parameters + Future configureAudioProcessing({ + bool enableNoiseReduction = true, + bool enableEchoCancellation = true, + double gainLevel = 1.0, + }); + + /// Enable or disable voice activity detection + Future setVoiceActivityDetection(bool enabled); + + /// Set audio quality level + Future setAudioQuality(AudioQuality quality); + + /// Test audio recording functionality + Future testAudioRecording(); + + /// Get the current recording file path (if recording) + String? get currentRecordingPath; + + /// Clean up resources and stop all audio operations + Future dispose(); +} + +/// Represents an audio input device +class AudioInputDevice { + final String id; + final String name; + final String type; // 'built-in', 'bluetooth', 'external' + final bool isDefault; + + const AudioInputDevice({ + required this.id, + required this.name, + required this.type, + this.isDefault = false, + }); + + @override + String toString() => 'AudioInputDevice(id: $id, name: $name, type: $type, isDefault: $isDefault)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AudioInputDevice && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; +} \ No newline at end of file diff --git a/lib/services/conversation_storage_service.dart b/lib/services/conversation_storage_service.dart new file mode 100644 index 0000000..e7c6095 --- /dev/null +++ b/lib/services/conversation_storage_service.dart @@ -0,0 +1,164 @@ +// ABOUTME: Service for storing and retrieving conversation history and recordings +// ABOUTME: Provides persistence and management of conversation data and audio files + +import 'dart:async'; + +import '../models/conversation_model.dart'; +import '../core/utils/logging_service.dart'; + +/// Service interface for conversation storage and retrieval +abstract class ConversationStorageService { + /// Get all conversations + Future> getAllConversations(); + + /// Get conversation by ID + Future getConversation(String id); + + /// Save a conversation + Future saveConversation(ConversationModel conversation); + + /// Delete a conversation + Future deleteConversation(String id); + + /// Update conversation + Future updateConversation(ConversationModel conversation); + + /// Search conversations + Future> searchConversations(String query); + + /// Get conversations by date range + Future> getConversationsByDateRange( + DateTime startDate, + DateTime endDate, + ); + + /// Stream of conversation updates + Stream> get conversationStream; +} + +/// In-memory implementation of conversation storage +/// This is a simple implementation for development/testing +class InMemoryConversationStorageService implements ConversationStorageService { + static const String _tag = 'InMemoryConversationStorageService'; + + final LoggingService _logger; + final List _conversations = []; + final StreamController> _conversationStreamController = + StreamController>.broadcast(); + + InMemoryConversationStorageService({required LoggingService logger}) + : _logger = logger; + + @override + Future> getAllConversations() async { + _logger.log(_tag, 'Getting all conversations', LogLevel.debug); + return List.from(_conversations); + } + + @override + Future getConversation(String id) async { + _logger.log(_tag, 'Getting conversation: $id', LogLevel.debug); + try { + return _conversations.firstWhere((c) => c.id == id); + } catch (e) { + return null; + } + } + + @override + Future saveConversation(ConversationModel conversation) async { + _logger.log(_tag, 'Saving conversation: ${conversation.id}', LogLevel.info); + + // Remove existing conversation with same ID + _conversations.removeWhere((c) => c.id == conversation.id); + + // Add new conversation + _conversations.add(conversation); + + // Sort by creation date (newest first) + _conversations.sort((a, b) => b.startTime.compareTo(a.startTime)); + + // Notify listeners + _conversationStreamController.add(List.from(_conversations)); + } + + @override + Future deleteConversation(String id) async { + _logger.log(_tag, 'Deleting conversation: $id', LogLevel.info); + + final originalLength = _conversations.length; + _conversations.removeWhere((c) => c.id == id); + + if (_conversations.length < originalLength) { + // Notify listeners + _conversationStreamController.add(List.from(_conversations)); + } + } + + @override + Future updateConversation(ConversationModel conversation) async { + _logger.log(_tag, 'Updating conversation: ${conversation.id}', LogLevel.info); + + final index = _conversations.indexWhere((c) => c.id == conversation.id); + if (index != -1) { + _conversations[index] = conversation; + + // Sort by creation date (newest first) + _conversations.sort((a, b) => b.startTime.compareTo(a.startTime)); + + // Notify listeners + _conversationStreamController.add(List.from(_conversations)); + } + } + + @override + Future> searchConversations(String query) async { + _logger.log(_tag, 'Searching conversations: $query', LogLevel.debug); + + final lowerQuery = query.toLowerCase(); + + return _conversations.where((conversation) { + // Search in title + if (conversation.title.toLowerCase().contains(lowerQuery)) { + return true; + } + + // Search in segments + for (final segment in conversation.segments) { + if (segment.text.toLowerCase().contains(lowerQuery)) { + return true; + } + } + + // Search in participant names + for (final participant in conversation.participants) { + if (participant.name.toLowerCase().contains(lowerQuery)) { + return true; + } + } + + return false; + }).toList(); + } + + @override + Future> getConversationsByDateRange( + DateTime startDate, + DateTime endDate, + ) async { + _logger.log(_tag, 'Getting conversations by date range: $startDate - $endDate', LogLevel.debug); + + return _conversations.where((conversation) { + return conversation.startTime.isAfter(startDate) && + conversation.startTime.isBefore(endDate); + }).toList(); + } + + @override + Stream> get conversationStream => _conversationStreamController.stream; + + /// Clean up resources + Future dispose() async { + await _conversationStreamController.close(); + } +} \ No newline at end of file diff --git a/lib/services/glasses_service.dart b/lib/services/glasses_service.dart new file mode 100644 index 0000000..09665e9 --- /dev/null +++ b/lib/services/glasses_service.dart @@ -0,0 +1,239 @@ +// ABOUTME: Glasses service interface for Even Realities smart glasses integration +// ABOUTME: Handles Bluetooth connectivity, HUD rendering, and device management + +import 'dart:async'; + +import '../models/glasses_connection_state.dart'; + +/// HUD display content type +enum HUDContentType { + text, + notification, + menu, + status, + image, +} + +/// Touch gesture types from glasses +enum TouchGesture { + tap, + doubleTap, + longPress, + swipeLeft, + swipeRight, + swipeUp, + swipeDown, +} + +/// Service interface for Even Realities smart glasses +abstract class GlassesService { + /// Current connection state + ConnectionStatus get connectionState; + + /// Connected glasses device info + GlassesDevice? get connectedDevice; + + /// Whether glasses are currently connected + bool get isConnected; + + /// Stream of connection state changes + Stream get connectionStateStream; + + /// Stream of discovered glasses devices + Stream> get discoveredDevicesStream; + + /// Stream of touch gestures from glasses + Stream get gestureStream; + + /// Stream of device status updates (battery, etc.) + Stream get deviceStatusStream; + + /// Initialize the glasses service + Future initialize(); + + /// Check if Bluetooth is available and enabled + Future isBluetoothAvailable(); + + /// Request Bluetooth permission + Future requestBluetoothPermission(); + + /// Start scanning for Even Realities glasses + Future startScanning({Duration timeout = const Duration(seconds: 30)}); + + /// Stop scanning for devices + Future stopScanning(); + + /// Connect to a specific glasses device + Future connectToDevice(String deviceId); + + /// Connect to the last known device + Future connectToLastDevice(); + + /// Disconnect from current device + Future disconnect(); + + /// Display text on the HUD + Future displayText( + String text, { + HUDPosition position = HUDPosition.center, + Duration? duration, + HUDStyle? style, + }); + + /// Display a notification on the HUD + Future displayNotification( + String title, + String message, { + NotificationPriority priority = NotificationPriority.normal, + Duration duration = const Duration(seconds: 5), + }); + + /// Clear the HUD display + Future clearDisplay(); + + /// Set HUD brightness + Future setBrightness(double brightness); // 0.0 to 1.0 + + /// Configure touch gesture settings + Future configureGestures({ + bool enableTap = true, + bool enableSwipe = true, + bool enableLongPress = true, + double sensitivity = 0.5, + }); + + /// Send custom command to glasses + Future sendCommand(String command, {Map? parameters}); + + /// Get device information + Future getDeviceInfo(); + + /// Get battery level (0.0 to 1.0) + Future getBatteryLevel(); + + /// Check device health and diagnostics + Future checkDeviceHealth(); + + /// Update device firmware (if available) + Future updateFirmware(); + + /// Clean up resources + Future dispose(); +} + +/// Represents a discovered or connected glasses device +class GlassesDevice { + final String id; + final String name; + final String? modelNumber; + final int signalStrength; // RSSI value + final bool isConnected; + + const GlassesDevice({ + required this.id, + required this.name, + this.modelNumber, + required this.signalStrength, + this.isConnected = false, + }); + + @override + String toString() => 'GlassesDevice(id: $id, name: $name, rssi: $signalStrength)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is GlassesDevice && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; +} + +/// HUD display position +enum HUDPosition { + topLeft, + topCenter, + topRight, + centerLeft, + center, + centerRight, + bottomLeft, + bottomCenter, + bottomRight, +} + +/// HUD text style +class HUDStyle { + final double fontSize; + final String color; + final String fontWeight; + final String alignment; + + const HUDStyle({ + this.fontSize = 16.0, + this.color = '#FFFFFF', + this.fontWeight = 'normal', + this.alignment = 'center', + }); +} + +/// Notification priority levels +enum NotificationPriority { + low, + normal, + high, + urgent, +} + +/// Device information +class GlassesDeviceInfo { + final String deviceId; + final String modelName; + final String firmwareVersion; + final String hardwareVersion; + final String serialNumber; + final DateTime lastConnected; + + const GlassesDeviceInfo({ + required this.deviceId, + required this.modelName, + required this.firmwareVersion, + required this.hardwareVersion, + required this.serialNumber, + required this.lastConnected, + }); +} + +/// Device status information +class GlassesDeviceStatus { + final double batteryLevel; + final bool isCharging; + final int signalStrength; + final String connectionQuality; // 'excellent', 'good', 'fair', 'poor' + final DateTime lastUpdate; + + const GlassesDeviceStatus({ + required this.batteryLevel, + required this.isCharging, + required this.signalStrength, + required this.connectionQuality, + required this.lastUpdate, + }); +} + +/// Device health status +class GlassesHealthStatus { + final bool isHealthy; + final List issues; + final Map diagnostics; + final String overallStatus; // 'good', 'warning', 'error' + + const GlassesHealthStatus({ + required this.isHealthy, + required this.issues, + required this.diagnostics, + required this.overallStatus, + }); +} \ No newline at end of file diff --git a/lib/services/implementations/audio_service_impl.dart b/lib/services/implementations/audio_service_impl.dart new file mode 100644 index 0000000..1b3c7ef --- /dev/null +++ b/lib/services/implementations/audio_service_impl.dart @@ -0,0 +1,825 @@ +// ABOUTME: Audio service implementation using flutter_sound for audio processing +// ABOUTME: Handles real-time audio capture, streaming, and voice activity detection + +import 'dart:async'; +import 'dart:io'; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:flutter_sound/flutter_sound.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:audio_session/audio_session.dart'; + +import '../audio_service.dart'; +import '../../models/audio_configuration.dart'; +import '../../core/utils/logging_service.dart'; +import '../../core/utils/exceptions.dart'; + +/// Implementation of AudioService using flutter_sound +class AudioServiceImpl implements AudioService { + static const String _tag = 'AudioServiceImpl'; + + final LoggingService _logger; + final FlutterSoundRecorder _recorder = FlutterSoundRecorder(); + final FlutterSoundPlayer _player = FlutterSoundPlayer(); + + final StreamController _audioStreamController = + StreamController.broadcast(); + final StreamController _audioLevelStreamController = + StreamController.broadcast(); + final StreamController _voiceActivityStreamController = + StreamController.broadcast(); + + AudioConfiguration _currentConfiguration = const AudioConfiguration(); + String? _currentRecordingPath; + Timer? _volumeTimer; + Timer? _vadTimer; + Timer? _durationTimer; + Timer? _streamingTimer; + bool _isInitialized = false; + bool _hasPermission = false; + bool _isRecording = false; + bool _isMockMode = false; + + // Voice Activity Detection state + double _currentVolume = 0.0; + double _vadThreshold = 0.01; + bool _isVoiceActive = false; + final List _volumeHistory = []; + int _volumeHistoryIndex = 0; + double _rollingVolumeSum = 0.0; // For efficient average calculation + static const int _volumeHistorySize = 5; // Reduced for better performance + + // Performance optimization constants + static const Duration _volumeUpdateInterval = Duration(milliseconds: 150); // Reduced frequency + static const Duration _vadUpdateInterval = Duration(milliseconds: 100); // Reduced frequency + static const Duration _durationUpdateInterval = Duration(milliseconds: 200); // Less frequent updates + + // Recording timing + DateTime? _recordingStartTime; + final StreamController _recordingDurationStreamController = + StreamController.broadcast(); + + AudioServiceImpl({required LoggingService logger}) : _logger = logger; + + @override + AudioConfiguration get configuration => _currentConfiguration; + + @override + bool get isRecording => _isRecording; + + @override + bool get hasPermission => _hasPermission; + + @override + String? get currentRecordingPath => _currentRecordingPath; + + /// Check current microphone permission status without requesting + Future checkPermissionStatus() async { + try { + final status = await Permission.microphone.status; + final previousPermission = _hasPermission; + _hasPermission = status.isGranted || status.isLimited || status.isProvisional; + + _logger.log(_tag, 'Current microphone permission status: ${status.name} (hasPermission: $previousPermission -> $_hasPermission)', LogLevel.debug); + return status; + } catch (e) { + _logger.log(_tag, 'Failed to check permission status: $e', LogLevel.error); + _hasPermission = false; + return PermissionStatus.denied; + } + } + + /// Open app settings for user to manually enable microphone permission + Future openPermissionSettings() async { + try { + _logger.log(_tag, 'Opening app settings for permission management', LogLevel.info); + return await openAppSettings(); + } catch (e) { + _logger.log(_tag, 'Failed to open app settings: $e', LogLevel.error); + return false; + } + } + + @override + Stream get audioStream => _audioStreamController.stream; + + @override + Stream get audioLevelStream => _audioLevelStreamController.stream; + + @override + Stream get voiceActivityStream => _voiceActivityStreamController.stream; + + @override + Stream get recordingDurationStream => _recordingDurationStreamController.stream; + + @override + Future initialize(AudioConfiguration config) async { + try { + _logger.log(_tag, 'Initializing audio service', LogLevel.info); + + _currentConfiguration = config; + + // Check platform compatibility and handle iOS 26 beta issues + if (Platform.isMacOS) { + try { + // Try to initialize recorder and player + await _recorder.openRecorder(); + await _player.openPlayer(); + } catch (e) { + _logger.log(_tag, 'flutter_sound not working on macOS, enabling mock mode: $e', LogLevel.warning); + // Set up for mock mode but still mark as initialized + _isMockMode = true; + _vadThreshold = _currentConfiguration.vadThreshold; + _isInitialized = true; + _logger.log(_tag, 'Audio service initialized in mock mode for macOS', LogLevel.info); + return; + } + } else if (Platform.isIOS) { + try { + // iOS-specific initialization with threading safety for iOS 26 beta + _logger.log(_tag, 'Initializing flutter_sound for iOS (handling iOS 26 beta compatibility)', LogLevel.info); + + // Add delay to avoid threading race conditions in iOS 26 beta + await Future.delayed(const Duration(milliseconds: 100)); + + await _recorder.openRecorder(); + await _player.openPlayer(); + } catch (e) { + _logger.log(_tag, 'flutter_sound initialization failed on iOS, enabling mock mode: $e', LogLevel.warning); + // Fallback to mock mode for iOS 26 beta if flutter_sound crashes + _isMockMode = true; + _vadThreshold = _currentConfiguration.vadThreshold; + _isInitialized = true; + _logger.log(_tag, 'Audio service initialized in mock mode for iOS (iOS 26 beta fallback)', LogLevel.info); + return; + } + } else { + // Initialize recorder and player for other platforms + await _recorder.openRecorder(); + await _player.openPlayer(); + } + + // Configure audio session + await _configureAudioSession(); + + _vadThreshold = _currentConfiguration.vadThreshold; + _isInitialized = true; + + _logger.log(_tag, 'Audio service initialized successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to initialize audio service: $e', LogLevel.error); + throw AudioException('Initialization failed: $e', originalError: e); + } + } + + @override + Future requestPermission() async { + try { + _logger.log(_tag, 'Requesting microphone permission', LogLevel.info); + + // For mock mode (macOS or iOS 26 beta fallback), simulate permission granted + if (_isMockMode) { + _hasPermission = true; + _logger.log(_tag, 'Mock mode: Microphone permission granted automatically', LogLevel.info); + return true; + } + + // Check if we should show rationale (Android only) + if (Platform.isAndroid) { + final shouldShowRationale = await Permission.microphone.shouldShowRequestRationale; + if (shouldShowRationale) { + _logger.log(_tag, 'Should show permission rationale to user', LogLevel.debug); + } + } + + final status = await Permission.microphone.request(); + + switch (status) { + case PermissionStatus.granted: + _hasPermission = true; + _logger.log(_tag, 'Microphone permission granted', LogLevel.info); + return true; + + case PermissionStatus.denied: + _hasPermission = false; + _logger.log(_tag, 'Microphone permission denied', LogLevel.warning); + return false; + + case PermissionStatus.permanentlyDenied: + _hasPermission = false; + _logger.log(_tag, 'Microphone permission permanently denied - user must enable in settings', LogLevel.error); + return false; + + case PermissionStatus.restricted: + _hasPermission = false; + _logger.log(_tag, 'Microphone permission restricted (parental controls)', LogLevel.warning); + return false; + + case PermissionStatus.limited: + _hasPermission = true; // Limited access is still usable + _logger.log(_tag, 'Microphone permission granted with limitations', LogLevel.info); + return true; + + case PermissionStatus.provisional: + _hasPermission = true; // Provisional access is usable + _logger.log(_tag, 'Microphone permission granted provisionally', LogLevel.info); + return true; + } + } catch (e) { + _logger.log(_tag, 'Failed to request microphone permission: $e', LogLevel.error); + _hasPermission = false; + return false; + } + } + + @override + Future startRecording() async { + if (!_isInitialized) { + throw const AudioException('Service not initialized'); + } + + if (!_hasPermission) { + throw const AudioException('Microphone permission required'); + } + + if (_isRecording) { + _logger.log(_tag, 'Already recording', LogLevel.warning); + return; + } + + try { + _logger.log(_tag, 'Starting audio recording${_isMockMode ? ' (mock mode)' : ''}', LogLevel.info); + + if (_isMockMode) { + // Mock mode: simulate recording without flutter_sound + _currentRecordingPath = await _createTempRecordingFile(); + _isRecording = true; + _recordingStartTime = DateTime.now(); + + // Start mock monitoring + _startMockVolumeMonitoring(); + _startVoiceActivityDetection(); + _startDurationTracking(); + + _logger.log(_tag, 'Mock recording started successfully', LogLevel.info); + return; + } + + // Real recording mode + // Create temporary file for recording + _currentRecordingPath = await _createTempRecordingFile(); + + // Configure recording codec and settings + final codec = _getCodecFromFormat(_currentConfiguration.format); + + await _recorder.startRecorder( + toFile: _currentRecordingPath, + codec: codec, + sampleRate: _currentConfiguration.sampleRate, + numChannels: _currentConfiguration.channels, + bitRate: _currentConfiguration.bitRate, + ); + + _isRecording = true; + _recordingStartTime = DateTime.now(); + + // Start volume monitoring and VAD + _startVolumeMonitoring(); + _startVoiceActivityDetection(); + _startDurationTracking(); + + // Start streaming audio data + if (_currentConfiguration.enableRealTimeStreaming) { + await _startAudioStreaming(); + } + + _logger.log(_tag, 'Recording started successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to start recording: $e', LogLevel.error); + _isRecording = false; + throw AudioException('Failed to start recording: $e', originalError: e); + } + } + + @override + Future stopRecording() async { + if (!_isRecording) { + return; + } + + try { + _logger.log(_tag, 'Stopping audio recording${_isMockMode ? ' (mock mode)' : ''}', LogLevel.info); + + // Stop timers + _volumeTimer?.cancel(); + _vadTimer?.cancel(); + _durationTimer?.cancel(); + _streamingTimer?.cancel(); + + // Stop recorder (only if not in mock mode) + if (!_isMockMode) { + await _recorder.stopRecorder(); + } + + _isRecording = false; + _recordingStartTime = null; + + _logger.log(_tag, 'Recording stopped successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to stop recording: $e', LogLevel.error); + throw AudioException('Failed to stop recording: $e', originalError: e); + } + } + + @override + Future pauseRecording() async { + if (!_isRecording) { + return; + } + + try { + await _recorder.pauseRecorder(); + _logger.log(_tag, 'Recording paused', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to pause recording: $e', LogLevel.error); + throw AudioException('Failed to pause recording: $e', originalError: e); + } + } + + @override + Future resumeRecording() async { + try { + await _recorder.resumeRecorder(); + _logger.log(_tag, 'Recording resumed', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to resume recording: $e', LogLevel.error); + throw AudioException('Failed to resume recording: $e', originalError: e); + } + } + + @override + Future startConversationRecording(String conversationId) async { + try { + if (!_hasPermission) { + throw const AudioException('Microphone permission required'); + } + + _logger.log(_tag, 'Starting conversation recording: $conversationId${_isMockMode ? ' (mock mode)' : ''}', LogLevel.info); + + // Create recording file for this conversation + final directory = Directory.systemTemp; + final timestamp = DateTime.now().millisecondsSinceEpoch; + final extension = _getFileExtension(_currentConfiguration.format); + _currentRecordingPath = '${directory.path}/helix_conversation_${conversationId}_$timestamp.$extension'; + + if (_isMockMode) { + // Mock mode: simulate conversation recording + _isRecording = true; + _recordingStartTime = DateTime.now(); + + // Start mock monitoring + _startMockVolumeMonitoring(); + _startVoiceActivityDetection(); + _startDurationTracking(); + + return _currentRecordingPath!; + } + + // Real recording mode + // Configure recording codec and settings + final codec = _getCodecFromFormat(_currentConfiguration.format); + + await _recorder.startRecorder( + toFile: _currentRecordingPath, + codec: codec, + sampleRate: _currentConfiguration.sampleRate, + numChannels: _currentConfiguration.channels, + bitRate: _currentConfiguration.bitRate, + ); + + _isRecording = true; + _recordingStartTime = DateTime.now(); + + // Start volume monitoring and VAD + _startVolumeMonitoring(); + _startVoiceActivityDetection(); + _startDurationTracking(); + + return _currentRecordingPath!; + } catch (e) { + _logger.log(_tag, 'Failed to start conversation recording: $e', LogLevel.error); + throw AudioException('Failed to start conversation recording: $e', originalError: e); + } + } + + @override + Future stopConversationRecording() async { + await stopRecording(); + } + + @override + Future> getInputDevices() async { + try { + // For now, return default devices + // In a full implementation, this would query actual devices + return [ + const AudioInputDevice( + id: 'default', + name: 'Default Microphone', + type: 'built-in', + isDefault: true, + ), + const AudioInputDevice( + id: 'bluetooth', + name: 'Bluetooth Microphone', + type: 'bluetooth', + isDefault: false, + ), + ]; + } catch (e) { + _logger.log(_tag, 'Failed to get input devices: $e', LogLevel.error); + throw AudioException('Failed to get input devices: $e', originalError: e); + } + } + + @override + Future selectInputDevice(String deviceId) async { + try { + _logger.log(_tag, 'Selecting input device: $deviceId', LogLevel.info); + // Implementation would depend on platform-specific audio routing + // For now, just log the action + } catch (e) { + _logger.log(_tag, 'Failed to select input device: $e', LogLevel.error); + throw AudioException('Failed to select input device: $e', originalError: e); + } + } + + @override + Future configureAudioProcessing({ + bool enableNoiseReduction = true, + bool enableEchoCancellation = true, + double gainLevel = 1.0, + }) async { + try { + _logger.log(_tag, 'Configuring audio processing', LogLevel.info); + + // Update configuration + _currentConfiguration = _currentConfiguration.copyWith( + enableNoiseReduction: enableNoiseReduction, + enableEchoCancellation: enableEchoCancellation, + gainLevel: gainLevel, + ); + + // Apply configuration if recording + if (_isRecording) { + await stopRecording(); + await startRecording(); + } + } catch (e) { + _logger.log(_tag, 'Failed to configure audio processing: $e', LogLevel.error); + throw AudioException('Failed to configure audio processing: $e', originalError: e); + } + } + + @override + Future setVoiceActivityDetection(bool enabled) async { + try { + _logger.log(_tag, 'Setting voice activity detection: $enabled', LogLevel.info); + + _currentConfiguration = _currentConfiguration.copyWith( + enableVoiceActivityDetection: enabled, + ); + + if (enabled && (_vadTimer?.isActive != true)) { + _startVoiceActivityDetection(); + } else if (!enabled && (_vadTimer?.isActive == true)) { + _vadTimer?.cancel(); + } + } catch (e) { + _logger.log(_tag, 'Failed to set voice activity detection: $e', LogLevel.error); + throw AudioException('Failed to set voice activity detection: $e', originalError: e); + } + } + + @override + Future setAudioQuality(AudioQuality quality) async { + try { + _logger.log(_tag, 'Setting audio quality: $quality', LogLevel.info); + + _currentConfiguration = _currentConfiguration.copyWith(quality: quality); + + // Apply quality settings + if (_isRecording) { + await stopRecording(); + await startRecording(); + } + } catch (e) { + _logger.log(_tag, 'Failed to set audio quality: $e', LogLevel.error); + throw AudioException('Failed to set audio quality: $e', originalError: e); + } + } + + @override + Future testAudioRecording() async { + try { + _logger.log(_tag, 'Testing audio recording', LogLevel.info); + + if (!_hasPermission) { + return false; + } + + // Start a short test recording + await startRecording(); + await Future.delayed(const Duration(seconds: 2)); + await stopRecording(); + + // Check if file was created + if (_currentRecordingPath != null) { + final file = File(_currentRecordingPath!); + final exists = await file.exists(); + if (exists) { + await file.delete(); // Clean up test file + } + return exists; + } + + return false; + } catch (e) { + _logger.log(_tag, 'Audio recording test failed: $e', LogLevel.error); + return false; + } + } + + @override + Future dispose() async { + try { + _logger.log(_tag, 'Disposing audio service', LogLevel.info); + + await stopRecording(); + + _volumeTimer?.cancel(); + _vadTimer?.cancel(); + _durationTimer?.cancel(); + _streamingTimer?.cancel(); + + await _recorder.closeRecorder(); + await _player.closePlayer(); + + await _audioStreamController.close(); + await _audioLevelStreamController.close(); + await _voiceActivityStreamController.close(); + await _recordingDurationStreamController.close(); + + // Clean up temporary files + if (_currentRecordingPath != null) { + final file = File(_currentRecordingPath!); + if (await file.exists()) { + await file.delete(); + } + } + + _isInitialized = false; + } catch (e) { + _logger.log(_tag, 'Error during disposal: $e', LogLevel.error); + } + } + + // Private helper methods + + Future _configureAudioSession() async { + try { + final session = await AudioSession.instance; + + // Configure the audio session for recording + await session.configure(AudioSessionConfiguration( + avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, + avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.defaultToSpeaker, + avAudioSessionMode: AVAudioSessionMode.measurement, + avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy, + avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, + androidAudioAttributes: const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + flags: AndroidAudioFlags.audibilityEnforced, + usage: AndroidAudioUsage.voiceCommunication, + ), + androidAudioFocusGainType: AndroidAudioFocusGainType.gain, + androidWillPauseWhenDucked: true, + )); + + _logger.log(_tag, 'Audio session configured successfully', LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Audio session configuration failed: $e', LogLevel.warning); + } + } + + Future _createTempRecordingFile() async { + final directory = Directory.systemTemp; + final timestamp = DateTime.now().millisecondsSinceEpoch; + final extension = _getFileExtension(_currentConfiguration.format); + return '${directory.path}/helix_recording_$timestamp.$extension'; + } + + Codec _getCodecFromFormat(AudioFormat format) { + switch (format) { + case AudioFormat.wav: + return Codec.pcm16WAV; + case AudioFormat.mp3: + return Codec.mp3; + case AudioFormat.aac: + return Codec.aacADTS; + case AudioFormat.flac: + return Codec.pcm16WAV; // Fallback to WAV for FLAC + } + } + + String _getFileExtension(AudioFormat format) { + switch (format) { + case AudioFormat.wav: + return 'wav'; + case AudioFormat.mp3: + return 'mp3'; + case AudioFormat.aac: + return 'aac'; + case AudioFormat.flac: + return 'flac'; + } + } + + void _startMockVolumeMonitoring() { + // Mock volume monitoring with simulated audio levels + _volumeTimer = Timer.periodic(_volumeUpdateInterval, (timer) { + if (!_isRecording) { + timer.cancel(); + return; + } + + // Generate realistic mock audio levels with variation + final baseLevel = 0.1 + (math.sin(DateTime.now().millisecondsSinceEpoch / 1000.0) * 0.3); + final noiseLevel = math.Random().nextDouble() * 0.2; + final volume = (baseLevel + noiseLevel).clamp(0.0, 1.0); + + _currentVolume = volume; + + // Only emit audio level if there are listeners + if (_audioLevelStreamController.hasListener) { + _audioLevelStreamController.add(volume); + } + + // Update volume history for VAD + _updateVolumeHistory(volume); + + _logger.log(_tag, 'Mock audio level: ${volume.toStringAsFixed(3)}', LogLevel.debug); + }); + } + + void _startVolumeMonitoring() { + // Subscribe to FlutterSound onProgress stream for real-time audio levels + _recorder.onProgress!.listen((RecordingDisposition disposition) { + try { + // Get real decibel level from FlutterSound + final decibels = disposition.decibels; + + if (decibels != null && decibels.isFinite) { + // Convert decibels to linear scale (0.0 to 1.0) + final volume = _decibelToLinear(decibels); + _currentVolume = volume; + + // Only emit audio level if there are listeners (performance optimization) + if (_audioLevelStreamController.hasListener) { + _audioLevelStreamController.add(volume); + } + + // Update volume history for VAD + _updateVolumeHistory(volume); + + _logger.log(_tag, 'Real audio level: ${decibels.toStringAsFixed(1)}dB -> ${volume.toStringAsFixed(3)}', LogLevel.debug); + } else { + // Handle null or invalid decibel values + _updateVolumeHistory(_currentVolume); + } + } catch (e) { + _logger.log(_tag, 'Error processing audio level from onProgress: $e', LogLevel.warning); + _updateVolumeHistory(_currentVolume); + } + }); + + // Backup timer-based monitoring for additional robustness + _volumeTimer = Timer.periodic(_volumeUpdateInterval, (timer) async { + try { + if (!_isRecording || !_recorder.isRecording) { + // Decay audio level when not recording + final decayRate = 0.1; + final volume = math.max(0.0, _currentVolume - decayRate); + _currentVolume = volume; + + if (_audioLevelStreamController.hasListener) { + _audioLevelStreamController.add(volume); + } + _updateVolumeHistory(volume); + } + } catch (e) { + _logger.log(_tag, 'Error in backup volume monitoring: $e', LogLevel.debug); + } + }); + } + + void _startVoiceActivityDetection() { + _vadTimer = Timer.periodic(_vadUpdateInterval, (timer) { + _updateVoiceActivityDetection(); + }); + } + + void _startDurationTracking() { + _durationTimer = Timer.periodic(_durationUpdateInterval, (timer) { + if (!_isRecording || _recordingStartTime == null) { + timer.cancel(); + _durationTimer = null; + return; + } + + final duration = DateTime.now().difference(_recordingStartTime!); + _recordingDurationStreamController.add(duration); + }); + } + + double _decibelToLinear(double decibels) { + // Convert decibels to linear scale + // Improved sensitivity for voice detection: + // -60 dB = silence threshold, -20 dB = normal speech, 0 dB = max + const minDb = -60.0; // More sensitive silence threshold + const maxDb = -10.0; // Normal speech range ceiling + + // Clamp input to expected range + final clampedDb = decibels.clamp(-80.0, 0.0); + + // Normalize to 0.0-1.0 range with better sensitivity + final normalizedDb = (clampedDb - minDb) / (maxDb - minDb); + final linearValue = normalizedDb.clamp(0.0, 1.0); + + // Apply slight curve to enhance low-level audio visibility + final enhancedValue = math.pow(linearValue, 0.7).toDouble(); + + return enhancedValue; + } + + void _updateVolumeHistory(double volume) { + // Efficient circular buffer approach to avoid frequent list operations + if (_volumeHistory.length < _volumeHistorySize) { + _volumeHistory.add(volume); + _rollingVolumeSum += volume; + } else { + // Replace oldest entry using circular indexing and update rolling sum + _rollingVolumeSum -= _volumeHistory[_volumeHistoryIndex]; + _volumeHistory[_volumeHistoryIndex] = volume; + _rollingVolumeSum += volume; + _volumeHistoryIndex = (_volumeHistoryIndex + 1) % _volumeHistorySize; + } + } + + void _updateVoiceActivityDetection() { + if (_volumeHistory.isEmpty) return; + + // Use rolling average for O(1) performance instead of O(n) reduce operation + final averageVolume = _rollingVolumeSum / _volumeHistory.length; + final wasActive = _isVoiceActive; + + // Simple VAD based on volume threshold with hysteresis to prevent fluttering + final threshold = _isVoiceActive ? _vadThreshold * 0.8 : _vadThreshold; // Lower threshold when already active + _isVoiceActive = averageVolume > threshold; + + if (wasActive != _isVoiceActive) { + // Only emit voice activity if there are listeners (performance optimization) + if (_voiceActivityStreamController.hasListener) { + _voiceActivityStreamController.add(_isVoiceActive); + } + _logger.log(_tag, 'Voice activity: $_isVoiceActive (avg: ${averageVolume.toStringAsFixed(3)})', LogLevel.debug); + } + } + + Future _startAudioStreaming() async { + try { + // Set up real-time audio streaming with optimized chunk size + _logger.log(_tag, 'Started real-time audio streaming', LogLevel.debug); + + // Use more efficient streaming interval based on configuration + final streamingInterval = Duration(milliseconds: math.max(50, _currentConfiguration.chunkDurationMs)); + + _streamingTimer = Timer.periodic(streamingInterval, (timer) { + if (!_isRecording) { + timer.cancel(); + _streamingTimer = null; + return; + } + + // Optimized: Only send empty chunks when needed to maintain stream flow + // In a real implementation, this would process actual audio buffer chunks + if (_audioStreamController.hasListener) { + _audioStreamController.add(Uint8List.fromList([])); + } + }); + } catch (e) { + _logger.log(_tag, 'Failed to start audio streaming: $e', LogLevel.error); + } + } +} \ No newline at end of file diff --git a/lib/services/implementations/even_realities_glasses_service.dart b/lib/services/implementations/even_realities_glasses_service.dart new file mode 100644 index 0000000..d5d8ae8 --- /dev/null +++ b/lib/services/implementations/even_realities_glasses_service.dart @@ -0,0 +1,527 @@ +// ABOUTME: Even Realities specific glasses service implementation +// ABOUTME: Implements the exact BLE protocol from Even Realities for text and bitmap display + +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../glasses_service.dart' as service; +import '../../models/glasses_connection_state.dart'; +import '../../core/utils/logging_service.dart' as logging; + +/// Even Realities specific glasses service implementing their BLE protocol +class EvenRealitiesGlassesService implements service.GlassesService { + static const String _tag = 'EvenRealitiesGlassesService'; + + // Even Realities specific UUIDs and constants + static const String EVEN_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"; + static const String EVEN_TX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"; + static const String EVEN_RX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"; + + // Protocol command bytes + static const int CMD_TEXT_DISPLAY = 0x4E; + static const int CMD_BITMAP_DATA = 0x15; + static const int CMD_MIC_CONTROL = 0x0E; + static const int CMD_MIC_DATA = 0xF1; + static const int CMD_CONTROL = 0xF5; + + // Control sub-commands + static const int CONTROL_START_AI = 0x01; + static const int CONTROL_CLEAR_DISPLAY = 0x02; + + final logging.LoggingService _logger; + + // Service state + bool _isInitialized = false; + ConnectionStatus _connectionState = ConnectionStatus.disconnected; + service.GlassesDevice? _connectedDevice; + List _discoveredDevices = []; + + // Bluetooth state + bool _bluetoothEnabled = false; + bool _hasPermissions = false; + StreamSubscription? _bluetoothStateSubscription; + StreamSubscription>? _scanSubscription; + + // Connected device state + BluetoothDevice? _bluetoothDevice; + BluetoothCharacteristic? _txCharacteristic; + BluetoothCharacteristic? _rxCharacteristic; + StreamSubscription? _connectionSubscription; + StreamSubscription>? _dataSubscription; + + // Stream controllers + final StreamController _connectionStateController = + StreamController.broadcast(); + final StreamController> _discoveredDevicesController = + StreamController>.broadcast(); + final StreamController _gestureController = + StreamController.broadcast(); + final StreamController _deviceStatusController = + StreamController.broadcast(); + + // Current device status + double _batteryLevel = 0.0; + bool _isMicrophoneActive = false; + + EvenRealitiesGlassesService({required logging.LoggingService logger}) : _logger = logger; + + @override + ConnectionStatus get connectionState => _connectionState; + + @override + service.GlassesDevice? get connectedDevice => _connectedDevice; + + @override + bool get isConnected => _connectionState == ConnectionStatus.connected; + + @override + Stream get connectionStateStream => _connectionStateController.stream; + + @override + Stream> get discoveredDevicesStream => _discoveredDevicesController.stream; + + @override + Stream get gestureStream => _gestureController.stream; + + @override + Stream get deviceStatusStream => _deviceStatusController.stream; + + @override + Future initialize() async { + if (_isInitialized) return; + + try { + _logger.log(_tag, 'Initializing Even Realities glasses service', logging.LogLevel.info); + + // Check Bluetooth availability + final isAvailable = await isBluetoothAvailable(); + if (!isAvailable) { + throw Exception('Bluetooth not available'); + } + + // Request permissions + final hasPermissions = await requestBluetoothPermission(); + if (!hasPermissions) { + throw Exception('Bluetooth permissions not granted'); + } + + _isInitialized = true; + _logger.log(_tag, 'Even Realities glasses service initialized', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to initialize glasses service: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future isBluetoothAvailable() async { + try { + if (!_bluetoothEnabled) { + final state = await FlutterBluePlus.adapterState.first; + _bluetoothEnabled = state == BluetoothAdapterState.on; + } + return _bluetoothEnabled; + } catch (e) { + _logger.log(_tag, 'Error checking Bluetooth availability: $e', logging.LogLevel.error); + return false; + } + } + + @override + Future requestBluetoothPermission() async { + try { + final permissions = [ + Permission.bluetooth, + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.location, + ]; + + bool allGranted = true; + for (final permission in permissions) { + final status = await permission.request(); + if (status != PermissionStatus.granted) { + allGranted = false; + _logger.log(_tag, 'Permission denied: $permission', logging.LogLevel.warning); + } + } + + _hasPermissions = allGranted; + return allGranted; + } catch (e) { + _logger.log(_tag, 'Error requesting Bluetooth permissions: $e', logging.LogLevel.error); + return false; + } + } + + @override + Future startScanning({Duration timeout = const Duration(seconds: 30)}) async { + if (!_isInitialized) { + throw Exception('Service not initialized'); + } + + try { + _logger.log(_tag, 'Starting scan for Even Realities glasses', logging.LogLevel.info); + + _discoveredDevices.clear(); + _discoveredDevicesController.add(_discoveredDevices); + + // Start scanning with Even Realities service UUID filter + await FlutterBluePlus.startScan( + withServices: [Guid(EVEN_SERVICE_UUID)], + timeout: timeout, + ); + + _scanSubscription = FlutterBluePlus.scanResults.listen((results) { + for (final result in results) { + final device = service.GlassesDevice( + id: result.device.remoteId.toString(), + name: result.advertisementData.advName.isNotEmpty + ? result.advertisementData.advName + : 'Even Realities Glasses', + signalStrength: result.rssi, + ); + + // Add if not already in list + if (!_discoveredDevices.any((d) => d.id == device.id)) { + _discoveredDevices.add(device); + _discoveredDevicesController.add(_discoveredDevices); + _logger.log(_tag, 'Found Even Realities device: ${device.name}', logging.LogLevel.info); + } + } + }); + + } catch (e) { + _logger.log(_tag, 'Error starting scan: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future stopScanning() async { + try { + await FlutterBluePlus.stopScan(); + _scanSubscription?.cancel(); + _logger.log(_tag, 'Stopped scanning', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error stopping scan: $e', logging.LogLevel.error); + } + } + + @override + Future connectToDevice(String deviceId) async { + try { + _logger.log(_tag, 'Connecting to device: $deviceId', logging.LogLevel.info); + + final device = _discoveredDevices.firstWhere((d) => d.id == deviceId); + final bluetoothDevice = BluetoothDevice.fromId(deviceId); + + _connectionState = ConnectionStatus.connecting; + _connectionStateController.add(_connectionState); + + // Connect to device + await bluetoothDevice.connect(); + _bluetoothDevice = bluetoothDevice; + + // Discover services + final services = await bluetoothDevice.discoverServices(); + final evenService = services.firstWhere( + (s) => s.uuid.toString().toUpperCase() == EVEN_SERVICE_UUID.toUpperCase(), + ); + + // Get characteristics + final characteristics = evenService.characteristics; + _txCharacteristic = characteristics.firstWhere( + (c) => c.uuid.toString().toUpperCase() == EVEN_TX_CHAR_UUID.toUpperCase(), + ); + _rxCharacteristic = characteristics.firstWhere( + (c) => c.uuid.toString().toUpperCase() == EVEN_RX_CHAR_UUID.toUpperCase(), + ); + + // Enable notifications on RX characteristic + await _rxCharacteristic!.setNotifyValue(true); + _dataSubscription = _rxCharacteristic!.lastValueStream.listen(_handleReceivedData); + + // Monitor connection state + _connectionSubscription = bluetoothDevice.connectionState.listen((state) { + if (state == BluetoothConnectionState.connected) { + _connectionState = ConnectionStatus.connected; + _connectedDevice = device; + } else { + _connectionState = ConnectionStatus.disconnected; + _connectedDevice = null; + } + _connectionStateController.add(_connectionState); + }); + + _logger.log(_tag, 'Connected to Even Realities glasses', logging.LogLevel.info); + } catch (e) { + _connectionState = ConnectionStatus.disconnected; + _connectionStateController.add(_connectionState); + _logger.log(_tag, 'Failed to connect: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future connectToLastDevice() async { + // TODO: Implement last device connection with shared preferences + throw UnimplementedError('connectToLastDevice not implemented yet'); + } + + @override + Future disconnect() async { + try { + _connectionSubscription?.cancel(); + _dataSubscription?.cancel(); + + if (_bluetoothDevice?.isConnected == true) { + await _bluetoothDevice!.disconnect(); + } + + _connectionState = ConnectionStatus.disconnected; + _connectedDevice = null; + _connectionStateController.add(_connectionState); + + _logger.log(_tag, 'Disconnected from glasses', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error disconnecting: $e', logging.LogLevel.error); + } + } + + /// Display text on Even Realities glasses using their protocol + @override + Future displayText( + String text, { + service.HUDPosition position = service.HUDPosition.center, + Duration? duration, + service.HUDStyle? style, + }) async { + if (!isConnected || _txCharacteristic == null) { + throw Exception('Glasses not connected'); + } + + try { + _logger.log(_tag, 'Displaying text: $text', logging.LogLevel.info); + + // Convert text to UTF-8 bytes + final textBytes = utf8.encode(text); + + // Create packet according to Even Realities protocol + final packet = Uint8List(4 + textBytes.length); + packet[0] = CMD_TEXT_DISPLAY; // Command byte + packet[1] = textBytes.length; // Length + packet[2] = 0x00; // Reserved + packet[3] = 0x00; // Reserved + + // Copy text data + for (int i = 0; i < textBytes.length; i++) { + packet[4 + i] = textBytes[i]; + } + + // Send packet + await _txCharacteristic!.write(packet, withoutResponse: false); + + _logger.log(_tag, 'Text sent to glasses successfully', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to send text: $e', logging.LogLevel.error); + rethrow; + } + } + + /// Send bitmap data to Even Realities glasses + Future displayBitmap(Uint8List bitmapData) async { + if (!isConnected || _txCharacteristic == null) { + throw Exception('Glasses not connected'); + } + + try { + _logger.log(_tag, 'Displaying bitmap data', logging.LogLevel.info); + + // Send bitmap in chunks according to protocol + const maxChunkSize = 16; // BLE packet size limit + + for (int i = 0; i < bitmapData.length; i += maxChunkSize) { + final endIndex = min(i + maxChunkSize, bitmapData.length); + final chunk = bitmapData.sublist(i, endIndex); + + // Create packet for this chunk + final packet = Uint8List(4 + chunk.length); + packet[0] = CMD_BITMAP_DATA; // Command byte + packet[1] = chunk.length; // Chunk length + packet[2] = (i >> 8) & 0xFF; // Offset high byte + packet[3] = i & 0xFF; // Offset low byte + + // Copy chunk data + for (int j = 0; j < chunk.length; j++) { + packet[4 + j] = chunk[j]; + } + + await _txCharacteristic!.write(packet, withoutResponse: false); + + // Small delay between chunks + await Future.delayed(const Duration(milliseconds: 10)); + } + + _logger.log(_tag, 'Bitmap sent to glasses successfully', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to send bitmap: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future displayNotification( + String title, + String message, { + service.NotificationPriority priority = service.NotificationPriority.normal, + Duration duration = const Duration(seconds: 5), + }) async { + // Combine title and message for display + final fullText = '$title\n$message'; + await displayText(fullText, duration: duration); + } + + @override + Future clearDisplay() async { + if (!isConnected || _txCharacteristic == null) { + throw Exception('Glasses not connected'); + } + + try { + _logger.log(_tag, 'Clearing display', logging.LogLevel.info); + + // Send clear display command + final packet = Uint8List(4); + packet[0] = CMD_CONTROL; // Control command + packet[1] = 0x01; // Length + packet[2] = CONTROL_CLEAR_DISPLAY; // Clear display sub-command + packet[3] = 0x00; // Reserved + + await _txCharacteristic!.write(packet, withoutResponse: false); + + _logger.log(_tag, 'Display cleared', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to clear display: $e', logging.LogLevel.error); + rethrow; + } + } + + /// Handle received data from glasses (touch events, etc.) + void _handleReceivedData(List data) { + try { + if (data.isEmpty) return; + + final command = data[0]; + + switch (command) { + case 0xF2: // Touch event + _handleTouchEvent(data); + break; + case CMD_MIC_DATA: // Microphone data + _handleMicrophoneData(data); + break; + default: + _logger.log(_tag, 'Unknown command received: 0x${command.toRadixString(16)}', logging.LogLevel.debug); + } + } catch (e) { + _logger.log(_tag, 'Error handling received data: $e', logging.LogLevel.error); + } + } + + void _handleTouchEvent(List data) { + if (data.length < 2) return; + + final touchType = data[1]; + service.TouchGesture? gesture; + + switch (touchType) { + case 0x01: + gesture = service.TouchGesture.tap; + break; + case 0x02: + gesture = service.TouchGesture.doubleTap; + break; + case 0x03: + gesture = service.TouchGesture.longPress; + break; + default: + _logger.log(_tag, 'Unknown touch type: $touchType', logging.LogLevel.debug); + return; + } + + _gestureController.add(gesture); + _logger.log(_tag, 'Touch gesture detected: $gesture', logging.LogLevel.debug); + } + + void _handleMicrophoneData(List data) { + // Handle microphone data if needed + _logger.log(_tag, 'Microphone data received: ${data.length} bytes', logging.LogLevel.debug); + } + + // Implement other required methods from GlassesService interface + @override + Future setBrightness(double brightness) async { + // TODO: Implement brightness control if supported by Even Realities protocol + _logger.log(_tag, 'setBrightness not implemented for Even Realities', logging.LogLevel.warning); + } + + @override + Future configureGestures({ + bool enableTap = true, + bool enableSwipe = true, + bool enableLongPress = true, + double sensitivity = 0.5, + }) async { + // TODO: Implement gesture configuration if supported + _logger.log(_tag, 'configureGestures not implemented for Even Realities', logging.LogLevel.warning); + } + + @override + Future sendCommand(String command, {Map? parameters}) async { + // TODO: Implement custom commands + _logger.log(_tag, 'sendCommand not implemented for Even Realities', logging.LogLevel.warning); + } + + @override + Future getDeviceInfo() async { + // TODO: Implement device info retrieval + throw UnimplementedError('getDeviceInfo not implemented yet'); + } + + @override + Future getBatteryLevel() async { + return _batteryLevel; + } + + @override + Future checkDeviceHealth() async { + // TODO: Implement health check + throw UnimplementedError('checkDeviceHealth not implemented yet'); + } + + @override + Future updateFirmware() async { + // TODO: Implement firmware update if supported + throw UnimplementedError('updateFirmware not implemented yet'); + } + + @override + Future dispose() async { + await disconnect(); + await stopScanning(); + + _connectionStateController.close(); + _discoveredDevicesController.close(); + _gestureController.close(); + _deviceStatusController.close(); + + _bluetoothStateSubscription?.cancel(); + _scanSubscription?.cancel(); + } +} \ No newline at end of file diff --git a/lib/services/implementations/glasses_service_impl.dart b/lib/services/implementations/glasses_service_impl.dart new file mode 100644 index 0000000..92804cd --- /dev/null +++ b/lib/services/implementations/glasses_service_impl.dart @@ -0,0 +1,785 @@ +// ABOUTME: Bluetooth glasses service implementation for Even Realities smart glasses +// ABOUTME: Handles device discovery, connection management, HUD rendering, and gesture input + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../glasses_service.dart' as service; +import '../../models/glasses_connection_state.dart'; +import '../../core/utils/logging_service.dart' as logging; +import '../../core/utils/constants.dart'; + +class GlassesServiceImpl implements service.GlassesService { + static const String _tag = 'GlassesServiceImpl'; + + final logging.LoggingService _logger; + + // Service state + bool _isInitialized = false; + ConnectionStatus _connectionState = ConnectionStatus.disconnected; + service.GlassesDevice? _connectedDevice; + List _discoveredDevices = []; + + // Bluetooth state + bool _bluetoothEnabled = false; + bool _hasPermissions = false; + StreamSubscription? _bluetoothStateSubscription; + StreamSubscription>? _scanSubscription; + + // Connected device state + BluetoothDevice? _bluetoothDevice; + BluetoothCharacteristic? _txCharacteristic; + BluetoothCharacteristic? _rxCharacteristic; + StreamSubscription? _connectionSubscription; + StreamSubscription>? _dataSubscription; + + // Stream controllers + final StreamController _connectionStateController = + StreamController.broadcast(); + final StreamController> _discoveredDevicesController = + StreamController>.broadcast(); + final StreamController _gestureController = + StreamController.broadcast(); + final StreamController _deviceStatusController = + StreamController.broadcast(); + + // Current device status + double _batteryLevel = 0.0; + double _currentBrightness = 0.8; + bool _gesturesEnabled = true; + + GlassesServiceImpl({required logging.LoggingService logger}) : _logger = logger; + + @override + ConnectionStatus get connectionState => _connectionState; + + @override + service.GlassesDevice? get connectedDevice => _connectedDevice; + + @override + bool get isConnected => _connectionState == ConnectionStatus.connected; + + @override + Stream get connectionStateStream => _connectionStateController.stream; + + @override + Stream> get discoveredDevicesStream => _discoveredDevicesController.stream; + + @override + Stream get gestureStream => _gestureController.stream; + + @override + Stream get deviceStatusStream => _deviceStatusController.stream; + + @override + Future initialize() async { + try { + _logger.log(_tag, 'Initializing glasses service', logging.LogLevel.info); + + // Check Bluetooth adapter state + final adapterState = await FlutterBluePlus.adapterState.first; + _bluetoothEnabled = adapterState == BluetoothAdapterState.on; + + // Listen to Bluetooth state changes + _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(_onBluetoothStateChanged); + + // Request permissions + _hasPermissions = await requestBluetoothPermission(); + + _isInitialized = true; + _logger.log(_tag, 'Glasses service initialized successfully', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to initialize glasses service: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future isBluetoothAvailable() async { + try { + if (!_bluetoothEnabled) { + final state = await FlutterBluePlus.adapterState.first; + _bluetoothEnabled = state == BluetoothAdapterState.on; + } + return _bluetoothEnabled; + } catch (e) { + _logger.log(_tag, 'Error checking Bluetooth availability: $e', logging.LogLevel.error); + return false; + } + } + + @override + Future requestBluetoothPermission() async { + try { + final permissions = [ + Permission.bluetooth, + Permission.bluetoothScan, + Permission.bluetoothConnect, + Permission.location, + ]; + + bool allGranted = true; + for (final permission in permissions) { + final status = await permission.request(); + if (status != PermissionStatus.granted) { + allGranted = false; + _logger.log(_tag, 'Permission denied: $permission', logging.LogLevel.warning); + } + } + + _hasPermissions = allGranted; + return allGranted; + } catch (e) { + _logger.log(_tag, 'Error requesting Bluetooth permissions: $e', logging.LogLevel.error); + return false; + } + } + + @override + Future startScanning({Duration timeout = const Duration(seconds: 30)}) async { + try { + if (!_isInitialized) { + throw Exception('Service not initialized'); + } + + if (!_bluetoothEnabled) { + _updateConnectionState(ConnectionStatus.error); + throw Exception('Bluetooth not enabled'); + } + + if (!_hasPermissions) { + _updateConnectionState(ConnectionStatus.unauthorized); + throw Exception('Bluetooth permissions not granted'); + } + + _logger.log(_tag, 'Starting scan for Even Realities glasses', logging.LogLevel.info); + _updateConnectionState(ConnectionStatus.scanning); + _discoveredDevices.clear(); + _discoveredDevicesController.add(_discoveredDevices); + + // Start scanning with timeout + await FlutterBluePlus.startScan( + timeout: timeout, + withServices: [Guid(BluetoothConstants.nordicUARTServiceUUID)], + ); + + // Listen to scan results + _scanSubscription = FlutterBluePlus.scanResults.listen(_onScanResult); + + // Handle scan timeout + Timer(timeout, () async { + if (_connectionState == ConnectionStatus.scanning) { + await stopScanning(); + if (_discoveredDevices.isEmpty) { + _updateConnectionState(ConnectionStatus.disconnected); + _logger.log(_tag, 'Scan completed - no devices found', logging.LogLevel.warning); + } else { + _logger.log(_tag, 'Scan completed - found ${_discoveredDevices.length} devices', logging.LogLevel.info); + } + } + }); + } catch (e) { + _logger.log(_tag, 'Error starting scan: $e', logging.LogLevel.error); + _updateConnectionState(ConnectionStatus.error); + rethrow; + } + } + + @override + Future stopScanning() async { + try { + await FlutterBluePlus.stopScan(); + await _scanSubscription?.cancel(); + _scanSubscription = null; + + if (_connectionState == ConnectionStatus.scanning) { + _updateConnectionState(ConnectionStatus.disconnected); + } + + _logger.log(_tag, 'Scan stopped', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error stopping scan: $e', logging.LogLevel.error); + } + } + + @override + Future connectToDevice(String deviceId) async { + try { + if (!_isInitialized) { + throw Exception('Service not initialized'); + } + + final device = _discoveredDevices.firstWhere( + (d) => d.id == deviceId, + orElse: () => throw Exception('Device not found: $deviceId'), + ); + + _logger.log(_tag, 'Connecting to device: ${device.name}', logging.LogLevel.info); + _updateConnectionState(ConnectionStatus.connecting); + + // Stop scanning if active + if (_connectionState == ConnectionStatus.scanning) { + await stopScanning(); + } + + // Get the Bluetooth device + final scanResults = await FlutterBluePlus.scanResults.first; + final scanResult = scanResults.firstWhere( + (result) => result.device.remoteId.toString() == deviceId, + orElse: () => throw Exception('Bluetooth device not found'), + ); + + _bluetoothDevice = scanResult.device; + + // Connect to device + await _bluetoothDevice!.connect(timeout: BluetoothConstants.connectionTimeout); + + // Listen to connection state changes + _connectionSubscription = _bluetoothDevice!.connectionState.listen(_onConnectionStateChanged); + + // Discover services and characteristics + await _discoverServices(); + + // Setup data communication + await _setupDataCommunication(); + + _connectedDevice = device; + _updateConnectionState(ConnectionStatus.connected); + + // Start periodic device status monitoring + _startDeviceStatusMonitoring(); + + _logger.log(_tag, 'Successfully connected to ${device.name}', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to connect to device: $e', logging.LogLevel.error); + _updateConnectionState(ConnectionStatus.error); + rethrow; + } + } + + @override + Future connectToLastDevice() async { + try { + // This would typically load the last connected device from persistent storage + // For now, just connect to the first discovered device if available + if (_discoveredDevices.isNotEmpty) { + await connectToDevice(_discoveredDevices.first.id); + } else { + throw Exception('No known devices to connect to'); + } + } catch (e) { + _logger.log(_tag, 'Failed to connect to last device: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future disconnect() async { + try { + _logger.log(_tag, 'Disconnecting from glasses', logging.LogLevel.info); + _updateConnectionState(ConnectionStatus.disconnecting); + + await _connectionSubscription?.cancel(); + await _dataSubscription?.cancel(); + + if (_bluetoothDevice != null) { + await _bluetoothDevice!.disconnect(); + } + + _bluetoothDevice = null; + _txCharacteristic = null; + _rxCharacteristic = null; + _connectedDevice = null; + + _updateConnectionState(ConnectionStatus.disconnected); + _logger.log(_tag, 'Disconnected from glasses', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error during disconnect: $e', logging.LogLevel.error); + _updateConnectionState(ConnectionStatus.error); + } + } + + @override + Future displayText( + String text, { + service.HUDPosition position = service.HUDPosition.center, + Duration? duration, + service.HUDStyle? style, + }) async { + try { + if (!isConnected) { + throw Exception('Device not connected'); + } + + final command = { + 'type': 'display_text', + 'content': text, + 'position': position.name, + 'duration': duration?.inSeconds ?? 5, + 'style': style != null ? { + 'fontSize': style.fontSize, + 'color': style.color, + 'fontWeight': style.fontWeight, + 'alignment': style.alignment, + } : null, + }; + + await _sendCommand(command); + _logger.log(_tag, 'Displayed text on HUD: $text', logging.LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Failed to display text: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future displayNotification( + String title, + String message, { + service.NotificationPriority priority = service.NotificationPriority.normal, + Duration duration = const Duration(seconds: 5), + }) async { + try { + if (!isConnected) { + throw Exception('Device not connected'); + } + + final command = { + 'type': 'display_notification', + 'title': title, + 'message': message, + 'priority': priority.name, + 'duration': duration.inSeconds, + }; + + await _sendCommand(command); + _logger.log(_tag, 'Displayed notification: $title', logging.LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Failed to display notification: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future clearDisplay() async { + try { + if (!isConnected) { + throw Exception('Device not connected'); + } + + final command = {'type': 'clear_display'}; + await _sendCommand(command); + _logger.log(_tag, 'Cleared HUD display', logging.LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Failed to clear display: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future setBrightness(double brightness) async { + try { + if (!isConnected) { + throw Exception('Device not connected'); + } + + _currentBrightness = brightness.clamp(0.0, 1.0); + final command = { + 'type': 'set_brightness', + 'value': _currentBrightness, + }; + + await _sendCommand(command); + _logger.log(_tag, 'Set brightness to: $_currentBrightness', logging.LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Failed to set brightness: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future configureGestures({ + bool enableTap = true, + bool enableSwipe = true, + bool enableLongPress = true, + double sensitivity = 0.5, + }) async { + try { + if (!isConnected) { + throw Exception('Device not connected'); + } + + final command = { + 'type': 'configure_gestures', + 'enableTap': enableTap, + 'enableSwipe': enableSwipe, + 'enableLongPress': enableLongPress, + 'sensitivity': sensitivity.clamp(0.0, 1.0), + }; + + await _sendCommand(command); + _gesturesEnabled = enableTap || enableSwipe || enableLongPress; + _logger.log(_tag, 'Configured gestures', logging.LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Failed to configure gestures: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future sendCommand(String command, {Map? parameters}) async { + try { + if (!isConnected) { + throw Exception('Device not connected'); + } + + final commandData = { + 'type': 'custom_command', + 'command': command, + 'parameters': parameters ?? {}, + }; + + await _sendCommand(commandData); + _logger.log(_tag, 'Sent custom command: $command', logging.LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Failed to send command: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future getDeviceInfo() async { + try { + if (!isConnected || _connectedDevice == null) { + throw Exception('Device not connected'); + } + + // Request device info from glasses + final command = {'type': 'get_device_info'}; + await _sendCommand(command); + + // In a real implementation, this would wait for a response + // For now, return basic info + return service.GlassesDeviceInfo( + deviceId: _connectedDevice!.id, + modelName: _connectedDevice!.modelNumber ?? 'G1', + firmwareVersion: '1.0.0', + hardwareVersion: '1.0', + serialNumber: 'SN${DateTime.now().millisecondsSinceEpoch}', + lastConnected: DateTime.now(), + ); + } catch (e) { + _logger.log(_tag, 'Failed to get device info: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future getBatteryLevel() async { + try { + if (!isConnected) { + throw Exception('Device not connected'); + } + + final command = {'type': 'get_battery_level'}; + await _sendCommand(command); + + // In a real implementation, this would wait for a response + return _batteryLevel; + } catch (e) { + _logger.log(_tag, 'Failed to get battery level: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future checkDeviceHealth() async { + try { + if (!isConnected) { + throw Exception('Device not connected'); + } + + final command = {'type': 'check_health'}; + await _sendCommand(command); + + // In a real implementation, this would analyze device status + return service.GlassesHealthStatus( + isHealthy: _batteryLevel > 0.1 && isConnected, + issues: _batteryLevel < 0.2 ? ['Low battery'] : [], + diagnostics: { + 'battery_level': _batteryLevel, + 'signal_strength': _connectedDevice?.signalStrength ?? -100, + 'connection_stable': isConnected, + }, + overallStatus: _batteryLevel > 0.2 ? 'good' : 'warning', + ); + } catch (e) { + _logger.log(_tag, 'Failed to check device health: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future updateFirmware() async { + try { + if (!isConnected) { + throw Exception('Device not connected'); + } + + _logger.log(_tag, 'Firmware update not implemented yet', logging.LogLevel.warning); + throw UnimplementedError('Firmware update not yet implemented'); + } catch (e) { + _logger.log(_tag, 'Failed to update firmware: $e', logging.LogLevel.error); + rethrow; + } + } + + @override + Future dispose() async { + try { + await disconnect(); + await _bluetoothStateSubscription?.cancel(); + await _scanSubscription?.cancel(); + await _connectionStateController.close(); + await _discoveredDevicesController.close(); + await _gestureController.close(); + await _deviceStatusController.close(); + + _logger.log(_tag, 'Glasses service disposed', logging.LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error disposing glasses service: $e', logging.LogLevel.error); + } + } + + // Private methods + + void _updateConnectionState(ConnectionStatus newState) { + if (_connectionState != newState) { + _connectionState = newState; + _connectionStateController.add(newState); + _logger.log(_tag, 'Connection state changed to: ${newState.name}', logging.LogLevel.debug); + } + } + + void _onBluetoothStateChanged(BluetoothAdapterState state) { + _bluetoothEnabled = state == BluetoothAdapterState.on; + _logger.log(_tag, 'Bluetooth state changed: $state', logging.LogLevel.debug); + + if (!_bluetoothEnabled && isConnected) { + disconnect(); + } + } + + void _onScanResult(List results) { + for (final result in results) { + final device = result.device; + + // Filter for Even Realities devices + if (_isEvenRealitiesDevice(device, result.advertisementData)) { + final glassesDevice = service.GlassesDevice( + id: device.remoteId.toString(), + name: device.platformName.isNotEmpty ? device.platformName : 'Even Realities G1', + modelNumber: 'G1', + signalStrength: result.rssi, + isConnected: false, + ); + + // Add or update device in discovered list + final existingIndex = _discoveredDevices.indexWhere((d) => d.id == glassesDevice.id); + if (existingIndex >= 0) { + _discoveredDevices[existingIndex] = glassesDevice; + } else { + _discoveredDevices.add(glassesDevice); + _logger.log(_tag, 'Discovered device: ${glassesDevice.name} (${glassesDevice.signalStrength} dBm)', logging.LogLevel.info); + } + + _discoveredDevicesController.add(List.from(_discoveredDevices)); + } + } + } + + bool _isEvenRealitiesDevice(BluetoothDevice device, AdvertisementData adData) { + // Check device name + if (BluetoothConstants.targetDeviceNames.any((name) => + device.platformName.toLowerCase().contains(name.toLowerCase()))) { + return true; + } + + // Check manufacturer data + if (adData.manufacturerData.isNotEmpty) { + // Even Realities would have specific manufacturer ID + return true; // Simplified for now + } + + // Check service UUIDs + if (adData.serviceUuids.contains(Guid(BluetoothConstants.nordicUARTServiceUUID))) { + return true; + } + + return false; + } + + void _onConnectionStateChanged(BluetoothConnectionState state) { + _logger.log(_tag, 'Bluetooth connection state: $state', logging.LogLevel.debug); + + switch (state) { + case BluetoothConnectionState.connected: + if (_connectionState == ConnectionStatus.connecting) { + // Service setup will be completed in connectToDevice() + } + break; + case BluetoothConnectionState.disconnected: + if (isConnected) { + _updateConnectionState(ConnectionStatus.disconnected); + _connectedDevice = null; + } + break; + case BluetoothConnectionState.connecting: + // Handle connecting state + break; + case BluetoothConnectionState.disconnecting: + // Handle disconnecting state + _updateConnectionState(ConnectionStatus.disconnecting); + break; + } + } + + Future _discoverServices() async { + if (_bluetoothDevice == null) return; + + final services = await _bluetoothDevice!.discoverServices(); + + for (final service in services) { + if (service.uuid.toString().toUpperCase() == BluetoothConstants.nordicUARTServiceUUID.toUpperCase()) { + for (final characteristic in service.characteristics) { + final uuid = characteristic.uuid.toString().toUpperCase(); + + if (uuid == BluetoothConstants.nordicUARTTXCharacteristicUUID.toUpperCase()) { + _txCharacteristic = characteristic; + } else if (uuid == BluetoothConstants.nordicUARTRXCharacteristicUUID.toUpperCase()) { + _rxCharacteristic = characteristic; + } + } + break; + } + } + + if (_txCharacteristic == null || _rxCharacteristic == null) { + throw Exception('Required characteristics not found'); + } + + _logger.log(_tag, 'Discovered Nordic UART service and characteristics', logging.LogLevel.debug); + } + + Future _setupDataCommunication() async { + if (_rxCharacteristic == null) return; + + // Enable notifications on RX characteristic + await _rxCharacteristic!.setNotifyValue(true); + + // Listen to incoming data + _dataSubscription = _rxCharacteristic!.lastValueStream.listen(_onDataReceived); + + _logger.log(_tag, 'Data communication setup completed', logging.LogLevel.debug); + } + + void _onDataReceived(List data) { + try { + final message = utf8.decode(data); + final parsed = jsonDecode(message); + + _logger.log(_tag, 'Received data: $message', logging.LogLevel.debug); + + // Handle different message types + switch (parsed['type']) { + case 'gesture': + _handleGestureMessage(parsed); + break; + case 'battery_update': + _handleBatteryUpdate(parsed); + break; + case 'status_update': + _handleStatusUpdate(parsed); + break; + default: + _logger.log(_tag, 'Unknown message type: ${parsed['type']}', logging.LogLevel.warning); + } + } catch (e) { + _logger.log(_tag, 'Error processing received data: $e', logging.LogLevel.error); + } + } + + void _handleGestureMessage(Map data) { + try { + final gestureStr = data['gesture'] as String; + final gesture = service.TouchGesture.values.firstWhere( + (g) => g.name == gestureStr, + orElse: () => service.TouchGesture.tap, + ); + + _gestureController.add(gesture); + _logger.log(_tag, 'Received gesture: ${gesture.name}', logging.LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Error handling gesture message: $e', logging.LogLevel.error); + } + } + + void _handleBatteryUpdate(Map data) { + try { + _batteryLevel = (data['level'] as num).toDouble(); + _logger.log(_tag, 'Battery level updated: ${(_batteryLevel * 100).round()}%', logging.LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Error handling battery update: $e', logging.LogLevel.error); + } + } + + void _handleStatusUpdate(Map data) { + try { + final status = service.GlassesDeviceStatus( + batteryLevel: _batteryLevel, + isCharging: data['charging'] ?? false, + signalStrength: data['rssi'] ?? -100, + connectionQuality: data['quality'] ?? 'good', + lastUpdate: DateTime.now(), + ); + + _deviceStatusController.add(status); + } catch (e) { + _logger.log(_tag, 'Error handling status update: $e', logging.LogLevel.error); + } + } + + Future _sendCommand(Map command) async { + if (_txCharacteristic == null) { + throw Exception('TX characteristic not available'); + } + + try { + final message = jsonEncode(command); + final data = utf8.encode(message); + + await _txCharacteristic!.write(data, withoutResponse: false); + _logger.log(_tag, 'Sent command: $message', logging.LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Error sending command: $e', logging.LogLevel.error); + rethrow; + } + } + + void _startDeviceStatusMonitoring() { + Timer.periodic(BluetoothConstants.heartbeatInterval, (timer) { + if (!isConnected) { + timer.cancel(); + return; + } + + // Request status update + _sendCommand({'type': 'get_status'}).catchError((e) { + _logger.log(_tag, 'Error requesting status update: $e', logging.LogLevel.warning); + }); + }); + } +} \ No newline at end of file diff --git a/lib/services/implementations/llm_service_impl.dart b/lib/services/implementations/llm_service_impl.dart new file mode 100644 index 0000000..11c43ba --- /dev/null +++ b/lib/services/implementations/llm_service_impl.dart @@ -0,0 +1,591 @@ +// ABOUTME: LLM service implementation for AI-powered conversation analysis +// ABOUTME: Integrates with OpenAI GPT and Anthropic APIs for fact-checking, summarization, and insights + +import 'dart:async'; + +import 'package:dio/dio.dart'; + +import '../llm_service.dart'; +import '../../models/analysis_result.dart'; +import '../../models/conversation_model.dart'; +import '../../core/utils/logging_service.dart'; +import '../../core/utils/constants.dart'; + +class LLMServiceImpl implements LLMService { + static const String _tag = 'LLMServiceImpl'; + + final LoggingService _logger; + final Dio _dio; + + // Service state + bool _isInitialized = false; + LLMProvider _currentProvider = LLMProvider.openai; + String? _openAIKey; + String? _anthropicKey; + + // Configuration + AnalysisConfiguration _analysisConfig = const AnalysisConfiguration(); + Map _analysisCache = {}; + + LLMServiceImpl({ + required LoggingService logger, + Dio? dio, + }) : _logger = logger, + _dio = dio ?? Dio(); + + @override + bool get isInitialized => _isInitialized; + + @override + LLMProvider get currentProvider => _currentProvider; + + @override + Future initialize({ + String? openAIKey, + String? anthropicKey, + LLMProvider? preferredProvider, + }) async { + try { + _logger.log(_tag, 'Initializing LLM service', LogLevel.info); + + _openAIKey = openAIKey; + _anthropicKey = anthropicKey; + + if (preferredProvider != null) { + _currentProvider = preferredProvider; + } + + // Configure HTTP client + _dio.options.connectTimeout = APIConstants.apiTimeout; + _dio.options.receiveTimeout = APIConstants.apiTimeout; + _dio.options.headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'Helix/1.0.0', + }; + + // Validate API keys + await _validateProvider(_currentProvider); + + _isInitialized = true; + _logger.log(_tag, 'LLM service initialized with provider: ${_currentProvider.name}', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to initialize LLM service: $e', LogLevel.error); + rethrow; + } + } + + @override + Future setProvider(LLMProvider provider) async { + try { + await _validateProvider(provider); + _currentProvider = provider; + _logger.log(_tag, 'Provider changed to: ${provider.name}', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to set provider: $e', LogLevel.error); + rethrow; + } + } + + @override + Future analyzeConversation( + String conversationText, { + AnalysisType type = AnalysisType.comprehensive, + AnalysisPriority priority = AnalysisPriority.normal, + LLMProvider? provider, + Map? context, + }) async { + try { + if (!_isInitialized) { + throw LLMException('Service not initialized', LLMErrorType.serviceNotReady); + } + + final analysisProvider = provider ?? _currentProvider; + final cacheKey = _generateCacheKey(conversationText, type, analysisProvider); + + // Check cache for recent analysis + if (_analysisCache.containsKey(cacheKey)) { + final cached = _analysisCache[cacheKey]; + if (DateTime.now().difference(cached['timestamp']).inMinutes < 10) { + _logger.log(_tag, 'Returning cached analysis result', LogLevel.debug); + return AnalysisResult.fromJson(cached['result']); + } + } + + _logger.log(_tag, 'Starting conversation analysis with ${analysisProvider.name}', LogLevel.info); + + final analysisResult = await _performAnalysis( + conversationText, + type, + analysisProvider, + context ?? {}, + ); + + // Cache the result + _analysisCache[cacheKey] = { + 'result': analysisResult.toJson(), + 'timestamp': DateTime.now(), + }; + + _logger.log(_tag, 'Analysis completed successfully', LogLevel.info); + return analysisResult; + } catch (e) { + _logger.log(_tag, 'Analysis failed: $e', LogLevel.error); + rethrow; + } + } + + @override + Future> checkFacts(List claims) async { + try { + if (!_isInitialized) { + throw LLMException('Service not initialized', LLMErrorType.serviceNotReady); + } + + _logger.log(_tag, 'Fact-checking ${claims.length} claims', LogLevel.info); + + final verifications = []; + + for (final claim in claims) { + final prompt = _buildFactCheckPrompt(claim); + final response = await _sendRequest(prompt, _currentProvider); + final verification = _parseFactCheckResponse(claim, response); + verifications.add(verification); + } + + return verifications; + } catch (e) { + _logger.log(_tag, 'Fact-checking failed: $e', LogLevel.error); + rethrow; + } + } + + @override + Future generateSummary( + ConversationModel conversation, { + bool includeKeyPoints = true, + bool includeActionItems = true, + int maxWords = 200, + }) async { + try { + if (!_isInitialized) { + throw LLMException('Service not initialized', LLMErrorType.serviceNotReady); + } + + final conversationText = conversation.segments.map((s) => s.text).join(' '); + final prompt = _buildSummaryPrompt(conversationText, maxWords, includeKeyPoints, includeActionItems); + + _logger.log(_tag, 'Generating conversation summary', LogLevel.info); + + final response = await _sendRequest(prompt, _currentProvider); + final summary = _parseSummaryResponse(response, conversation.id); + + return summary; + } catch (e) { + _logger.log(_tag, 'Summary generation failed: $e', LogLevel.error); + rethrow; + } + } + + @override + Future> extractActionItems( + String conversationText, { + bool includeDeadlines = true, + bool includePriority = true, + }) async { + try { + if (!_isInitialized) { + throw LLMException('Service not initialized', LLMErrorType.serviceNotReady); + } + + final prompt = _buildActionItemPrompt(conversationText, includeDeadlines, includePriority); + + _logger.log(_tag, 'Extracting action items', LogLevel.info); + + final response = await _sendRequest(prompt, _currentProvider); + final actionItems = _parseActionItemsResponse(response); + + return actionItems; + } catch (e) { + _logger.log(_tag, 'Action item extraction failed: $e', LogLevel.error); + rethrow; + } + } + + @override + Future analyzeSentiment(String text) async { + try { + if (!_isInitialized) { + throw LLMException('Service not initialized', LLMErrorType.serviceNotReady); + } + + final prompt = _buildSentimentPrompt(text); + final response = await _sendRequest(prompt, _currentProvider); + final sentiment = _parseSentimentResponse(response); + + return sentiment; + } catch (e) { + _logger.log(_tag, 'Sentiment analysis failed: $e', LogLevel.error); + rethrow; + } + } + + @override + Future askQuestion( + String question, + String context, { + LLMProvider? provider, + }) async { + try { + if (!_isInitialized) { + throw LLMException('Service not initialized', LLMErrorType.serviceNotReady); + } + + final prompt = _buildQuestionPrompt(question, context); + final analysisProvider = provider ?? _currentProvider; + + _logger.log(_tag, 'Processing question with context', LogLevel.info); + + final response = await _sendRequest(prompt, analysisProvider); + return _parseQuestionResponse(response); + } catch (e) { + _logger.log(_tag, 'Question processing failed: $e', LogLevel.error); + rethrow; + } + } + + @override + Future configureAnalysis(AnalysisConfiguration config) async { + try { + _analysisConfig = config; + _logger.log(_tag, 'Analysis configuration updated', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to configure analysis: $e', LogLevel.error); + rethrow; + } + } + + @override + Future clearCache() async { + try { + _analysisCache.clear(); + _logger.log(_tag, 'Analysis cache cleared', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to clear cache: $e', LogLevel.error); + rethrow; + } + } + + @override + Future> getUsageStats() async { + try { + // In a real implementation, this would track API usage, costs, etc. + return { + 'provider': _currentProvider.name, + 'cache_size': _analysisCache.length, + 'initialized': _isInitialized, + 'analysis_config': _analysisConfig.toJson(), + }; + } catch (e) { + _logger.log(_tag, 'Failed to get usage stats: $e', LogLevel.error); + rethrow; + } + } + + @override + Future dispose() async { + try { + await clearCache(); + _dio.close(); + _isInitialized = false; + _logger.log(_tag, 'LLM service disposed', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error disposing LLM service: $e', LogLevel.error); + } + } + + // Private methods + + Future _validateProvider(LLMProvider provider) async { + switch (provider) { + case LLMProvider.openai: + if (_openAIKey == null || _openAIKey!.isEmpty) { + throw LLMException('OpenAI API key required', LLMErrorType.invalidApiKey); + } + break; + case LLMProvider.anthropic: + if (_anthropicKey == null || _anthropicKey!.isEmpty) { + throw LLMException('Anthropic API key required', LLMErrorType.invalidApiKey); + } + break; + case LLMProvider.local: + // Local models don't require API keys + break; + } + } + + Future _performAnalysis( + String conversationText, + AnalysisType type, + LLMProvider provider, + Map context, + ) async { + final prompt = _buildAnalysisPrompt(conversationText, type, context); + final response = await _sendRequest(prompt, provider); + return _parseAnalysisResponse(response, conversationText); + } + + Future _sendRequest(String prompt, LLMProvider provider) async { + switch (provider) { + case LLMProvider.openai: + return _sendOpenAIRequest(prompt); + case LLMProvider.anthropic: + return _sendAnthropicRequest(prompt); + case LLMProvider.local: + throw LLMException('Local provider not implemented yet', LLMErrorType.serviceNotReady); + } + } + + Future _sendOpenAIRequest(String prompt) async { + try { + final response = await _dio.post( + '${APIConstants.openAIBaseURL}${APIConstants.chatCompletionsEndpoint}', + data: { + 'model': APIConstants.defaultOpenAIModel, + 'messages': [ + {'role': 'user', 'content': prompt} + ], + 'max_tokens': 1000, + 'temperature': 0.1, + }, + options: Options( + headers: { + 'Authorization': 'Bearer $_openAIKey', + }, + ), + ); + + return response.data['choices'][0]['message']['content']; + } catch (e) { + if (e is DioException) { + throw LLMException( + 'OpenAI API error: ${e.message}', + LLMErrorType.apiError, + originalError: e, + ); + } + rethrow; + } + } + + Future _sendAnthropicRequest(String prompt) async { + try { + final response = await _dio.post( + '${APIConstants.anthropicBaseURL}${APIConstants.anthropicMessagesEndpoint}', + data: { + 'model': APIConstants.defaultAnthropicModel, + 'max_tokens': 1000, + 'messages': [ + {'role': 'user', 'content': prompt} + ], + }, + options: Options( + headers: { + 'x-api-key': _anthropicKey, + 'anthropic-version': '2023-06-01', + }, + ), + ); + + return response.data['content'][0]['text']; + } catch (e) { + if (e is DioException) { + throw LLMException( + 'Anthropic API error: ${e.message}', + LLMErrorType.apiError, + originalError: e, + ); + } + rethrow; + } + } + + String _buildAnalysisPrompt( + String conversationText, + AnalysisType type, + Map context, + ) { + switch (type) { + case AnalysisType.factCheck: + return AnalysisConstants.factCheckPromptTemplate.replaceAll( + '{conversation_text}', + conversationText, + ); + case AnalysisType.summary: + return AnalysisConstants.summaryPromptTemplate.replaceAll( + '{conversation_text}', + conversationText, + ); + case AnalysisType.comprehensive: + return ''' +Analyze the following conversation comprehensively: + +$conversationText + +Provide: +1. Key topics and themes +2. Factual claims that can be verified +3. Action items and follow-ups +4. Overall sentiment and tone +5. Summary of main points + +Format your response as structured JSON. +'''; + case AnalysisType.actionItems: + case AnalysisType.sentiment: + case AnalysisType.topics: + return ''' +Analyze the following conversation for ${type.name}: + +$conversationText + +Provide structured analysis results. +'''; + } + } + + String _buildFactCheckPrompt(String claim) { + return ''' +Fact-check the following claim: + +"$claim" + +Provide verification status, confidence level, and sources if possible. +Format as JSON with fields: status, confidence, sources, explanation. +'''; + } + + String _buildSummaryPrompt( + String conversationText, + int maxWords, + bool includeKeyPoints, + bool includeActionItems, + ) { + return ''' +Summarize the following conversation in approximately $maxWords words: + +$conversationText + +${includeKeyPoints ? 'Include key points discussed.' : ''} +${includeActionItems ? 'Include any action items or follow-ups.' : ''} + +Provide a clear, concise summary. +'''; + } + + String _buildActionItemPrompt( + String conversationText, + bool includeDeadlines, + bool includePriority, + ) { + return ''' +Extract action items from the following conversation: + +$conversationText + +For each action item, identify: +- What needs to be done +- Who is responsible (if mentioned) +${includeDeadlines ? '- Any deadlines or timeframes' : ''} +${includePriority ? '- Priority level (high/medium/low)' : ''} + +Format as JSON array. +'''; + } + + String _buildSentimentPrompt(String text) { + return ''' +Analyze the sentiment of the following text: + +$text + +Provide: +- Overall sentiment (positive/negative/neutral) +- Confidence score (0-1) +- Emotional tone (if applicable) +- Key sentiment indicators + +Format as JSON. +'''; + } + + String _buildQuestionPrompt(String question, String context) { + return ''' +Based on the following context: + +$context + +Answer this question: $question + +Provide a clear, accurate answer based only on the given context. +'''; + } + + AnalysisResult _parseAnalysisResponse(String response, String originalText) { + // In a real implementation, this would parse the JSON response + // For now, return a basic result + return AnalysisResult( + id: 'analysis_${DateTime.now().millisecondsSinceEpoch}', + conversationId: 'conv_${DateTime.now().millisecondsSinceEpoch}', + type: AnalysisType.comprehensive, + status: AnalysisStatus.completed, + startTime: DateTime.now().subtract(const Duration(seconds: 5)), + completionTime: DateTime.now(), + provider: _currentProvider.name, + confidence: 0.8, + ); + } + + FactCheckResult _parseFactCheckResponse(String claim, String response) { + return FactCheckResult( + id: 'fact_${DateTime.now().millisecondsSinceEpoch}', + claim: claim, + status: FactCheckStatus.uncertain, + confidence: 0.5, + sources: [], + explanation: response, + ); + } + + ConversationSummary _parseSummaryResponse(String response, String conversationId) { + return ConversationSummary( + summary: response, + keyPoints: [], + decisions: [], + questions: [], + topics: [], + confidence: 0.8, + ); + } + + List _parseActionItemsResponse(String response) { + // Basic implementation - would parse JSON in real version + return []; + } + + SentimentAnalysisResult _parseSentimentResponse(String response) { + return SentimentAnalysisResult( + overallSentiment: SentimentType.neutral, + confidence: 0.5, + emotions: {}, + ); + } + + String _parseQuestionResponse(String response) { + return response.trim(); + } + + String _generateCacheKey(String text, AnalysisType type, LLMProvider provider) { + final hash = text.hashCode.toString(); + return '${provider.name}_${type.name}_$hash'; + } +} \ No newline at end of file diff --git a/lib/services/implementations/settings_service_impl.dart b/lib/services/implementations/settings_service_impl.dart new file mode 100644 index 0000000..0df0ed4 --- /dev/null +++ b/lib/services/implementations/settings_service_impl.dart @@ -0,0 +1,746 @@ +// ABOUTME: Settings service implementation using SharedPreferences for persistence +// ABOUTME: Manages app configuration, user preferences, and secure API key storage + +import 'dart:async'; +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import '../settings_service.dart'; +import '../../core/utils/logging_service.dart'; + +class SettingsServiceImpl implements SettingsService { + static const String _tag = 'SettingsServiceImpl'; + + final LoggingService _logger; + final SharedPreferences _prefs; + + // Stream controller for settings changes + final StreamController _settingsChangeController = + StreamController.broadcast(); + + // Settings keys + static const String _themeKey = 'theme_mode'; + static const String _languageKey = 'language'; + static const String _privacyLevelKey = 'privacy_level'; + + // Audio settings keys + static const String _audioDeviceKey = 'audio_device'; + static const String _audioQualityKey = 'audio_quality'; + static const String _noiseReductionKey = 'noise_reduction'; + static const String _vadSensitivityKey = 'vad_sensitivity'; + + // Transcription settings keys + static const String _transcriptionBackendKey = 'transcription_backend'; + static const String _transcriptionLanguageKey = 'transcription_language'; + static const String _autoBackendSwitchKey = 'auto_backend_switch'; + + // AI settings keys + static const String _aiProviderKey = 'ai_provider'; + static const String _apiKeysKey = 'api_keys'; + static const String _factCheckingKey = 'fact_checking'; + static const String _realTimeAnalysisKey = 'real_time_analysis'; + static const String _factCheckThresholdKey = 'fact_check_threshold'; + + // Glasses settings keys + static const String _lastGlassesKey = 'last_glasses'; + static const String _autoConnectGlassesKey = 'auto_connect_glasses'; + static const String _hudBrightnessKey = 'hud_brightness'; + static const String _gestureSensitivityKey = 'gesture_sensitivity'; + + // Privacy settings keys + static const String _dataRetentionKey = 'data_retention_days'; + static const String _autoCleanupKey = 'auto_cleanup'; + static const String _analyticsConsentKey = 'analytics_consent'; + static const String _crashReportingKey = 'crash_reporting'; + + // Backup settings keys + static const String _cloudSyncKey = 'cloud_sync'; + static const String _backupFrequencyKey = 'backup_frequency'; + + // Accessibility settings keys + static const String _largeTextKey = 'large_text'; + static const String _highContrastKey = 'high_contrast'; + static const String _reducedMotionKey = 'reduced_motion'; + + // Advanced settings keys + static const String _developerModeKey = 'developer_mode'; + static const String _debugLoggingKey = 'debug_logging'; + static const String _betaFeaturesKey = 'beta_features'; + + SettingsServiceImpl({ + required LoggingService logger, + required SharedPreferences prefs, + }) : _logger = logger, _prefs = prefs; + + @override + Stream get settingsChangeStream => _settingsChangeController.stream; + + @override + Future initialize() async { + try { + _logger.log(_tag, 'Initializing settings service', LogLevel.info); + + // Initialize default values if not set + await _initializeDefaults(); + + _logger.log(_tag, 'Settings service initialized successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to initialize settings service: $e', LogLevel.error); + rethrow; + } + } + + // ========================================================================== + // General App Settings + // ========================================================================== + + @override + Future getThemeMode() async { + final mode = _prefs.getString(_themeKey) ?? 'system'; + return ThemeMode.values.firstWhere( + (e) => e.name == mode, + orElse: () => ThemeMode.system, + ); + } + + @override + Future setThemeMode(ThemeMode mode) async { + await _setSetting(_themeKey, mode.name); + } + + @override + Future getLanguage() async { + return _prefs.getString(_languageKey) ?? 'en-US'; + } + + @override + Future setLanguage(String languageCode) async { + await _setSetting(_languageKey, languageCode); + } + + @override + Future getPrivacyLevel() async { + final level = _prefs.getString(_privacyLevelKey) ?? 'balanced'; + return PrivacyLevel.values.firstWhere( + (e) => e.name == level, + orElse: () => PrivacyLevel.balanced, + ); + } + + @override + Future setPrivacyLevel(PrivacyLevel level) async { + await _setSetting(_privacyLevelKey, level.name); + } + + // ========================================================================== + // Audio Settings + // ========================================================================== + + @override + Future getPreferredAudioDevice() async { + return _prefs.getString(_audioDeviceKey); + } + + @override + Future setPreferredAudioDevice(String deviceId) async { + await _setSetting(_audioDeviceKey, deviceId); + } + + @override + Future getAudioQuality() async { + return _prefs.getString(_audioQualityKey) ?? 'medium'; + } + + @override + Future setAudioQuality(String quality) async { + await _setSetting(_audioQualityKey, quality); + } + + @override + Future getNoiseReductionEnabled() async { + return _prefs.getBool(_noiseReductionKey) ?? true; + } + + @override + Future setNoiseReductionEnabled(bool enabled) async { + await _setSetting(_noiseReductionKey, enabled); + } + + @override + Future getVADSensitivity() async { + return _prefs.getDouble(_vadSensitivityKey) ?? 0.5; + } + + @override + Future setVADSensitivity(double sensitivity) async { + await _setSetting(_vadSensitivityKey, sensitivity.clamp(0.0, 1.0)); + } + + // ========================================================================== + // Transcription Settings + // ========================================================================== + + @override + Future getPreferredTranscriptionBackend() async { + return _prefs.getString(_transcriptionBackendKey) ?? 'local'; + } + + @override + Future setPreferredTranscriptionBackend(String backend) async { + await _setSetting(_transcriptionBackendKey, backend); + } + + @override + Future getTranscriptionLanguage() async { + return _prefs.getString(_transcriptionLanguageKey) ?? 'en-US'; + } + + @override + Future setTranscriptionLanguage(String languageCode) async { + await _setSetting(_transcriptionLanguageKey, languageCode); + } + + @override + Future getAutomaticBackendSwitching() async { + return _prefs.getBool(_autoBackendSwitchKey) ?? true; + } + + @override + Future setAutomaticBackendSwitching(bool enabled) async { + await _setSetting(_autoBackendSwitchKey, enabled); + } + + // ========================================================================== + // AI Service Settings + // ========================================================================== + + @override + Future getPreferredAIProvider() async { + return _prefs.getString(_aiProviderKey) ?? 'openai'; + } + + @override + Future setPreferredAIProvider(String provider) async { + await _setSetting(_aiProviderKey, provider); + } + + @override + Future getAPIKey(String provider) async { + final apiKeys = _getAPIKeysMap(); + return apiKeys[provider]; + } + + @override + Future setAPIKey(String provider, String apiKey) async { + final apiKeys = _getAPIKeysMap(); + apiKeys[provider] = apiKey; + await _setSetting(_apiKeysKey, jsonEncode(apiKeys)); + } + + @override + Future removeAPIKey(String provider) async { + final apiKeys = _getAPIKeysMap(); + apiKeys.remove(provider); + await _setSetting(_apiKeysKey, jsonEncode(apiKeys)); + } + + @override + Future getFactCheckingEnabled() async { + return _prefs.getBool(_factCheckingKey) ?? true; + } + + @override + Future setFactCheckingEnabled(bool enabled) async { + await _setSetting(_factCheckingKey, enabled); + } + + @override + Future getRealTimeAnalysisEnabled() async { + return _prefs.getBool(_realTimeAnalysisKey) ?? false; + } + + @override + Future setRealTimeAnalysisEnabled(bool enabled) async { + await _setSetting(_realTimeAnalysisKey, enabled); + } + + @override + Future getFactCheckThreshold() async { + return _prefs.getDouble(_factCheckThresholdKey) ?? 0.7; + } + + @override + Future setFactCheckThreshold(double threshold) async { + await _setSetting(_factCheckThresholdKey, threshold.clamp(0.0, 1.0)); + } + + // ========================================================================== + // Glasses Settings + // ========================================================================== + + @override + Future getLastConnectedGlasses() async { + return _prefs.getString(_lastGlassesKey); + } + + @override + Future setLastConnectedGlasses(String deviceId) async { + await _setSetting(_lastGlassesKey, deviceId); + } + + @override + Future getAutoConnectGlasses() async { + return _prefs.getBool(_autoConnectGlassesKey) ?? true; + } + + @override + Future setAutoConnectGlasses(bool enabled) async { + await _setSetting(_autoConnectGlassesKey, enabled); + } + + @override + Future getHUDBrightness() async { + return _prefs.getDouble(_hudBrightnessKey) ?? 0.8; + } + + @override + Future setHUDBrightness(double brightness) async { + await _setSetting(_hudBrightnessKey, brightness.clamp(0.0, 1.0)); + } + + @override + Future getGestureSensitivity() async { + return _prefs.getDouble(_gestureSensitivityKey) ?? 0.5; + } + + @override + Future setGestureSensitivity(double sensitivity) async { + await _setSetting(_gestureSensitivityKey, sensitivity.clamp(0.0, 1.0)); + } + + // ========================================================================== + // Data & Privacy Settings + // ========================================================================== + + @override + Future getDataRetentionDays() async { + return _prefs.getInt(_dataRetentionKey) ?? 30; + } + + @override + Future setDataRetentionDays(int days) async { + await _setSetting(_dataRetentionKey, days); + } + + @override + Future getAutomaticDataCleanup() async { + return _prefs.getBool(_autoCleanupKey) ?? true; + } + + @override + Future setAutomaticDataCleanup(bool enabled) async { + await _setSetting(_autoCleanupKey, enabled); + } + + @override + Future getAnalyticsConsent() async { + return _prefs.getBool(_analyticsConsentKey) ?? false; + } + + @override + Future setAnalyticsConsent(bool consent) async { + await _setSetting(_analyticsConsentKey, consent); + } + + @override + Future getCrashReportingConsent() async { + return _prefs.getBool(_crashReportingKey) ?? false; + } + + @override + Future setCrashReportingConsent(bool consent) async { + await _setSetting(_crashReportingKey, consent); + } + + // ========================================================================== + // Backup & Sync Settings + // ========================================================================== + + @override + Future getCloudSyncEnabled() async { + return _prefs.getBool(_cloudSyncKey) ?? false; + } + + @override + Future setCloudSyncEnabled(bool enabled) async { + await _setSetting(_cloudSyncKey, enabled); + } + + @override + Future getBackupFrequency() async { + return _prefs.getString(_backupFrequencyKey) ?? 'weekly'; + } + + @override + Future setBackupFrequency(String frequency) async { + await _setSetting(_backupFrequencyKey, frequency); + } + + // ========================================================================== + // Accessibility Settings + // ========================================================================== + + @override + Future getLargeTextEnabled() async { + return _prefs.getBool(_largeTextKey) ?? false; + } + + @override + Future setLargeTextEnabled(bool enabled) async { + await _setSetting(_largeTextKey, enabled); + } + + @override + Future getHighContrastEnabled() async { + return _prefs.getBool(_highContrastKey) ?? false; + } + + @override + Future setHighContrastEnabled(bool enabled) async { + await _setSetting(_highContrastKey, enabled); + } + + @override + Future getReducedMotionEnabled() async { + return _prefs.getBool(_reducedMotionKey) ?? false; + } + + @override + Future setReducedMotionEnabled(bool enabled) async { + await _setSetting(_reducedMotionKey, enabled); + } + + // ========================================================================== + // Advanced Settings + // ========================================================================== + + @override + Future getDeveloperModeEnabled() async { + return _prefs.getBool(_developerModeKey) ?? false; + } + + @override + Future setDeveloperModeEnabled(bool enabled) async { + await _setSetting(_developerModeKey, enabled); + } + + @override + Future getDebugLoggingEnabled() async { + return _prefs.getBool(_debugLoggingKey) ?? false; + } + + @override + Future setDebugLoggingEnabled(bool enabled) async { + await _setSetting(_debugLoggingKey, enabled); + } + + @override + Future getBetaFeaturesEnabled() async { + return _prefs.getBool(_betaFeaturesKey) ?? false; + } + + @override + Future setBetaFeaturesEnabled(bool enabled) async { + await _setSetting(_betaFeaturesKey, enabled); + } + + // ========================================================================== + // Utility Methods + // ========================================================================== + + @override + Future exportSettings() async { + try { + final allSettings = await getAllSettings(); + return jsonEncode(allSettings); + } catch (e) { + _logger.log(_tag, 'Failed to export settings: $e', LogLevel.error); + rethrow; + } + } + + @override + Future importSettings(String settingsJson) async { + try { + final settings = jsonDecode(settingsJson) as Map; + + for (final entry in settings.entries) { + final key = entry.key; + final value = entry.value; + + // Skip API keys for security + if (key == _apiKeysKey) continue; + + // Set the value based on type + if (value is bool) { + await _prefs.setBool(key, value); + } else if (value is int) { + await _prefs.setInt(key, value); + } else if (value is double) { + await _prefs.setDouble(key, value); + } else if (value is String) { + await _prefs.setString(key, value); + } + } + + _logger.log(_tag, 'Settings imported successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to import settings: $e', LogLevel.error); + rethrow; + } + } + + @override + Future resetToDefaults() async { + try { + // Clear all preferences + await _prefs.clear(); + + // Reinitialize defaults + await _initializeDefaults(); + + _logger.log(_tag, 'All settings reset to defaults', LogLevel.info); + + // Notify listeners + _settingsChangeController.add(SettingsChangeEvent( + key: 'all', + oldValue: 'various', + newValue: 'defaults', + timestamp: DateTime.now(), + )); + } catch (e) { + _logger.log(_tag, 'Failed to reset settings: $e', LogLevel.error); + rethrow; + } + } + + @override + Future resetCategory(SettingsCategory category) async { + try { + final keysToReset = _getCategoryKeys(category); + + for (final key in keysToReset) { + await _prefs.remove(key); + } + + // Reinitialize defaults for this category + await _initializeDefaults(); + + _logger.log(_tag, 'Settings category ${category.name} reset to defaults', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to reset category: $e', LogLevel.error); + rethrow; + } + } + + @override + Future> getAllSettings() async { + try { + final allKeys = _prefs.getKeys(); + final settings = {}; + + for (final key in allKeys) { + final value = _prefs.get(key); + if (value != null) { + // Don't export API keys for security + if (key != _apiKeysKey) { + settings[key] = value; + } + } + } + + return settings; + } catch (e) { + _logger.log(_tag, 'Failed to get all settings: $e', LogLevel.error); + rethrow; + } + } + + @override + Future dispose() async { + try { + await _settingsChangeController.close(); + _logger.log(_tag, 'Settings service disposed', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error disposing settings service: $e', LogLevel.error); + } + } + + // Private methods + + Future _initializeDefaults() async { + // General defaults + if (!_prefs.containsKey(_themeKey)) { + await _prefs.setString(_themeKey, ThemeMode.system.name); + } + if (!_prefs.containsKey(_languageKey)) { + await _prefs.setString(_languageKey, 'en-US'); + } + if (!_prefs.containsKey(_privacyLevelKey)) { + await _prefs.setString(_privacyLevelKey, PrivacyLevel.balanced.name); + } + + // Audio defaults + if (!_prefs.containsKey(_audioQualityKey)) { + await _prefs.setString(_audioQualityKey, 'medium'); + } + if (!_prefs.containsKey(_noiseReductionKey)) { + await _prefs.setBool(_noiseReductionKey, true); + } + if (!_prefs.containsKey(_vadSensitivityKey)) { + await _prefs.setDouble(_vadSensitivityKey, 0.5); + } + + // Transcription defaults + if (!_prefs.containsKey(_transcriptionBackendKey)) { + await _prefs.setString(_transcriptionBackendKey, 'local'); + } + if (!_prefs.containsKey(_transcriptionLanguageKey)) { + await _prefs.setString(_transcriptionLanguageKey, 'en-US'); + } + if (!_prefs.containsKey(_autoBackendSwitchKey)) { + await _prefs.setBool(_autoBackendSwitchKey, true); + } + + // AI defaults + if (!_prefs.containsKey(_aiProviderKey)) { + await _prefs.setString(_aiProviderKey, 'openai'); + } + if (!_prefs.containsKey(_factCheckingKey)) { + await _prefs.setBool(_factCheckingKey, true); + } + if (!_prefs.containsKey(_realTimeAnalysisKey)) { + await _prefs.setBool(_realTimeAnalysisKey, false); + } + if (!_prefs.containsKey(_factCheckThresholdKey)) { + await _prefs.setDouble(_factCheckThresholdKey, 0.7); + } + + // Glasses defaults + if (!_prefs.containsKey(_autoConnectGlassesKey)) { + await _prefs.setBool(_autoConnectGlassesKey, true); + } + if (!_prefs.containsKey(_hudBrightnessKey)) { + await _prefs.setDouble(_hudBrightnessKey, 0.8); + } + if (!_prefs.containsKey(_gestureSensitivityKey)) { + await _prefs.setDouble(_gestureSensitivityKey, 0.5); + } + + // Privacy defaults + if (!_prefs.containsKey(_dataRetentionKey)) { + await _prefs.setInt(_dataRetentionKey, 30); + } + if (!_prefs.containsKey(_autoCleanupKey)) { + await _prefs.setBool(_autoCleanupKey, true); + } + if (!_prefs.containsKey(_analyticsConsentKey)) { + await _prefs.setBool(_analyticsConsentKey, false); + } + if (!_prefs.containsKey(_crashReportingKey)) { + await _prefs.setBool(_crashReportingKey, false); + } + + // Backup defaults + if (!_prefs.containsKey(_cloudSyncKey)) { + await _prefs.setBool(_cloudSyncKey, false); + } + if (!_prefs.containsKey(_backupFrequencyKey)) { + await _prefs.setString(_backupFrequencyKey, 'weekly'); + } + + // Accessibility defaults + if (!_prefs.containsKey(_largeTextKey)) { + await _prefs.setBool(_largeTextKey, false); + } + if (!_prefs.containsKey(_highContrastKey)) { + await _prefs.setBool(_highContrastKey, false); + } + if (!_prefs.containsKey(_reducedMotionKey)) { + await _prefs.setBool(_reducedMotionKey, false); + } + + // Advanced defaults + if (!_prefs.containsKey(_developerModeKey)) { + await _prefs.setBool(_developerModeKey, false); + } + if (!_prefs.containsKey(_debugLoggingKey)) { + await _prefs.setBool(_debugLoggingKey, false); + } + if (!_prefs.containsKey(_betaFeaturesKey)) { + await _prefs.setBool(_betaFeaturesKey, false); + } + } + + Map _getAPIKeysMap() { + final apiKeysJson = _prefs.getString(_apiKeysKey); + if (apiKeysJson == null) return {}; + + try { + final decoded = jsonDecode(apiKeysJson) as Map; + return decoded.cast(); + } catch (e) { + _logger.log(_tag, 'Error parsing API keys: $e', LogLevel.warning); + return {}; + } + } + + Future _setSetting(String key, dynamic value) async { + final oldValue = _prefs.get(key); + + // Set the value based on type + if (value is bool) { + await _prefs.setBool(key, value); + } else if (value is int) { + await _prefs.setInt(key, value); + } else if (value is double) { + await _prefs.setDouble(key, value); + } else if (value is String) { + await _prefs.setString(key, value); + } else { + throw ArgumentError('Unsupported setting type: ${value.runtimeType}'); + } + + // Notify listeners of the change + _settingsChangeController.add(SettingsChangeEvent( + key: key, + oldValue: oldValue, + newValue: value, + timestamp: DateTime.now(), + )); + + _logger.log(_tag, 'Setting changed: $key = $value', LogLevel.debug); + } + + List _getCategoryKeys(SettingsCategory category) { + switch (category) { + case SettingsCategory.general: + return [_themeKey, _languageKey, _privacyLevelKey]; + case SettingsCategory.audio: + return [_audioDeviceKey, _audioQualityKey, _noiseReductionKey, _vadSensitivityKey]; + case SettingsCategory.transcription: + return [_transcriptionBackendKey, _transcriptionLanguageKey, _autoBackendSwitchKey]; + case SettingsCategory.ai: + return [_aiProviderKey, _apiKeysKey, _factCheckingKey, _realTimeAnalysisKey, _factCheckThresholdKey]; + case SettingsCategory.glasses: + return [_lastGlassesKey, _autoConnectGlassesKey, _hudBrightnessKey, _gestureSensitivityKey]; + case SettingsCategory.privacy: + return [_dataRetentionKey, _autoCleanupKey, _analyticsConsentKey, _crashReportingKey, _cloudSyncKey, _backupFrequencyKey]; + case SettingsCategory.accessibility: + return [_largeTextKey, _highContrastKey, _reducedMotionKey]; + case SettingsCategory.advanced: + return [_developerModeKey, _debugLoggingKey, _betaFeaturesKey]; + } + } +} \ No newline at end of file diff --git a/lib/services/implementations/test.cu b/lib/services/implementations/test.cu new file mode 100644 index 0000000..e69de29 diff --git a/lib/services/implementations/transcription_service_impl.dart b/lib/services/implementations/transcription_service_impl.dart new file mode 100644 index 0000000..86f6d46 --- /dev/null +++ b/lib/services/implementations/transcription_service_impl.dart @@ -0,0 +1,454 @@ +// ABOUTME: Transcription service implementation using speech_to_text package +// ABOUTME: Handles real-time speech recognition with speaker identification and confidence scoring + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:speech_to_text/speech_to_text.dart' as stt; + +import '../transcription_service.dart'; +import '../../models/transcription_segment.dart'; +import '../../core/utils/logging_service.dart'; + +class TranscriptionServiceImpl implements TranscriptionService { + static const String _tag = 'TranscriptionServiceImpl'; + + final LoggingService _logger; + final stt.SpeechToText _speechToText = stt.SpeechToText(); + + // State management + bool _isInitialized = false; + bool _isTranscribing = false; + bool _hasPermissions = false; + String _currentLanguage = 'en-US'; + TranscriptionBackend _currentBackend = TranscriptionBackend.device; + TranscriptionQuality _currentQuality = TranscriptionQuality.standard; + double _vadSensitivity = 0.5; + + // Stream controllers + final StreamController _transcriptionController = + StreamController.broadcast(); + final StreamController _confidenceController = + StreamController.broadcast(); + + // Current transcription state + String _currentTranscription = ''; + double _lastConfidence = 0.0; + DateTime? _segmentStartTime; + int _segmentCounter = 0; + + // Available languages cache + List _availableLanguages = []; + + TranscriptionServiceImpl({required LoggingService logger}) : _logger = logger; + + @override + bool get isInitialized => _isInitialized; + + @override + bool get isTranscribing => _isTranscribing; + + @override + bool get hasPermissions => _hasPermissions; + + @override + bool get isAvailable => _speechToText.isAvailable; + + @override + String get currentLanguage => _currentLanguage; + + @override + TranscriptionBackend get currentBackend => _currentBackend; + + @override + TranscriptionQuality get currentQuality => _currentQuality; + + @override + double get vadSensitivity => _vadSensitivity; + + @override + Stream get transcriptionStream => _transcriptionController.stream; + + @override + Stream get confidenceStream => _confidenceController.stream; + + @override + Future initialize() async { + try { + _logger.log(_tag, 'Initializing transcription service', LogLevel.info); + + // Initialize speech to text + _isInitialized = await _speechToText.initialize( + onStatus: _onStatusChange, + onError: _onError, + debugLogging: false, + ); + + if (!_isInitialized) { + throw TranscriptionServiceException( + 'Failed to initialize speech recognition', + TranscriptionErrorType.initializationFailed, + ); + } + + // Check permissions + _hasPermissions = await requestPermissions(); + + // Load available languages + await _loadAvailableLanguages(); + + _logger.log(_tag, 'Transcription service initialized successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to initialize transcription service: $e', LogLevel.error); + rethrow; + } + } + + @override + Future requestPermissions() async { + try { + _hasPermissions = await _speechToText.hasPermission; + if (!_hasPermissions) { + _logger.log(_tag, 'Microphone permission not granted', LogLevel.warning); + } + return _hasPermissions; + } catch (e) { + _logger.log(_tag, 'Error checking permissions: $e', LogLevel.error); + return false; + } + } + + @override + Future startTranscription({ + bool enableCapitalization = true, + bool enablePunctuation = true, + String? language, + TranscriptionBackend? preferredBackend, + }) async { + try { + if (!_isInitialized) { + throw TranscriptionServiceException( + 'Service not initialized', + TranscriptionErrorType.serviceNotReady, + ); + } + + if (!_hasPermissions) { + throw TranscriptionServiceException( + 'Microphone permission required', + TranscriptionErrorType.permissionDenied, + ); + } + + if (_isTranscribing) { + _logger.log(_tag, 'Already transcribing, stopping current session', LogLevel.warning); + await stopTranscription(); + } + + // Set language if provided + if (language != null && language != _currentLanguage) { + await setLanguage(language); + } + + // Configure backend if provided + if (preferredBackend != null && preferredBackend != _currentBackend) { + await configureBackend(preferredBackend); + } + + _logger.log(_tag, 'Starting transcription with language: $_currentLanguage', LogLevel.info); + + // Reset state + _currentTranscription = ''; + _segmentCounter = 0; + _segmentStartTime = DateTime.now(); + + // Start listening with optimized settings for real-time transcription + await _speechToText.listen( + onResult: _onSpeechResult, + listenFor: const Duration(minutes: 30), // Long session support + pauseFor: const Duration(milliseconds: 1500), // Shorter pause for better real-time response + localeId: _currentLanguage, + listenOptions: stt.SpeechListenOptions( + partialResults: true, // Critical for real-time feedback + listenMode: stt.ListenMode.dictation, // Better for continuous speech + cancelOnError: false, + sampleRate: 16000, // Optimal for speech recognition + ), + ); + + _isTranscribing = true; + _logger.log(_tag, 'Transcription started successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to start transcription: $e', LogLevel.error); + rethrow; + } + } + + @override + Future stopTranscription() async { + try { + if (!_isTranscribing) { + _logger.log(_tag, 'Not currently transcribing', LogLevel.debug); + return; + } + + await _speechToText.stop(); + _isTranscribing = false; + + // Send final segment if we have content + if (_currentTranscription.isNotEmpty) { + _sendTranscriptionSegment(isFinal: true); + } + + _logger.log(_tag, 'Transcription stopped', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error stopping transcription: $e', LogLevel.error); + rethrow; + } + } + + @override + Future pauseTranscription() async { + try { + if (_isTranscribing) { + await _speechToText.stop(); + _isTranscribing = false; + _logger.log(_tag, 'Transcription paused', LogLevel.info); + } + } catch (e) { + _logger.log(_tag, 'Error pausing transcription: $e', LogLevel.error); + rethrow; + } + } + + @override + Future resumeTranscription() async { + try { + if (!_isTranscribing) { + await startTranscription(); + _logger.log(_tag, 'Transcription resumed', LogLevel.info); + } + } catch (e) { + _logger.log(_tag, 'Error resuming transcription: $e', LogLevel.error); + rethrow; + } + } + + @override + Future setLanguage(String languageCode) async { + try { + if (!_availableLanguages.contains(languageCode)) { + throw TranscriptionServiceException( + 'Language not supported: $languageCode', + TranscriptionErrorType.unsupportedLanguage, + ); + } + + _currentLanguage = languageCode; + _logger.log(_tag, 'Language set to: $languageCode', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error setting language: $e', LogLevel.error); + rethrow; + } + } + + @override + Future configureQuality(TranscriptionQuality quality) async { + try { + _currentQuality = quality; + _logger.log(_tag, 'Quality set to: ${quality.name}', LogLevel.info); + + // Restart transcription if active to apply new quality settings + if (_isTranscribing) { + await stopTranscription(); + await startTranscription(); + } + } catch (e) { + _logger.log(_tag, 'Error configuring quality: $e', LogLevel.error); + rethrow; + } + } + + @override + Future configureBackend(TranscriptionBackend backend) async { + try { + _currentBackend = backend; + _logger.log(_tag, 'Backend set to: ${backend.name}', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error configuring backend: $e', LogLevel.error); + rethrow; + } + } + + @override + Future> getAvailableLanguages() async { + if (_availableLanguages.isEmpty) { + await _loadAvailableLanguages(); + } + return List.from(_availableLanguages); + } + + @override + double getLastConfidence() => _lastConfidence; + + @override + Future transcribeAudio(String audioPath) async { + throw UnimplementedError('File transcription not yet implemented'); + } + + @override + Future calibrateVoiceActivity() async { + try { + _logger.log(_tag, 'Calibrating voice activity detection', LogLevel.info); + // In this implementation, VAD is handled by the speech_to_text package + // Future implementation could add custom VAD calibration + _logger.log(_tag, 'Voice activity calibration completed', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error calibrating VAD: $e', LogLevel.error); + rethrow; + } + } + + @override + Future setVADSensitivity(double sensitivity) async { + try { + _vadSensitivity = math.max(0.0, math.min(1.0, sensitivity)); + _logger.log(_tag, 'VAD sensitivity set to: $_vadSensitivity', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error setting VAD sensitivity: $e', LogLevel.error); + rethrow; + } + } + + @override + Future dispose() async { + try { + await stopTranscription(); + await _transcriptionController.close(); + await _confidenceController.close(); + _logger.log(_tag, 'Transcription service disposed', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error disposing transcription service: $e', LogLevel.error); + } + } + + // Private methods + + Future _loadAvailableLanguages() async { + try { + final locales = await _speechToText.locales(); + _availableLanguages = locales.map((locale) => locale.localeId).toList(); + _logger.log(_tag, 'Loaded ${_availableLanguages.length} available languages', LogLevel.debug); + } catch (e) { + _logger.log(_tag, 'Error loading available languages: $e', LogLevel.error); + _availableLanguages = ['en-US']; // Fallback + } + } + + void _onSpeechResult(result) { + try { + final previousTranscription = _currentTranscription; + _currentTranscription = result.recognizedWords; + _lastConfidence = result.confidence; + + // Emit confidence update for UI feedback + _confidenceController.add(_lastConfidence); + + // Always send partial results for immediate feedback (lower confidence threshold) + if (_currentTranscription.isNotEmpty && result.confidence > 0.1) { + _sendTranscriptionSegment( + isFinal: result.finalResult, + isPartialUpdate: !result.finalResult && _currentTranscription != previousTranscription, + ); + } + + // If final result, prepare for next segment + if (result.finalResult && _currentTranscription.isNotEmpty) { + _segmentCounter++; + _segmentStartTime = DateTime.now(); + // Don't clear transcription immediately to allow for better continuity + } + } catch (e) { + _logger.log(_tag, 'Error processing speech result: $e', LogLevel.error); + } + } + + void _sendTranscriptionSegment({ + required bool isFinal, + bool isPartialUpdate = false + }) { + if (_currentTranscription.isEmpty || _segmentStartTime == null) return; + + try { + final now = DateTime.now(); + final processingTime = now.difference(_segmentStartTime!).inMilliseconds; + + final segment = TranscriptionSegment( + text: _currentTranscription.trim(), + speakerId: _detectSpeaker(), // Simple speaker detection + confidence: _lastConfidence, + startTime: _segmentStartTime!, + endTime: now, + isFinal: isFinal, + segmentId: isFinal + ? 'seg_${_segmentCounter}_${now.millisecondsSinceEpoch}' + : 'partial_${_segmentCounter}_${now.millisecondsSinceEpoch}', + language: _currentLanguage, + backend: _currentBackend, + processingTimeMs: processingTime, + metadata: { + 'isPartialUpdate': isPartialUpdate, + 'wordCount': _currentTranscription.trim().split(' ').length, + 'quality': _currentQuality.name, + }, + ); + + _transcriptionController.add(segment); + + // Log different types of results + if (isFinal) { + _logger.log(_tag, 'Final transcription segment: "${segment.text}" (${processingTime}ms)', LogLevel.info); + } else if (isPartialUpdate) { + _logger.log(_tag, 'Partial update: "${segment.text}" (confidence: ${_lastConfidence.toStringAsFixed(2)})', LogLevel.debug); + } + } catch (e) { + _logger.log(_tag, 'Error sending transcription segment: $e', LogLevel.error); + } + } + + String? _detectSpeaker() { + // Simple speaker identification based on audio characteristics + // In a real implementation, this would use more sophisticated techniques + return 'speaker_1'; + } + + void _onStatusChange(String status) { + _logger.log(_tag, 'Speech recognition status: $status', LogLevel.debug); + } + + void _onError(error) { + _logger.log(_tag, 'Speech recognition error: ${error.errorMsg}', LogLevel.error); + + final transcriptionError = TranscriptionServiceException( + error.errorMsg, + _mapErrorType(error.errorMsg), + originalError: error, + ); + + // Emit error through stream if needed + _transcriptionController.addError(transcriptionError); + } + + TranscriptionErrorType _mapErrorType(String errorMessage) { + final message = errorMessage.toLowerCase(); + if (message.contains('permission')) { + return TranscriptionErrorType.permissionDenied; + } else if (message.contains('network')) { + return TranscriptionErrorType.networkError; + } else if (message.contains('audio')) { + return TranscriptionErrorType.audioError; + } else { + return TranscriptionErrorType.unknown; + } + } +} \ No newline at end of file diff --git a/lib/services/llm_service.dart b/lib/services/llm_service.dart new file mode 100644 index 0000000..ff67515 --- /dev/null +++ b/lib/services/llm_service.dart @@ -0,0 +1,165 @@ +// ABOUTME: LLM service interface for AI analysis and conversation intelligence +// ABOUTME: Supports multiple AI providers with fallback and load balancing + +import 'dart:async'; + +import '../models/analysis_result.dart'; +import '../models/conversation_model.dart'; + +/// Available AI providers +enum LLMProvider { + openai, + anthropic, + local, // Future: local AI models +} + +/// Analysis request priority +enum AnalysisPriority { + low, // Batch processing + normal, // Standard processing + high, // Real-time processing + urgent, // Immediate processing +} + +/// Service interface for Large Language Model operations +abstract class LLMService { + /// Whether the service is initialized + bool get isInitialized; + + /// Currently active provider + LLMProvider get currentProvider; + + /// Initialize the LLM service with API keys + Future initialize({ + String? openAIKey, + String? anthropicKey, + LLMProvider? preferredProvider, + }); + + /// Set the active provider + Future setProvider(LLMProvider provider); + + /// Analyze conversation text + Future analyzeConversation( + String conversationText, { + AnalysisType type = AnalysisType.comprehensive, + AnalysisPriority priority = AnalysisPriority.normal, + LLMProvider? provider, + Map? context, + }); + + /// Perform fact-checking on claims + Future> checkFacts(List claims); + + /// Generate conversation summary + Future generateSummary( + ConversationModel conversation, { + bool includeKeyPoints = true, + bool includeActionItems = true, + int maxWords = 200, + }); + + /// Extract action items from conversation + Future> extractActionItems( + String conversationText, { + bool includeDeadlines = true, + bool includePriority = true, + }); + + /// Analyze conversation sentiment and tone + Future analyzeSentiment(String text); + + /// Ask a custom question about the conversation + Future askQuestion( + String question, + String context, { + LLMProvider? provider, + }); + + /// Configure analysis settings + Future configureAnalysis(AnalysisConfiguration config); + + /// Get usage statistics + Future> getUsageStats(); + + /// Clear analysis cache + Future clearCache(); + + /// Clean up resources + Future dispose(); +} + +/// Exception types for LLM errors +enum LLMErrorType { + serviceNotReady, + invalidApiKey, + apiError, + networkError, + quotaExceeded, + invalidResponse, + timeout, + unknown, +} + +/// LLM service usage statistics +class LLMUsageStats { + final Map requestCounts; + final Map totalProcessingTime; + final Map averageResponseTime; + final int totalTokensUsed; + final double estimatedCost; + + const LLMUsageStats({ + required this.requestCounts, + required this.totalProcessingTime, + required this.averageResponseTime, + required this.totalTokensUsed, + required this.estimatedCost, + }); +} + +/// Configuration for analysis behavior +class AnalysisConfiguration { + final bool enableCaching; + final Duration cacheTimeout; + final int maxRetries; + final double confidenceThreshold; + final bool enableBatching; + final int batchSize; + + const AnalysisConfiguration({ + this.enableCaching = true, + this.cacheTimeout = const Duration(minutes: 10), + this.maxRetries = 3, + this.confidenceThreshold = 0.5, + this.enableBatching = false, + this.batchSize = 5, + }); + + Map toJson() => { + 'enableCaching': enableCaching, + 'cacheTimeoutMs': cacheTimeout.inMilliseconds, + 'maxRetries': maxRetries, + 'confidenceThreshold': confidenceThreshold, + 'enableBatching': enableBatching, + 'batchSize': batchSize, + }; +} + +/// Exception class for LLM service errors +class LLMException implements Exception { + final String message; + final LLMErrorType type; + final dynamic originalError; + + const LLMException( + this.message, + this.type, { + this.originalError, + }); + + @override + String toString() { + return 'LLMException: $message (type: $type)'; + } +} \ No newline at end of file diff --git a/lib/services/real_time_transcription_service.dart b/lib/services/real_time_transcription_service.dart new file mode 100644 index 0000000..06b0823 --- /dev/null +++ b/lib/services/real_time_transcription_service.dart @@ -0,0 +1,820 @@ +// ABOUTME: Real-time transcription pipeline service that connects audio capture to speech recognition +// ABOUTME: Handles audio streaming, format conversion, buffering and provides real-time transcription results + +import 'dart:async'; +import 'dart:typed_data'; + +import '../models/transcription_segment.dart'; +import '../core/utils/logging_service.dart'; +import 'audio_service.dart'; +import 'transcription_service.dart'; + +/// State of the real-time transcription pipeline +enum TranscriptionPipelineState { + idle, + initializing, + active, + paused, + error, +} + +/// Configuration for real-time transcription pipeline +class TranscriptionPipelineConfig { + /// Audio chunk size for processing (in milliseconds) + final int audioChunkDurationMs; + + /// Target latency for real-time transcription (in milliseconds) + final int targetLatencyMs; + + /// Enable partial results for immediate feedback + final bool enablePartialResults; + + /// Maximum transcription session duration (in minutes) + final int maxSessionDurationMinutes; + + /// Memory management settings + final int maxBufferedSegments; + + const TranscriptionPipelineConfig({ + this.audioChunkDurationMs = 100, // 100ms chunks for low latency + this.targetLatencyMs = 500, // Target <500ms end-to-end latency + this.enablePartialResults = true, + this.maxSessionDurationMinutes = 60, + this.maxBufferedSegments = 1000, + }); +} + +/// Real-time transcription service that connects AudioService to TranscriptionService +abstract class RealTimeTranscriptionService { + /// Current pipeline state + TranscriptionPipelineState get state; + + /// Whether the pipeline is actively transcribing + bool get isActive; + + /// Current configuration + TranscriptionPipelineConfig get config; + + /// Stream of real-time transcription segments + Stream get transcriptionStream; + + /// Stream of intermediate/partial transcription results + Stream get partialTranscriptionStream; + + /// Stream of pipeline state changes + Stream get stateStream; + + /// Stream of processing latency metrics (in milliseconds) + Stream get latencyStream; + + /// Initialize the transcription pipeline + Future initialize(TranscriptionPipelineConfig config); + + /// Start real-time transcription with audio pipeline + Future startTranscription({ + String? language, + TranscriptionBackend? preferredBackend, + }); + + /// Stop real-time transcription + Future stopTranscription(); + + /// Pause transcription (can be resumed) + Future pauseTranscription(); + + /// Resume paused transcription + Future resumeTranscription(); + + /// Get current buffered segments + List getCurrentSegments(); + + /// Clear current session data + Future clearSession(); + + /// Get performance metrics + Map getPerformanceMetrics(); + + /// Clean up resources + Future dispose(); +} + +/// Implementation of real-time transcription pipeline +class RealTimeTranscriptionServiceImpl implements RealTimeTranscriptionService { + static const String _tag = 'RealTimeTranscriptionService'; + + final LoggingService _logger; + final AudioService _audioService; + final TranscriptionService _transcriptionService; + + // Pipeline state + TranscriptionPipelineState _state = TranscriptionPipelineState.idle; + TranscriptionPipelineConfig _config = const TranscriptionPipelineConfig(); + + // Stream controllers + final StreamController _transcriptionController = + StreamController.broadcast(); + final StreamController _partialTranscriptionController = + StreamController.broadcast(); + final StreamController _stateController = + StreamController.broadcast(); + final StreamController _latencyController = + StreamController.broadcast(); + + // Audio processing + StreamSubscription? _audioStreamSubscription; + StreamSubscription? _transcriptionSubscription; + StreamSubscription? _voiceActivitySubscription; + + // Session management + final List _currentSegments = []; + DateTime? _sessionStartTime; + Timer? _sessionTimer; + + // Transcription buffering and finalization + final List _pendingWords = []; + final List _pendingConfidences = []; + String _currentSentenceBuffer = ''; + Timer? _sentenceFinalizationTimer; + + // Performance tracking + DateTime? _lastAudioChunkTime; + final List _latencyMeasurements = []; + int _processedChunks = 0; + int _droppedChunks = 0; + + // Voice activity detection + bool _isVoiceActive = false; + DateTime? _voiceActivityStartTime; + + // Transcription buffering and sentence completion + final List _partialSegments = []; + + // Performance optimization + Timer? _performanceMonitorTimer; + double _currentProcessingLoad = 0.0; + static const int _maxLatencyMs = 500; // Target max latency + + // Memory management + Timer? _memoryCleanupTimer; + int _totalWordsProcessed = 0; + static const int _memoryCleanupIntervalMinutes = 5; + + RealTimeTranscriptionServiceImpl({ + required LoggingService logger, + required AudioService audioService, + required TranscriptionService transcriptionService, + }) : _logger = logger, + _audioService = audioService, + _transcriptionService = transcriptionService; + + @override + TranscriptionPipelineState get state => _state; + + @override + bool get isActive => _state == TranscriptionPipelineState.active; + + @override + TranscriptionPipelineConfig get config => _config; + + @override + Stream get transcriptionStream => _transcriptionController.stream; + + @override + Stream get partialTranscriptionStream => _partialTranscriptionController.stream; + + @override + Stream get stateStream => _stateController.stream; + + @override + Stream get latencyStream => _latencyController.stream; + + @override + Future initialize(TranscriptionPipelineConfig config) async { + try { + _logger.log(_tag, 'Initializing real-time transcription pipeline', LogLevel.info); + _setState(TranscriptionPipelineState.initializing); + + _config = config; + + // Initialize audio service if needed + if (!_audioService.hasPermission) { + final hasPermission = await _audioService.requestPermission(); + if (!hasPermission) { + throw Exception('Audio permission required for transcription'); + } + } + + // Initialize transcription service + if (!_transcriptionService.isInitialized) { + await _transcriptionService.initialize(); + } + + // Request permissions if needed + if (!_transcriptionService.hasPermissions) { + final hasPermission = await _transcriptionService.requestPermissions(); + if (!hasPermission) { + throw Exception('Microphone permission required for transcription'); + } + } + _setState(TranscriptionPipelineState.idle); + _logger.log(_tag, 'Real-time transcription pipeline initialized successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to initialize transcription pipeline: $e', LogLevel.error); + _setState(TranscriptionPipelineState.error); + rethrow; + } + } + + @override + Future startTranscription({ + String? language, + TranscriptionBackend? preferredBackend, + }) async { + try { + if (_state != TranscriptionPipelineState.idle) { + _logger.log(_tag, 'Pipeline not in idle state, current state: $_state', LogLevel.warning); + if (_state == TranscriptionPipelineState.active) { + await stopTranscription(); + } + } + + _logger.log(_tag, 'Starting real-time transcription pipeline', LogLevel.info); + _setState(TranscriptionPipelineState.initializing); + + // Clear previous session data + await clearSession(); + _sessionStartTime = DateTime.now(); + + // Start transcription service + await _transcriptionService.startTranscription( + language: language, + preferredBackend: preferredBackend, + enableCapitalization: true, + enablePunctuation: true, + ); + + // Set up transcription result subscription + _transcriptionSubscription = _transcriptionService.transcriptionStream.listen( + _handleTranscriptionResult, + onError: _handleTranscriptionError, + ); + + // Start audio recording and streaming + await _audioService.startRecording(); + + // Set up audio stream subscription for real-time processing + _audioStreamSubscription = _audioService.audioStream.listen( + _handleAudioChunk, + onError: _handleAudioError, + ); + + // Set up voice activity detection subscription + _voiceActivitySubscription = _audioService.voiceActivityStream.listen( + _handleVoiceActivity, + onError: (error) => _logger.log(_tag, 'Voice activity error: $error', LogLevel.warning), + ); + + // Start session management timer + _startSessionTimer(); + + // Start performance monitoring and memory management + _startPerformanceMonitoring(); + _startMemoryManagement(); + _setState(TranscriptionPipelineState.active); + _logger.log(_tag, 'Real-time transcription pipeline started successfully', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Failed to start transcription pipeline: $e', LogLevel.error); + _setState(TranscriptionPipelineState.error); + rethrow; + } + } + + @override + Future stopTranscription() async { + try { + _logger.log(_tag, 'Stopping real-time transcription pipeline', LogLevel.info); + + // Cancel subscriptions + await _audioStreamSubscription?.cancel(); + _audioStreamSubscription = null; + + await _transcriptionSubscription?.cancel(); + _transcriptionSubscription = null; + + await _voiceActivitySubscription?.cancel(); + _voiceActivitySubscription = null; + + // Stop services + await _audioService.stopRecording(); + await _transcriptionService.stopTranscription(); + + // Stop session timer, performance monitoring, and memory management + _sessionTimer?.cancel(); + _sessionTimer = null; + _performanceMonitorTimer?.cancel(); + _performanceMonitorTimer = null; + _memoryCleanupTimer?.cancel(); + _memoryCleanupTimer = null; + + _setState(TranscriptionPipelineState.idle); + + // Log performance metrics + _logPerformanceMetrics(); + + _logger.log(_tag, 'Real-time transcription pipeline stopped', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error stopping transcription pipeline: $e', LogLevel.error); + _setState(TranscriptionPipelineState.error); + rethrow; + } + } + + @override + Future pauseTranscription() async { + try { + if (_state != TranscriptionPipelineState.active) { + return; + } + + await _audioService.pauseRecording(); + await _transcriptionService.pauseTranscription(); + + _setState(TranscriptionPipelineState.paused); + _logger.log(_tag, 'Transcription pipeline paused', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error pausing transcription pipeline: $e', LogLevel.error); + rethrow; + } + } + + @override + Future resumeTranscription() async { + try { + if (_state != TranscriptionPipelineState.paused) { + return; + } + + await _audioService.resumeRecording(); + await _transcriptionService.resumeTranscription(); + + _setState(TranscriptionPipelineState.active); + _logger.log(_tag, 'Transcription pipeline resumed', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error resuming transcription pipeline: $e', LogLevel.error); + rethrow; + } + } + + @override + List getCurrentSegments() { + return List.from(_currentSegments); + } + + @override + Future clearSession() async { + _currentSegments.clear(); + _sessionStartTime = null; + _latencyMeasurements.clear(); + _processedChunks = 0; + _droppedChunks = 0; + _isVoiceActive = false; + _voiceActivityStartTime = null; + + // Clear transcription buffering + _partialSegments.clear(); + _pendingWords.clear(); + _pendingConfidences.clear(); + _currentSentenceBuffer = ''; + _sentenceFinalizationTimer?.cancel(); + _sentenceFinalizationTimer = null; + + // Reset memory tracking + _totalWordsProcessed = 0; + + _logger.log(_tag, 'Session data, buffers, and memory tracking cleared', LogLevel.debug); + } + + @override + Map getPerformanceMetrics() { + final now = DateTime.now(); + final sessionDuration = _sessionStartTime != null + ? now.difference(_sessionStartTime!).inMilliseconds + : 0; + + final avgLatency = _latencyMeasurements.isNotEmpty + ? _latencyMeasurements.reduce((a, b) => a + b) / _latencyMeasurements.length + : 0.0; + + return { + 'sessionDurationMs': sessionDuration, + 'processedChunks': _processedChunks, + 'droppedChunks': _droppedChunks, + 'averageLatencyMs': avgLatency, + 'currentSegments': _currentSegments.length, + 'processingRate': sessionDuration > 0 ? (_processedChunks * 1000.0) / sessionDuration : 0.0, + 'targetLatencyMs': _config.targetLatencyMs, + 'isPerformingWell': avgLatency <= _config.targetLatencyMs, + 'totalWordsProcessed': _totalWordsProcessed, + 'bufferedWords': _pendingWords.length, + 'processingLoad': _currentProcessingLoad, + 'latencyMeasurements': _latencyMeasurements.length, + }; + } + + @override + Future dispose() async { + try { + await stopTranscription(); + + // Cancel all timers + _sessionTimer?.cancel(); + _sentenceFinalizationTimer?.cancel(); + _performanceMonitorTimer?.cancel(); + _memoryCleanupTimer?.cancel(); + await _transcriptionController.close(); + await _partialTranscriptionController.close(); + await _stateController.close(); + await _latencyController.close(); + + _logger.log(_tag, 'Real-time transcription service disposed', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error disposing transcription service: $e', LogLevel.error); + } + } + + // Private methods + + void _setState(TranscriptionPipelineState newState) { + if (_state != newState) { + _state = newState; + _stateController.add(newState); + _logger.log(_tag, 'Pipeline state changed to: ${newState.name}', LogLevel.debug); + } + } + + void _handleAudioChunk(Uint8List audioData) { + try { + final now = DateTime.now(); + _lastAudioChunkTime = now; + _processedChunks++; + + // The speech_to_text package handles audio processing internally + // This handler tracks audio flow for performance monitoring + if (audioData.isNotEmpty) { + _logger.log(_tag, 'Processed audio chunk: ${audioData.length} bytes', LogLevel.debug); + } + } catch (e) { + _droppedChunks++; + _logger.log(_tag, 'Error processing audio chunk: $e', LogLevel.warning); + } + } + + void _handleVoiceActivity(bool isActive) { + if (_isVoiceActive != isActive) { + _isVoiceActive = isActive; + + if (isActive) { + _voiceActivityStartTime = DateTime.now(); + _logger.log(_tag, 'Voice activity detected', LogLevel.debug); + } else { + final duration = _voiceActivityStartTime != null + ? DateTime.now().difference(_voiceActivityStartTime!).inMilliseconds + : 0; + _logger.log(_tag, 'Voice activity ended (duration: ${duration}ms)', LogLevel.debug); + } + } + } + void _handleTranscriptionResult(TranscriptionSegment segment) { + try { + final now = DateTime.now(); + + // Calculate latency if we have timing information + if (_lastAudioChunkTime != null) { + final latency = now.difference(_lastAudioChunkTime!).inMilliseconds; + _latencyMeasurements.add(latency); + _latencyController.add(latency); + + // Keep only recent latency measurements for accurate averages + if (_latencyMeasurements.length > 100) { + _latencyMeasurements.removeAt(0); + } + + // Log performance warning if latency exceeds target + if (latency > _config.targetLatencyMs) { + _logger.log(_tag, 'High latency detected: ${latency}ms (target: ${_config.targetLatencyMs}ms)', LogLevel.warning); + } + } + + // Handle partial vs final results with buffering + if (segment.isFinal) { + // Process final segment with sentence completion and punctuation + final processedSegment = _processAndBufferFinalSegment(segment); + + // Add to current segments buffer + _currentSegments.add(processedSegment); + + // Memory management - remove old segments if buffer is too large + if (_currentSegments.length > _config.maxBufferedSegments) { + _currentSegments.removeAt(0); + } + + _transcriptionController.add(processedSegment); + _logger.log(_tag, 'Final transcription: "${processedSegment.text}" (confidence: ${processedSegment.confidence})', LogLevel.info); + } else if (_config.enablePartialResults) { + // Handle partial results with word-by-word processing + final partialSegment = _processPartialSegment(segment); + _partialTranscriptionController.add(partialSegment); + _logger.log(_tag, 'Partial transcription: "${partialSegment.text}"', LogLevel.debug); + } + } catch (e) { + _logger.log(_tag, 'Error handling transcription result: $e', LogLevel.error); + } + } + + /// Process and enhance final transcription segment with sentence completion + TranscriptionSegment _processAndBufferFinalSegment(TranscriptionSegment segment) { + try { + // Add sentence completion and punctuation + String processedText = _addSentenceCompletionAndPunctuation(segment.text); + + // Update sentence buffer for context + _currentSentenceBuffer = processedText; + + // Reset sentence finalization timer + _sentenceFinalizationTimer?.cancel(); + + return segment.copyWith( + text: processedText, + metadata: { + ...segment.metadata, + 'processedForCompletion': true, + 'originalText': segment.text, + }, + ); + } catch (e) { + _logger.log(_tag, 'Error processing final segment: $e', LogLevel.warning); + return segment; + } + } + + /// Process partial segment with word buffering for immediate feedback + TranscriptionSegment _processPartialSegment(TranscriptionSegment segment) { + try { + // Buffer words and confidences for analysis + final words = segment.text.trim().split(' '); + final newWords = _getNewWords(words); + + // Add new words to buffer and track total processed + for (final word in newWords) { + _pendingWords.add(word); + _pendingConfidences.add(segment.confidence); + _totalWordsProcessed++; + } + + // Keep buffer size manageable + if (_pendingWords.length > 50) { + _pendingWords.removeRange(0, _pendingWords.length - 50); + _pendingConfidences.removeRange(0, _pendingConfidences.length - 50); + } + + // Process text for better readability + String processedText = _processPartialText(segment.text); + + // Start or reset sentence finalization timer for incomplete sentences + _startSentenceFinalizationTimer(); + + return segment.copyWith( + text: processedText, + metadata: { + ...segment.metadata, + 'wordCount': words.length, + 'newWordCount': newWords.length, + 'bufferSize': _pendingWords.length, + }, + ); + } catch (e) { + _logger.log(_tag, 'Error processing partial segment: $e', LogLevel.warning); + return segment; + } + } + + /// Identify new words in current transcription vs buffered words + List _getNewWords(List currentWords) { + if (_pendingWords.isEmpty) return currentWords; + + // Find words that weren't in the previous buffer + final previousText = _pendingWords.join(' ').toLowerCase(); + final currentText = currentWords.join(' ').toLowerCase(); + + if (currentText.length > previousText.length && currentText.startsWith(previousText)) { + // New words added at the end + final newPortion = currentText.substring(previousText.length).trim(); + return newPortion.split(' ').where((word) => word.isNotEmpty).toList(); + } + + // Fallback: return all words if we can't determine new ones + return currentWords; + } + + /// Add sentence completion and punctuation to text + String _addSentenceCompletionAndPunctuation(String text) { + if (text.isEmpty) return text; + + String processedText = text.trim(); + + // Add period if sentence doesn't end with punctuation + final lastChar = processedText[processedText.length - 1]; + if (!'.,!?;:'.contains(lastChar)) { + // Only add period if it looks like a complete sentence + if (processedText.split(' ').length >= 3) { + processedText += '.'; + } + } + + // Capitalize first letter + if (processedText.isNotEmpty) { + processedText = processedText[0].toUpperCase() + processedText.substring(1); + } + + return processedText; + } + + /// Process partial text for better real-time display + String _processPartialText(String text) { + if (text.isEmpty) return text; + + String processedText = text.trim(); + + // Capitalize first letter + if (processedText.isNotEmpty) { + processedText = processedText[0].toUpperCase() + processedText.substring(1); + } + + // Add ellipsis to indicate ongoing speech (for partial results) + if (processedText.isNotEmpty && !processedText.endsWith('...')) { + processedText += '...'; + } + + return processedText; + } + + /// Start timer to finalize incomplete sentences after a delay + void _startSentenceFinalizationTimer() { + _sentenceFinalizationTimer?.cancel(); + _sentenceFinalizationTimer = Timer(const Duration(seconds: 2), () { + // After 2 seconds of no updates, we can consider finalizing the current partial + if (_currentSentenceBuffer.isNotEmpty) { + _logger.log(_tag, 'Sentence finalization timeout - buffer: "$_currentSentenceBuffer"', LogLevel.debug); + } + }); + } + + void _handleTranscriptionError(dynamic error) { + _logger.log(_tag, 'Transcription error: $error', LogLevel.error); + _setState(TranscriptionPipelineState.error); + } + + void _handleAudioError(dynamic error) { + _logger.log(_tag, 'Audio stream error: $error', LogLevel.error); + _setState(TranscriptionPipelineState.error); + } + + void _startSessionTimer() { + _sessionTimer = Timer.periodic(const Duration(minutes: 1), (timer) { + if (_sessionStartTime != null) { + final elapsed = DateTime.now().difference(_sessionStartTime!); + if (elapsed.inMinutes >= _config.maxSessionDurationMinutes) { + _logger.log(_tag, 'Maximum session duration reached, stopping transcription', LogLevel.warning); + stopTranscription(); + } + } + }); + } + + void _logPerformanceMetrics() { + final metrics = getPerformanceMetrics(); + _logger.log(_tag, 'Performance metrics: $metrics', LogLevel.info); + } + + /// Start performance monitoring for latency optimization + void _startPerformanceMonitoring() { + _performanceMonitorTimer = Timer.periodic(const Duration(seconds: 5), (timer) { + _monitorAndOptimizePerformance(); + }); + } + + /// Monitor performance and adjust configuration for optimal latency + void _monitorAndOptimizePerformance() { + if (_latencyMeasurements.isEmpty) return; + + final avgLatency = _latencyMeasurements.reduce((a, b) => a + b) / _latencyMeasurements.length; + final maxLatency = _latencyMeasurements.reduce((a, b) => a > b ? a : b); + + // Calculate processing load based on chunks processed vs time + final sessionDuration = _sessionStartTime != null + ? DateTime.now().difference(_sessionStartTime!).inMilliseconds + : 1; + _currentProcessingLoad = (_processedChunks * 1000.0) / sessionDuration; + + _logger.log(_tag, 'Performance metrics - Avg latency: ${avgLatency.toStringAsFixed(1)}ms, Max: ${maxLatency}ms, Load: ${_currentProcessingLoad.toStringAsFixed(1)} chunks/sec', LogLevel.debug); + + // Adaptive performance optimization + if (avgLatency > _maxLatencyMs) { + _logger.log(_tag, 'High latency detected (${avgLatency.toStringAsFixed(1)}ms), attempting optimization', LogLevel.warning); + _optimizeForLatency(); + } + + // Check for dropped chunks + if (_droppedChunks > 0) { + final dropRate = (_droppedChunks / (_processedChunks + _droppedChunks)) * 100; + if (dropRate > 5.0) { // More than 5% drop rate + _logger.log(_tag, 'High chunk drop rate: ${dropRate.toStringAsFixed(1)}%', LogLevel.warning); + } + } + } + + /// Optimize configuration to reduce latency + void _optimizeForLatency() { + try { + // Reduce buffer sizes for faster processing + if (_pendingWords.length > 20) { + _pendingWords.removeRange(0, _pendingWords.length - 20); + _pendingConfidences.removeRange(0, _pendingConfidences.length - 20); + } + + // Clear old latency measurements to get fresh data + if (_latencyMeasurements.length > 20) { + _latencyMeasurements.removeRange(0, _latencyMeasurements.length - 20); + } + + _logger.log(_tag, 'Applied latency optimization - reduced buffer sizes', LogLevel.info); + } catch (e) { + _logger.log(_tag, 'Error optimizing for latency: $e', LogLevel.error); + } + } + + /// Start memory management for long conversations + void _startMemoryManagement() { + _memoryCleanupTimer = Timer.periodic( + Duration(minutes: _memoryCleanupIntervalMinutes), + (timer) { + _performMemoryCleanup(); + }, + ); + } + + /// Perform periodic memory cleanup for long conversations + void _performMemoryCleanup() { + try { + final segmentsBefore = _currentSegments.length; + final wordsBefore = _pendingWords.length; + final latencyMeasurementsBefore = _latencyMeasurements.length; + + // Clean up old segments (keep last 200 for context) + if (_currentSegments.length > 200) { + final removeCount = _currentSegments.length - 200; + _currentSegments.removeRange(0, removeCount); + } + + // Clean up word buffer (keep last 30 words for context) + if (_pendingWords.length > 30) { + final removeCount = _pendingWords.length - 30; + _pendingWords.removeRange(0, removeCount); + _pendingConfidences.removeRange(0, removeCount); + } + + // Clean up old latency measurements (keep last 50) + if (_latencyMeasurements.length > 50) { + final removeCount = _latencyMeasurements.length - 50; + _latencyMeasurements.removeRange(0, removeCount); + } + + final segmentsAfter = _currentSegments.length; + final wordsAfter = _pendingWords.length; + final latencyMeasurementsAfter = _latencyMeasurements.length; + + if (segmentsBefore > segmentsAfter || wordsBefore > wordsAfter || latencyMeasurementsBefore > latencyMeasurementsAfter) { + _logger.log(_tag, 'Memory cleanup completed - Segments: $segmentsBefore→$segmentsAfter, Words: $wordsBefore→$wordsAfter, Latency measurements: $latencyMeasurementsBefore→$latencyMeasurementsAfter', LogLevel.info); + } + + // Log memory statistics + _logMemoryStatistics(); + + } catch (e) { + _logger.log(_tag, 'Error during memory cleanup: $e', LogLevel.error); + } + } + + /// Log current memory usage statistics + void _logMemoryStatistics() { + final sessionDuration = _sessionStartTime != null + ? DateTime.now().difference(_sessionStartTime!).inMinutes + : 0; + + final avgWordsPerMinute = sessionDuration > 0 + ? (_totalWordsProcessed / sessionDuration).toStringAsFixed(1) + : '0.0'; + + _logger.log(_tag, 'Memory stats - Session: ${sessionDuration}min, Total words: $_totalWordsProcessed, Avg: $avgWordsPerMinute words/min, Buffered segments: ${_currentSegments.length}, Buffered words: ${_pendingWords.length}', LogLevel.debug); + } +} \ No newline at end of file diff --git a/lib/services/service_locator.dart b/lib/services/service_locator.dart new file mode 100644 index 0000000..2043ce0 --- /dev/null +++ b/lib/services/service_locator.dart @@ -0,0 +1,93 @@ +// ABOUTME: Dependency injection service locator using get_it package +// ABOUTME: Registers and provides access to all application services + +import 'package:get_it/get_it.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../core/utils/logging_service.dart'; +import 'audio_service.dart'; +import 'conversation_storage_service.dart'; +import 'glasses_service.dart'; +import 'llm_service.dart'; +import 'settings_service.dart'; +import 'transcription_service.dart'; +import 'implementations/audio_service_impl.dart'; +import 'implementations/glasses_service_impl.dart'; +import 'implementations/llm_service_impl.dart'; +import 'implementations/settings_service_impl.dart'; +import 'implementations/transcription_service_impl.dart'; +import 'real_time_transcription_service.dart'; + +class ServiceLocator { + static final GetIt _getIt = GetIt.instance; + + static ServiceLocator get instance => ServiceLocator._(); + ServiceLocator._(); + + T get() => _getIt.get(); + + bool isRegistered() => _getIt.isRegistered(); + + Future reset() async { + await _getIt.reset(); + } +} + +Future setupServiceLocator() async { + final getIt = GetIt.instance; + + // Core utilities - LoggingService is a singleton + getIt.registerLazySingleton(() => LoggingService.instance); + + // Initialize SharedPreferences for settings service + final prefs = await SharedPreferences.getInstance(); + final logger = getIt.get(); + + // Core services with dependencies + getIt.registerLazySingleton(() => SettingsServiceImpl( + logger: logger, + prefs: prefs, + )); + + getIt.registerLazySingleton(() => InMemoryConversationStorageService( + logger: logger, + )); + + // Audio and transcription services + getIt.registerLazySingleton(() => AudioServiceImpl( + logger: logger, + )); + + getIt.registerLazySingleton(() => TranscriptionServiceImpl( + logger: logger, + )); + + // Real-time transcription pipeline service + getIt.registerLazySingleton(() => RealTimeTranscriptionServiceImpl( + logger: logger, + audioService: getIt.get(), + transcriptionService: getIt.get(), + )); + + // AI and LLM services + getIt.registerLazySingleton(() => LLMServiceImpl( + logger: logger, + )); + + // Glasses/hardware services + getIt.registerLazySingleton(() => GlassesServiceImpl( + logger: logger, + )); + + // Initialize services that need async setup + try { + final settingsService = getIt.get(); + await settingsService.initialize(); + + // Other services will be initialized when first accessed + + } catch (e) { + // Log error but don't prevent app startup + logger.error('ServiceLocator', 'Some services failed to initialize', e); + } +} \ No newline at end of file diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart new file mode 100644 index 0000000..38bf783 --- /dev/null +++ b/lib/services/settings_service.dart @@ -0,0 +1,238 @@ +// ABOUTME: Settings service interface for app configuration and persistence +// ABOUTME: Manages user preferences, API keys, and device settings + +import 'dart:async'; + +/// Theme mode options +enum ThemeMode { + system, + light, + dark, +} + +/// Privacy level settings +enum PrivacyLevel { + minimal, // Local processing only + balanced, // Some cloud processing + full, // Full cloud processing +} + +/// Service interface for app settings and configuration +abstract class SettingsService { + /// Stream of settings changes + Stream get settingsChangeStream; + + /// Initialize the settings service + Future initialize(); + + // ========================================================================== + // General App Settings + // ========================================================================== + + /// Get/set theme mode + Future getThemeMode(); + Future setThemeMode(ThemeMode mode); + + /// Get/set language + Future getLanguage(); + Future setLanguage(String languageCode); + + /// Get/set privacy level + Future getPrivacyLevel(); + Future setPrivacyLevel(PrivacyLevel level); + + // ========================================================================== + // Audio Settings + // ========================================================================== + + /// Get/set preferred audio input device + Future getPreferredAudioDevice(); + Future setPreferredAudioDevice(String deviceId); + + /// Get/set audio quality + Future getAudioQuality(); // 'low', 'medium', 'high' + Future setAudioQuality(String quality); + + /// Get/set noise reduction enabled + Future getNoiseReductionEnabled(); + Future setNoiseReductionEnabled(bool enabled); + + /// Get/set voice activity detection sensitivity + Future getVADSensitivity(); // 0.0 to 1.0 + Future setVADSensitivity(double sensitivity); + + // ========================================================================== + // Transcription Settings + // ========================================================================== + + /// Get/set preferred transcription backend + Future getPreferredTranscriptionBackend(); // 'local', 'whisper', 'hybrid' + Future setPreferredTranscriptionBackend(String backend); + + /// Get/set transcription language + Future getTranscriptionLanguage(); + Future setTranscriptionLanguage(String languageCode); + + /// Get/set automatic backend switching + Future getAutomaticBackendSwitching(); + Future setAutomaticBackendSwitching(bool enabled); + + // ========================================================================== + // AI Service Settings + // ========================================================================== + + /// Get/set preferred AI provider + Future getPreferredAIProvider(); // 'openai', 'anthropic' + Future setPreferredAIProvider(String provider); + + /// Get/set API keys (stored securely) + Future getAPIKey(String provider); + Future setAPIKey(String provider, String apiKey); + Future removeAPIKey(String provider); + + /// Get/set AI analysis settings + Future getFactCheckingEnabled(); + Future setFactCheckingEnabled(bool enabled); + + Future getRealTimeAnalysisEnabled(); + Future setRealTimeAnalysisEnabled(bool enabled); + + Future getFactCheckThreshold(); // 0.0 to 1.0 + Future setFactCheckThreshold(double threshold); + + // ========================================================================== + // Glasses Settings + // ========================================================================== + + /// Get/set last connected glasses device + Future getLastConnectedGlasses(); + Future setLastConnectedGlasses(String deviceId); + + /// Get/set auto-connect to glasses + Future getAutoConnectGlasses(); + Future setAutoConnectGlasses(bool enabled); + + /// Get/set HUD brightness + Future getHUDBrightness(); // 0.0 to 1.0 + Future setHUDBrightness(double brightness); + + /// Get/set gesture sensitivity + Future getGestureSensitivity(); // 0.0 to 1.0 + Future setGestureSensitivity(double sensitivity); + + // ========================================================================== + // Data & Privacy Settings + // ========================================================================== + + /// Get/set data retention period in days + Future getDataRetentionDays(); + Future setDataRetentionDays(int days); + + /// Get/set automatic data cleanup + Future getAutomaticDataCleanup(); + Future setAutomaticDataCleanup(bool enabled); + + /// Get/set analytics collection consent + Future getAnalyticsConsent(); + Future setAnalyticsConsent(bool consent); + + /// Get/set crash reporting consent + Future getCrashReportingConsent(); + Future setCrashReportingConsent(bool consent); + + // ========================================================================== + // Backup & Sync Settings + // ========================================================================== + + /// Get/set cloud sync enabled + Future getCloudSyncEnabled(); + Future setCloudSyncEnabled(bool enabled); + + /// Get/set backup frequency + Future getBackupFrequency(); // 'never', 'daily', 'weekly' + Future setBackupFrequency(String frequency); + + // ========================================================================== + // Accessibility Settings + // ========================================================================== + + /// Get/set large text enabled + Future getLargeTextEnabled(); + Future setLargeTextEnabled(bool enabled); + + /// Get/set high contrast enabled + Future getHighContrastEnabled(); + Future setHighContrastEnabled(bool enabled); + + /// Get/set reduced motion enabled + Future getReducedMotionEnabled(); + Future setReducedMotionEnabled(bool enabled); + + // ========================================================================== + // Advanced Settings + // ========================================================================== + + /// Get/set developer mode enabled + Future getDeveloperModeEnabled(); + Future setDeveloperModeEnabled(bool enabled); + + /// Get/set debug logging enabled + Future getDebugLoggingEnabled(); + Future setDebugLoggingEnabled(bool enabled); + + /// Get/set beta features enabled + Future getBetaFeaturesEnabled(); + Future setBetaFeaturesEnabled(bool enabled); + + // ========================================================================== + // Utility Methods + // ========================================================================== + + /// Export all settings to a JSON string + Future exportSettings(); + + /// Import settings from a JSON string + Future importSettings(String settingsJson); + + /// Reset all settings to defaults + Future resetToDefaults(); + + /// Reset specific category of settings + Future resetCategory(SettingsCategory category); + + /// Get all settings as a map + Future> getAllSettings(); + + /// Clean up resources + Future dispose(); +} + +/// Categories of settings for organized reset +enum SettingsCategory { + general, + audio, + transcription, + ai, + glasses, + privacy, + accessibility, + advanced, +} + +/// Settings change event +class SettingsChangeEvent { + final String key; + final dynamic oldValue; + final dynamic newValue; + final DateTime timestamp; + + const SettingsChangeEvent({ + required this.key, + required this.oldValue, + required this.newValue, + required this.timestamp, + }); + + @override + String toString() => 'SettingsChangeEvent($key: $oldValue -> $newValue)'; +} \ No newline at end of file diff --git a/lib/services/transcription_service.dart b/lib/services/transcription_service.dart new file mode 100644 index 0000000..efb5bfb --- /dev/null +++ b/lib/services/transcription_service.dart @@ -0,0 +1,154 @@ +// ABOUTME: Transcription service interface for speech-to-text conversion +// ABOUTME: Supports both local and remote transcription backends with quality switching + +import 'dart:async'; + +import '../models/transcription_segment.dart'; + +/// Backend type for transcription processing +enum TranscriptionBackend { + device, // On-device speech recognition + whisper, // OpenAI Whisper API + hybrid, // Automatic selection based on quality/connectivity +} + +/// Transcription quality settings +enum TranscriptionQuality { + low, // Fast, lower accuracy + standard, // Balanced speed and accuracy + high, // High accuracy, slower processing +} + +/// Real-time transcription state +enum TranscriptionState { + idle, + listening, + processing, + error, +} + +/// Transcription error types +enum TranscriptionErrorType { + initializationFailed, + permissionDenied, + serviceNotReady, + networkError, + audioError, + unsupportedLanguage, + unknown, +} + +/// Custom exception for transcription errors with specific error types +class TranscriptionServiceException implements Exception { + final String message; + final TranscriptionErrorType type; + final dynamic originalError; + + const TranscriptionServiceException( + this.message, + this.type, { + this.originalError, + }); + + @override + String toString() => 'TranscriptionServiceException: $message (type: $type)'; +} + +/// Service interface for speech-to-text transcription +abstract class TranscriptionService { + /// Whether the service is initialized + bool get isInitialized; + + /// Whether currently transcribing + bool get isTranscribing; + + /// Whether microphone permissions are granted + bool get hasPermissions; + + /// Whether speech recognition is available + bool get isAvailable; + + /// Current language code + String get currentLanguage; + + /// Current transcription backend + TranscriptionBackend get currentBackend; + + /// Current quality setting + TranscriptionQuality get currentQuality; + + /// Current VAD sensitivity (0.0 to 1.0) + double get vadSensitivity; + + /// Stream of real-time transcription segments + Stream get transcriptionStream; + + /// Stream of confidence scores + Stream get confidenceStream; + + /// Initialize the transcription service + Future initialize(); + + /// Request microphone permissions + Future requestPermissions(); + + /// Start real-time transcription + Future startTranscription({ + bool enableCapitalization = true, + bool enablePunctuation = true, + String? language, + TranscriptionBackend? preferredBackend, + }); + + /// Stop real-time transcription + Future stopTranscription(); + + /// Pause transcription (can be resumed) + Future pauseTranscription(); + + /// Resume paused transcription + Future resumeTranscription(); + + /// Set transcription language + Future setLanguage(String languageCode); + + /// Configure transcription quality + Future configureQuality(TranscriptionQuality quality); + + /// Configure backend + Future configureBackend(TranscriptionBackend backend); + + /// Get available languages + Future> getAvailableLanguages(); + + /// Get last confidence score + double getLastConfidence(); + + /// Transcribe audio file + Future transcribeAudio(String audioPath); + + /// Calibrate voice activity detection + Future calibrateVoiceActivity(); + + /// Set VAD sensitivity + Future setVADSensitivity(double sensitivity); + + /// Clean up resources + Future dispose(); +} + +/// Speaker diarization result +class SpeakerInfo { + final String speakerId; + final String? name; + final double confidence; + + const SpeakerInfo({ + required this.speakerId, + this.name, + required this.confidence, + }); + + @override + String toString() => 'SpeakerInfo(id: $speakerId, name: $name, confidence: $confidence)'; +} \ No newline at end of file diff --git a/lib/ui/screens/home_screen.dart b/lib/ui/screens/home_screen.dart new file mode 100644 index 0000000..c4e3734 --- /dev/null +++ b/lib/ui/screens/home_screen.dart @@ -0,0 +1,110 @@ +// ABOUTME: Main home screen with bottom navigation and tab management +// ABOUTME: Provides access to conversation, analysis, glasses, history, and settings + +import 'package:flutter/material.dart'; + +import '../../core/utils/constants.dart'; +import '../widgets/conversation_tab.dart'; +import '../widgets/analysis_tab.dart'; +import '../widgets/glasses_tab.dart'; +import '../widgets/history_tab.dart'; +import '../widgets/settings_tab.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + int _currentIndex = 0; + + List get _tabs => [ + ConversationTab(onHistoryTap: () => _navigateToHistory()), + const AnalysisTab(), + const GlassesTab(), + const HistoryTab(), + const SettingsTab(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: IndexedStack( + index: _currentIndex, + children: _tabs, + ), + bottomNavigationBar: BottomNavigationBar( + currentIndex: _currentIndex, + onTap: (index) { + setState(() { + _currentIndex = index; + }); + }, + type: BottomNavigationBarType.fixed, + items: [ + BottomNavigationBarItem( + icon: _buildTabIcon(Icons.mic, 0, false), + label: UIConstants.tabLabels[0], + ), + BottomNavigationBarItem( + icon: _buildTabIcon(Icons.analytics, 1, false), + label: UIConstants.tabLabels[1], + ), + BottomNavigationBarItem( + icon: _buildTabIcon(Icons.remove_red_eye, 2, false), // Use different icon + label: UIConstants.tabLabels[2], + ), + BottomNavigationBarItem( + icon: _buildTabIcon(Icons.history, 3, false), + label: UIConstants.tabLabels[3], + ), + BottomNavigationBarItem( + icon: _buildTabIcon(Icons.settings, 4, false), + label: UIConstants.tabLabels[4], + ), + ], + ), + floatingActionButton: _currentIndex == 0 ? _buildRecordingFab() : null, + ); + } + + Widget _buildTabIcon(IconData icon, int tabIndex, bool isActive) { + if (isActive && tabIndex != _currentIndex) { + return Stack( + children: [ + Icon(icon), + Positioned( + right: 0, + top: 0, + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: tabIndex == 0 ? Colors.red : Colors.green, + shape: BoxShape.circle, + ), + ), + ), + ], + ); + } + return Icon(icon); + } + + void _navigateToHistory() { + setState(() { + _currentIndex = 3; // History tab index + }); + } + + Widget _buildRecordingFab() { + return FloatingActionButton( + onPressed: () { + // TODO: Connect to audio service in Phase 2 + }, + child: const Icon(Icons.mic), + ); + } +} \ No newline at end of file diff --git a/lib/ui/screens/loading_screen.dart b/lib/ui/screens/loading_screen.dart new file mode 100644 index 0000000..e0cc0d0 --- /dev/null +++ b/lib/ui/screens/loading_screen.dart @@ -0,0 +1,91 @@ +// ABOUTME: Loading screen shown during app initialization and updates +// ABOUTME: Displays app logo, loading indicator, and optional status message + +import 'package:flutter/material.dart'; + +class LoadingScreen extends StatelessWidget { + final String? message; + + const LoadingScreen({ + super.key, + this.message, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // App logo/icon + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + ), + child: Icon( + Icons.visibility, + size: 60, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 32), + + // App name + Text( + 'Helix', + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + + const SizedBox(height: 8), + + // Tagline + Text( + 'AI-Powered Conversation Intelligence', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 48), + + // Loading indicator + SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Theme.of(context).colorScheme.primary, + ), + ), + ), + + const SizedBox(height: 16), + + // Status message + if (message != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Text( + message!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/theme/app_theme.dart b/lib/ui/theme/app_theme.dart new file mode 100644 index 0000000..d3c7382 --- /dev/null +++ b/lib/ui/theme/app_theme.dart @@ -0,0 +1,144 @@ +// ABOUTME: App theme configuration with light and dark mode definitions +// ABOUTME: Defines colors, typography, and component styling for consistent UI + +import 'package:flutter/material.dart'; + +class AppTheme { + // Colors + static const Color primaryColor = Color(0xFF2196F3); + static const Color primaryVariant = Color(0xFF1976D2); + static const Color secondaryColor = Color(0xFF03DAC6); + static const Color surfaceColor = Color(0xFFFAFAFA); + static const Color backgroundColor = Color(0xFFFFFFFF); + static const Color errorColor = Color(0xFFB00020); + + // Dark theme colors + static const Color darkPrimaryColor = Color(0xFF90CAF9); + static const Color darkSurfaceColor = Color(0xFF121212); + static const Color darkBackgroundColor = Color(0xFF121212); + + static ThemeData get lightTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.light, + colorScheme: const ColorScheme.light( + primary: primaryColor, + secondary: secondaryColor, + surface: surfaceColor, + error: errorColor, + ), + appBarTheme: const AppBarTheme( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + elevation: 2, + centerTitle: true, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + cardTheme: CardTheme( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.all(8), + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + selectedItemColor: primaryColor, + unselectedItemColor: Colors.grey, + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: primaryColor, width: 2), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ); + } + + static ThemeData get darkTheme { + return ThemeData( + useMaterial3: true, + brightness: Brightness.dark, + colorScheme: const ColorScheme.dark( + primary: darkPrimaryColor, + secondary: secondaryColor, + surface: darkSurfaceColor, + error: errorColor, + ), + appBarTheme: const AppBarTheme( + backgroundColor: darkSurfaceColor, + foregroundColor: Colors.white, + elevation: 2, + centerTitle: true, + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: darkPrimaryColor, + foregroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + cardTheme: CardTheme( + elevation: 4, + color: darkSurfaceColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.all(8), + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: darkPrimaryColor, + foregroundColor: Colors.black, + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + selectedItemColor: darkPrimaryColor, + unselectedItemColor: Colors.grey, + backgroundColor: darkSurfaceColor, + ), + inputDecorationTheme: InputDecorationTheme( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Colors.grey), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: darkPrimaryColor, width: 2), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/widgets/analysis_tab.dart b/lib/ui/widgets/analysis_tab.dart new file mode 100644 index 0000000..6b19484 --- /dev/null +++ b/lib/ui/widgets/analysis_tab.dart @@ -0,0 +1,854 @@ +// ABOUTME: Enhanced analysis tab with fact-checking cards and AI insights +// ABOUTME: Displays real-time AI analysis, fact-checking, summaries, and action items + +import 'package:flutter/material.dart'; + +class AnalysisTab extends StatefulWidget { + const AnalysisTab({super.key}); + + @override + State createState() => _AnalysisTabState(); +} + +class _AnalysisTabState extends State with TickerProviderStateMixin { + late TabController _tabController; + bool _isAnalyzing = false; + + // Sample data for demonstration + final List _factChecks = [ + FactCheckResult( + claim: 'The iPhone was first released in 2007', + status: FactCheckStatus.verified, + confidence: 0.98, + sources: ['Apple Inc.', 'TechCrunch', 'Wikipedia'], + explanation: 'Apple officially announced the iPhone on January 9, 2007, at the Macworld Conference & Expo.', + ), + FactCheckResult( + claim: 'Climate change is causing sea levels to rise globally', + status: FactCheckStatus.verified, + confidence: 0.95, + sources: ['NASA', 'NOAA', 'IPCC Report 2023'], + explanation: 'Multiple scientific studies confirm global sea level rise due to thermal expansion and ice sheet melting.', + ), + FactCheckResult( + claim: 'Electric cars produce zero emissions', + status: FactCheckStatus.disputed, + confidence: 0.82, + sources: ['EPA', 'Union of Concerned Scientists'], + explanation: 'While electric cars produce no direct emissions, electricity generation and battery production do create emissions.', + ), + ]; + + final ConversationSummary _summary = ConversationSummary( + summary: 'Discussion covered technology innovation, environmental impact, and the future of transportation. Key focus on electric vehicles and their environmental benefits versus traditional vehicles.', + keyPoints: [ + 'Electric vehicle adoption is accelerating globally', + 'Battery technology improvements are driving longer ranges', + 'Charging infrastructure needs continued expansion', + 'Environmental benefits depend on electricity source' + ], + decisions: [ + 'Research electric vehicle options for company fleet', + 'Schedule meeting with sustainability team' + ], + questions: [ + 'What is the total cost of ownership for EVs?', + 'How long until charging network is fully developed?' + ], + topics: ['Technology', 'Environment', 'Transportation', 'Sustainability'], + confidence: 0.89, + ); + + final List _actionItems = [ + ActionItemResult( + id: '1', + description: 'Research electric vehicle models for company fleet replacement', + assignee: 'Fleet Manager', + dueDate: DateTime.now().add(const Duration(days: 7)), + priority: ActionItemPriority.high, + confidence: 0.91, + status: ActionItemStatus.pending, + ), + ActionItemResult( + id: '2', + description: 'Schedule sustainability team meeting to discuss carbon footprint', + priority: ActionItemPriority.medium, + confidence: 0.85, + status: ActionItemStatus.pending, + ), + ActionItemResult( + id: '3', + description: 'Calculate total cost of ownership comparison between gas and electric vehicles', + dueDate: DateTime.now().add(const Duration(days: 14)), + priority: ActionItemPriority.low, + confidence: 0.78, + status: ActionItemStatus.pending, + ), + ]; + + final SentimentAnalysisResult _sentiment = SentimentAnalysisResult( + overallSentiment: SentimentType.positive, + confidence: 0.87, + emotions: { + 'optimism': 0.7, + 'curiosity': 0.8, + 'concern': 0.3, + 'excitement': 0.6, + }, + ); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 4, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('AI Analysis'), + backgroundColor: theme.colorScheme.surface, + elevation: 0, + actions: [ + IconButton( + icon: Icon(_isAnalyzing ? Icons.stop : Icons.refresh), + onPressed: () { + setState(() { + _isAnalyzing = !_isAnalyzing; + }); + }, + ), + PopupMenuButton( + onSelected: (value) { + // Handle menu actions + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'export', + child: Row( + children: [ + Icon(Icons.download), + SizedBox(width: 8), + Text('Export Analysis'), + ], + ), + ), + const PopupMenuItem( + value: 'settings', + child: Row( + children: [ + Icon(Icons.settings), + SizedBox(width: 8), + Text('Analysis Settings'), + ], + ), + ), + ], + ), + ], + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(icon: Icon(Icons.fact_check), text: 'Facts'), + Tab(icon: Icon(Icons.summarize), text: 'Summary'), + Tab(icon: Icon(Icons.assignment), text: 'Actions'), + Tab(icon: Icon(Icons.sentiment_satisfied), text: 'Sentiment'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildFactCheckTab(theme), + _buildSummaryTab(theme), + _buildActionItemsTab(theme), + _buildSentimentTab(theme), + ], + ), + ); + } + + Widget _buildFactCheckTab(ThemeData theme) { + if (_factChecks.isEmpty) { + return _buildEmptyState( + theme, + Icons.fact_check_outlined, + 'No Facts to Check', + 'Start a conversation to see AI-powered fact-checking results', + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _factChecks.length, + itemBuilder: (context, index) { + final factCheck = _factChecks[index]; + return FactCheckCard(factCheck: factCheck); + }, + ); + } + + Widget _buildSummaryTab(ThemeData theme) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SummaryCard(summary: _summary), + const SizedBox(height: 16), + _buildInsightsList(theme), + ], + ), + ); + } + + Widget _buildActionItemsTab(ThemeData theme) { + if (_actionItems.isEmpty) { + return _buildEmptyState( + theme, + Icons.assignment_outlined, + 'No Action Items', + 'AI will extract action items from your conversations', + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _actionItems.length, + itemBuilder: (context, index) { + final actionItem = _actionItems[index]; + return ActionItemCard(actionItem: actionItem); + }, + ); + } + + Widget _buildSentimentTab(ThemeData theme) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + SentimentCard(sentiment: _sentiment), + const SizedBox(height: 16), + _buildEmotionBreakdown(theme), + ], + ), + ); + } + + Widget _buildEmptyState(ThemeData theme, IconData icon, String title, String subtitle) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 64, + color: theme.colorScheme.outline, + ), + const SizedBox(height: 24), + Text( + title, + style: theme.textTheme.headlineSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Text( + subtitle, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildInsightsList(ThemeData theme) { + final insights = [ + 'Conversation showed high engagement with technical topics', + 'Environmental consciousness is a key decision factor', + 'Cost analysis is needed before making final decisions', + 'Timeline expectations are realistic and achievable', + ]; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.lightbulb_outlined, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'AI Insights', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 12), + ...insights.map((insight) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 6, + height: 6, + margin: const EdgeInsets.only(top: 6, right: 8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: theme.colorScheme.primary, + ), + ), + Expanded( + child: Text( + insight, + style: theme.textTheme.bodyMedium, + ), + ), + ], + ), + )), + ], + ), + ), + ); + } + + Widget _buildEmotionBreakdown(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Emotion Breakdown', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + ..._sentiment.emotions.entries.map((entry) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + entry.key.toUpperCase(), + style: theme.textTheme.labelMedium, + ), + Text( + '${(entry.value * 100).round()}%', + style: theme.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 4), + LinearProgressIndicator( + value: entry.value, + backgroundColor: theme.colorScheme.outline.withOpacity(0.2), + valueColor: AlwaysStoppedAnimation( + _getEmotionColor(entry.key), + ), + ), + ], + ), + ); + }), + ], + ), + ), + ); + } + + Color _getEmotionColor(String emotion) { + switch (emotion.toLowerCase()) { + case 'optimism': + case 'excitement': + return Colors.green; + case 'curiosity': + return Colors.blue; + case 'concern': + return Colors.orange; + default: + return Colors.grey; + } + } +} + +// Helper Models +class FactCheckResult { + final String claim; + final FactCheckStatus status; + final double confidence; + final List sources; + final String explanation; + + FactCheckResult({ + required this.claim, + required this.status, + required this.confidence, + required this.sources, + required this.explanation, + }); +} + +enum FactCheckStatus { verified, disputed, uncertain } + +class ConversationSummary { + final String summary; + final List keyPoints; + final List decisions; + final List questions; + final List topics; + final double confidence; + + ConversationSummary({ + required this.summary, + required this.keyPoints, + required this.decisions, + required this.questions, + required this.topics, + required this.confidence, + }); +} + +class ActionItemResult { + final String id; + final String description; + final String? assignee; + final DateTime? dueDate; + final ActionItemPriority priority; + final double confidence; + final ActionItemStatus status; + + ActionItemResult({ + required this.id, + required this.description, + this.assignee, + this.dueDate, + required this.priority, + required this.confidence, + required this.status, + }); +} + +enum ActionItemPriority { low, medium, high, urgent } +enum ActionItemStatus { pending, inProgress, completed, cancelled } + +class SentimentAnalysisResult { + final SentimentType overallSentiment; + final double confidence; + final Map emotions; + + SentimentAnalysisResult({ + required this.overallSentiment, + required this.confidence, + required this.emotions, + }); +} + +enum SentimentType { positive, negative, neutral, mixed } + +// Custom Card Widgets +class FactCheckCard extends StatelessWidget { + final FactCheckResult factCheck; + + const FactCheckCard({super.key, required this.factCheck}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + Color statusColor; + IconData statusIcon; + switch (factCheck.status) { + case FactCheckStatus.verified: + statusColor = Colors.green; + statusIcon = Icons.check_circle; + break; + case FactCheckStatus.disputed: + statusColor = Colors.red; + statusIcon = Icons.cancel; + break; + case FactCheckStatus.uncertain: + statusColor = Colors.orange; + statusIcon = Icons.help_outline; + break; + } + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(statusIcon, color: statusColor, size: 20), + const SizedBox(width: 8), + Text( + factCheck.status.name.toUpperCase(), + style: theme.textTheme.labelMedium?.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${(factCheck.confidence * 100).round()}%', + style: theme.textTheme.labelSmall?.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + factCheck.claim, + style: theme.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + factCheck.explanation, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + if (factCheck.sources.isNotEmpty) ...[ + const SizedBox(height: 12), + Wrap( + spacing: 8, + children: factCheck.sources.map((source) => Chip( + label: Text(source), + backgroundColor: theme.colorScheme.surfaceVariant, + labelStyle: theme.textTheme.labelSmall, + )).toList(), + ), + ], + ], + ), + ), + ); + } +} + +class SummaryCard extends StatelessWidget { + final ConversationSummary summary; + + const SummaryCard({super.key, required this.summary}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.summarize, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Conversation Summary', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${(summary.confidence * 100).round()}%', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + summary.summary, + style: theme.textTheme.bodyMedium, + ), + if (summary.keyPoints.isNotEmpty) ...[ + const SizedBox(height: 16), + Text( + 'Key Points', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + ...summary.keyPoints.map((point) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 4, + height: 4, + margin: const EdgeInsets.only(top: 8, right: 8), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: theme.colorScheme.primary, + ), + ), + Expanded(child: Text(point, style: theme.textTheme.bodyMedium)), + ], + ), + )), + ], + if (summary.topics.isNotEmpty) ...[ + const SizedBox(height: 12), + Wrap( + spacing: 8, + children: summary.topics.map((topic) => Chip( + label: Text(topic), + backgroundColor: theme.colorScheme.secondaryContainer, + labelStyle: theme.textTheme.labelSmall, + )).toList(), + ), + ], + ], + ), + ), + ); + } +} + +class ActionItemCard extends StatelessWidget { + final ActionItemResult actionItem; + + const ActionItemCard({super.key, required this.actionItem}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + Color priorityColor; + switch (actionItem.priority) { + case ActionItemPriority.urgent: + priorityColor = Colors.red; + break; + case ActionItemPriority.high: + priorityColor = Colors.orange; + break; + case ActionItemPriority.medium: + priorityColor = Colors.blue; + break; + case ActionItemPriority.low: + priorityColor = Colors.green; + break; + } + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: priorityColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Text( + actionItem.priority.name.toUpperCase(), + style: theme.textTheme.labelMedium?.copyWith( + color: priorityColor, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + if (actionItem.dueDate != null) + Text( + _formatDueDate(actionItem.dueDate!), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + actionItem.description, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + if (actionItem.assignee != null) ...[ + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.person_outline, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + actionItem.assignee!, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ], + ], + ), + ), + ); + } + + String _formatDueDate(DateTime dueDate) { + final now = DateTime.now(); + final difference = dueDate.difference(now).inDays; + + if (difference == 0) { + return 'Due today'; + } else if (difference == 1) { + return 'Due tomorrow'; + } else if (difference > 0) { + return 'Due in $difference days'; + } else { + return 'Overdue by ${difference.abs()} days'; + } + } +} + +class SentimentCard extends StatelessWidget { + final SentimentAnalysisResult sentiment; + + const SentimentCard({super.key, required this.sentiment}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + Color sentimentColor; + IconData sentimentIcon; + String sentimentText; + + switch (sentiment.overallSentiment) { + case SentimentType.positive: + sentimentColor = Colors.green; + sentimentIcon = Icons.sentiment_very_satisfied; + sentimentText = 'Positive'; + break; + case SentimentType.negative: + sentimentColor = Colors.red; + sentimentIcon = Icons.sentiment_very_dissatisfied; + sentimentText = 'Negative'; + break; + case SentimentType.neutral: + sentimentColor = Colors.grey; + sentimentIcon = Icons.sentiment_neutral; + sentimentText = 'Neutral'; + break; + case SentimentType.mixed: + sentimentColor = Colors.orange; + sentimentIcon = Icons.sentiment_satisfied; + sentimentText = 'Mixed'; + break; + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + Icon(sentimentIcon, color: sentimentColor, size: 32), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Overall Sentiment', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + Text( + sentimentText, + style: theme.textTheme.bodyLarge?.copyWith( + color: sentimentColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: sentimentColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + '${(sentiment.confidence * 100).round()}%', + style: theme.textTheme.labelMedium?.copyWith( + color: sentimentColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/widgets/conversation_tab.dart b/lib/ui/widgets/conversation_tab.dart new file mode 100644 index 0000000..1c7890b --- /dev/null +++ b/lib/ui/widgets/conversation_tab.dart @@ -0,0 +1,1033 @@ +// ABOUTME: Enhanced conversation tab with real-time transcription display +// ABOUTME: Features recording controls, live transcription, speaker identification, and audio levels + +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:io'; +import 'dart:math' as math; + +import '../../services/audio_service.dart'; +import '../../services/implementations/audio_service_impl.dart'; +import '../../services/conversation_storage_service.dart'; +import '../../services/service_locator.dart'; +import '../../models/audio_configuration.dart'; +import '../../models/conversation_model.dart'; +import '../../models/transcription_segment.dart'; +import '../../services/transcription_service.dart'; +import '../../services/real_time_transcription_service.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class ConversationTab extends StatefulWidget { + final VoidCallback? onHistoryTap; + + const ConversationTab({super.key, this.onHistoryTap}); + + @override + State createState() => _ConversationTabState(); +} + +class _ConversationTabState extends State with TickerProviderStateMixin { + bool _isRecording = false; + bool _isPaused = false; + bool _isProcessingRecordingToggle = false; + double _audioLevel = 0.0; + final List _audioLevelHistory = []; + late AnimationController _waveController; + late AnimationController _pulseController; + + // Service integration + late AudioService _audioService; + late ConversationStorageService _storageService; + late RealTimeTranscriptionService _transcriptionPipelineService; + StreamSubscription? _audioLevelSubscription; + StreamSubscription? _voiceActivitySubscription; + StreamSubscription? _recordingDurationSubscription; + StreamSubscription? _transcriptionSubscription; + StreamSubscription? _partialTranscriptionSubscription; + + // Current conversation state + String? _currentConversationId; + + // Recording timer + Timer? _timerUpdateTimer; + Duration _recordingDuration = Duration.zero; + + // Dynamic transcription segments populated by real-time transcription + final List _transcriptSegments = []; + TranscriptionSegment? _currentPartialSegment; + + @override + void initState() { + super.initState(); + _waveController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _pulseController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + ); + + _initializeAudioService(); + } + + Future _initializeAudioService() async { + try { + _audioService = ServiceLocator.instance.get(); + _storageService = ServiceLocator.instance.get(); + _transcriptionPipelineService = ServiceLocator.instance.get(); + + final audioConfig = AudioConfiguration.speechRecognition().copyWith( + enableRealTimeStreaming: true, + vadThreshold: 0.01, + chunkDurationMs: 100, // Optimized for real-time transcription + ); + + await _audioService.initialize(audioConfig); + + // Initialize transcription pipeline + const transcriptionConfig = TranscriptionPipelineConfig( + audioChunkDurationMs: 100, + targetLatencyMs: 200, // Target 200ms for word-by-word updates + enablePartialResults: true, + maxBufferedSegments: 500, + ); + await _transcriptionPipelineService.initialize(transcriptionConfig); + + await _checkInitialPermissionStatus(); + + // Set up audio level subscription for real-time waveform + _audioLevelSubscription = _audioService.audioLevelStream.listen( + (level) { + if (mounted && _isRecording) { + setState(() { + _audioLevel = level; + // Keep history for smoother waveform + _audioLevelHistory.add(level); + if (_audioLevelHistory.length > 50) { + _audioLevelHistory.removeAt(0); + } + }); + } + }, + onError: (error) { + debugPrint('Audio level stream error: $error'); + }, + ); + + // Set up voice activity subscription + _voiceActivitySubscription = _audioService.voiceActivityStream.listen( + (isActive) { + if (mounted && _isRecording) { + // Could add voice activity indicator here + debugPrint('Voice activity: $isActive'); + } + }, + ); + + // Set up recording duration subscription + _recordingDurationSubscription = _audioService.recordingDurationStream.listen( + (duration) { + if (mounted && _isRecording) { + setState(() { + _recordingDuration = duration; + }); + } + }, + ); + + // Set up real-time transcription subscriptions + _transcriptionSubscription = _transcriptionPipelineService.transcriptionStream.listen( + (segment) { + if (mounted) { + setState(() { + // Add final transcription segments to the list + if (segment.isFinal) { + _transcriptSegments.add(segment); + _currentPartialSegment = null; // Clear partial segment + + // Keep list manageable (last 100 segments) + if (_transcriptSegments.length > 100) { + _transcriptSegments.removeAt(0); + } + } + }); + } + }, + onError: (error) { + debugPrint('Transcription stream error: $error'); + }, + ); + + _partialTranscriptionSubscription = _transcriptionPipelineService.partialTranscriptionStream.listen( + (segment) { + if (mounted) { + setState(() { + // Update current partial segment for immediate UI feedback + _currentPartialSegment = segment; + }); + } + }, + onError: (error) { + debugPrint('Partial transcription stream error: $error'); + }, + ); + + debugPrint('AudioService and transcription pipeline initialized successfully'); + } catch (e) { + debugPrint('Failed to initialize AudioService: $e'); + } + } + + Future _checkInitialPermissionStatus() async { + try { + final audioServiceImpl = _audioService as AudioServiceImpl; + final status = await audioServiceImpl.checkPermissionStatus(); + + debugPrint('Initial microphone permission status: ${status.name}'); + + // Update UI based on permission status if needed + if (mounted) { + setState(() { + // Permission status is already updated in the service + }); + } + } catch (e) { + debugPrint('Failed to check initial permission status: $e'); + } + } + + @override + void dispose() { + _audioLevelSubscription?.cancel(); + _voiceActivitySubscription?.cancel(); + _recordingDurationSubscription?.cancel(); + _transcriptionSubscription?.cancel(); + _partialTranscriptionSubscription?.cancel(); + _timerUpdateTimer?.cancel(); + _waveController.dispose(); + _pulseController.dispose(); + super.dispose(); + } + + + String _generateConversationId() { + // Simple UUID-like ID generator + final random = math.Random(); + final timestamp = DateTime.now().millisecondsSinceEpoch; + final randomPart = random.nextInt(999999); + return 'conv_${timestamp}_$randomPart'; + } + + Future _toggleRecording() async { + // Prevent multiple simultaneous calls + if (_isProcessingRecordingToggle) return; + _isProcessingRecordingToggle = true; + + try { + // Ensure AudioService is initialized + if (_audioService == null) { + debugPrint('AudioService not initialized, initializing now...'); + await _initializeAudioService(); + if (_audioService == null) { + throw Exception('Failed to initialize AudioService'); + } + } + if (_isRecording) { + debugPrint('Stopping recording...'); + + try { + // Stop transcription pipeline first + await _transcriptionPipelineService.stopTranscription(); + _pulseController.stop(); + + // Create and save conversation + await _saveCurrentConversation(); + + setState(() { + _isRecording = false; + _isPaused = false; + _audioLevel = 0.0; + _currentPartialSegment = null; + }); + + // Clear current conversation state + _currentConversationId = null; + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Recording stopped and saved'), + duration: Duration(seconds: 2), + ), + ); + } + } catch (e) { + debugPrint('Error stopping recording: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to stop recording: $e')), + ); + } + } + } else { + debugPrint('Starting recording...'); + + // Always check current permission status first + final audioServiceImpl = _audioService as AudioServiceImpl; + final currentStatus = await audioServiceImpl.checkPermissionStatus(); + debugPrint('Current permission status: ${currentStatus.name}'); + + if (currentStatus != PermissionStatus.granted && + currentStatus != PermissionStatus.limited && + currentStatus != PermissionStatus.provisional) { + // Only skip requesting if permanently denied - go straight to settings + if (currentStatus == PermissionStatus.permanentlyDenied) { + debugPrint('Permission permanently denied, showing settings dialog'); + _showPermissionPermanentlyDeniedDialog(); + return; + } + + debugPrint('Requesting microphone permission...'); + final granted = await _audioService.requestPermission(); + debugPrint('Permission request result: $granted'); + + if (!granted) { + if (mounted) { + // Re-check status after request + final newStatus = await audioServiceImpl.checkPermissionStatus(); + debugPrint('Permission request failed with final status: ${newStatus.name}'); + + if (newStatus == PermissionStatus.permanentlyDenied || newStatus == PermissionStatus.denied) { + // Show dialog to guide user to settings + _showPermissionPermanentlyDeniedDialog(); + } else { + String message = 'Microphone permission required for recording'; + if (newStatus == PermissionStatus.restricted) { + message = 'Microphone access is restricted (parental controls)'; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + duration: const Duration(seconds: 4), + action: SnackBarAction( + label: 'Retry', + onPressed: () => _toggleRecording(), + ), + ), + ); + } + } + return; + } else { + debugPrint('Microphone permission granted successfully'); + } + } else { + debugPrint('Microphone permission already available: ${currentStatus.name}'); + } + + try { + // Generate conversation ID and start recording with transcription + _currentConversationId = _generateConversationId(); + + // Start the real-time transcription pipeline + await _transcriptionPipelineService.startTranscription( + language: 'en-US', + preferredBackend: TranscriptionBackend.device, + ); + + _pulseController.repeat(); + + setState(() { + _isRecording = true; + _isPaused = false; + // Clear previous transcription data + _transcriptSegments.clear(); + _currentPartialSegment = null; + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Recording started'), + duration: Duration(seconds: 2), + ), + ); + } + } catch (e) { + debugPrint('Error starting recording: $e'); + _currentConversationId = null; + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to start recording: $e')), + ); + } + } + } + } catch (e) { + debugPrint('Unexpected error in recording toggle: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Recording error: $e')), + ); + } + } finally { + _isProcessingRecordingToggle = false; + } + } + + Future _saveCurrentConversation() async { + if (_currentConversationId == null) { + debugPrint('Cannot save conversation: No conversation ID'); + return; + } + + try { + debugPrint('Saving conversation: $_currentConversationId'); + + // Get the audio file path from the AudioService + String? audioFilePath; + String? audioFormat; + int? audioFileSize; + + // Get the actual recording file path from AudioService + audioFilePath = _audioService.currentRecordingPath; + if (audioFilePath != null) { + audioFormat = audioFilePath.split('.').last; + // Try to get actual file size + try { + final file = File(audioFilePath); + if (await file.exists()) { + audioFileSize = await file.length(); + } + } catch (e) { + debugPrint('Could not get file size: $e'); + audioFileSize = null; + } + } + + // Create conversation from current transcription segments + final conversation = ConversationModel( + id: _currentConversationId!, + title: 'Conversation ${DateTime.now().toLocal().toString().split(' ')[0]}', + startTime: DateTime.now().subtract(_recordingDuration), + endTime: DateTime.now(), + lastUpdated: DateTime.now(), + status: ConversationStatus.completed, + participants: [ + const ConversationParticipant( + id: 'user_1', + name: 'You', + isOwner: true, + ), + const ConversationParticipant( + id: 'speaker_2', + name: 'Speaker 2', + isOwner: false, + ), + ], + segments: _transcriptSegments, + audioFilePath: audioFilePath, + audioFormat: audioFormat, + audioFileSize: audioFileSize, + audioQuality: 0.8, // Placeholder quality score + transcriptionConfidence: 0.85, // Placeholder confidence + ); + + await _storageService.saveConversation(conversation); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Conversation and audio saved')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to save conversation: $e')), + ); + } + } + + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final minutes = twoDigits(duration.inMinutes); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + return '$minutes:$seconds'; + } + + void _showPermissionPermanentlyDeniedDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Microphone Permission Required'), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Helix needs microphone access to record conversations. Please enable it in Settings:', + style: TextStyle(fontSize: 16), + ), + SizedBox(height: 12), + Text( + '1. Tap "Open Settings" below\n' + '2. Find "Flutter Helix" in the list\n' + '3. Toggle ON "Microphone"\n' + '4. Return to the app and try recording again', + style: TextStyle(fontSize: 14, color: Colors.grey), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + final audioServiceImpl = _audioService as AudioServiceImpl; + await audioServiceImpl.openPermissionSettings(); + }, + child: const Text('Open Settings'), + ), + ], + ); + }, + ); + } + + void _togglePause() { + setState(() { + _isPaused = !_isPaused; + }); + + if (_isPaused) { + _pulseController.stop(); + } else { + _pulseController.repeat(); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Live Conversation'), + backgroundColor: theme.colorScheme.surface, + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.settings_outlined), + onPressed: () { + // TODO: Open recording settings + }, + ), + IconButton( + icon: const Icon(Icons.share_outlined), + onPressed: () { + // TODO: Share transcript + }, + ), + ], + ), + body: Column( + children: [ + // Modern Recording Status Bar + Container( + height: 80, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _isRecording + ? theme.colorScheme.errorContainer.withOpacity(0.1) + : theme.colorScheme.surface, + border: _isRecording + ? Border( + bottom: BorderSide( + color: theme.colorScheme.error.withOpacity(0.3), + width: 1, + ), + ) + : null, + ), + child: Row( + children: [ + // Recording Status + AnimatedBuilder( + animation: _pulseController, + builder: (context, child) { + return Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _isRecording + ? Colors.red.withOpacity(0.8 + 0.2 * _pulseController.value) + : theme.colorScheme.outline, + ), + child: Icon( + _isRecording + ? (_isPaused ? Icons.pause : Icons.mic) + : Icons.mic_off, + color: Colors.white, + size: 24, + ), + ); + }, + ), + const SizedBox(width: 16), + + // Audio Level Bars + Expanded( + child: _isRecording + ? ReactiveWaveform( + level: _audioLevel, + levelHistory: _audioLevelHistory, + isRecording: _isRecording, + ) + : Container(), + ), + + // Duration + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: theme.colorScheme.outline.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + _formatDuration(_recordingDuration), + style: theme.textTheme.labelMedium?.copyWith( + fontFamily: 'monospace', + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + // Transcription Area + Expanded( + child: Container( + padding: const EdgeInsets.all(16), + child: _transcriptSegments.isEmpty + ? _buildEmptyState(theme) + : _buildTranscriptList(theme), + ), + ), + + // Control Panel + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + top: BorderSide( + color: theme.colorScheme.outline.withOpacity(0.2), + width: 1, + ), + ), + ), + child: SafeArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Secondary Actions + IconButton( + onPressed: widget.onHistoryTap, + icon: const Icon(Icons.history), + iconSize: 28, + ), + + // Pause/Resume (only when recording) + if (_isRecording) + IconButton( + onPressed: _togglePause, + icon: Icon(_isPaused ? Icons.play_arrow : Icons.pause), + iconSize: 32, + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.secondaryContainer, + foregroundColor: theme.colorScheme.onSecondaryContainer, + ), + ), + + // Modern Record Button + Material( + color: Colors.transparent, + child: InkWell( + onTap: _toggleRecording, + borderRadius: BorderRadius.circular(36), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 72, + height: 72, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _isRecording + ? theme.colorScheme.error + : theme.colorScheme.primary, + boxShadow: _isRecording ? [ + BoxShadow( + color: theme.colorScheme.error.withOpacity(0.3), + blurRadius: 12, + spreadRadius: 2, + ), + ] : null, + ), + child: Icon( + _isRecording ? Icons.stop : Icons.mic, + color: Colors.white, + size: 32, + ), + ), + ), + ), + + // AI Analysis Toggle + IconButton( + onPressed: () { + // TODO: Toggle AI analysis + }, + icon: const Icon(Icons.psychology), + iconSize: 28, + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState(ThemeData theme) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.graphic_eq, + size: 64, + color: theme.colorScheme.outline, + ), + const SizedBox(height: 24), + Text( + 'Ready to Record', + style: theme.textTheme.headlineSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Text( + 'Tap the microphone to start live transcription', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildTranscriptList(ThemeData theme) { + // Combine final segments with current partial segment for display + final displaySegments = List.from(_transcriptSegments); + if (_currentPartialSegment != null) { + displaySegments.add(_currentPartialSegment!); + } + + return ListView.separated( + padding: const EdgeInsets.only(top: 8), + itemCount: displaySegments.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: theme.colorScheme.outline.withOpacity(0.1), + ), + itemBuilder: (context, index) { + final segment = displaySegments[index]; + final isCurrentUser = segment.speakerId == 'user_1' || segment.speakerId == 'speaker_1'; + final isPartial = !segment.isFinal; + final speakerName = segment.speakerName ?? (isCurrentUser ? 'You' : 'Speaker'); + final duration = segment.endTime.difference(segment.startTime); + + return AnimatedContainer( + duration: Duration(milliseconds: isPartial ? 100 : 0), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: isPartial ? BoxDecoration( + color: theme.colorScheme.primaryContainer.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: theme.colorScheme.primary.withOpacity(0.3), + width: 1, + ), + ) : null, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Compact header with speaker info and metadata + Row( + children: [ + // Speaker indicator + AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 12, + height: 12, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isCurrentUser + ? theme.colorScheme.primary + : theme.colorScheme.secondary, + ), + child: isPartial ? Container( + width: 4, + height: 4, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + ) : null, + ), + const SizedBox(width: 8), + + // Speaker name + Text( + speakerName, + style: theme.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w600, + color: isCurrentUser + ? theme.colorScheme.primary + : theme.colorScheme.secondary, + ), + ), + const SizedBox(width: 12), + + // Timestamp + Text( + _formatTimestamp(segment.startTime), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: 8), + + // Duration + Text( + '${duration.inSeconds}s', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + + const Spacer(), + + // Confidence indicator or partial indicator + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: isPartial + ? theme.colorScheme.primary.withOpacity(0.1) + : _getConfidenceColor(segment.confidence).withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + isPartial + ? 'LIVE' + : '${(segment.confidence * 100).round()}%', + style: theme.textTheme.labelSmall?.copyWith( + color: isPartial + ? theme.colorScheme.primary + : _getConfidenceColor(segment.confidence), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + + // Transcript text - compact formatting + Padding( + padding: const EdgeInsets.only(left: 20), + child: Text( + segment.text, + style: theme.textTheme.bodyMedium?.copyWith( + height: 1.3, // Slightly tighter line height for density + ), + ), + ), + ], + ), + ); + }, + ); + } + + Color _getConfidenceColor(double confidence) { + if (confidence >= 0.8) return Colors.green; + if (confidence >= 0.6) return Colors.orange; + return Colors.red; + } + + String _formatTimestamp(DateTime timestamp) { + final now = DateTime.now(); + final diff = now.difference(timestamp); + + if (diff.inMinutes < 1) { + return 'now'; + } else if (diff.inMinutes < 60) { + return '${diff.inMinutes}m ago'; + } else { + return '${timestamp.hour.toString().padLeft(2, '0')}:${timestamp.minute.toString().padLeft(2, '0')}'; + } + } +} + + +// Custom Widgets +class ReactiveWaveform extends StatefulWidget { + final double level; + final List levelHistory; + final bool isRecording; + + const ReactiveWaveform({ + super.key, + required this.level, + required this.levelHistory, + required this.isRecording, + }); + + @override + State createState() => _ReactiveWaveformState(); +} + +class _ReactiveWaveformState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + _animationController.repeat(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + const barCount = 30; + const baseHeight = 4.0; + const maxHeight = 32.0; + + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(barCount, (index) { + // Use history for smoother animation + final historyIndex = (widget.levelHistory.length * index / barCount).floor(); + final historicalLevel = historyIndex < widget.levelHistory.length + ? widget.levelHistory[historyIndex] + : 0.0; + + // Create wave pattern + final normalizedIndex = index / barCount; + final centerDistance = (normalizedIndex - 0.5).abs() * 2; // 0 at center, 1 at edges + final waveMultiplier = (1.0 - centerDistance * 0.6).clamp(0.2, 1.0); + + // Combine current level with historical data for smoother visualization + final combinedLevel = (widget.level * 0.7 + historicalLevel * 0.3).clamp(0.0, 1.0); + + // Add subtle animation for more dynamic feel + final animationOffset = (1.0 + 0.1 * math.sin( + _animationController.value * 2 * math.pi + index * 0.3 + )); + + // Calculate final height + final barHeight = baseHeight + + (combinedLevel * maxHeight * waveMultiplier * animationOffset); + + // Dynamic color based on audio level + Color barColor; + if (combinedLevel < 0.1) { + barColor = Colors.grey.withOpacity(0.3); + } else if (combinedLevel < 0.3) { + barColor = Colors.blue.withOpacity(0.6 + 0.4 * combinedLevel); + } else if (combinedLevel < 0.7) { + barColor = Colors.green.withOpacity(0.7 + 0.3 * combinedLevel); + } else { + barColor = Colors.orange.withOpacity(0.8 + 0.2 * combinedLevel); + } + + return Container( + width: 2.5, + height: barHeight.clamp(baseHeight, maxHeight), + margin: const EdgeInsets.symmetric(horizontal: 0.5), + decoration: BoxDecoration( + color: barColor, + borderRadius: BorderRadius.circular(1.25), + boxShadow: widget.isRecording && combinedLevel > 0.5 ? [ + BoxShadow( + color: barColor.withOpacity(0.5), + blurRadius: 2, + spreadRadius: 0.5, + ), + ] : null, + ), + ); + }), + ); + }, + ); + } +} + +class ConfidenceBadge extends StatelessWidget { + final double confidence; + + const ConfidenceBadge({super.key, required this.confidence}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final confidencePercent = (confidence * 100).round(); + + Color badgeColor; + if (confidence >= 0.9) { + badgeColor = Colors.green; + } else if (confidence >= 0.7) { + badgeColor = Colors.orange; + } else { + badgeColor = Colors.red; + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: badgeColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: badgeColor.withOpacity(0.3)), + ), + child: Text( + '$confidencePercent%', + style: theme.textTheme.labelSmall?.copyWith( + color: badgeColor, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/widgets/glasses_tab.dart b/lib/ui/widgets/glasses_tab.dart new file mode 100644 index 0000000..a6dfa9d --- /dev/null +++ b/lib/ui/widgets/glasses_tab.dart @@ -0,0 +1,968 @@ +// ABOUTME: Enhanced glasses tab with connection management and HUD controls +// ABOUTME: Manages Even Realities smart glasses connection, battery, and display controls + +import 'package:flutter/material.dart'; +import 'dart:typed_data'; +import 'dart:math'; + +import '../../services/glasses_service.dart' as service; +import '../../services/implementations/even_realities_glasses_service.dart'; +import '../../services/service_locator.dart'; +import '../../core/utils/logging_service.dart'; +import '../../models/glasses_connection_state.dart'; + +class GlassesTab extends StatefulWidget { + const GlassesTab({super.key}); + + @override + State createState() => _GlassesTabState(); +} + +class _GlassesTabState extends State with TickerProviderStateMixin { + late AnimationController _scanController; + late AnimationController _pulseController; + + // Even Realities glasses service + late EvenRealitiesGlassesService _glassesService; + + GlassesConnectionStatus _connectionStatus = GlassesConnectionStatus.disconnected; + bool _isScanning = false; + double _batteryLevel = 0.85; + double _brightness = 0.7; + bool _isHUDEnabled = true; + + // Testing controls + final TextEditingController _testTextController = TextEditingController(); + + final List _discoveredDevices = [ + DiscoveredDevice( + id: 'even_realities_001', + name: 'Even Realities G1', + rssi: -45, + batteryLevel: 0.85, + ), + DiscoveredDevice( + id: 'even_realities_002', + name: 'Even Realities G1 Pro', + rssi: -62, + batteryLevel: 0.92, + ), + ]; + + String? _connectedDeviceId; + String _lastSyncTime = '2 minutes ago'; + + @override + void initState() { + super.initState(); + _scanController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + ); + _pulseController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + ); + + // Initialize Even Realities glasses service + _initializeGlassesService(); + + // Set initial test text + _testTextController.text = 'Hello Even Realities!'; + } + + Future _initializeGlassesService() async { + try { + final logger = ServiceLocator.instance.get(); + _glassesService = EvenRealitiesGlassesService(logger: logger); + await _glassesService.initialize(); + + // Listen to connection state changes + _glassesService.connectionStateStream.listen((status) { + if (mounted) { + setState(() { + _connectionStatus = _mapConnectionStatus(status); + }); + } + }); + + // Listen to discovered devices + _glassesService.discoveredDevicesStream.listen((devices) { + if (mounted) { + setState(() { + _discoveredDevices.clear(); + for (final device in devices) { + _discoveredDevices.add(DiscoveredDevice( + id: device.id, + name: device.name, + rssi: device.signalStrength, + batteryLevel: 0.85, // Default battery level + )); + } + }); + } + }); + + } catch (e) { + debugPrint('Failed to initialize glasses service: $e'); + } + } + + GlassesConnectionStatus _mapConnectionStatus(ConnectionStatus status) { + switch (status) { + case ConnectionStatus.connected: + return GlassesConnectionStatus.connected; + case ConnectionStatus.connecting: + return GlassesConnectionStatus.connecting; + case ConnectionStatus.disconnected: + return GlassesConnectionStatus.disconnected; + default: + return GlassesConnectionStatus.disconnected; + } + } + + @override + void dispose() { + _scanController.dispose(); + _pulseController.dispose(); + _testTextController.dispose(); + _glassesService.dispose(); + super.dispose(); + } + + // Even Realities Testing Methods + Future _displayDeviceInfo() async { + try { + final connectedDevice = _discoveredDevices.firstWhere( + (device) => device.id == _connectedDeviceId, + orElse: () => _discoveredDevices.first, + ); + + final infoText = 'Device: ${connectedDevice.name}\nBattery: ${(_batteryLevel * 100).round()}%\nSignal: ${connectedDevice.rssi} dBm'; + await _glassesService.displayText(infoText); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Device info displayed on glasses')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to display info: $e')), + ); + } + } + + Future _clearDisplay() async { + try { + await _glassesService.clearDisplay(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Display cleared')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to clear display: $e')), + ); + } + } + + Future _showTestAlert() async { + try { + await _glassesService.displayNotification( + 'Test Alert', + 'This is a test notification on your Even Realities glasses!', + priority: service.NotificationPriority.normal, + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Test alert sent to glasses')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to show alert: $e')), + ); + } + } + + Future _displayCustomText() async { + if (_testTextController.text.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please enter some text to display')), + ); + return; + } + + try { + await _glassesService.displayText(_testTextController.text); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Custom text displayed on glasses')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to display text: $e')), + ); + } + } + + Future _displayTestBitmap() async { + try { + // Create a simple test bitmap (64x32 pixels) + final bitmap = _generateTestBitmap(); + await _glassesService.displayBitmap(bitmap); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Test image displayed on glasses')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to display image: $e')), + ); + } + } + + Future _displayProgressAnimation() async { + try { + for (int i = 0; i <= 10; i++) { + final progressText = 'Progress: ${'█' * i}${'░' * (10 - i)} ${i * 10}%'; + await _glassesService.displayText(progressText); + await Future.delayed(const Duration(milliseconds: 500)); + } + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Progress animation completed')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Animation failed: $e')), + ); + } + } + + Uint8List _generateTestBitmap() { + // Generate a simple test pattern - checkered pattern + const width = 64; + const height = 32; + final bitmap = Uint8List(width * height ~/ 8); // 1 bit per pixel + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final pixelIndex = y * width + x; + final byteIndex = pixelIndex ~/ 8; + final bitIndex = pixelIndex % 8; + + // Create checkerboard pattern + if ((x ~/ 8 + y ~/ 8) % 2 == 0) { + bitmap[byteIndex] |= (1 << (7 - bitIndex)); + } + } + } + + return bitmap; + } + + Future _startScanning() async { + setState(() { + _isScanning = true; + }); + _scanController.repeat(); + + try { + await _glassesService.startScanning(timeout: const Duration(seconds: 30)); + + // Stop scanning after 30 seconds + Future.delayed(const Duration(seconds: 30), () { + if (mounted && _isScanning) { + _stopScanning(); + } + }); + } catch (e) { + debugPrint('Failed to start scanning: $e'); + if (mounted) { + setState(() { + _isScanning = false; + }); + _scanController.stop(); + } + } + } + + Future _stopScanning() async { + try { + await _glassesService.stopScanning(); + } catch (e) { + debugPrint('Failed to stop scanning: $e'); + } + + if (mounted) { + setState(() { + _isScanning = false; + }); + _scanController.stop(); + } + } + + Future _connectToDevice(DiscoveredDevice device) async { + setState(() { + _connectionStatus = GlassesConnectionStatus.connecting; + }); + + _pulseController.repeat(); + + try { + await _glassesService.connectToDevice(device.id); + _connectedDeviceId = device.id; + _batteryLevel = device.batteryLevel; + _pulseController.stop(); + } catch (e) { + debugPrint('Failed to connect to device: $e'); + if (mounted) { + setState(() { + _connectionStatus = GlassesConnectionStatus.disconnected; + }); + _pulseController.stop(); + } + } + } + + Future _disconnect() async { + try { + await _glassesService.disconnect(); + _connectedDeviceId = null; + } catch (e) { + debugPrint('Failed to disconnect: $e'); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Smart Glasses'), + backgroundColor: theme.colorScheme.surface, + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.help_outline), + onPressed: () { + _showHelpDialog(context); + }, + ), + PopupMenuButton( + onSelected: (value) { + switch (value) { + case 'calibrate': + _showCalibrationDialog(context); + break; + case 'reset': + _showResetDialog(context); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'calibrate', + child: Row( + children: [ + Icon(Icons.tune), + SizedBox(width: 8), + Text('Calibrate Display'), + ], + ), + ), + const PopupMenuItem( + value: 'reset', + child: Row( + children: [ + Icon(Icons.refresh), + SizedBox(width: 8), + Text('Reset Connection'), + ], + ), + ), + ], + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildConnectionCard(theme), + const SizedBox(height: 16), + if (_connectionStatus == GlassesConnectionStatus.connected) ...[ + _buildHUDControlCard(theme), + const SizedBox(height: 16), + _buildDeviceInfoCard(theme), + const SizedBox(height: 16), + ], + if (_connectionStatus == GlassesConnectionStatus.disconnected) + _buildDeviceDiscoveryCard(theme), + ], + ), + ), + ); + } + + Widget _buildConnectionCard(ThemeData theme) { + Color statusColor; + IconData statusIcon; + String statusText; + String statusSubtitle; + + switch (_connectionStatus) { + case GlassesConnectionStatus.connected: + statusColor = Colors.green; + statusIcon = Icons.check_circle; + statusText = 'Connected'; + statusSubtitle = 'Even Realities G1 • Last sync: $_lastSyncTime'; + break; + case GlassesConnectionStatus.connecting: + statusColor = Colors.orange; + statusIcon = Icons.sync; + statusText = 'Connecting...'; + statusSubtitle = 'Establishing secure connection'; + break; + case GlassesConnectionStatus.disconnected: + statusColor = Colors.grey; + statusIcon = Icons.bluetooth_disabled; + statusText = 'Disconnected'; + statusSubtitle = 'No glasses connected'; + break; + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + children: [ + AnimatedBuilder( + animation: _connectionStatus == GlassesConnectionStatus.connecting + ? _pulseController : const AlwaysStoppedAnimation(0), + builder: (context, child) { + return Container( + width: 64, + height: 64, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: statusColor.withOpacity( + _connectionStatus == GlassesConnectionStatus.connecting + ? 0.3 + 0.4 * _pulseController.value + : 0.1 + ), + ), + child: Icon( + statusIcon, + size: 32, + color: statusColor, + ), + ); + }, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + statusText, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + color: statusColor, + ), + ), + const SizedBox(height: 4), + Text( + statusSubtitle, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + if (_connectionStatus == GlassesConnectionStatus.connected) + Column( + children: [ + Icon( + Icons.battery_std, + color: _batteryLevel > 0.2 ? Colors.green : Colors.red, + ), + Text( + '${(_batteryLevel * 100).round()}%', + style: theme.textTheme.labelSmall, + ), + ], + ), + ], + ), + if (_connectionStatus == GlassesConnectionStatus.connected) ...[ + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _disconnect, + icon: const Icon(Icons.bluetooth_disabled), + label: const Text('Disconnect'), + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.errorContainer, + foregroundColor: theme.colorScheme.onErrorContainer, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + onPressed: () { + // TODO: Test HUD display + }, + icon: const Icon(Icons.visibility), + label: const Text('Test Display'), + ), + ), + ], + ), + ], + ], + ), + ), + ); + } + + Widget _buildHUDControlCard(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.display_settings, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'HUD Controls', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + + // HUD Enable/Disable + SwitchListTile( + title: const Text('Enable HUD Display'), + subtitle: const Text('Show information on glasses display'), + value: _isHUDEnabled, + onChanged: (value) { + setState(() { + _isHUDEnabled = value; + }); + }, + ), + + const Divider(), + + // Brightness Control + ListTile( + title: const Text('Display Brightness'), + subtitle: Slider( + value: _brightness, + onChanged: _isHUDEnabled ? (value) { + setState(() { + _brightness = value; + }); + } : null, + divisions: 10, + label: '${(_brightness * 100).round()}%', + ), + ), + + const SizedBox(height: 8), + + // Quick Actions + Wrap( + spacing: 8, + children: [ + ActionChip( + avatar: const Icon(Icons.info, size: 16), + label: const Text('Show Info'), + onPressed: _isHUDEnabled && _connectionStatus == GlassesConnectionStatus.connected + ? _displayDeviceInfo : null, + ), + ActionChip( + avatar: const Icon(Icons.clear, size: 16), + label: const Text('Clear Display'), + onPressed: _isHUDEnabled && _connectionStatus == GlassesConnectionStatus.connected + ? _clearDisplay : null, + ), + ActionChip( + avatar: const Icon(Icons.notifications, size: 16), + label: const Text('Test Alert'), + onPressed: _isHUDEnabled && _connectionStatus == GlassesConnectionStatus.connected + ? _showTestAlert : null, + ), + ], + ), + + const SizedBox(height: 16), + + // Advanced Testing Section + if (_connectionStatus == GlassesConnectionStatus.connected) ...[ + const Divider(), + Text( + 'Even Realities Testing', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + + // Custom Text Input + TextField( + controller: _testTextController, + decoration: const InputDecoration( + labelText: 'Custom Text', + hintText: 'Enter text to display on glasses', + border: OutlineInputBorder(), + ), + maxLines: 2, + ), + const SizedBox(height: 8), + + // Text Display Actions + Wrap( + spacing: 8, + children: [ + ElevatedButton.icon( + onPressed: _displayCustomText, + icon: const Icon(Icons.text_fields, size: 16), + label: const Text('Display Text'), + ), + ElevatedButton.icon( + onPressed: _displayTestBitmap, + icon: const Icon(Icons.image, size: 16), + label: const Text('Test Image'), + ), + ElevatedButton.icon( + onPressed: _displayProgressAnimation, + icon: const Icon(Icons.animation, size: 16), + label: const Text('Animation'), + ), + ], + ), + ], + ], + ), + ), + ); + } + + Widget _buildDeviceInfoCard(ThemeData theme) { + final connectedDevice = _discoveredDevices.firstWhere( + (device) => device.id == _connectedDeviceId, + orElse: () => _discoveredDevices.first, + ); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.info_outline, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Device Information', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + + _buildInfoRow('Device Name', connectedDevice.name), + _buildInfoRow('Device ID', connectedDevice.id), + _buildInfoRow('Signal Strength', '${connectedDevice.rssi} dBm'), + _buildInfoRow('Battery Level', '${(connectedDevice.batteryLevel * 100).round()}%'), + _buildInfoRow('Firmware Version', '1.2.3'), + _buildInfoRow('Connection Type', 'Bluetooth Low Energy'), + _buildInfoRow('Last Sync', _lastSyncTime), + ], + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + Text( + value, + style: const TextStyle(color: Colors.grey), + ), + ], + ), + ); + } + + Widget _buildDeviceDiscoveryCard(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.bluetooth_searching, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Available Devices', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + if (_isScanning) + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(theme.colorScheme.primary), + ), + ) + else + IconButton( + onPressed: _startScanning, + icon: const Icon(Icons.refresh), + tooltip: 'Scan for devices', + ), + ], + ), + const SizedBox(height: 16), + + if (_discoveredDevices.isEmpty && !_isScanning) + Center( + child: Column( + children: [ + Icon( + Icons.bluetooth_disabled, + size: 48, + color: theme.colorScheme.outline, + ), + const SizedBox(height: 16), + Text( + 'No Devices Found', + style: theme.textTheme.titleMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Text( + 'Make sure your glasses are in pairing mode', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: _startScanning, + icon: const Icon(Icons.search), + label: const Text('Scan for Devices'), + ), + ], + ), + ) + else + ...(_discoveredDevices.map((device) => DeviceListTile( + device: device, + onConnect: () => _connectToDevice(device), + ))), + ], + ), + ), + ); + } + + void _showHelpDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Glasses Help'), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Connection Tips:', style: TextStyle(fontWeight: FontWeight.bold)), + SizedBox(height: 8), + Text('• Make sure your glasses are charged'), + Text('• Enable Bluetooth on your device'), + Text('• Place glasses in pairing mode'), + Text('• Keep glasses within 10 feet'), + SizedBox(height: 16), + Text('Troubleshooting:', style: TextStyle(fontWeight: FontWeight.bold)), + SizedBox(height: 8), + Text('• Restart Bluetooth if connection fails'), + Text('• Reset glasses if problems persist'), + Text('• Check for firmware updates'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ), + ); + } + + void _showCalibrationDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Calibrate Display'), + content: const Text( + 'This will guide you through calibrating the HUD display position and brightness for optimal viewing.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // TODO: Start calibration process + }, + child: const Text('Start Calibration'), + ), + ], + ), + ); + } + + void _showResetDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Reset Connection'), + content: const Text( + 'This will disconnect and clear all saved connection data for your glasses. You will need to pair them again.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + _disconnect(); + // TODO: Clear saved connection data + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Reset'), + ), + ], + ), + ); + } +} + +// Helper Models +class DiscoveredDevice { + final String id; + final String name; + final int rssi; + final double batteryLevel; + + DiscoveredDevice({ + required this.id, + required this.name, + required this.rssi, + required this.batteryLevel, + }); +} + +enum GlassesConnectionStatus { + disconnected, + connecting, + connected, +} + +// Custom Widgets +class DeviceListTile extends StatelessWidget { + final DiscoveredDevice device; + final VoidCallback onConnect; + + const DeviceListTile({ + super.key, + required this.device, + required this.onConnect, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + leading: CircleAvatar( + backgroundColor: theme.colorScheme.primaryContainer, + child: Icon( + Icons.remove_red_eye, + color: theme.colorScheme.onPrimaryContainer, + ), + ), + title: Text( + device.name, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Signal: ${device.rssi} dBm'), + Row( + children: [ + Icon( + Icons.battery_std, + size: 16, + color: device.batteryLevel > 0.2 ? Colors.green : Colors.red, + ), + const SizedBox(width: 4), + Text('${(device.batteryLevel * 100).round()}%'), + ], + ), + ], + ), + trailing: ElevatedButton( + onPressed: onConnect, + child: const Text('Connect'), + ), + isThreeLine: true, + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/widgets/history_tab.dart b/lib/ui/widgets/history_tab.dart new file mode 100644 index 0000000..aec63d7 --- /dev/null +++ b/lib/ui/widgets/history_tab.dart @@ -0,0 +1,1272 @@ +// ABOUTME: Enhanced history tab with search, filtering, and export capabilities +// ABOUTME: Comprehensive conversation history management with analytics and insights + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'dart:async'; + +import '../../services/conversation_storage_service.dart'; +import '../../services/service_locator.dart'; +import '../../models/conversation_model.dart'; + +class HistoryTab extends StatefulWidget { + const HistoryTab({super.key}); + + @override + State createState() => _HistoryTabState(); +} + +class _HistoryTabState extends State with TickerProviderStateMixin { + late TabController _tabController; + final TextEditingController _searchController = TextEditingController(); + + String _searchQuery = ''; + ConversationFilter _currentFilter = ConversationFilter.all; + ConversationSort _currentSort = ConversationSort.newest; + bool _isSearching = false; + + // Storage service integration + late ConversationStorageService _storageService; + StreamSubscription>? _conversationSubscription; + List _conversations = []; + + final List _mockConversations = [ + ConversationHistory( + id: 'conv_001', + title: 'Team Meeting Discussion', + date: DateTime.now().subtract(const Duration(hours: 2)), + duration: const Duration(minutes: 45), + participantCount: 4, + transcriptLength: 2847, + summary: 'Discussion about Q4 planning, budget allocation, and upcoming product launches.', + tags: ['meeting', 'planning', 'business'], + sentiment: SentimentType.positive, + hasFactChecks: true, + hasActionItems: true, + isStarred: true, + ), + ConversationHistory( + id: 'conv_002', + title: 'Technical Architecture Review', + date: DateTime.now().subtract(const Duration(days: 1)), + duration: const Duration(minutes: 67), + participantCount: 3, + transcriptLength: 4192, + summary: 'Deep dive into system architecture, performance optimization, and scalability concerns.', + tags: ['technical', 'architecture', 'performance'], + sentiment: SentimentType.neutral, + hasFactChecks: true, + hasActionItems: false, + isStarred: false, + ), + ConversationHistory( + id: 'conv_003', + title: 'Client Feedback Session', + date: DateTime.now().subtract(const Duration(days: 3)), + duration: const Duration(minutes: 32), + participantCount: 2, + transcriptLength: 1654, + summary: 'Client expressed concerns about delivery timeline and feature completeness.', + tags: ['client', 'feedback', 'concerns'], + sentiment: SentimentType.negative, + hasFactChecks: false, + hasActionItems: true, + isStarred: false, + ), + ConversationHistory( + id: 'conv_004', + title: 'Innovation Brainstorm', + date: DateTime.now().subtract(const Duration(days: 5)), + duration: const Duration(minutes: 89), + participantCount: 6, + transcriptLength: 5234, + summary: 'Creative session exploring new features, market opportunities, and technology trends.', + tags: ['innovation', 'brainstorm', 'creative'], + sentiment: SentimentType.positive, + hasFactChecks: false, + hasActionItems: true, + isStarred: true, + ), + ]; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _searchController.addListener(_onSearchChanged); + _initializeStorageService(); + } + + Future _initializeStorageService() async { + try { + _storageService = ServiceLocator.instance.get(); + + // Load existing conversations + final conversations = await _storageService.getAllConversations(); + setState(() { + _conversations = conversations; + }); + + // Listen for conversation updates + _conversationSubscription = _storageService.conversationStream.listen((conversations) { + if (mounted) { + setState(() { + _conversations = conversations; + }); + } + }); + } catch (e) { + debugPrint('Failed to initialize storage service: $e'); + } + } + + @override + void dispose() { + _tabController.dispose(); + _searchController.dispose(); + _conversationSubscription?.cancel(); + super.dispose(); + } + + void _onSearchChanged() { + setState(() { + _searchQuery = _searchController.text; + }); + } + + List get _filteredConversations { + var filtered = _conversations.where((conv) { + // Search filter + if (_searchQuery.isNotEmpty) { + final query = _searchQuery.toLowerCase(); + if (!conv.title.toLowerCase().contains(query)) { + // Also search in conversation segments + final hasMatchingSegment = conv.segments.any((segment) => + segment.text.toLowerCase().contains(query)); + if (!hasMatchingSegment) { + return false; + } + } + } + + // Category filter + switch (_currentFilter) { + case ConversationFilter.starred: + return conv.isPinned; // Use isPinned as starred + case ConversationFilter.withFactChecks: + return conv.hasAIAnalysis; // Use hasAIAnalysis as fact checks + case ConversationFilter.withActions: + return false; // No action items in ConversationModel yet + case ConversationFilter.thisWeek: + return conv.startTime.isAfter(DateTime.now().subtract(const Duration(days: 7))); + case ConversationFilter.all: + default: + return true; + } + }).toList(); + + // Sort + switch (_currentSort) { + case ConversationSort.newest: + filtered.sort((a, b) => b.startTime.compareTo(a.startTime)); + break; + case ConversationSort.oldest: + filtered.sort((a, b) => a.startTime.compareTo(b.startTime)); + break; + case ConversationSort.longest: + filtered.sort((a, b) => b.duration.compareTo(a.duration)); + break; + case ConversationSort.mostParticipants: + filtered.sort((a, b) => b.participants.length.compareTo(a.participants.length)); + break; + } + + return filtered; + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: _isSearching + ? TextField( + controller: _searchController, + autofocus: true, + decoration: const InputDecoration( + hintText: 'Search conversations...', + border: InputBorder.none, + ), + style: theme.textTheme.titleLarge, + ) + : const Text('Conversation History'), + backgroundColor: theme.colorScheme.surface, + elevation: 0, + actions: [ + IconButton( + icon: Icon(_isSearching ? Icons.close : Icons.search), + onPressed: () { + setState(() { + _isSearching = !_isSearching; + if (!_isSearching) { + _searchController.clear(); + } + }); + }, + ), + if (!_isSearching) + PopupMenuButton( + onSelected: (value) { + switch (value) { + case 'export_all': + _showExportDialog(context); + break; + case 'analytics': + _showAnalyticsDialog(context); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'export_all', + child: Row( + children: [ + Icon(Icons.download), + SizedBox(width: 8), + Text('Export All'), + ], + ), + ), + const PopupMenuItem( + value: 'analytics', + child: Row( + children: [ + Icon(Icons.analytics), + SizedBox(width: 8), + Text('View Analytics'), + ], + ), + ), + ], + ), + ], + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(icon: Icon(Icons.list), text: 'Conversations'), + Tab(icon: Icon(Icons.insights), text: 'Insights'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildConversationsTab(theme), + _buildInsightsTab(theme), + ], + ), + ); + } + + Widget _buildConversationsTab(ThemeData theme) { + return Column( + children: [ + // Filter and Sort Controls + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outline.withOpacity(0.2), + ), + ), + ), + child: Row( + children: [ + // Filter + Expanded( + child: DropdownButtonFormField( + value: _currentFilter, + decoration: const InputDecoration( + labelText: 'Filter', + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + items: ConversationFilter.values.map((filter) { + return DropdownMenuItem( + value: filter, + child: Text(_getFilterLabel(filter)), + ); + }).toList(), + onChanged: (value) { + setState(() { + _currentFilter = value!; + }); + }, + ), + ), + const SizedBox(width: 12), + // Sort + Expanded( + child: DropdownButtonFormField( + value: _currentSort, + decoration: const InputDecoration( + labelText: 'Sort By', + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + items: ConversationSort.values.map((sort) { + return DropdownMenuItem( + value: sort, + child: Text(_getSortLabel(sort)), + ); + }).toList(), + onChanged: (value) { + setState(() { + _currentSort = value!; + }); + }, + ), + ), + ], + ), + ), + + // Conversations List + Expanded( + child: _filteredConversations.isEmpty + ? _buildEmptyState(theme) + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _filteredConversations.length, + itemBuilder: (context, index) { + final conversation = _filteredConversations[index]; + return ConversationCard( + conversation: conversation, + onTap: () => _openConversationDetail(conversation), + onStar: () => _toggleStar(conversation), + onShare: () => _shareConversation(conversation), + onDelete: () => _deleteConversation(conversation), + ); + }, + ), + ), + ], + ); + } + + Widget _buildInsightsTab(ThemeData theme) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStatsCards(theme), + const SizedBox(height: 16), + _buildTrendChart(theme), + const SizedBox(height: 16), + _buildTopicsCard(theme), + const SizedBox(height: 16), + _buildSentimentCard(theme), + ], + ), + ); + } + + Widget _buildEmptyState(ThemeData theme) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _searchQuery.isNotEmpty ? Icons.search_off : Icons.history, + size: 64, + color: theme.colorScheme.outline, + ), + const SizedBox(height: 24), + Text( + _searchQuery.isNotEmpty ? 'No Results Found' : 'No Conversations Yet', + style: theme.textTheme.headlineSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Text( + _searchQuery.isNotEmpty + ? 'Try adjusting your search terms or filters' + : 'Start a conversation to see it here', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + if (_searchQuery.isNotEmpty) ...[ + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + _searchController.clear(); + setState(() { + _currentFilter = ConversationFilter.all; + }); + }, + child: const Text('Clear Search'), + ), + ], + ], + ), + ); + } + + Widget _buildStatsCards(ThemeData theme) { + return Row( + children: [ + Expanded( + child: _buildStatCard( + theme, + 'Total Conversations', + '${_conversations.length}', + Icons.chat_bubble_outline, + theme.colorScheme.primary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + theme, + 'Total Duration', + _formatTotalDuration(), + Icons.schedule, + Colors.green, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + theme, + 'Avg Participants', + _getAverageParticipants(), + Icons.group, + Colors.orange, + ), + ), + ], + ); + } + + Widget _buildStatCard(ThemeData theme, String label, String value, IconData icon, Color color) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Icon(icon, color: color, size: 24), + const SizedBox(height: 8), + Text( + value, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: color, + ), + ), + Text( + label, + style: theme.textTheme.labelSmall, + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + Widget _buildTrendChart(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.trending_up, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Activity Trend', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + height: 100, + child: Center( + child: Text( + 'Trend visualization would go here', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTopicsCard(ThemeData theme) { + final allTags = {}; + for (final conv in _conversations) { + allTags.addAll(conv.tags); + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.tag, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Popular Topics', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: allTags.map((tag) => Chip( + label: Text(tag), + backgroundColor: theme.colorScheme.secondaryContainer, + )).toList(), + ), + ], + ), + ), + ); + } + + Widget _buildSentimentCard(ThemeData theme) { + final sentimentCounts = {}; + for (final conv in _conversations) { + // Default to neutral sentiment for ConversationModel since it doesn't have sentiment + sentimentCounts[SentimentType.neutral] = (sentimentCounts[SentimentType.neutral] ?? 0) + 1; + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.sentiment_satisfied, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Sentiment Distribution', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + ...sentimentCounts.entries.map((entry) { + final percentage = (entry.value / _conversations.length * 100).round(); + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Icon( + _getSentimentIcon(entry.key), + color: _getSentimentColor(entry.key), + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + entry.key.name.toUpperCase(), + style: theme.textTheme.labelMedium, + ), + ), + Text( + '$percentage%', + style: theme.textTheme.labelMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + }), + ], + ), + ), + ); + } + + String _getFilterLabel(ConversationFilter filter) { + switch (filter) { + case ConversationFilter.all: + return 'All Conversations'; + case ConversationFilter.starred: + return 'Starred'; + case ConversationFilter.withFactChecks: + return 'With Fact Checks'; + case ConversationFilter.withActions: + return 'With Action Items'; + case ConversationFilter.thisWeek: + return 'This Week'; + } + } + + String _getSortLabel(ConversationSort sort) { + switch (sort) { + case ConversationSort.newest: + return 'Newest First'; + case ConversationSort.oldest: + return 'Oldest First'; + case ConversationSort.longest: + return 'Longest First'; + case ConversationSort.mostParticipants: + return 'Most Participants'; + } + } + + String _formatTotalDuration() { + final totalMinutes = _conversations.fold( + 0, (sum, conv) => sum + conv.duration.inMinutes, + ); + final hours = totalMinutes ~/ 60; + final minutes = totalMinutes % 60; + return '${hours}h ${minutes}m'; + } + + String _getAverageParticipants() { + if (_conversations.isEmpty) return '0'; + final avg = _conversations.fold( + 0, (sum, conv) => sum + conv.participants.length, + ) / _conversations.length; + return avg.toStringAsFixed(1); + } + + IconData _getSentimentIcon(SentimentType sentiment) { + switch (sentiment) { + case SentimentType.positive: + return Icons.sentiment_very_satisfied; + case SentimentType.negative: + return Icons.sentiment_very_dissatisfied; + case SentimentType.neutral: + return Icons.sentiment_neutral; + case SentimentType.mixed: + return Icons.sentiment_satisfied; + } + } + + Color _getSentimentColor(SentimentType sentiment) { + switch (sentiment) { + case SentimentType.positive: + return Colors.green; + case SentimentType.negative: + return Colors.red; + case SentimentType.neutral: + return Colors.grey; + case SentimentType.mixed: + return Colors.orange; + } + } + + void _openConversationDetail(ConversationModel conversation) { + // TODO: Navigate to conversation detail page + } + + void _toggleStar(ConversationModel conversation) async { + try { + final updatedConversation = conversation.copyWith(isPinned: !conversation.isPinned); + await _storageService.saveConversation(updatedConversation); + // The conversation stream will automatically update the UI + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update conversation: $e')), + ); + } + } + } + + void _shareConversation(ConversationModel conversation) { + // TODO: Implement share functionality + } + + void _deleteConversation(ConversationModel conversation) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Conversation'), + content: Text('Are you sure you want to delete "${conversation.title}"? This action cannot be undone.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () async { + try { + await _storageService.deleteConversation(conversation.id); + Navigator.of(context).pop(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Conversation deleted')), + ); + } + } catch (e) { + Navigator.of(context).pop(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to delete conversation: $e')), + ); + } + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Delete'), + ), + ], + ), + ); + } + + void _showExportDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Export Conversations'), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Choose export format:'), + SizedBox(height: 16), + ListTile( + leading: Icon(Icons.text_snippet), + title: Text('Plain Text'), + subtitle: Text('Simple text format'), + ), + ListTile( + leading: Icon(Icons.table_chart), + title: Text('CSV'), + subtitle: Text('Spreadsheet compatible'), + ), + ListTile( + leading: Icon(Icons.code), + title: Text('JSON'), + subtitle: Text('Machine readable format'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // TODO: Implement export functionality + }, + child: const Text('Export'), + ), + ], + ), + ); + } + + void _showAnalyticsDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => const AlertDialog( + title: Text('Detailed Analytics'), + content: Text('Advanced analytics dashboard would be implemented here with charts and detailed metrics.'), + ), + ); + } +} + +// Helper Models +class ConversationHistory { + final String id; + final String title; + final DateTime date; + final Duration duration; + final int participantCount; + final int transcriptLength; + final String summary; + final List tags; + final SentimentType sentiment; + final bool hasFactChecks; + final bool hasActionItems; + final bool isStarred; + + ConversationHistory({ + required this.id, + required this.title, + required this.date, + required this.duration, + required this.participantCount, + required this.transcriptLength, + required this.summary, + required this.tags, + required this.sentiment, + required this.hasFactChecks, + required this.hasActionItems, + required this.isStarred, + }); + + ConversationHistory copyWith({ + String? id, + String? title, + DateTime? date, + Duration? duration, + int? participantCount, + int? transcriptLength, + String? summary, + List? tags, + SentimentType? sentiment, + bool? hasFactChecks, + bool? hasActionItems, + bool? isStarred, + }) { + return ConversationHistory( + id: id ?? this.id, + title: title ?? this.title, + date: date ?? this.date, + duration: duration ?? this.duration, + participantCount: participantCount ?? this.participantCount, + transcriptLength: transcriptLength ?? this.transcriptLength, + summary: summary ?? this.summary, + tags: tags ?? this.tags, + sentiment: sentiment ?? this.sentiment, + hasFactChecks: hasFactChecks ?? this.hasFactChecks, + hasActionItems: hasActionItems ?? this.hasActionItems, + isStarred: isStarred ?? this.isStarred, + ); + } +} + +enum SentimentType { positive, negative, neutral, mixed } +enum ConversationFilter { all, starred, withFactChecks, withActions, thisWeek } +enum ConversationSort { newest, oldest, longest, mostParticipants } + +// Custom Widgets +class ConversationCard extends StatelessWidget { + final ConversationModel conversation; + final VoidCallback onTap; + final VoidCallback onStar; + final VoidCallback onShare; + final VoidCallback onDelete; + + const ConversationCard({ + super.key, + required this.conversation, + required this.onTap, + required this.onStar, + required this.onShare, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Card( + margin: const EdgeInsets.only(bottom: 12), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + conversation.title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + IconButton( + onPressed: onStar, + icon: Icon( + conversation.isPinned ? Icons.star : Icons.star_border, + color: conversation.isPinned ? Colors.amber : null, + ), + ), + PopupMenuButton( + onSelected: (value) { + switch (value) { + case 'share': + onShare(); + break; + case 'delete': + onDelete(); + break; + } + }, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'share', + child: Row( + children: [ + Icon(Icons.share), + SizedBox(width: 8), + Text('Share'), + ], + ), + ), + const PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon(Icons.delete, color: Colors.red), + SizedBox(width: 8), + Text('Delete', style: TextStyle(color: Colors.red)), + ], + ), + ), + ], + ), + ], + ), + const SizedBox(height: 8), + Text( + conversation.description ?? + (conversation.segments.isNotEmpty + ? conversation.segments.take(2).map((s) => s.text).join(' ').length > 100 + ? '${conversation.segments.take(2).map((s) => s.text).join(' ').substring(0, 100)}...' + : conversation.segments.take(2).map((s) => s.text).join(' ') + : 'No content available'), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 12), + + // Tags + if (conversation.tags.isNotEmpty) + Wrap( + spacing: 6, + runSpacing: 4, + children: conversation.tags.take(3).map((tag) => Chip( + label: Text(tag), + backgroundColor: theme.colorScheme.surfaceVariant, + labelStyle: theme.textTheme.labelSmall, + visualDensity: VisualDensity.compact, + )).toList(), + ), + + const SizedBox(height: 12), + + // Metadata + Row( + children: [ + Icon( + Icons.schedule, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + DateFormat('MMM d, h:mm a').format(conversation.startTime), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: 16), + Icon( + Icons.timer, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + '${conversation.duration.inMinutes}m', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: 16), + Icon( + Icons.people, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + '${conversation.participants.length}', + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const Spacer(), + + // Features + if (conversation.hasAIAnalysis) + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'AI', + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.green, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + + // Audio Playback Controls (if audio file exists) + if (conversation.audioFilePath != null) ...[ + const SizedBox(height: 12), + AudioPlaybackControls( + audioFilePath: conversation.audioFilePath!, + duration: conversation.duration, + ), + ], + ], + ), + ), + ), + ); + } +} + +class AudioPlaybackControls extends StatefulWidget { + final String audioFilePath; + final Duration duration; + + const AudioPlaybackControls({ + super.key, + required this.audioFilePath, + required this.duration, + }); + + @override + State createState() => _AudioPlaybackControlsState(); +} + +class _AudioPlaybackControlsState extends State { + bool _isPlaying = false; + bool _isLoading = false; + Duration _currentPosition = Duration.zero; + String? _errorMessage; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.3), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: theme.colorScheme.outline.withOpacity(0.2), + ), + ), + child: Column( + children: [ + // Error message if any + if (_errorMessage != null) ...[ + Row( + children: [ + Icon(Icons.error_outline, size: 16, color: theme.colorScheme.error), + const SizedBox(width: 8), + Expanded( + child: Text( + _errorMessage!, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.error, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + ], + + // Audio controls + Row( + children: [ + // Play/Pause button + _isLoading + ? SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : IconButton( + onPressed: _togglePlayback, + icon: Icon( + _isPlaying ? Icons.pause : Icons.play_arrow, + size: 24, + ), + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + minimumSize: const Size(32, 32), + padding: EdgeInsets.zero, + ), + ), + + const SizedBox(width: 12), + + // Progress indicator + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Progress bar + LinearProgressIndicator( + value: widget.duration.inMilliseconds > 0 + ? _currentPosition.inMilliseconds / widget.duration.inMilliseconds + : 0.0, + backgroundColor: theme.colorScheme.outline.withOpacity(0.2), + valueColor: AlwaysStoppedAnimation(theme.colorScheme.primary), + ), + const SizedBox(height: 4), + + // Time display + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(_currentPosition), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + Text( + _formatDuration(widget.duration), + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(width: 8), + + // Audio file info + Icon( + Icons.audiotrack, + size: 16, + color: theme.colorScheme.onSurfaceVariant, + ), + ], + ), + ], + ), + ); + } + + void _togglePlayback() async { + if (_errorMessage != null) { + setState(() { + _errorMessage = null; + }); + } + + setState(() { + _isLoading = true; + }); + + try { + // For now, just simulate playback since we need a proper audio player service + // In a real implementation, you'd use flutter_sound player or similar + await Future.delayed(const Duration(milliseconds: 500)); + + setState(() { + _isPlaying = !_isPlaying; + _isLoading = false; + }); + + // Simulate progress updates + if (_isPlaying) { + _startProgressSimulation(); + } + } catch (e) { + setState(() { + _isLoading = false; + _errorMessage = 'Could not play audio: ${e.toString()}'; + }); + } + } + + void _startProgressSimulation() { + if (!_isPlaying) return; + + Future.delayed(const Duration(milliseconds: 100), () { + if (_isPlaying && mounted) { + setState(() { + _currentPosition = Duration( + milliseconds: (_currentPosition.inMilliseconds + 100).clamp( + 0, + widget.duration.inMilliseconds, + ), + ); + }); + + if (_currentPosition < widget.duration) { + _startProgressSimulation(); + } else { + // Playback finished + setState(() { + _isPlaying = false; + _currentPosition = Duration.zero; + }); + } + } + }); + } + + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final minutes = twoDigits(duration.inMinutes); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + return '$minutes:$seconds'; + } + + @override + void dispose() { + _isPlaying = false; + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/ui/widgets/settings_tab.dart b/lib/ui/widgets/settings_tab.dart new file mode 100644 index 0000000..c32568c --- /dev/null +++ b/lib/ui/widgets/settings_tab.dart @@ -0,0 +1,899 @@ +// ABOUTME: Comprehensive settings interface with categorized options +// ABOUTME: Full-featured settings management for API keys, audio, AI, privacy, and app preferences + +import 'package:flutter/material.dart'; + +class SettingsTab extends StatefulWidget { + const SettingsTab({super.key}); + + @override + State createState() => _SettingsTabState(); +} + +class _SettingsTabState extends State { + // Theme Settings + bool _isDarkMode = false; + bool _useSystemTheme = true; + + // AI Settings + String _currentLLMProvider = 'openai'; + double _analysisConfidenceThreshold = 0.8; + bool _enableFactChecking = true; + bool _enableSentimentAnalysis = true; + bool _enableActionItemExtraction = true; + + // Audio Settings + double _audioQuality = 1.0; // 0.0 = low, 0.5 = medium, 1.0 = high + bool _enableNoiseReduction = true; + bool _enableAutoGainControl = true; + double _microphoneSensitivity = 0.7; + + // Privacy Settings + bool _enableDataCollection = false; + bool _enableCrashReporting = true; + bool _enableUsageAnalytics = false; + String _dataRetentionPeriod = '30 days'; + + // Glasses Settings + double _hudBrightness = 0.7; + String _hudPosition = 'center'; + bool _enableHapticFeedback = true; + bool _enableAudioAlerts = false; + + // Notification Settings + bool _enablePushNotifications = true; + bool _enableFactCheckAlerts = true; + bool _enableActionItemReminders = true; + + final TextEditingController _openaiKeyController = TextEditingController(); + final TextEditingController _anthropicKeyController = TextEditingController(); + + @override + void dispose() { + _openaiKeyController.dispose(); + _anthropicKeyController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Settings'), + backgroundColor: theme.colorScheme.surface, + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.restore), + onPressed: _showResetDialog, + tooltip: 'Reset to defaults', + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildAISettingsCard(theme), + const SizedBox(height: 16), + _buildAudioSettingsCard(theme), + const SizedBox(height: 16), + _buildGlassesSettingsCard(theme), + const SizedBox(height: 16), + _buildPrivacySettingsCard(theme), + const SizedBox(height: 16), + _buildNotificationSettingsCard(theme), + const SizedBox(height: 16), + _buildAppearanceSettingsCard(theme), + const SizedBox(height: 16), + _buildAboutCard(theme), + ], + ), + ); + } + + Widget _buildAISettingsCard(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.psychology, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'AI & Analysis', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + + // API Keys Section + Text( + 'API Configuration', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + + // OpenAI API Key + TextField( + controller: _openaiKeyController, + decoration: InputDecoration( + labelText: 'OpenAI API Key', + hintText: 'sk-...', + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.help_outline), + onPressed: () => _showAPIKeyHelp('OpenAI'), + ), + ), + obscureText: true, + ), + const SizedBox(height: 12), + + // Anthropic API Key + TextField( + controller: _anthropicKeyController, + decoration: InputDecoration( + labelText: 'Anthropic API Key', + hintText: 'sk-ant-...', + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.help_outline), + onPressed: () => _showAPIKeyHelp('Anthropic'), + ), + ), + obscureText: true, + ), + const SizedBox(height: 16), + + // LLM Provider Selection + DropdownButtonFormField( + value: _currentLLMProvider, + decoration: const InputDecoration( + labelText: 'Default AI Provider', + border: OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: 'openai', child: Text('OpenAI GPT')), + DropdownMenuItem(value: 'anthropic', child: Text('Anthropic AI')), + DropdownMenuItem(value: 'auto', child: Text('Auto Select')), + ], + onChanged: (value) { + setState(() { + _currentLLMProvider = value!; + }); + }, + ), + const SizedBox(height: 16), + + // Analysis Features + Text( + 'Analysis Features', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + + SwitchListTile( + title: const Text('Fact Checking'), + subtitle: const Text('Real-time claim verification'), + value: _enableFactChecking, + onChanged: (value) { + setState(() { + _enableFactChecking = value; + }); + }, + ), + + SwitchListTile( + title: const Text('Sentiment Analysis'), + subtitle: const Text('Conversation mood detection'), + value: _enableSentimentAnalysis, + onChanged: (value) { + setState(() { + _enableSentimentAnalysis = value; + }); + }, + ), + + SwitchListTile( + title: const Text('Action Item Extraction'), + subtitle: const Text('Automatic task identification'), + value: _enableActionItemExtraction, + onChanged: (value) { + setState(() { + _enableActionItemExtraction = value; + }); + }, + ), + + // Confidence Threshold + ListTile( + title: const Text('Analysis Confidence Threshold'), + subtitle: Text('${(_analysisConfidenceThreshold * 100).round()}% minimum confidence'), + ), + Slider( + value: _analysisConfidenceThreshold, + min: 0.5, + max: 1.0, + divisions: 10, + label: '${(_analysisConfidenceThreshold * 100).round()}%', + onChanged: (value) { + setState(() { + _analysisConfidenceThreshold = value; + }); + }, + ), + ], + ), + ), + ); + } + + Widget _buildAudioSettingsCard(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.mic, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Audio Recording', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Audio Quality + ListTile( + title: const Text('Recording Quality'), + subtitle: Text(_getAudioQualityLabel(_audioQuality)), + ), + Slider( + value: _audioQuality, + min: 0.0, + max: 1.0, + divisions: 2, + label: _getAudioQualityLabel(_audioQuality), + onChanged: (value) { + setState(() { + _audioQuality = value; + }); + }, + ), + + // Microphone Sensitivity + ListTile( + title: const Text('Microphone Sensitivity'), + subtitle: Text('${(_microphoneSensitivity * 100).round()}%'), + ), + Slider( + value: _microphoneSensitivity, + min: 0.1, + max: 1.0, + divisions: 9, + label: '${(_microphoneSensitivity * 100).round()}%', + onChanged: (value) { + setState(() { + _microphoneSensitivity = value; + }); + }, + ), + + SwitchListTile( + title: const Text('Noise Reduction'), + subtitle: const Text('Filter background noise'), + value: _enableNoiseReduction, + onChanged: (value) { + setState(() { + _enableNoiseReduction = value; + }); + }, + ), + + SwitchListTile( + title: const Text('Auto Gain Control'), + subtitle: const Text('Automatic volume adjustment'), + value: _enableAutoGainControl, + onChanged: (value) { + setState(() { + _enableAutoGainControl = value; + }); + }, + ), + ], + ), + ), + ); + } + + Widget _buildGlassesSettingsCard(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.remove_red_eye, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Smart Glasses', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + + // HUD Brightness + ListTile( + title: const Text('HUD Brightness'), + subtitle: Text('${(_hudBrightness * 100).round()}%'), + ), + Slider( + value: _hudBrightness, + min: 0.1, + max: 1.0, + divisions: 9, + label: '${(_hudBrightness * 100).round()}%', + onChanged: (value) { + setState(() { + _hudBrightness = value; + }); + }, + ), + + // HUD Position + DropdownButtonFormField( + value: _hudPosition, + decoration: const InputDecoration( + labelText: 'HUD Position', + border: OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: 'top', child: Text('Top')), + DropdownMenuItem(value: 'center', child: Text('Center')), + DropdownMenuItem(value: 'bottom', child: Text('Bottom')), + ], + onChanged: (value) { + setState(() { + _hudPosition = value!; + }); + }, + ), + const SizedBox(height: 16), + + SwitchListTile( + title: const Text('Haptic Feedback'), + subtitle: const Text('Vibration for notifications'), + value: _enableHapticFeedback, + onChanged: (value) { + setState(() { + _enableHapticFeedback = value; + }); + }, + ), + + SwitchListTile( + title: const Text('Audio Alerts'), + subtitle: const Text('Sound notifications'), + value: _enableAudioAlerts, + onChanged: (value) { + setState(() { + _enableAudioAlerts = value; + }); + }, + ), + ], + ), + ), + ); + } + + Widget _buildPrivacySettingsCard(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.privacy_tip, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Privacy & Data', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + + SwitchListTile( + title: const Text('Data Collection'), + subtitle: const Text('Allow anonymous usage data collection'), + value: _enableDataCollection, + onChanged: (value) { + setState(() { + _enableDataCollection = value; + }); + }, + ), + + SwitchListTile( + title: const Text('Crash Reporting'), + subtitle: const Text('Help improve app stability'), + value: _enableCrashReporting, + onChanged: (value) { + setState(() { + _enableCrashReporting = value; + }); + }, + ), + + SwitchListTile( + title: const Text('Usage Analytics'), + subtitle: const Text('Anonymous feature usage tracking'), + value: _enableUsageAnalytics, + onChanged: (value) { + setState(() { + _enableUsageAnalytics = value; + }); + }, + ), + + // Data Retention + DropdownButtonFormField( + value: _dataRetentionPeriod, + decoration: const InputDecoration( + labelText: 'Data Retention Period', + border: OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: '7 days', child: Text('7 days')), + DropdownMenuItem(value: '30 days', child: Text('30 days')), + DropdownMenuItem(value: '90 days', child: Text('90 days')), + DropdownMenuItem(value: '1 year', child: Text('1 year')), + DropdownMenuItem(value: 'forever', child: Text('Keep forever')), + ], + onChanged: (value) { + setState(() { + _dataRetentionPeriod = value!; + }); + }, + ), + const SizedBox(height: 16), + + Center( + child: TextButton( + onPressed: _showPrivacyPolicy, + child: const Text('View Privacy Policy'), + ), + ), + ], + ), + ), + ); + } + + Widget _buildNotificationSettingsCard(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.notifications, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Notifications', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + + SwitchListTile( + title: const Text('Push Notifications'), + subtitle: const Text('General app notifications'), + value: _enablePushNotifications, + onChanged: (value) { + setState(() { + _enablePushNotifications = value; + }); + }, + ), + + SwitchListTile( + title: const Text('Fact Check Alerts'), + subtitle: const Text('Notifications for disputed claims'), + value: _enableFactCheckAlerts, + onChanged: _enablePushNotifications ? (value) { + setState(() { + _enableFactCheckAlerts = value; + }); + } : null, + ), + + SwitchListTile( + title: const Text('Action Item Reminders'), + subtitle: const Text('Reminders for pending tasks'), + value: _enableActionItemReminders, + onChanged: _enablePushNotifications ? (value) { + setState(() { + _enableActionItemReminders = value; + }); + } : null, + ), + ], + ), + ), + ); + } + + Widget _buildAppearanceSettingsCard(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.palette, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Appearance', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + + SwitchListTile( + title: const Text('Use System Theme'), + subtitle: const Text('Follow device theme settings'), + value: _useSystemTheme, + onChanged: (value) { + setState(() { + _useSystemTheme = value; + }); + }, + ), + + SwitchListTile( + title: const Text('Dark Mode'), + subtitle: const Text('Use dark theme'), + value: _isDarkMode, + onChanged: _useSystemTheme ? null : (value) { + setState(() { + _isDarkMode = value; + }); + }, + ), + ], + ), + ), + ); + } + + Widget _buildAboutCard(ThemeData theme) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.info, color: theme.colorScheme.primary), + const SizedBox(width: 8), + Text( + 'About', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 16), + + ListTile( + title: const Text('Version'), + subtitle: const Text('1.0.0 (Build 1)'), + trailing: const Icon(Icons.info_outline), + onTap: _showAboutDialog, + ), + + ListTile( + title: const Text('Licenses'), + subtitle: const Text('Open source licenses'), + trailing: const Icon(Icons.article), + onTap: _showLicensePage, + ), + + ListTile( + title: const Text('Help & Support'), + subtitle: const Text('Get help and support'), + trailing: const Icon(Icons.help), + onTap: _showHelpDialog, + ), + + ListTile( + title: const Text('Feedback'), + subtitle: const Text('Send feedback and suggestions'), + trailing: const Icon(Icons.feedback), + onTap: _showFeedbackDialog, + ), + ], + ), + ), + ); + } + + String _getAudioQualityLabel(double quality) { + if (quality <= 0.33) return 'Low (8kHz)'; + if (quality <= 0.66) return 'Medium (16kHz)'; + return 'High (44.1kHz)'; + } + + void _showAPIKeyHelp(String provider) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('$provider API Key'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('To use $provider services, you need an API key:'), + const SizedBox(height: 12), + if (provider == 'OpenAI') ...[ + const Text('• Visit https://platform.openai.com'), + const Text('• Create an account or sign in'), + const Text('• Go to API Keys section'), + const Text('• Create a new secret key'), + ] else ...[ + const Text('• Visit https://console.anthropic.com'), + const Text('• Create an account or sign in'), + const Text('• Go to API Keys section'), + const Text('• Generate a new API key'), + ], + const SizedBox(height: 12), + const Text( + 'Your API key is stored securely on your device and never shared.', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ), + ); + } + + void _showResetDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Reset to Defaults'), + content: const Text( + 'This will reset all settings to their default values. Your API keys will be cleared. This action cannot be undone.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + _resetToDefaults(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Reset'), + ), + ], + ), + ); + } + + void _resetToDefaults() { + setState(() { + _isDarkMode = false; + _useSystemTheme = true; + _currentLLMProvider = 'openai'; + _analysisConfidenceThreshold = 0.8; + _enableFactChecking = true; + _enableSentimentAnalysis = true; + _enableActionItemExtraction = true; + _audioQuality = 1.0; + _enableNoiseReduction = true; + _enableAutoGainControl = true; + _microphoneSensitivity = 0.7; + _enableDataCollection = false; + _enableCrashReporting = true; + _enableUsageAnalytics = false; + _dataRetentionPeriod = '30 days'; + _hudBrightness = 0.7; + _hudPosition = 'center'; + _enableHapticFeedback = true; + _enableAudioAlerts = false; + _enablePushNotifications = true; + _enableFactCheckAlerts = true; + _enableActionItemReminders = true; + }); + + _openaiKeyController.clear(); + _anthropicKeyController.clear(); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Settings reset to defaults'), + ), + ); + } + + void _showAboutDialog() { + showAboutDialog( + context: context, + applicationName: 'Helix', + applicationVersion: '1.0.0', + applicationLegalese: 'AI-Powered Conversation Intelligence for smart glasses.', + children: [ + const SizedBox(height: 16), + const Text( + 'Helix transforms conversations into actionable insights using advanced AI analysis, real-time fact-checking, and seamless integration with Even Realities smart glasses.', + ), + ], + ); + } + + void _showLicensePage() { + showLicensePage( + context: context, + applicationName: 'Helix', + applicationVersion: '1.0.0', + applicationLegalese: 'AI-Powered Conversation Intelligence for smart glasses.', + ); + } + + void _showPrivacyPolicy() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Privacy Policy'), + content: const SingleChildScrollView( + child: Text( + 'Helix Privacy Policy\n\n' + 'Data Collection:\n' + 'We collect only the data necessary to provide our services. Audio recordings are processed locally when possible and are never stored without your explicit consent.\n\n' + 'AI Processing:\n' + 'Conversation data may be sent to AI providers (OpenAI, Anthropic) for analysis. These services have their own privacy policies.\n\n' + 'Data Storage:\n' + 'Your data is stored securely on your device. Cloud sync is optional and encrypted.\n\n' + 'For the complete privacy policy, visit: https://helix.example.com/privacy', + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ), + ); + } + + void _showHelpDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Help & Support'), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Getting Started:', style: TextStyle(fontWeight: FontWeight.bold)), + SizedBox(height: 8), + Text('• Add your AI provider API keys in the AI settings'), + Text('• Connect your Even Realities smart glasses'), + Text('• Start a conversation to see real-time analysis'), + SizedBox(height: 16), + Text('Troubleshooting:', style: TextStyle(fontWeight: FontWeight.bold)), + SizedBox(height: 8), + Text('• Check microphone permissions'), + Text('• Ensure Bluetooth is enabled for glasses'), + Text('• Verify your API keys are valid'), + SizedBox(height: 16), + Text('Contact: support@helix.example.com'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close'), + ), + ], + ), + ); + } + + void _showFeedbackDialog() { + final feedbackController = TextEditingController(); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Send Feedback'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('We love hearing from you! Share your thoughts, suggestions, or report issues.'), + const SizedBox(height: 16), + TextField( + controller: feedbackController, + decoration: const InputDecoration( + labelText: 'Your feedback', + border: OutlineInputBorder(), + ), + maxLines: 3, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // TODO: Send feedback + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Thank you for your feedback!'), + ), + ); + }, + child: const Text('Send'), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/libs/EvenDemoApp b/libs/EvenDemoApp deleted file mode 160000 index 9fbd4ee..0000000 --- a/libs/EvenDemoApp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9fbd4ee95445bee6b8be6d58c724fccca29c59ee diff --git a/libs/even_glasses b/libs/even_glasses deleted file mode 160000 index b3fac76..0000000 --- a/libs/even_glasses +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b3fac76fd9b81635cb5f5246fa6ee80538221fb5 diff --git a/libs/g1_flutter_blue_plus b/libs/g1_flutter_blue_plus deleted file mode 160000 index f79be30..0000000 --- a/libs/g1_flutter_blue_plus +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f79be30dbac6ba01b3cbcc28bf49f49a78da2f04 diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..c45f350 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_helix") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.evenrealities.flutter_helix") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..5af16a5 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,130 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "flutter_helix"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "flutter_helix"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..dc3c866 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audio_session +import flutter_blue_plus_darwin +import path_provider_foundation +import shared_preferences_foundation +import speech_to_text_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) + FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SpeechToTextMacosPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextMacosPlugin")) +} diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..29c8eb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Podfile.lock b/macos/Podfile.lock new file mode 100644 index 0000000..cc51af2 --- /dev/null +++ b/macos/Podfile.lock @@ -0,0 +1,49 @@ +PODS: + - audio_session (0.0.1): + - FlutterMacOS + - flutter_blue_plus_darwin (0.0.2): + - Flutter + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - speech_to_text_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`) + - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - speech_to_text_macos (from `Flutter/ephemeral/.symlinks/plugins/speech_to_text_macos/macos`) + +EXTERNAL SOURCES: + audio_session: + :path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos + flutter_blue_plus_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + speech_to_text_macos: + :path: Flutter/ephemeral/.symlinks/plugins/speech_to_text_macos/macos + +SPEC CHECKSUMS: + audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e + flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + speech_to_text_macos: cb920dff8288c218a7e8c96c8c931b17e801dae7 + +PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 + +COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ada7c01 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 52BD3EA9F7AC4BFFDB9D10DD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3C836924AC10DA4AC4DBFE6 /* Pods_RunnerTests.framework */; }; + 7863D70A9A0957124B9A43CB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 192EFCDB15B557C81AD181D7 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 192EFCDB15B557C81AD181D7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* flutter_helix.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_helix.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 34B517D87C8A99C48757084E /* 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 = ""; }; + 542C418042BDFAA48152DB8D /* 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 = ""; }; + 553EAF61E32830C02B98361C /* 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; path = Release.xcconfig; sourceTree = ""; }; + 95F787B4B8A3BCF4548EE4C3 /* 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; path = Debug.xcconfig; sourceTree = ""; }; + 98C18F0D6D8D8AD8865CC660 /* 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 = ""; }; + D06D38D8AE64786A838F89DE /* 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 = ""; }; + D3C836924AC10DA4AC4DBFE6 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 52BD3EA9F7AC4BFFDB9D10DD /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7863D70A9A0957124B9A43CB /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + A4C78193BBFF944001BC18CD /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutter_helix.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + A4C78193BBFF944001BC18CD /* Pods */ = { + isa = PBXGroup; + children = ( + 95F787B4B8A3BCF4548EE4C3 /* Pods-Runner.debug.xcconfig */, + 542C418042BDFAA48152DB8D /* Pods-Runner.release.xcconfig */, + 553EAF61E32830C02B98361C /* Pods-Runner.profile.xcconfig */, + 34B517D87C8A99C48757084E /* Pods-RunnerTests.debug.xcconfig */, + 98C18F0D6D8D8AD8865CC660 /* Pods-RunnerTests.release.xcconfig */, + D06D38D8AE64786A838F89DE /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 192EFCDB15B557C81AD181D7 /* Pods_Runner.framework */, + D3C836924AC10DA4AC4DBFE6 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + E91CC58A299C1343983A6777 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 70F30E45F89824993FE4B30D /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + C63B82D22878425B151C5717 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutter_helix.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 70F30E45F89824993FE4B30D /* [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; + }; + C63B82D22878425B151C5717 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + E91CC58A299C1343983A6777 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 34B517D87C8A99C48757084E /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_helix.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_helix"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 98C18F0D6D8D8AD8865CC660 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_helix.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_helix"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D06D38D8AE64786A838F89DE /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/flutter_helix.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/flutter_helix"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e9d5452 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..22605c4 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_helix + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.evenrealities.flutterHelix + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.evenrealities. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..37504bd --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1073 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: abf63d42450c7ad6d8188887d16eeba2f1ff92ea8d8dc673213e99fb3c02b194 + url: "https://pub.dev" + source: hosted + version: "7.5.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + audio_session: + dependency: "direct main" + description: + name: audio_session + sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" + url: "https://pub.dev" + source: hosted + version: "0.1.25" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + url: "https://pub.dev" + source: hosted + version: "4.0.4" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" + url: "https://pub.dev" + source: hosted + version: "8.10.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_openai: + dependency: "direct main" + description: + name: dart_openai + sha256: "853bb57fed6a71c3ba0324af5cb40c16d196cf3aa55b91d244964ae4a241ccf1" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + dio: + dependency: "direct main" + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + fake_async: + dependency: "direct dev" + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + fetch_api: + dependency: transitive + description: + name: fetch_api + sha256: "24cbd5616f3d4008c335c197bb90bfa0eb43b9e55c6de5c60d1f805092636034" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + fetch_client: + dependency: transitive + description: + name: fetch_client + sha256: "375253f4efe64303c793fb17fe90771c591320b2ae11fb29cb5b406cc8533c00" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: bfae0d24619940516261045d8b3c74b4c80ca82222426e05ffbf7f3ea9dbfb1a + url: "https://pub.dev" + source: hosted + version: "1.35.5" + flutter_blue_plus_android: + dependency: transitive + description: + name: flutter_blue_plus_android + sha256: "9723dd4ba7dcc3f27f8202e1159a302eb4cdb88ae482bb8e0dd733b82230a258" + url: "https://pub.dev" + source: hosted + version: "4.0.5" + flutter_blue_plus_darwin: + dependency: transitive + description: + name: flutter_blue_plus_darwin + sha256: f34123795352a9761e321589aa06356d3b53f007f13f7e23e3c940e733259b2d + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_blue_plus_linux: + dependency: transitive + description: + name: flutter_blue_plus_linux + sha256: "635443d1d333e3695733fd70e81ee0d87fa41e78aa81844103d2a8a854b0d593" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_blue_plus_platform_interface: + dependency: transitive + description: + name: flutter_blue_plus_platform_interface + sha256: a4bb70fa6fd09e0be163b004d773bf19e31104e257a4eb846b67f884ddd87de2 + url: "https://pub.dev" + source: hosted + version: "4.0.2" + flutter_blue_plus_web: + dependency: transitive + description: + name: flutter_blue_plus_web + sha256: "03023c259dbbba1bc5ce0fcd4e88b364f43eec01d45425f393023b9b2722cf4d" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_sound: + dependency: "direct main" + description: + name: flutter_sound + sha256: ef89477f6e8ce2fa395158ebc4a8b11982e3ada440b4021c06fd97a4e771554b + url: "https://pub.dev" + source: hosted + version: "9.28.0" + flutter_sound_platform_interface: + dependency: transitive + description: + name: flutter_sound_platform_interface + sha256: "3394d7e664a09796818014ff85a81db0dec397f4c286cbe52f8783886fa5a497" + url: "https://pub.dev" + source: hosted + version: "9.28.0" + flutter_sound_web: + dependency: transitive + description: + name: flutter_sound_web + sha256: "4e10c94a8574bd93bb8668af59bf76f5312a890bccd3778d73168a7133217dc5" + url: "https://pub.dev" + source: hosted + version: "9.28.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" + url: "https://pub.dev" + source: hosted + version: "2.5.8" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 + url: "https://pub.dev" + source: hosted + version: "7.7.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + golden_toolkit: + dependency: "direct dev" + description: + name: golden_toolkit + sha256: "8f74adab33154fe7b731395782797021f97d2edc52f7bfb85ff4f1b5c4a215f0" + url: "https://pub.dev" + source: hosted + version: "0.15.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + http: + dependency: transitive + description: + name: http + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + url: "https://pub.dev" + source: hosted + version: "6.9.5" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.dev" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logger: + dependency: transitive + description: + name: logger + sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "4546eac99e8967ea91bae633d2ca7698181d008e95fa4627330cf903d573277a" + url: "https://pub.dev" + source: hosted + version: "5.4.6" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pedantic: + dependency: transitive + description: + name: pedantic + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 + url: "https://pub.dev" + source: hosted + version: "10.4.5" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" + url: "https://pub.dev" + source: hosted + version: "10.3.6" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1" + url: "https://pub.dev" + source: hosted + version: "1.3.6" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + speech_to_text: + dependency: "direct main" + description: + name: speech_to_text + sha256: "97425fd8cc60424061a0584b6c418c0eedab5201cc5e96ef15a946d7fab7b9b7" + url: "https://pub.dev" + source: hosted + version: "6.6.2" + speech_to_text_macos: + dependency: transitive + description: + name: speech_to_text_macos + sha256: e685750f7542fcaa087a5396ee471e727ec648bf681f4da83c84d086322173f6 + url: "https://pub.dev" + source: hosted + version: "1.1.0" + speech_to_text_platform_interface: + dependency: transitive + description: + name: speech_to_text_platform_interface + sha256: a1935847704e41ee468aad83181ddd2423d0833abe55d769c59afca07adb5114 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.7.2 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..3ba8dd1 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,86 @@ +name: flutter_helix +description: "Helix - Cross-platform companion app for Even Realities smart glasses" +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.7.2 + +dependencies: + flutter: + sdk: flutter + + # UI and Material Design + cupertino_icons: ^1.0.8 + + # State Management + provider: ^6.1.1 + + # Dependency Injection + get_it: ^7.6.4 + + # Bluetooth for Even Realities Glasses + flutter_blue_plus: ^1.4.4 + + # Audio Processing + flutter_sound: ^9.2.13 + audio_session: ^0.1.16 + speech_to_text: ^6.6.0 + + # Platform Permissions + permission_handler: ^10.2.0 + + # HTTP Client for AI APIs + dio: ^5.4.3+1 + + # OpenAI Integration + dart_openai: ^5.1.0 + + # Data Persistence + shared_preferences: ^2.2.2 + + # Data Models and Serialization + freezed_annotation: ^2.4.1 + json_annotation: ^4.8.1 + + # Internationalization + intl: ^0.19.0 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + + # Testing Dependencies + mockito: ^5.4.2 + fake_async: ^1.3.1 + golden_toolkit: ^0.15.0 + + # Linting and Code Quality + flutter_lints: ^5.0.0 + + # Code Generation + build_runner: ^2.4.7 + json_serializable: ^6.7.1 + freezed: ^2.4.7 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + uses-material-design: true + + # Add app icon + # assets: + # - images/ + + # Add custom fonts + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic \ No newline at end of file diff --git a/settings.local.json b/settings.local.json new file mode 100644 index 0000000..9dfde26 --- /dev/null +++ b/settings.local.json @@ -0,0 +1,46 @@ +{ + "permissions": { + "allow": [ + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(xcodebuild:*)", + "Bash(xcrun simctl boot:*)", + "Bash(true)", + "Bash(grep:*)", + "Bash(xcrun simctl list:*)", + "Bash(xcrun simctl install:*)", + "Bash(xcrun simctl launch:*)", + "Bash(xcrun simctl spawn:*)", + "Bash(find:*)", + "Bash(swiftc:*)", + "Bash(xcrun simctl terminate:*)", + "Bash(open:*)", + "Bash(rm:*)", + "mcp__ide__getDiagnostics", + "Bash(touch:*)", + "Bash(flutter create:*)", + "Bash(mkdir:*)", + "Bash(flutter pub:*)", + "Bash(flutter analyze:*)", + "Bash(flutter test:*)", + "Bash(flutter packages pub run:*)", + "Bash(flutter packages:*)", + "Bash(flutter build:*)", + "Bash(dart run build_runner build:*)", + "Bash(flutter:*)", + "Bash(mv:*)", + "Bash(pod install:*)", + "Bash(dart devtools:*)", + "Bash(code:*)", + "Bash(ls:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(flutter analyze:*)", + "Bash(flutter build:*)", + "Bash(find:*)", + "Bash(flutter:*)", + "Bash(gh issue create:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/test/integration/recording_workflow_test.dart b/test/integration/recording_workflow_test.dart new file mode 100644 index 0000000..2a8062d --- /dev/null +++ b/test/integration/recording_workflow_test.dart @@ -0,0 +1,553 @@ +// ABOUTME: Integration tests for complete recording workflow +// ABOUTME: Tests end-to-end recording, transcription, and conversation storage + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; +import 'package:provider/provider.dart'; +import 'dart:async'; +import 'dart:typed_data'; + +import '../../lib/services/audio_service.dart'; +import '../../lib/services/conversation_storage_service.dart'; +import '../../lib/services/transcription_service.dart'; +import '../../lib/services/service_locator.dart'; +import '../../lib/models/conversation_model.dart'; +import '../../lib/models/transcription_segment.dart'; +import '../../lib/models/audio_configuration.dart'; +import '../../lib/ui/widgets/conversation_tab.dart'; +import '../../lib/ui/screens/home_screen.dart'; +import '../../lib/core/utils/logging_service.dart'; + +import '../test_helpers.dart'; +import 'recording_workflow_test.mocks.dart'; + +@GenerateMocks([ + AudioService, + ConversationStorageService, + TranscriptionService, + LoggingService, +]) +void main() { + group('Recording Workflow Integration Tests', () { + late MockAudioService mockAudioService; + late MockConversationStorageService mockStorageService; + late MockTranscriptionService mockTranscriptionService; + late MockLoggingService mockLoggingService; + + setUp(() { + mockAudioService = MockAudioService(); + mockStorageService = MockConversationStorageService(); + mockTranscriptionService = MockTranscriptionService(); + mockLoggingService = MockLoggingService(); + + // Setup default mock behaviors + when(mockAudioService.hasPermission).thenReturn(true); + when(mockAudioService.isRecording).thenReturn(false); + when(mockAudioService.initialize(any)).thenAnswer((_) async {}); + when(mockAudioService.requestPermission()).thenAnswer((_) async => true); + when(mockAudioService.startRecording()).thenAnswer((_) async {}); + when(mockAudioService.stopRecording()).thenAnswer((_) async {}); + when(mockAudioService.startConversationRecording(any)) + .thenAnswer((_) async => '/path/to/recording.wav'); + when(mockAudioService.stopConversationRecording()) + .thenAnswer((_) async {}); + + // Setup audio level stream + when(mockAudioService.audioLevelStream) + .thenAnswer((_) => Stream.value(0.5)); + when(mockAudioService.recordingDurationStream) + .thenAnswer((_) => Stream.value(const Duration(seconds: 30))); + when(mockAudioService.voiceActivityStream) + .thenAnswer((_) => Stream.value(true)); + + // Setup storage service + when(mockStorageService.getAllConversations()) + .thenAnswer((_) async => []); + when(mockStorageService.conversationStream) + .thenAnswer((_) => Stream.value([])); + when(mockStorageService.saveConversation(any)) + .thenAnswer((_) async {}); + + // Setup service locator mocks + _setupServiceLocatorMocks(); + }); + + void _setupServiceLocatorMocks() { + // Note: In a real app, you'd set up proper dependency injection + // For testing, we'll assume ServiceLocator can be mocked + } + + testWidgets('Complete recording workflow - start to finish', + (WidgetTester tester) async { + // Build the conversation tab + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Find the record button + final recordButton = find.byIcon(Icons.mic); + expect(recordButton, findsOneWidget); + + // Tap to start recording + await tester.tap(recordButton); + await tester.pump(); + + // Verify recording started + verify(mockAudioService.startConversationRecording(any)).called(1); + + // Simulate some audio level changes + final audioLevelController = StreamController(); + when(mockAudioService.audioLevelStream) + .thenAnswer((_) => audioLevelController.stream); + + // Emit some audio levels + audioLevelController.add(0.3); + await tester.pump(); + audioLevelController.add(0.7); + await tester.pump(); + audioLevelController.add(0.5); + await tester.pump(); + + // Find the stop button (should be showing now) + final stopButton = find.byIcon(Icons.stop); + expect(stopButton, findsOneWidget); + + // Tap to stop recording + await tester.tap(stopButton); + await tester.pump(); + + // Verify recording stopped + verify(mockAudioService.stopRecording()).called(1); + + // Verify conversation was saved + verify(mockStorageService.saveConversation(any)).called(1); + + // Cleanup + await audioLevelController.close(); + }); + + testWidgets('Recording with permission request', + (WidgetTester tester) async { + // Setup permission not granted initially + when(mockAudioService.hasPermission).thenReturn(false); + when(mockAudioService.requestPermission()).thenAnswer((_) async => true); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Tap record button + final recordButton = find.byIcon(Icons.mic); + await tester.tap(recordButton); + await tester.pump(); + + // Verify permission was requested + verify(mockAudioService.requestPermission()).called(1); + + // Verify recording started after permission granted + verify(mockAudioService.startConversationRecording(any)).called(1); + }); + + testWidgets('Recording with permission denied', + (WidgetTester tester) async { + // Setup permission denied + when(mockAudioService.hasPermission).thenReturn(false); + when(mockAudioService.requestPermission()).thenAnswer((_) async => false); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Tap record button + final recordButton = find.byIcon(Icons.mic); + await tester.tap(recordButton); + await tester.pump(); + + // Verify permission was requested + verify(mockAudioService.requestPermission()).called(1); + + // Verify recording was NOT started + verifyNever(mockAudioService.startConversationRecording(any)); + + // Verify error message is shown + expect(find.text('Microphone permission required for recording'), + findsOneWidget); + }); + + testWidgets('Recording duration timer updates', + (WidgetTester tester) async { + // Setup duration stream + final durationController = StreamController(); + when(mockAudioService.recordingDurationStream) + .thenAnswer((_) => durationController.stream); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Start recording + final recordButton = find.byIcon(Icons.mic); + await tester.tap(recordButton); + await tester.pump(); + + // Emit duration updates + durationController.add(const Duration(seconds: 5)); + await tester.pump(); + + // Verify timer display updated + expect(find.text('00:05'), findsOneWidget); + + durationController.add(const Duration(minutes: 1, seconds: 30)); + await tester.pump(); + + // Verify timer display updated + expect(find.text('01:30'), findsOneWidget); + + // Cleanup + await durationController.close(); + }); + + testWidgets('Audio level visualization updates', + (WidgetTester tester) async { + // Setup audio level stream + final audioLevelController = StreamController(); + when(mockAudioService.audioLevelStream) + .thenAnswer((_) => audioLevelController.stream); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Start recording + final recordButton = find.byIcon(Icons.mic); + await tester.tap(recordButton); + await tester.pump(); + + // Emit different audio levels + audioLevelController.add(0.1); // Low level + await tester.pump(); + + audioLevelController.add(0.8); // High level + await tester.pump(); + + audioLevelController.add(0.0); // Silence + await tester.pump(); + + // Verify audio level bars are displayed + expect(find.byType(AudioLevelBars), findsOneWidget); + + // Cleanup + await audioLevelController.close(); + }); + + testWidgets('Recording error handling', + (WidgetTester tester) async { + // Setup recording to throw error + when(mockAudioService.startConversationRecording(any)) + .thenThrow(Exception('Recording failed')); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Tap record button + final recordButton = find.byIcon(Icons.mic); + await tester.tap(recordButton); + await tester.pump(); + + // Verify error message is shown + expect(find.textContaining('Recording error'), findsOneWidget); + }); + + testWidgets('History navigation from conversation tab', + (WidgetTester tester) async { + bool historyTapped = false; + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () { + historyTapped = true; + }, + ), + ), + ); + + // Find and tap the history button + final historyButton = find.byIcon(Icons.history); + expect(historyButton, findsOneWidget); + + await tester.tap(historyButton); + await tester.pump(); + + // Verify history callback was called + expect(historyTapped, isTrue); + }); + + testWidgets('Conversation saving with transcription segments', + (WidgetTester tester) async { + // Capture the saved conversation + ConversationModel? savedConversation; + when(mockStorageService.saveConversation(any)) + .thenAnswer((invocation) async { + savedConversation = invocation.positionalArguments[0]; + }); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Start recording + final recordButton = find.byIcon(Icons.mic); + await tester.tap(recordButton); + await tester.pump(); + + // Stop recording + final stopButton = find.byIcon(Icons.stop); + await tester.tap(stopButton); + await tester.pump(); + + // Verify conversation was saved + expect(savedConversation, isNotNull); + expect(savedConversation!.participants, hasLength(2)); + expect(savedConversation!.participants.first.name, equals('You')); + expect(savedConversation!.participants.last.name, equals('Speaker 2')); + }); + + testWidgets('Recording pause and resume functionality', + (WidgetTester tester) async { + // Setup pause/resume methods + when(mockAudioService.pauseRecording()).thenAnswer((_) async {}); + when(mockAudioService.resumeRecording()).thenAnswer((_) async {}); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Start recording + final recordButton = find.byIcon(Icons.mic); + await tester.tap(recordButton); + await tester.pump(); + + // Find pause button (should be visible during recording) + final pauseButton = find.byIcon(Icons.pause); + expect(pauseButton, findsOneWidget); + + // Tap pause + await tester.tap(pauseButton); + await tester.pump(); + + // Find resume button + final resumeButton = find.byIcon(Icons.play_arrow); + expect(resumeButton, findsOneWidget); + + // Tap resume + await tester.tap(resumeButton); + await tester.pump(); + + // Verify pause button is back + expect(find.byIcon(Icons.pause), findsOneWidget); + }); + + testWidgets('Multiple recording sessions', + (WidgetTester tester) async { + int recordingCount = 0; + when(mockAudioService.startConversationRecording(any)) + .thenAnswer((_) async { + recordingCount++; + return '/path/to/recording_$recordingCount.wav'; + }); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // First recording session + await tester.tap(find.byIcon(Icons.mic)); + await tester.pump(); + await tester.tap(find.byIcon(Icons.stop)); + await tester.pump(); + + // Second recording session + await tester.tap(find.byIcon(Icons.mic)); + await tester.pump(); + await tester.tap(find.byIcon(Icons.stop)); + await tester.pump(); + + // Verify two recordings were made + expect(recordingCount, equals(2)); + verify(mockStorageService.saveConversation(any)).called(2); + }); + + testWidgets('Recording state persistence across widget rebuilds', + (WidgetTester tester) async { + // Setup recording state + when(mockAudioService.isRecording).thenReturn(true); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Start recording + await tester.tap(find.byIcon(Icons.mic)); + await tester.pump(); + + // Trigger widget rebuild + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Verify recording state is maintained + expect(find.byIcon(Icons.stop), findsOneWidget); + }); + + group('Performance Tests', () { + testWidgets('Rapid button tapping handling', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Rapidly tap record button multiple times + final recordButton = find.byIcon(Icons.mic); + for (int i = 0; i < 5; i++) { + await tester.tap(recordButton); + await tester.pump(const Duration(milliseconds: 10)); + } + + // Should only start recording once + verify(mockAudioService.startConversationRecording(any)).called(1); + }); + + testWidgets('High frequency audio level updates', + (WidgetTester tester) async { + final audioLevelController = StreamController(); + when(mockAudioService.audioLevelStream) + .thenAnswer((_) => audioLevelController.stream); + + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Start recording + await tester.tap(find.byIcon(Icons.mic)); + await tester.pump(); + + // Send rapid audio level updates + for (int i = 0; i < 100; i++) { + audioLevelController.add(i / 100.0); + if (i % 10 == 0) { + await tester.pump(const Duration(milliseconds: 1)); + } + } + + // Should handle updates without errors + expect(tester.takeException(), isNull); + + await audioLevelController.close(); + }); + }); + + group('Edge Cases', () { + testWidgets('Recording during app backgrounding', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Start recording + await tester.tap(find.byIcon(Icons.mic)); + await tester.pump(); + + // Simulate app lifecycle change + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + const MethodChannel('flutter/lifecycle'), + (methodCall) async { + return null; + }, + ); + + // App should handle lifecycle changes gracefully + expect(tester.takeException(), isNull); + }); + + testWidgets('Recording with zero duration', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: ConversationTab( + onHistoryTap: () {}, + ), + ), + ); + + // Start and immediately stop recording + await tester.tap(find.byIcon(Icons.mic)); + await tester.pump(); + await tester.tap(find.byIcon(Icons.stop)); + await tester.pump(); + + // Should still save conversation + verify(mockStorageService.saveConversation(any)).called(1); + }); + }); + }); +} \ No newline at end of file diff --git a/test/integration/recording_workflow_test.mocks.dart b/test/integration/recording_workflow_test.mocks.dart new file mode 100644 index 0000000..b69bec5 --- /dev/null +++ b/test/integration/recording_workflow_test.mocks.dart @@ -0,0 +1,785 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in flutter_helix/test/integration/recording_workflow_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; +import 'dart:typed_data' as _i6; + +import 'package:flutter_helix/core/utils/logging_service.dart' as _i11; +import 'package:flutter_helix/models/audio_configuration.dart' as _i2; +import 'package:flutter_helix/models/conversation_model.dart' as _i9; +import 'package:flutter_helix/models/transcription_segment.dart' as _i3; +import 'package:flutter_helix/services/audio_service.dart' as _i4; +import 'package:flutter_helix/services/conversation_storage_service.dart' + as _i8; +import 'package:flutter_helix/services/transcription_service.dart' as _i10; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i7; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeAudioConfiguration_0 extends _i1.SmartFake + implements _i2.AudioConfiguration { + _FakeAudioConfiguration_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeTranscriptionSegment_1 extends _i1.SmartFake + implements _i3.TranscriptionSegment { + _FakeTranscriptionSegment_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [AudioService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAudioService extends _i1.Mock implements _i4.AudioService { + MockAudioService() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.AudioConfiguration get configuration => + (super.noSuchMethod( + Invocation.getter(#configuration), + returnValue: _FakeAudioConfiguration_0( + this, + Invocation.getter(#configuration), + ), + ) + as _i2.AudioConfiguration); + + @override + bool get isRecording => + (super.noSuchMethod(Invocation.getter(#isRecording), returnValue: false) + as bool); + + @override + bool get hasPermission => + (super.noSuchMethod(Invocation.getter(#hasPermission), returnValue: false) + as bool); + + @override + _i5.Stream<_i6.Uint8List> get audioStream => + (super.noSuchMethod( + Invocation.getter(#audioStream), + returnValue: _i5.Stream<_i6.Uint8List>.empty(), + ) + as _i5.Stream<_i6.Uint8List>); + + @override + _i5.Stream get audioLevelStream => + (super.noSuchMethod( + Invocation.getter(#audioLevelStream), + returnValue: _i5.Stream.empty(), + ) + as _i5.Stream); + + @override + _i5.Stream get voiceActivityStream => + (super.noSuchMethod( + Invocation.getter(#voiceActivityStream), + returnValue: _i5.Stream.empty(), + ) + as _i5.Stream); + + @override + _i5.Stream get recordingDurationStream => + (super.noSuchMethod( + Invocation.getter(#recordingDurationStream), + returnValue: _i5.Stream.empty(), + ) + as _i5.Stream); + + @override + _i5.Future initialize(_i2.AudioConfiguration? config) => + (super.noSuchMethod( + Invocation.method(#initialize, [config]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future requestPermission() => + (super.noSuchMethod( + Invocation.method(#requestPermission, []), + returnValue: _i5.Future.value(false), + ) + as _i5.Future); + + @override + _i5.Future startRecording() => + (super.noSuchMethod( + Invocation.method(#startRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future stopRecording() => + (super.noSuchMethod( + Invocation.method(#stopRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future pauseRecording() => + (super.noSuchMethod( + Invocation.method(#pauseRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future resumeRecording() => + (super.noSuchMethod( + Invocation.method(#resumeRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future startConversationRecording(String? conversationId) => + (super.noSuchMethod( + Invocation.method(#startConversationRecording, [conversationId]), + returnValue: _i5.Future.value( + _i7.dummyValue( + this, + Invocation.method(#startConversationRecording, [ + conversationId, + ]), + ), + ), + ) + as _i5.Future); + + @override + _i5.Future stopConversationRecording() => + (super.noSuchMethod( + Invocation.method(#stopConversationRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future> getInputDevices() => + (super.noSuchMethod( + Invocation.method(#getInputDevices, []), + returnValue: _i5.Future>.value( + <_i4.AudioInputDevice>[], + ), + ) + as _i5.Future>); + + @override + _i5.Future selectInputDevice(String? deviceId) => + (super.noSuchMethod( + Invocation.method(#selectInputDevice, [deviceId]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future configureAudioProcessing({ + bool? enableNoiseReduction = true, + bool? enableEchoCancellation = true, + double? gainLevel = 1.0, + }) => + (super.noSuchMethod( + Invocation.method(#configureAudioProcessing, [], { + #enableNoiseReduction: enableNoiseReduction, + #enableEchoCancellation: enableEchoCancellation, + #gainLevel: gainLevel, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future setVoiceActivityDetection(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setVoiceActivityDetection, [enabled]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future setAudioQuality(_i2.AudioQuality? quality) => + (super.noSuchMethod( + Invocation.method(#setAudioQuality, [quality]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future testAudioRecording() => + (super.noSuchMethod( + Invocation.method(#testAudioRecording, []), + returnValue: _i5.Future.value(false), + ) + as _i5.Future); + + @override + _i5.Future dispose() => + (super.noSuchMethod( + Invocation.method(#dispose, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); +} + +/// A class which mocks [ConversationStorageService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockConversationStorageService extends _i1.Mock + implements _i8.ConversationStorageService { + MockConversationStorageService() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.Stream> get conversationStream => + (super.noSuchMethod( + Invocation.getter(#conversationStream), + returnValue: _i5.Stream>.empty(), + ) + as _i5.Stream>); + + @override + _i5.Future> getAllConversations() => + (super.noSuchMethod( + Invocation.method(#getAllConversations, []), + returnValue: _i5.Future>.value( + <_i9.ConversationModel>[], + ), + ) + as _i5.Future>); + + @override + _i5.Future<_i9.ConversationModel?> getConversation(String? id) => + (super.noSuchMethod( + Invocation.method(#getConversation, [id]), + returnValue: _i5.Future<_i9.ConversationModel?>.value(), + ) + as _i5.Future<_i9.ConversationModel?>); + + @override + _i5.Future saveConversation(_i9.ConversationModel? conversation) => + (super.noSuchMethod( + Invocation.method(#saveConversation, [conversation]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future deleteConversation(String? id) => + (super.noSuchMethod( + Invocation.method(#deleteConversation, [id]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future updateConversation(_i9.ConversationModel? conversation) => + (super.noSuchMethod( + Invocation.method(#updateConversation, [conversation]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future> searchConversations(String? query) => + (super.noSuchMethod( + Invocation.method(#searchConversations, [query]), + returnValue: _i5.Future>.value( + <_i9.ConversationModel>[], + ), + ) + as _i5.Future>); + + @override + _i5.Future> getConversationsByDateRange( + DateTime? startDate, + DateTime? endDate, + ) => + (super.noSuchMethod( + Invocation.method(#getConversationsByDateRange, [ + startDate, + endDate, + ]), + returnValue: _i5.Future>.value( + <_i9.ConversationModel>[], + ), + ) + as _i5.Future>); +} + +/// A class which mocks [TranscriptionService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTranscriptionService extends _i1.Mock + implements _i10.TranscriptionService { + MockTranscriptionService() { + _i1.throwOnMissingStub(this); + } + + @override + bool get isInitialized => + (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) + as bool); + + @override + bool get isTranscribing => + (super.noSuchMethod( + Invocation.getter(#isTranscribing), + returnValue: false, + ) + as bool); + + @override + bool get hasPermissions => + (super.noSuchMethod( + Invocation.getter(#hasPermissions), + returnValue: false, + ) + as bool); + + @override + bool get isAvailable => + (super.noSuchMethod(Invocation.getter(#isAvailable), returnValue: false) + as bool); + + @override + String get currentLanguage => + (super.noSuchMethod( + Invocation.getter(#currentLanguage), + returnValue: _i7.dummyValue( + this, + Invocation.getter(#currentLanguage), + ), + ) + as String); + + @override + _i10.TranscriptionBackend get currentBackend => + (super.noSuchMethod( + Invocation.getter(#currentBackend), + returnValue: _i10.TranscriptionBackend.device, + ) + as _i10.TranscriptionBackend); + + @override + _i10.TranscriptionQuality get currentQuality => + (super.noSuchMethod( + Invocation.getter(#currentQuality), + returnValue: _i10.TranscriptionQuality.low, + ) + as _i10.TranscriptionQuality); + + @override + double get vadSensitivity => + (super.noSuchMethod(Invocation.getter(#vadSensitivity), returnValue: 0.0) + as double); + + @override + _i5.Stream<_i3.TranscriptionSegment> get transcriptionStream => + (super.noSuchMethod( + Invocation.getter(#transcriptionStream), + returnValue: _i5.Stream<_i3.TranscriptionSegment>.empty(), + ) + as _i5.Stream<_i3.TranscriptionSegment>); + + @override + _i5.Stream get confidenceStream => + (super.noSuchMethod( + Invocation.getter(#confidenceStream), + returnValue: _i5.Stream.empty(), + ) + as _i5.Stream); + + @override + _i5.Future initialize() => + (super.noSuchMethod( + Invocation.method(#initialize, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future requestPermissions() => + (super.noSuchMethod( + Invocation.method(#requestPermissions, []), + returnValue: _i5.Future.value(false), + ) + as _i5.Future); + + @override + _i5.Future startTranscription({ + bool? enableCapitalization = true, + bool? enablePunctuation = true, + String? language, + _i10.TranscriptionBackend? preferredBackend, + }) => + (super.noSuchMethod( + Invocation.method(#startTranscription, [], { + #enableCapitalization: enableCapitalization, + #enablePunctuation: enablePunctuation, + #language: language, + #preferredBackend: preferredBackend, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future stopTranscription() => + (super.noSuchMethod( + Invocation.method(#stopTranscription, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future pauseTranscription() => + (super.noSuchMethod( + Invocation.method(#pauseTranscription, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future resumeTranscription() => + (super.noSuchMethod( + Invocation.method(#resumeTranscription, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future setLanguage(String? languageCode) => + (super.noSuchMethod( + Invocation.method(#setLanguage, [languageCode]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future configureQuality(_i10.TranscriptionQuality? quality) => + (super.noSuchMethod( + Invocation.method(#configureQuality, [quality]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future configureBackend(_i10.TranscriptionBackend? backend) => + (super.noSuchMethod( + Invocation.method(#configureBackend, [backend]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future> getAvailableLanguages() => + (super.noSuchMethod( + Invocation.method(#getAvailableLanguages, []), + returnValue: _i5.Future>.value([]), + ) + as _i5.Future>); + + @override + double getLastConfidence() => + (super.noSuchMethod( + Invocation.method(#getLastConfidence, []), + returnValue: 0.0, + ) + as double); + + @override + _i5.Future<_i3.TranscriptionSegment> transcribeAudio(String? audioPath) => + (super.noSuchMethod( + Invocation.method(#transcribeAudio, [audioPath]), + returnValue: _i5.Future<_i3.TranscriptionSegment>.value( + _FakeTranscriptionSegment_1( + this, + Invocation.method(#transcribeAudio, [audioPath]), + ), + ), + ) + as _i5.Future<_i3.TranscriptionSegment>); + + @override + _i5.Future calibrateVoiceActivity() => + (super.noSuchMethod( + Invocation.method(#calibrateVoiceActivity, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future setVADSensitivity(double? sensitivity) => + (super.noSuchMethod( + Invocation.method(#setVADSensitivity, [sensitivity]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future dispose() => + (super.noSuchMethod( + Invocation.method(#dispose, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); +} + +/// A class which mocks [LoggingService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLoggingService extends _i1.Mock implements _i11.LoggingService { + MockLoggingService() { + _i1.throwOnMissingStub(this); + } + + @override + void setLogLevel(_i11.LogLevel? level) => super.noSuchMethod( + Invocation.method(#setLogLevel, [level]), + returnValueForMissingStub: null, + ); + + @override + void log(String? tag, String? message, _i11.LogLevel? level) => + super.noSuchMethod( + Invocation.method(#log, [tag, message, level]), + returnValueForMissingStub: null, + ); + + @override + void debug(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#debug, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void info(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#info, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void warning(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#warning, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void error( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#error, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + void critical( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#critical, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + List<_i11.LogEntry> getRecentLogs([int? limit]) => + (super.noSuchMethod( + Invocation.method(#getRecentLogs, [limit]), + returnValue: <_i11.LogEntry>[], + ) + as List<_i11.LogEntry>); + + @override + void clearLogs() => super.noSuchMethod( + Invocation.method(#clearLogs, []), + returnValueForMissingStub: null, + ); + + @override + _i5.Future enableFileLogging(String? filePath) => + (super.noSuchMethod( + Invocation.method(#enableFileLogging, [filePath]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + void disableFileLogging() => super.noSuchMethod( + Invocation.method(#disableFileLogging, []), + returnValueForMissingStub: null, + ); + + @override + void enablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#enablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void disablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#disablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void startPerformanceTimer(String? markerId) => super.noSuchMethod( + Invocation.method(#startPerformanceTimer, [markerId]), + returnValueForMissingStub: null, + ); + + @override + void endPerformanceTimer(String? markerId, [String? operation]) => + super.noSuchMethod( + Invocation.method(#endPerformanceTimer, [markerId, operation]), + returnValueForMissingStub: null, + ); + + @override + void addTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#addTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void removeTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#removeTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void clearTagFilters() => super.noSuchMethod( + Invocation.method(#clearTagFilters, []), + returnValueForMissingStub: null, + ); + + @override + void setMessageFilter(String? filter) => super.noSuchMethod( + Invocation.method(#setMessageFilter, [filter]), + returnValueForMissingStub: null, + ); + + @override + List<_i11.LogEntry> getFilteredLogs({ + _i11.LogLevel? minLevel, + String? tag, + DateTime? since, + int? limit, + }) => + (super.noSuchMethod( + Invocation.method(#getFilteredLogs, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + #limit: limit, + }), + returnValue: <_i11.LogEntry>[], + ) + as List<_i11.LogEntry>); + + @override + String exportLogsAsJson({ + _i11.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i7.dummyValue( + this, + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + String exportLogsAsText({ + _i11.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i7.dummyValue( + this, + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + Map getLoggingStats() => + (super.noSuchMethod( + Invocation.method(#getLoggingStats, []), + returnValue: {}, + ) + as Map); +} diff --git a/test/test_helpers.dart b/test/test_helpers.dart new file mode 100644 index 0000000..28c8f42 --- /dev/null +++ b/test/test_helpers.dart @@ -0,0 +1,363 @@ +// ABOUTME: Test utilities and helpers for consistent test setup +// ABOUTME: Provides mock data, widget wrappers, and common test patterns + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:provider/provider.dart'; + +import 'package:flutter_helix/services/audio_service.dart'; +import 'package:flutter_helix/services/transcription_service.dart'; +import 'package:flutter_helix/services/llm_service.dart'; +import 'package:flutter_helix/services/glasses_service.dart'; +import 'package:flutter_helix/services/settings_service.dart'; +import 'package:flutter_helix/models/transcription_segment.dart'; +import 'package:flutter_helix/models/analysis_result.dart'; +import 'package:flutter_helix/models/conversation_model.dart'; +import 'package:flutter_helix/core/utils/logging_service.dart'; + +import 'test_helpers.mocks.dart'; + +// Generate mocks for all services +@GenerateMocks([ + AudioService, + TranscriptionService, + LLMService, + GlassesService, + SettingsService, + LoggingService, +]) +void main() {} + +/// Test utilities and data factories for Helix tests +class TestHelpers { + /// Creates a MaterialApp wrapper with mock providers for widget testing + static Widget createTestApp({ + Widget? child, + List children = const [], + MockAudioService? audioService, + MockTranscriptionService? transcriptionService, + MockLLMService? llmService, + MockGlassesService? glassesService, + MockSettingsService? settingsService, + }) { + return MaterialApp( + home: MultiProvider( + providers: [ + Provider( + create: (_) => audioService ?? MockAudioService(), + ), + Provider( + create: (_) => transcriptionService ?? MockTranscriptionService(), + ), + Provider( + create: (_) => llmService ?? MockLLMService(), + ), + Provider( + create: (_) => glassesService ?? MockGlassesService(), + ), + Provider( + create: (_) => settingsService ?? MockSettingsService(), + ), + ], + child: child ?? Scaffold( + body: Column(children: children), + ), + ), + ); + } + + /// Creates a test TranscriptionSegment with default values + static TranscriptionSegment createTestSegment({ + String? speaker, + String? text, + DateTime? timestamp, + double? confidence, + }) { + final now = timestamp ?? DateTime.now(); + return TranscriptionSegment( + text: text ?? 'This is a test transcription segment', + startTime: now, + endTime: now.add(const Duration(seconds: 2)), + confidence: confidence ?? 0.95, + speakerId: speaker ?? 'test_speaker', + speakerName: speaker ?? 'Test Speaker', + ); + } + + /// Creates a sample TranscriptionSegment for conversation model testing + static TranscriptionSegment createSampleSegment({ + String? id, + String? participantId, + String? content, + DateTime? timestamp, + double? confidence, + String? language, + TranscriptionBackend? backend, + }) { + final now = timestamp ?? DateTime.now(); + return TranscriptionSegment( + text: content ?? 'This is a test segment content', + startTime: now, + endTime: now.add(const Duration(seconds: 2)), + confidence: confidence ?? 0.95, + speakerId: participantId ?? 'participant_1', + segmentId: id ?? 'seg_${DateTime.now().millisecondsSinceEpoch}', + language: language ?? 'en-US', + backend: backend ?? TranscriptionBackend.device, + ); + } + + /// Creates a sample ConversationModel for testing + static ConversationModel createSampleConversation({ + String? id, + String? title, + DateTime? startTime, + DateTime? endTime, + List? participants, + List? segments, + }) { + final now = DateTime.now(); + + return ConversationModel( + id: id ?? 'test_conv_${now.millisecondsSinceEpoch}', + title: title ?? 'Test Conversation', + startTime: startTime ?? now.subtract(const Duration(hours: 1)), + endTime: endTime ?? now, + lastUpdated: now, + participants: participants ?? [ + const ConversationParticipant( + id: 'participant_1', + name: 'Alice', + isOwner: true, + ), + const ConversationParticipant( + id: 'participant_2', + name: 'Bob', + isOwner: false, + ), + ], + segments: segments ?? [ + createSampleSegment( + participantId: 'participant_1', + content: 'Hello, how are you?', + timestamp: now.subtract(const Duration(minutes: 5)), + ), + createSampleSegment( + participantId: 'participant_2', + content: 'I\'m doing well, thanks for asking!', + timestamp: now.subtract(const Duration(minutes: 4)), + ), + ], + ); + } + + /// Creates a test AnalysisResult with default values + static AnalysisResult createTestAnalysisResult({ + String? summary, + List? factChecks, + List? actionItems, + SentimentAnalysisResult? sentiment, + double? confidence, + }) { + return AnalysisResult( + summary: summary ?? 'Test analysis summary', + keyPoints: ['Key point 1', 'Key point 2'], + decisions: ['Decision 1'], + questions: ['Question 1'], + topics: ['Test Topic'], + factChecks: factChecks ?? [createTestFactCheck()], + actionItems: actionItems ?? [createTestActionItem()], + sentiment: sentiment ?? createTestSentiment(), + confidence: confidence ?? 0.88, + ); + } + + /// Creates a test FactCheckResult + static FactCheckResult createTestFactCheck({ + String? claim, + FactCheckStatus? status, + double? confidence, + List? sources, + String? explanation, + }) { + return FactCheckResult( + claim: claim ?? 'Test claim to be fact-checked', + status: status ?? FactCheckStatus.verified, + confidence: confidence ?? 0.92, + sources: sources ?? ['Test Source 1', 'Test Source 2'], + explanation: explanation ?? 'This claim has been verified by multiple sources.', + ); + } + + /// Creates a test ActionItemResult + static ActionItemResult createTestActionItem({ + String? id, + String? description, + String? assignee, + DateTime? dueDate, + ActionItemPriority? priority, + double? confidence, + ActionItemStatus? status, + }) { + return ActionItemResult( + id: id ?? 'test-action-1', + description: description ?? 'Test action item description', + assignee: assignee, + dueDate: dueDate, + priority: priority ?? ActionItemPriority.medium, + confidence: confidence ?? 0.87, + status: status ?? ActionItemStatus.pending, + ); + } + + /// Creates a test SentimentAnalysisResult + static SentimentAnalysisResult createTestSentiment({ + SentimentType? overallSentiment, + double? confidence, + Map? emotions, + }) { + return SentimentAnalysisResult( + overallSentiment: overallSentiment ?? SentimentType.positive, + confidence: confidence ?? 0.84, + emotions: emotions ?? { + 'happiness': 0.7, + 'excitement': 0.6, + 'curiosity': 0.8, + 'concern': 0.2, + }, + ); + } + + /// Creates test audio data for testing + static List createTestAudioData({ + int durationSeconds = 5, + int sampleRate = 16000, + }) { + final totalSamples = durationSeconds * sampleRate; + return List.generate(totalSamples, (index) { + // Generate simple sine wave for testing + final frequency = 440; // A4 note + final amplitude = 32767; // 16-bit max + final value = (amplitude * 0.5 * + (1 + (index * frequency * 2 * 3.14159 / sampleRate).sin())).round(); + return value; + }); + } + + /// Waits for widget animations to complete + static Future pumpAndSettle(WidgetTester tester, { + Duration timeout = const Duration(seconds: 10), + }) async { + await tester.pumpAndSettle(timeout); + } + + /// Finds widget by its semantic label + static Finder findBySemantic(String label) { + return find.bySemanticsLabel(label); + } + + /// Verifies that a widget exists and is visible + static void expectWidgetVisible(Finder finder) { + expect(finder, findsOneWidget); + expect(tester.widget(finder), isA()); + } + + /// Common test timeout duration + static const testTimeout = Duration(seconds: 30); + + /// Audio levels for testing various scenarios + static const double lowAudioLevel = 0.1; + static const double mediumAudioLevel = 0.5; + static const double highAudioLevel = 0.9; + + /// Test API keys for different providers + static const String testOpenAIKey = 'sk-test-openai-key-1234567890'; + static const String testAnthropicKey = 'sk-ant-test-anthropic-key-1234567890'; + + /// Test device information for Bluetooth testing + static const String testGlassesDeviceId = 'test-glasses-device-001'; + static const String testGlassesDeviceName = 'Test Even Realities G1'; + static const int testGlassesRSSI = -45; + static const double testGlassesBattery = 0.85; +} + +/// Extension methods for common test operations +extension WidgetTesterExtensions on WidgetTester { + /// Enters text into a TextField by its key + Future enterTextByKey(String key, String text) async { + await enterText(find.byKey(ValueKey(key)), text); + await pump(); + } + + /// Taps a widget by its key + Future tapByKey(String key) async { + await tap(find.byKey(ValueKey(key))); + await pump(); + } + + /// Taps a widget by its text + Future tapByText(String text) async { + await tap(find.text(text)); + await pump(); + } + + /// Verifies a text widget exists + void expectText(String text) { + expect(find.text(text), findsOneWidget); + } + + /// Verifies a widget by key exists + void expectWidgetByKey(String key) { + expect(find.byKey(ValueKey(key)), findsOneWidget); + } + + /// Scrolls until a widget is visible + Future scrollUntilVisible( + Finder finder, + Finder scrollable, { + double delta = 100.0, + }) async { + await scrollUntilVisible(finder, scrollable, scrollDelta: delta); + } +} + +/// Mock data constants for consistent testing +class TestData { + static const List sampleSpeakers = [ + 'Alice Johnson', + 'Bob Smith', + 'Carol Davis', + 'David Wilson', + ]; + + static const List sampleTexts = [ + 'Hello, welcome to our meeting today.', + 'I think we should focus on the quarterly results.', + 'The new product launch is scheduled for next month.', + 'We need to review the budget allocation.', + 'Has everyone had a chance to review the documents?', + ]; + + static const List sampleTopics = [ + 'Business Meeting', + 'Product Development', + 'Budget Planning', + 'Team Collaboration', + 'Technical Discussion', + ]; + + static const List sampleFactClaims = [ + 'The quarterly revenue increased by 15%', + 'Our customer satisfaction score is above 90%', + 'The new feature has been adopted by 75% of users', + 'Market research shows growing demand', + ]; + + static const List sampleActionItems = [ + 'Review and approve the budget proposal', + 'Schedule follow-up meeting with stakeholders', + 'Prepare presentation for board meeting', + 'Update project timeline and deliverables', + ]; +} \ No newline at end of file diff --git a/test/test_helpers.mocks.dart b/test/test_helpers.mocks.dart new file mode 100644 index 0000000..c78ff94 --- /dev/null +++ b/test/test_helpers.mocks.dart @@ -0,0 +1,1873 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in flutter_helix/test/test_helpers.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i7; +import 'dart:typed_data' as _i8; + +import 'package:flutter_helix/core/utils/logging_service.dart' as _i15; +import 'package:flutter_helix/models/analysis_result.dart' as _i4; +import 'package:flutter_helix/models/audio_configuration.dart' as _i2; +import 'package:flutter_helix/models/conversation_model.dart' as _i12; +import 'package:flutter_helix/models/glasses_connection_state.dart' as _i13; +import 'package:flutter_helix/models/transcription_segment.dart' as _i3; +import 'package:flutter_helix/services/audio_service.dart' as _i6; +import 'package:flutter_helix/services/glasses_service.dart' as _i5; +import 'package:flutter_helix/services/llm_service.dart' as _i11; +import 'package:flutter_helix/services/settings_service.dart' as _i14; +import 'package:flutter_helix/services/transcription_service.dart' as _i10; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i9; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeAudioConfiguration_0 extends _i1.SmartFake + implements _i2.AudioConfiguration { + _FakeAudioConfiguration_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeTranscriptionSegment_1 extends _i1.SmartFake + implements _i3.TranscriptionSegment { + _FakeTranscriptionSegment_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeAnalysisResult_2 extends _i1.SmartFake + implements _i4.AnalysisResult { + _FakeAnalysisResult_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeConversationSummary_3 extends _i1.SmartFake + implements _i4.ConversationSummary { + _FakeConversationSummary_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeSentimentAnalysisResult_4 extends _i1.SmartFake + implements _i4.SentimentAnalysisResult { + _FakeSentimentAnalysisResult_4(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeGlassesDeviceInfo_5 extends _i1.SmartFake + implements _i5.GlassesDeviceInfo { + _FakeGlassesDeviceInfo_5(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeGlassesHealthStatus_6 extends _i1.SmartFake + implements _i5.GlassesHealthStatus { + _FakeGlassesHealthStatus_6(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [AudioService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAudioService extends _i1.Mock implements _i6.AudioService { + MockAudioService() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.AudioConfiguration get configuration => + (super.noSuchMethod( + Invocation.getter(#configuration), + returnValue: _FakeAudioConfiguration_0( + this, + Invocation.getter(#configuration), + ), + ) + as _i2.AudioConfiguration); + + @override + bool get isRecording => + (super.noSuchMethod(Invocation.getter(#isRecording), returnValue: false) + as bool); + + @override + bool get hasPermission => + (super.noSuchMethod(Invocation.getter(#hasPermission), returnValue: false) + as bool); + + @override + _i7.Stream<_i8.Uint8List> get audioStream => + (super.noSuchMethod( + Invocation.getter(#audioStream), + returnValue: _i7.Stream<_i8.Uint8List>.empty(), + ) + as _i7.Stream<_i8.Uint8List>); + + @override + _i7.Stream get audioLevelStream => + (super.noSuchMethod( + Invocation.getter(#audioLevelStream), + returnValue: _i7.Stream.empty(), + ) + as _i7.Stream); + + @override + _i7.Stream get voiceActivityStream => + (super.noSuchMethod( + Invocation.getter(#voiceActivityStream), + returnValue: _i7.Stream.empty(), + ) + as _i7.Stream); + + @override + _i7.Stream get recordingDurationStream => + (super.noSuchMethod( + Invocation.getter(#recordingDurationStream), + returnValue: _i7.Stream.empty(), + ) + as _i7.Stream); + + @override + _i7.Future initialize(_i2.AudioConfiguration? config) => + (super.noSuchMethod( + Invocation.method(#initialize, [config]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future requestPermission() => + (super.noSuchMethod( + Invocation.method(#requestPermission, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future startRecording() => + (super.noSuchMethod( + Invocation.method(#startRecording, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future stopRecording() => + (super.noSuchMethod( + Invocation.method(#stopRecording, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future pauseRecording() => + (super.noSuchMethod( + Invocation.method(#pauseRecording, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future resumeRecording() => + (super.noSuchMethod( + Invocation.method(#resumeRecording, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future startConversationRecording(String? conversationId) => + (super.noSuchMethod( + Invocation.method(#startConversationRecording, [conversationId]), + returnValue: _i7.Future.value( + _i9.dummyValue( + this, + Invocation.method(#startConversationRecording, [ + conversationId, + ]), + ), + ), + ) + as _i7.Future); + + @override + _i7.Future stopConversationRecording() => + (super.noSuchMethod( + Invocation.method(#stopConversationRecording, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future> getInputDevices() => + (super.noSuchMethod( + Invocation.method(#getInputDevices, []), + returnValue: _i7.Future>.value( + <_i6.AudioInputDevice>[], + ), + ) + as _i7.Future>); + + @override + _i7.Future selectInputDevice(String? deviceId) => + (super.noSuchMethod( + Invocation.method(#selectInputDevice, [deviceId]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future configureAudioProcessing({ + bool? enableNoiseReduction = true, + bool? enableEchoCancellation = true, + double? gainLevel = 1.0, + }) => + (super.noSuchMethod( + Invocation.method(#configureAudioProcessing, [], { + #enableNoiseReduction: enableNoiseReduction, + #enableEchoCancellation: enableEchoCancellation, + #gainLevel: gainLevel, + }), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future setVoiceActivityDetection(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setVoiceActivityDetection, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future setAudioQuality(_i2.AudioQuality? quality) => + (super.noSuchMethod( + Invocation.method(#setAudioQuality, [quality]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future testAudioRecording() => + (super.noSuchMethod( + Invocation.method(#testAudioRecording, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future dispose() => + (super.noSuchMethod( + Invocation.method(#dispose, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); +} + +/// A class which mocks [TranscriptionService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTranscriptionService extends _i1.Mock + implements _i10.TranscriptionService { + MockTranscriptionService() { + _i1.throwOnMissingStub(this); + } + + @override + bool get isInitialized => + (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) + as bool); + + @override + bool get isTranscribing => + (super.noSuchMethod( + Invocation.getter(#isTranscribing), + returnValue: false, + ) + as bool); + + @override + bool get hasPermissions => + (super.noSuchMethod( + Invocation.getter(#hasPermissions), + returnValue: false, + ) + as bool); + + @override + bool get isAvailable => + (super.noSuchMethod(Invocation.getter(#isAvailable), returnValue: false) + as bool); + + @override + String get currentLanguage => + (super.noSuchMethod( + Invocation.getter(#currentLanguage), + returnValue: _i9.dummyValue( + this, + Invocation.getter(#currentLanguage), + ), + ) + as String); + + @override + _i10.TranscriptionBackend get currentBackend => + (super.noSuchMethod( + Invocation.getter(#currentBackend), + returnValue: _i10.TranscriptionBackend.device, + ) + as _i10.TranscriptionBackend); + + @override + _i10.TranscriptionQuality get currentQuality => + (super.noSuchMethod( + Invocation.getter(#currentQuality), + returnValue: _i10.TranscriptionQuality.low, + ) + as _i10.TranscriptionQuality); + + @override + double get vadSensitivity => + (super.noSuchMethod(Invocation.getter(#vadSensitivity), returnValue: 0.0) + as double); + + @override + _i7.Stream<_i3.TranscriptionSegment> get transcriptionStream => + (super.noSuchMethod( + Invocation.getter(#transcriptionStream), + returnValue: _i7.Stream<_i3.TranscriptionSegment>.empty(), + ) + as _i7.Stream<_i3.TranscriptionSegment>); + + @override + _i7.Stream get confidenceStream => + (super.noSuchMethod( + Invocation.getter(#confidenceStream), + returnValue: _i7.Stream.empty(), + ) + as _i7.Stream); + + @override + _i7.Future initialize() => + (super.noSuchMethod( + Invocation.method(#initialize, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future requestPermissions() => + (super.noSuchMethod( + Invocation.method(#requestPermissions, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future startTranscription({ + bool? enableCapitalization = true, + bool? enablePunctuation = true, + String? language, + _i10.TranscriptionBackend? preferredBackend, + }) => + (super.noSuchMethod( + Invocation.method(#startTranscription, [], { + #enableCapitalization: enableCapitalization, + #enablePunctuation: enablePunctuation, + #language: language, + #preferredBackend: preferredBackend, + }), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future stopTranscription() => + (super.noSuchMethod( + Invocation.method(#stopTranscription, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future pauseTranscription() => + (super.noSuchMethod( + Invocation.method(#pauseTranscription, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future resumeTranscription() => + (super.noSuchMethod( + Invocation.method(#resumeTranscription, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future setLanguage(String? languageCode) => + (super.noSuchMethod( + Invocation.method(#setLanguage, [languageCode]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future configureQuality(_i10.TranscriptionQuality? quality) => + (super.noSuchMethod( + Invocation.method(#configureQuality, [quality]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future configureBackend(_i10.TranscriptionBackend? backend) => + (super.noSuchMethod( + Invocation.method(#configureBackend, [backend]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future> getAvailableLanguages() => + (super.noSuchMethod( + Invocation.method(#getAvailableLanguages, []), + returnValue: _i7.Future>.value([]), + ) + as _i7.Future>); + + @override + double getLastConfidence() => + (super.noSuchMethod( + Invocation.method(#getLastConfidence, []), + returnValue: 0.0, + ) + as double); + + @override + _i7.Future<_i3.TranscriptionSegment> transcribeAudio(String? audioPath) => + (super.noSuchMethod( + Invocation.method(#transcribeAudio, [audioPath]), + returnValue: _i7.Future<_i3.TranscriptionSegment>.value( + _FakeTranscriptionSegment_1( + this, + Invocation.method(#transcribeAudio, [audioPath]), + ), + ), + ) + as _i7.Future<_i3.TranscriptionSegment>); + + @override + _i7.Future calibrateVoiceActivity() => + (super.noSuchMethod( + Invocation.method(#calibrateVoiceActivity, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future setVADSensitivity(double? sensitivity) => + (super.noSuchMethod( + Invocation.method(#setVADSensitivity, [sensitivity]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future dispose() => + (super.noSuchMethod( + Invocation.method(#dispose, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); +} + +/// A class which mocks [LLMService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLLMService extends _i1.Mock implements _i11.LLMService { + MockLLMService() { + _i1.throwOnMissingStub(this); + } + + @override + bool get isInitialized => + (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) + as bool); + + @override + _i11.LLMProvider get currentProvider => + (super.noSuchMethod( + Invocation.getter(#currentProvider), + returnValue: _i11.LLMProvider.openai, + ) + as _i11.LLMProvider); + + @override + _i7.Future initialize({ + String? openAIKey, + String? anthropicKey, + _i11.LLMProvider? preferredProvider, + }) => + (super.noSuchMethod( + Invocation.method(#initialize, [], { + #openAIKey: openAIKey, + #anthropicKey: anthropicKey, + #preferredProvider: preferredProvider, + }), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future setProvider(_i11.LLMProvider? provider) => + (super.noSuchMethod( + Invocation.method(#setProvider, [provider]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future<_i4.AnalysisResult> analyzeConversation( + String? conversationText, { + _i4.AnalysisType? type = _i4.AnalysisType.comprehensive, + _i11.AnalysisPriority? priority = _i11.AnalysisPriority.normal, + _i11.LLMProvider? provider, + Map? context, + }) => + (super.noSuchMethod( + Invocation.method( + #analyzeConversation, + [conversationText], + { + #type: type, + #priority: priority, + #provider: provider, + #context: context, + }, + ), + returnValue: _i7.Future<_i4.AnalysisResult>.value( + _FakeAnalysisResult_2( + this, + Invocation.method( + #analyzeConversation, + [conversationText], + { + #type: type, + #priority: priority, + #provider: provider, + #context: context, + }, + ), + ), + ), + ) + as _i7.Future<_i4.AnalysisResult>); + + @override + _i7.Future> checkFacts(List? claims) => + (super.noSuchMethod( + Invocation.method(#checkFacts, [claims]), + returnValue: _i7.Future>.value( + <_i4.FactCheckResult>[], + ), + ) + as _i7.Future>); + + @override + _i7.Future<_i4.ConversationSummary> generateSummary( + _i12.ConversationModel? conversation, { + bool? includeKeyPoints = true, + bool? includeActionItems = true, + int? maxWords = 200, + }) => + (super.noSuchMethod( + Invocation.method( + #generateSummary, + [conversation], + { + #includeKeyPoints: includeKeyPoints, + #includeActionItems: includeActionItems, + #maxWords: maxWords, + }, + ), + returnValue: _i7.Future<_i4.ConversationSummary>.value( + _FakeConversationSummary_3( + this, + Invocation.method( + #generateSummary, + [conversation], + { + #includeKeyPoints: includeKeyPoints, + #includeActionItems: includeActionItems, + #maxWords: maxWords, + }, + ), + ), + ), + ) + as _i7.Future<_i4.ConversationSummary>); + + @override + _i7.Future> extractActionItems( + String? conversationText, { + bool? includeDeadlines = true, + bool? includePriority = true, + }) => + (super.noSuchMethod( + Invocation.method( + #extractActionItems, + [conversationText], + { + #includeDeadlines: includeDeadlines, + #includePriority: includePriority, + }, + ), + returnValue: _i7.Future>.value( + <_i4.ActionItemResult>[], + ), + ) + as _i7.Future>); + + @override + _i7.Future<_i4.SentimentAnalysisResult> analyzeSentiment(String? text) => + (super.noSuchMethod( + Invocation.method(#analyzeSentiment, [text]), + returnValue: _i7.Future<_i4.SentimentAnalysisResult>.value( + _FakeSentimentAnalysisResult_4( + this, + Invocation.method(#analyzeSentiment, [text]), + ), + ), + ) + as _i7.Future<_i4.SentimentAnalysisResult>); + + @override + _i7.Future askQuestion( + String? question, + String? context, { + _i11.LLMProvider? provider, + }) => + (super.noSuchMethod( + Invocation.method( + #askQuestion, + [question, context], + {#provider: provider}, + ), + returnValue: _i7.Future.value( + _i9.dummyValue( + this, + Invocation.method( + #askQuestion, + [question, context], + {#provider: provider}, + ), + ), + ), + ) + as _i7.Future); + + @override + _i7.Future configureAnalysis(_i11.AnalysisConfiguration? config) => + (super.noSuchMethod( + Invocation.method(#configureAnalysis, [config]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future> getUsageStats() => + (super.noSuchMethod( + Invocation.method(#getUsageStats, []), + returnValue: _i7.Future>.value( + {}, + ), + ) + as _i7.Future>); + + @override + _i7.Future clearCache() => + (super.noSuchMethod( + Invocation.method(#clearCache, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future dispose() => + (super.noSuchMethod( + Invocation.method(#dispose, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); +} + +/// A class which mocks [GlassesService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGlassesService extends _i1.Mock implements _i5.GlassesService { + MockGlassesService() { + _i1.throwOnMissingStub(this); + } + + @override + _i13.ConnectionStatus get connectionState => + (super.noSuchMethod( + Invocation.getter(#connectionState), + returnValue: _i13.ConnectionStatus.disconnected, + ) + as _i13.ConnectionStatus); + + @override + bool get isConnected => + (super.noSuchMethod(Invocation.getter(#isConnected), returnValue: false) + as bool); + + @override + _i7.Stream<_i13.ConnectionStatus> get connectionStateStream => + (super.noSuchMethod( + Invocation.getter(#connectionStateStream), + returnValue: _i7.Stream<_i13.ConnectionStatus>.empty(), + ) + as _i7.Stream<_i13.ConnectionStatus>); + + @override + _i7.Stream> get discoveredDevicesStream => + (super.noSuchMethod( + Invocation.getter(#discoveredDevicesStream), + returnValue: _i7.Stream>.empty(), + ) + as _i7.Stream>); + + @override + _i7.Stream<_i5.TouchGesture> get gestureStream => + (super.noSuchMethod( + Invocation.getter(#gestureStream), + returnValue: _i7.Stream<_i5.TouchGesture>.empty(), + ) + as _i7.Stream<_i5.TouchGesture>); + + @override + _i7.Stream<_i5.GlassesDeviceStatus> get deviceStatusStream => + (super.noSuchMethod( + Invocation.getter(#deviceStatusStream), + returnValue: _i7.Stream<_i5.GlassesDeviceStatus>.empty(), + ) + as _i7.Stream<_i5.GlassesDeviceStatus>); + + @override + _i7.Future initialize() => + (super.noSuchMethod( + Invocation.method(#initialize, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future isBluetoothAvailable() => + (super.noSuchMethod( + Invocation.method(#isBluetoothAvailable, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future requestBluetoothPermission() => + (super.noSuchMethod( + Invocation.method(#requestBluetoothPermission, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future startScanning({ + Duration? timeout = const Duration(seconds: 30), + }) => + (super.noSuchMethod( + Invocation.method(#startScanning, [], {#timeout: timeout}), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future stopScanning() => + (super.noSuchMethod( + Invocation.method(#stopScanning, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future connectToDevice(String? deviceId) => + (super.noSuchMethod( + Invocation.method(#connectToDevice, [deviceId]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future connectToLastDevice() => + (super.noSuchMethod( + Invocation.method(#connectToLastDevice, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future disconnect() => + (super.noSuchMethod( + Invocation.method(#disconnect, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future displayText( + String? text, { + _i5.HUDPosition? position = _i5.HUDPosition.center, + Duration? duration, + _i5.HUDStyle? style, + }) => + (super.noSuchMethod( + Invocation.method( + #displayText, + [text], + {#position: position, #duration: duration, #style: style}, + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future displayNotification( + String? title, + String? message, { + _i5.NotificationPriority? priority = _i5.NotificationPriority.normal, + Duration? duration = const Duration(seconds: 5), + }) => + (super.noSuchMethod( + Invocation.method( + #displayNotification, + [title, message], + {#priority: priority, #duration: duration}, + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future clearDisplay() => + (super.noSuchMethod( + Invocation.method(#clearDisplay, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future setBrightness(double? brightness) => + (super.noSuchMethod( + Invocation.method(#setBrightness, [brightness]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future configureGestures({ + bool? enableTap = true, + bool? enableSwipe = true, + bool? enableLongPress = true, + double? sensitivity = 0.5, + }) => + (super.noSuchMethod( + Invocation.method(#configureGestures, [], { + #enableTap: enableTap, + #enableSwipe: enableSwipe, + #enableLongPress: enableLongPress, + #sensitivity: sensitivity, + }), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future sendCommand( + String? command, { + Map? parameters, + }) => + (super.noSuchMethod( + Invocation.method( + #sendCommand, + [command], + {#parameters: parameters}, + ), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future<_i5.GlassesDeviceInfo> getDeviceInfo() => + (super.noSuchMethod( + Invocation.method(#getDeviceInfo, []), + returnValue: _i7.Future<_i5.GlassesDeviceInfo>.value( + _FakeGlassesDeviceInfo_5( + this, + Invocation.method(#getDeviceInfo, []), + ), + ), + ) + as _i7.Future<_i5.GlassesDeviceInfo>); + + @override + _i7.Future getBatteryLevel() => + (super.noSuchMethod( + Invocation.method(#getBatteryLevel, []), + returnValue: _i7.Future.value(0.0), + ) + as _i7.Future); + + @override + _i7.Future<_i5.GlassesHealthStatus> checkDeviceHealth() => + (super.noSuchMethod( + Invocation.method(#checkDeviceHealth, []), + returnValue: _i7.Future<_i5.GlassesHealthStatus>.value( + _FakeGlassesHealthStatus_6( + this, + Invocation.method(#checkDeviceHealth, []), + ), + ), + ) + as _i7.Future<_i5.GlassesHealthStatus>); + + @override + _i7.Future updateFirmware() => + (super.noSuchMethod( + Invocation.method(#updateFirmware, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future dispose() => + (super.noSuchMethod( + Invocation.method(#dispose, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); +} + +/// A class which mocks [SettingsService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockSettingsService extends _i1.Mock implements _i14.SettingsService { + MockSettingsService() { + _i1.throwOnMissingStub(this); + } + + @override + _i7.Stream<_i14.SettingsChangeEvent> get settingsChangeStream => + (super.noSuchMethod( + Invocation.getter(#settingsChangeStream), + returnValue: _i7.Stream<_i14.SettingsChangeEvent>.empty(), + ) + as _i7.Stream<_i14.SettingsChangeEvent>); + + @override + _i7.Future initialize() => + (super.noSuchMethod( + Invocation.method(#initialize, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future<_i14.ThemeMode> getThemeMode() => + (super.noSuchMethod( + Invocation.method(#getThemeMode, []), + returnValue: _i7.Future<_i14.ThemeMode>.value( + _i14.ThemeMode.system, + ), + ) + as _i7.Future<_i14.ThemeMode>); + + @override + _i7.Future setThemeMode(_i14.ThemeMode? mode) => + (super.noSuchMethod( + Invocation.method(#setThemeMode, [mode]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getLanguage() => + (super.noSuchMethod( + Invocation.method(#getLanguage, []), + returnValue: _i7.Future.value( + _i9.dummyValue(this, Invocation.method(#getLanguage, [])), + ), + ) + as _i7.Future); + + @override + _i7.Future setLanguage(String? languageCode) => + (super.noSuchMethod( + Invocation.method(#setLanguage, [languageCode]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future<_i14.PrivacyLevel> getPrivacyLevel() => + (super.noSuchMethod( + Invocation.method(#getPrivacyLevel, []), + returnValue: _i7.Future<_i14.PrivacyLevel>.value( + _i14.PrivacyLevel.minimal, + ), + ) + as _i7.Future<_i14.PrivacyLevel>); + + @override + _i7.Future setPrivacyLevel(_i14.PrivacyLevel? level) => + (super.noSuchMethod( + Invocation.method(#setPrivacyLevel, [level]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getPreferredAudioDevice() => + (super.noSuchMethod( + Invocation.method(#getPreferredAudioDevice, []), + returnValue: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future setPreferredAudioDevice(String? deviceId) => + (super.noSuchMethod( + Invocation.method(#setPreferredAudioDevice, [deviceId]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getAudioQuality() => + (super.noSuchMethod( + Invocation.method(#getAudioQuality, []), + returnValue: _i7.Future.value( + _i9.dummyValue( + this, + Invocation.method(#getAudioQuality, []), + ), + ), + ) + as _i7.Future); + + @override + _i7.Future setAudioQuality(String? quality) => + (super.noSuchMethod( + Invocation.method(#setAudioQuality, [quality]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getNoiseReductionEnabled() => + (super.noSuchMethod( + Invocation.method(#getNoiseReductionEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setNoiseReductionEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setNoiseReductionEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getVADSensitivity() => + (super.noSuchMethod( + Invocation.method(#getVADSensitivity, []), + returnValue: _i7.Future.value(0.0), + ) + as _i7.Future); + + @override + _i7.Future setVADSensitivity(double? sensitivity) => + (super.noSuchMethod( + Invocation.method(#setVADSensitivity, [sensitivity]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getPreferredTranscriptionBackend() => + (super.noSuchMethod( + Invocation.method(#getPreferredTranscriptionBackend, []), + returnValue: _i7.Future.value( + _i9.dummyValue( + this, + Invocation.method(#getPreferredTranscriptionBackend, []), + ), + ), + ) + as _i7.Future); + + @override + _i7.Future setPreferredTranscriptionBackend(String? backend) => + (super.noSuchMethod( + Invocation.method(#setPreferredTranscriptionBackend, [backend]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getTranscriptionLanguage() => + (super.noSuchMethod( + Invocation.method(#getTranscriptionLanguage, []), + returnValue: _i7.Future.value( + _i9.dummyValue( + this, + Invocation.method(#getTranscriptionLanguage, []), + ), + ), + ) + as _i7.Future); + + @override + _i7.Future setTranscriptionLanguage(String? languageCode) => + (super.noSuchMethod( + Invocation.method(#setTranscriptionLanguage, [languageCode]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getAutomaticBackendSwitching() => + (super.noSuchMethod( + Invocation.method(#getAutomaticBackendSwitching, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setAutomaticBackendSwitching(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setAutomaticBackendSwitching, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getPreferredAIProvider() => + (super.noSuchMethod( + Invocation.method(#getPreferredAIProvider, []), + returnValue: _i7.Future.value( + _i9.dummyValue( + this, + Invocation.method(#getPreferredAIProvider, []), + ), + ), + ) + as _i7.Future); + + @override + _i7.Future setPreferredAIProvider(String? provider) => + (super.noSuchMethod( + Invocation.method(#setPreferredAIProvider, [provider]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getAPIKey(String? provider) => + (super.noSuchMethod( + Invocation.method(#getAPIKey, [provider]), + returnValue: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future setAPIKey(String? provider, String? apiKey) => + (super.noSuchMethod( + Invocation.method(#setAPIKey, [provider, apiKey]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future removeAPIKey(String? provider) => + (super.noSuchMethod( + Invocation.method(#removeAPIKey, [provider]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getFactCheckingEnabled() => + (super.noSuchMethod( + Invocation.method(#getFactCheckingEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setFactCheckingEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setFactCheckingEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getRealTimeAnalysisEnabled() => + (super.noSuchMethod( + Invocation.method(#getRealTimeAnalysisEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setRealTimeAnalysisEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setRealTimeAnalysisEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getFactCheckThreshold() => + (super.noSuchMethod( + Invocation.method(#getFactCheckThreshold, []), + returnValue: _i7.Future.value(0.0), + ) + as _i7.Future); + + @override + _i7.Future setFactCheckThreshold(double? threshold) => + (super.noSuchMethod( + Invocation.method(#setFactCheckThreshold, [threshold]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getLastConnectedGlasses() => + (super.noSuchMethod( + Invocation.method(#getLastConnectedGlasses, []), + returnValue: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future setLastConnectedGlasses(String? deviceId) => + (super.noSuchMethod( + Invocation.method(#setLastConnectedGlasses, [deviceId]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getAutoConnectGlasses() => + (super.noSuchMethod( + Invocation.method(#getAutoConnectGlasses, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setAutoConnectGlasses(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setAutoConnectGlasses, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getHUDBrightness() => + (super.noSuchMethod( + Invocation.method(#getHUDBrightness, []), + returnValue: _i7.Future.value(0.0), + ) + as _i7.Future); + + @override + _i7.Future setHUDBrightness(double? brightness) => + (super.noSuchMethod( + Invocation.method(#setHUDBrightness, [brightness]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getGestureSensitivity() => + (super.noSuchMethod( + Invocation.method(#getGestureSensitivity, []), + returnValue: _i7.Future.value(0.0), + ) + as _i7.Future); + + @override + _i7.Future setGestureSensitivity(double? sensitivity) => + (super.noSuchMethod( + Invocation.method(#setGestureSensitivity, [sensitivity]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getDataRetentionDays() => + (super.noSuchMethod( + Invocation.method(#getDataRetentionDays, []), + returnValue: _i7.Future.value(0), + ) + as _i7.Future); + + @override + _i7.Future setDataRetentionDays(int? days) => + (super.noSuchMethod( + Invocation.method(#setDataRetentionDays, [days]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getAutomaticDataCleanup() => + (super.noSuchMethod( + Invocation.method(#getAutomaticDataCleanup, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setAutomaticDataCleanup(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setAutomaticDataCleanup, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getAnalyticsConsent() => + (super.noSuchMethod( + Invocation.method(#getAnalyticsConsent, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setAnalyticsConsent(bool? consent) => + (super.noSuchMethod( + Invocation.method(#setAnalyticsConsent, [consent]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getCrashReportingConsent() => + (super.noSuchMethod( + Invocation.method(#getCrashReportingConsent, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setCrashReportingConsent(bool? consent) => + (super.noSuchMethod( + Invocation.method(#setCrashReportingConsent, [consent]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getCloudSyncEnabled() => + (super.noSuchMethod( + Invocation.method(#getCloudSyncEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setCloudSyncEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setCloudSyncEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getBackupFrequency() => + (super.noSuchMethod( + Invocation.method(#getBackupFrequency, []), + returnValue: _i7.Future.value( + _i9.dummyValue( + this, + Invocation.method(#getBackupFrequency, []), + ), + ), + ) + as _i7.Future); + + @override + _i7.Future setBackupFrequency(String? frequency) => + (super.noSuchMethod( + Invocation.method(#setBackupFrequency, [frequency]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getLargeTextEnabled() => + (super.noSuchMethod( + Invocation.method(#getLargeTextEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setLargeTextEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setLargeTextEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getHighContrastEnabled() => + (super.noSuchMethod( + Invocation.method(#getHighContrastEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setHighContrastEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setHighContrastEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getReducedMotionEnabled() => + (super.noSuchMethod( + Invocation.method(#getReducedMotionEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setReducedMotionEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setReducedMotionEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getDeveloperModeEnabled() => + (super.noSuchMethod( + Invocation.method(#getDeveloperModeEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setDeveloperModeEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setDeveloperModeEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getDebugLoggingEnabled() => + (super.noSuchMethod( + Invocation.method(#getDebugLoggingEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setDebugLoggingEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setDebugLoggingEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future getBetaFeaturesEnabled() => + (super.noSuchMethod( + Invocation.method(#getBetaFeaturesEnabled, []), + returnValue: _i7.Future.value(false), + ) + as _i7.Future); + + @override + _i7.Future setBetaFeaturesEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setBetaFeaturesEnabled, [enabled]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future exportSettings() => + (super.noSuchMethod( + Invocation.method(#exportSettings, []), + returnValue: _i7.Future.value( + _i9.dummyValue( + this, + Invocation.method(#exportSettings, []), + ), + ), + ) + as _i7.Future); + + @override + _i7.Future importSettings(String? settingsJson) => + (super.noSuchMethod( + Invocation.method(#importSettings, [settingsJson]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future resetToDefaults() => + (super.noSuchMethod( + Invocation.method(#resetToDefaults, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future resetCategory(_i14.SettingsCategory? category) => + (super.noSuchMethod( + Invocation.method(#resetCategory, [category]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + _i7.Future> getAllSettings() => + (super.noSuchMethod( + Invocation.method(#getAllSettings, []), + returnValue: _i7.Future>.value( + {}, + ), + ) + as _i7.Future>); + + @override + _i7.Future dispose() => + (super.noSuchMethod( + Invocation.method(#dispose, []), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); +} + +/// A class which mocks [LoggingService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLoggingService extends _i1.Mock implements _i15.LoggingService { + MockLoggingService() { + _i1.throwOnMissingStub(this); + } + + @override + void setLogLevel(_i15.LogLevel? level) => super.noSuchMethod( + Invocation.method(#setLogLevel, [level]), + returnValueForMissingStub: null, + ); + + @override + void log(String? tag, String? message, _i15.LogLevel? level) => + super.noSuchMethod( + Invocation.method(#log, [tag, message, level]), + returnValueForMissingStub: null, + ); + + @override + void debug(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#debug, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void info(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#info, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void warning(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#warning, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void error( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#error, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + void critical( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#critical, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + List<_i15.LogEntry> getRecentLogs([int? limit]) => + (super.noSuchMethod( + Invocation.method(#getRecentLogs, [limit]), + returnValue: <_i15.LogEntry>[], + ) + as List<_i15.LogEntry>); + + @override + void clearLogs() => super.noSuchMethod( + Invocation.method(#clearLogs, []), + returnValueForMissingStub: null, + ); + + @override + _i7.Future enableFileLogging(String? filePath) => + (super.noSuchMethod( + Invocation.method(#enableFileLogging, [filePath]), + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) + as _i7.Future); + + @override + void disableFileLogging() => super.noSuchMethod( + Invocation.method(#disableFileLogging, []), + returnValueForMissingStub: null, + ); + + @override + void enablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#enablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void disablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#disablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void startPerformanceTimer(String? markerId) => super.noSuchMethod( + Invocation.method(#startPerformanceTimer, [markerId]), + returnValueForMissingStub: null, + ); + + @override + void endPerformanceTimer(String? markerId, [String? operation]) => + super.noSuchMethod( + Invocation.method(#endPerformanceTimer, [markerId, operation]), + returnValueForMissingStub: null, + ); + + @override + void addTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#addTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void removeTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#removeTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void clearTagFilters() => super.noSuchMethod( + Invocation.method(#clearTagFilters, []), + returnValueForMissingStub: null, + ); + + @override + void setMessageFilter(String? filter) => super.noSuchMethod( + Invocation.method(#setMessageFilter, [filter]), + returnValueForMissingStub: null, + ); + + @override + List<_i15.LogEntry> getFilteredLogs({ + _i15.LogLevel? minLevel, + String? tag, + DateTime? since, + int? limit, + }) => + (super.noSuchMethod( + Invocation.method(#getFilteredLogs, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + #limit: limit, + }), + returnValue: <_i15.LogEntry>[], + ) + as List<_i15.LogEntry>); + + @override + String exportLogsAsJson({ + _i15.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i9.dummyValue( + this, + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + String exportLogsAsText({ + _i15.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i9.dummyValue( + this, + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + Map getLoggingStats() => + (super.noSuchMethod( + Invocation.method(#getLoggingStats, []), + returnValue: {}, + ) + as Map); +} diff --git a/test/unit/services/audio_service_test.dart b/test/unit/services/audio_service_test.dart new file mode 100644 index 0000000..6671d71 --- /dev/null +++ b/test/unit/services/audio_service_test.dart @@ -0,0 +1,326 @@ +// ABOUTME: Unit tests for AudioService implementation +// ABOUTME: Tests audio recording, processing, and noise reduction functionality + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:fake_async/fake_async.dart'; + +import 'package:flutter_helix/services/implementations/audio_service_impl.dart'; +import 'package:flutter_helix/services/audio_service.dart'; +import 'package:flutter_helix/core/utils/exceptions.dart'; +import '../../test_helpers.dart'; + +void main() { + group('AudioService', () { + late AudioServiceImpl audioService; + late StreamController audioLevelController; + + setUp(() { + audioLevelController = StreamController.broadcast(); + audioService = AudioServiceImpl(); + }); + + tearDown(() { + audioLevelController.close(); + audioService.dispose(); + }); + + group('Initialization', () { + test('should initialize with correct default state', () { + expect(audioService.isRecording, isFalse); + expect(audioService.isPlaying, isFalse); + expect(audioService.currentAudioLevel, equals(0.0)); + }); + + test('should configure audio session on initialization', () async { + // AudioServiceImpl should configure audio session internally + expect(audioService.isInitialized, isTrue); + }); + }); + + group('Recording', () { + test('should start recording with correct configuration', () async { + // Act + await audioService.startRecording(); + + // Assert + expect(audioService.isRecording, isTrue); + expect(audioService.recordingPath, isNotNull); + }); + + test('should stop recording and return file path', () async { + // Arrange + await audioService.startRecording(); + expect(audioService.isRecording, isTrue); + + // Act + final filePath = await audioService.stopRecording(); + + // Assert + expect(audioService.isRecording, isFalse); + expect(filePath, isNotNull); + expect(filePath, isNotEmpty); + }); + + test('should throw exception when starting recording while already recording', () async { + // Arrange + await audioService.startRecording(); + + // Act & Assert + expect( + () async => await audioService.startRecording(), + throwsA(isA()), + ); + }); + + test('should throw exception when stopping recording while not recording', () async { + // Act & Assert + expect( + () async => await audioService.stopRecording(), + throwsA(isA()), + ); + }); + + test('should handle recording errors gracefully', () async { + // This would require mocking the underlying flutter_sound recorder + // For now, we test the error handling structure + expect(audioService.isRecording, isFalse); + }); + }); + + group('Audio Level Monitoring', () { + test('should provide audio level stream during recording', () async { + fakeAsync((async) { + // Arrange + final audioLevels = []; + final subscription = audioService.audioLevelStream.listen( + (level) => audioLevels.add(level), + ); + + // Act + audioService.startRecording(); + async.elapse(const Duration(seconds: 2)); + + // Assert + expect(audioLevels, isNotEmpty); + expect(audioLevels.every((level) => level >= 0.0 && level <= 1.0), isTrue); + + subscription.cancel(); + }); + }); + + test('should emit zero audio level when not recording', () { + // Arrange + double? lastLevel; + final subscription = audioService.audioLevelStream.listen( + (level) => lastLevel = level, + ); + + // Act - not recording + + // Assert + expect(lastLevel ?? 0.0, equals(0.0)); + subscription.cancel(); + }); + }); + + group('Audio Processing', () { + test('should process audio data with noise reduction', () async { + // Arrange + final testAudioData = TestHelpers.createTestAudioData( + durationSeconds: 2, + sampleRate: 16000, + ); + + // Act + final processedData = await audioService.processAudioData( + testAudioData, + enableNoiseReduction: true, + ); + + // Assert + expect(processedData, isNotNull); + expect(processedData.length, equals(testAudioData.length)); + // Processed data should be different from original (noise reduction applied) + expect(processedData, isNot(equals(testAudioData))); + }); + + test('should return original data when noise reduction disabled', () async { + // Arrange + final testAudioData = TestHelpers.createTestAudioData( + durationSeconds: 1, + sampleRate: 16000, + ); + + // Act + final processedData = await audioService.processAudioData( + testAudioData, + enableNoiseReduction: false, + ); + + // Assert + expect(processedData, equals(testAudioData)); + }); + + test('should handle empty audio data', () async { + // Arrange + final emptyData = []; + + // Act + final processedData = await audioService.processAudioData( + emptyData, + enableNoiseReduction: true, + ); + + // Assert + expect(processedData, isEmpty); + }); + }); + + group('Playback', () { + test('should start playback of audio file', () async { + // Arrange + const testFilePath = '/test/path/to/audio.wav'; + + // Act + await audioService.startPlayback(testFilePath); + + // Assert + expect(audioService.isPlaying, isTrue); + }); + + test('should stop playback', () async { + // Arrange + const testFilePath = '/test/path/to/audio.wav'; + await audioService.startPlayback(testFilePath); + expect(audioService.isPlaying, isTrue); + + // Act + await audioService.stopPlayback(); + + // Assert + expect(audioService.isPlaying, isFalse); + }); + + test('should handle playback completion', () async { + fakeAsync((async) { + // Arrange + const testFilePath = '/test/path/to/audio.wav'; + bool playbackCompleted = false; + + audioService.playbackCompleteStream.listen((_) { + playbackCompleted = true; + }); + + // Act + audioService.startPlayback(testFilePath); + async.elapse(const Duration(seconds: 5)); // Simulate playback duration + + // Assert + expect(playbackCompleted, isTrue); + expect(audioService.isPlaying, isFalse); + }); + }); + }); + + group('Audio Quality', () { + test('should configure different quality settings', () async { + // Test high quality + await audioService.setRecordingQuality(AudioQuality.high); + expect(audioService.currentQuality, equals(AudioQuality.high)); + + // Test medium quality + await audioService.setRecordingQuality(AudioQuality.medium); + expect(audioService.currentQuality, equals(AudioQuality.medium)); + + // Test low quality + await audioService.setRecordingQuality(AudioQuality.low); + expect(audioService.currentQuality, equals(AudioQuality.low)); + }); + + test('should use appropriate sample rates for quality settings', () async { + // High quality should use 44.1kHz + await audioService.setRecordingQuality(AudioQuality.high); + expect(audioService.sampleRate, equals(44100)); + + // Medium quality should use 16kHz + await audioService.setRecordingQuality(AudioQuality.medium); + expect(audioService.sampleRate, equals(16000)); + + // Low quality should use 8kHz + await audioService.setRecordingQuality(AudioQuality.low); + expect(audioService.sampleRate, equals(8000)); + }); + }); + + group('Voice Activity Detection', () { + test('should detect voice activity in audio data', () { + // Arrange + final silentData = List.filled(1000, 0); // Silent audio + final loudData = TestHelpers.createTestAudioData(); // Audio with signal + + // Act + final silentVAD = audioService.detectVoiceActivity(silentData); + final loudVAD = audioService.detectVoiceActivity(loudData); + + // Assert + expect(silentVAD, isFalse); + expect(loudVAD, isTrue); + }); + + test('should use configurable VAD threshold', () { + // Arrange + final moderateData = TestHelpers.createTestAudioData(); + + // Test with high threshold (should not detect voice) + audioService.setVADThreshold(0.9); + expect(audioService.detectVoiceActivity(moderateData), isFalse); + + // Test with low threshold (should detect voice) + audioService.setVADThreshold(0.1); + expect(audioService.detectVoiceActivity(moderateData), isTrue); + }); + }); + + group('Resource Management', () { + test('should dispose resources properly', () { + // Arrange + audioService.startRecording(); + + // Act + audioService.dispose(); + + // Assert + expect(audioService.isRecording, isFalse); + expect(audioService.isPlaying, isFalse); + }); + + test('should handle multiple dispose calls safely', () { + // Act & Assert - should not throw + audioService.dispose(); + audioService.dispose(); + audioService.dispose(); + }); + }); + + group('Error Handling', () { + test('should handle microphone permission denied', () async { + // This would require platform-specific mocking + // For now, test the exception structure + expect(() => const AudioException('Permission denied'), + throwsA(isA())); + }); + + test('should handle disk space issues', () async { + expect(() => const AudioException('Insufficient disk space'), + throwsA(isA())); + }); + + test('should handle audio format issues', () async { + expect(() => const AudioException('Unsupported audio format'), + throwsA(isA())); + }); + }); + }); +} \ No newline at end of file diff --git a/test/unit/services/conversation_storage_service_test.dart b/test/unit/services/conversation_storage_service_test.dart new file mode 100644 index 0000000..205bab2 --- /dev/null +++ b/test/unit/services/conversation_storage_service_test.dart @@ -0,0 +1,422 @@ +// ABOUTME: Unit tests for conversation storage service implementations +// ABOUTME: Tests all CRUD operations, search, filtering, and stream functionality + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; + +import '../../../lib/services/conversation_storage_service.dart'; +import '../../../lib/models/conversation_model.dart'; +import '../../../lib/models/transcription_segment.dart'; +import '../../../lib/core/utils/logging_service.dart'; + +import 'conversation_storage_service_test.mocks.dart'; +import '../../test_helpers.dart'; + +@GenerateMocks([LoggingService]) +void main() { + group('InMemoryConversationStorageService', () { + late InMemoryConversationStorageService storageService; + late MockLoggingService mockLogger; + + setUp(() { + mockLogger = MockLoggingService(); + storageService = InMemoryConversationStorageService(logger: mockLogger); + }); + + tearDown(() async { + await storageService.dispose(); + }); + + group('Basic CRUD Operations', () { + test('should start with empty conversations list', () async { + final conversations = await storageService.getAllConversations(); + expect(conversations, isEmpty); + }); + + test('should save and retrieve a conversation', () async { + final conversation = TestHelpers.createSampleConversation(); + + await storageService.saveConversation(conversation); + + final retrieved = await storageService.getConversation(conversation.id); + expect(retrieved, isNotNull); + expect(retrieved!.id, equals(conversation.id)); + expect(retrieved.title, equals(conversation.title)); + }); + + test('should return null for non-existent conversation', () async { + final retrieved = await storageService.getConversation('non-existent'); + expect(retrieved, isNull); + }); + + test('should update existing conversation', () async { + final conversation = TestHelpers.createSampleConversation(); + await storageService.saveConversation(conversation); + + final updatedConversation = conversation.copyWith( + title: 'Updated Title', + lastUpdated: DateTime.now(), + ); + + await storageService.updateConversation(updatedConversation); + + final retrieved = await storageService.getConversation(conversation.id); + expect(retrieved!.title, equals('Updated Title')); + }); + + test('should delete conversation', () async { + final conversation = TestHelpers.createSampleConversation(); + await storageService.saveConversation(conversation); + + await storageService.deleteConversation(conversation.id); + + final retrieved = await storageService.getConversation(conversation.id); + expect(retrieved, isNull); + + final allConversations = await storageService.getAllConversations(); + expect(allConversations, isEmpty); + }); + + test('should replace conversation with same ID when saving', () async { + final conversation = TestHelpers.createSampleConversation(); + await storageService.saveConversation(conversation); + + final updatedConversation = conversation.copyWith( + title: 'New Title', + lastUpdated: DateTime.now(), + ); + + await storageService.saveConversation(updatedConversation); + + final allConversations = await storageService.getAllConversations(); + expect(allConversations, hasLength(1)); + expect(allConversations.first.title, equals('New Title')); + }); + }); + + group('Multiple Conversations', () { + test('should handle multiple conversations', () async { + final conversation1 = TestHelpers.createSampleConversation(id: 'conv1'); + final conversation2 = TestHelpers.createSampleConversation(id: 'conv2'); + final conversation3 = TestHelpers.createSampleConversation(id: 'conv3'); + + await storageService.saveConversation(conversation1); + await storageService.saveConversation(conversation2); + await storageService.saveConversation(conversation3); + + final allConversations = await storageService.getAllConversations(); + expect(allConversations, hasLength(3)); + }); + + test('should sort conversations by start time (newest first)', () async { + final now = DateTime.now(); + final conversation1 = TestHelpers.createSampleConversation( + id: 'conv1', + startTime: now.subtract(const Duration(hours: 2)), + ); + final conversation2 = TestHelpers.createSampleConversation( + id: 'conv2', + startTime: now.subtract(const Duration(hours: 1)), + ); + final conversation3 = TestHelpers.createSampleConversation( + id: 'conv3', + startTime: now, + ); + + // Save in random order + await storageService.saveConversation(conversation1); + await storageService.saveConversation(conversation3); + await storageService.saveConversation(conversation2); + + final allConversations = await storageService.getAllConversations(); + expect(allConversations[0].id, equals('conv3')); // Newest + expect(allConversations[1].id, equals('conv2')); // Middle + expect(allConversations[2].id, equals('conv1')); // Oldest + }); + }); + + group('Search Functionality', () { + late ConversationModel conversation1; + late ConversationModel conversation2; + late ConversationModel conversation3; + + setUp(() async { + conversation1 = TestHelpers.createSampleConversation( + id: 'conv1', + title: 'Team Meeting', + segments: [ + TestHelpers.createSampleSegment(content: 'Let\'s discuss the project'), + TestHelpers.createSampleSegment(content: 'We need to finish by Friday'), + ], + ); + + conversation2 = TestHelpers.createSampleConversation( + id: 'conv2', + title: 'Client Call', + segments: [ + TestHelpers.createSampleSegment(content: 'The client wants changes'), + TestHelpers.createSampleSegment(content: 'Budget approval needed'), + ], + ); + + conversation3 = TestHelpers.createSampleConversation( + id: 'conv3', + title: 'Code Review', + segments: [ + TestHelpers.createSampleSegment(content: 'This function needs optimization'), + TestHelpers.createSampleSegment(content: 'Unit tests are missing'), + ], + ); + + await storageService.saveConversation(conversation1); + await storageService.saveConversation(conversation2); + await storageService.saveConversation(conversation3); + }); + + test('should search conversations by title', () async { + final results = await storageService.searchConversations('Team'); + + expect(results, hasLength(1)); + expect(results.first.id, equals('conv1')); + }); + + test('should search conversations by segment content', () async { + final results = await storageService.searchConversations('client'); + + expect(results, hasLength(1)); + expect(results.first.id, equals('conv2')); + }); + + test('should search conversations by participant name', () async { + final results = await storageService.searchConversations('Alice'); + + expect(results, hasLength(3)); // All conversations have Alice + }); + + test('should return empty results for non-matching query', () async { + final results = await storageService.searchConversations('nonexistent'); + + expect(results, isEmpty); + }); + + test('should be case insensitive', () async { + final results = await storageService.searchConversations('TEAM'); + + expect(results, hasLength(1)); + expect(results.first.id, equals('conv1')); + }); + }); + + group('Date Range Filtering', () { + test('should filter conversations by date range', () async { + final now = DateTime.now(); + final yesterday = now.subtract(const Duration(days: 1)); + final tomorrow = now.add(const Duration(days: 1)); + + final conversation1 = TestHelpers.createSampleConversation( + id: 'conv1', + startTime: yesterday, + ); + final conversation2 = TestHelpers.createSampleConversation( + id: 'conv2', + startTime: now, + ); + final conversation3 = TestHelpers.createSampleConversation( + id: 'conv3', + startTime: tomorrow, + ); + + await storageService.saveConversation(conversation1); + await storageService.saveConversation(conversation2); + await storageService.saveConversation(conversation3); + + final results = await storageService.getConversationsByDateRange( + yesterday.subtract(const Duration(hours: 1)), + now.add(const Duration(hours: 1)), + ); + + expect(results, hasLength(2)); + expect(results.map((c) => c.id), containsAll(['conv1', 'conv2'])); + }); + + test('should return empty results for non-matching date range', () async { + final conversation = TestHelpers.createSampleConversation(); + await storageService.saveConversation(conversation); + + final futureStart = DateTime.now().add(const Duration(days: 1)); + final futureEnd = DateTime.now().add(const Duration(days: 2)); + + final results = await storageService.getConversationsByDateRange( + futureStart, + futureEnd, + ); + + expect(results, isEmpty); + }); + }); + + group('Stream Functionality', () { + test('should emit conversation updates via stream', () async { + final conversation = TestHelpers.createSampleConversation(); + + expectLater( + storageService.conversationStream, + emitsInOrder([ + [conversation], // After save + [], // After delete + ]), + ); + + await storageService.saveConversation(conversation); + await storageService.deleteConversation(conversation.id); + }); + + test('should emit updates when conversation is updated', () async { + final conversation = TestHelpers.createSampleConversation(); + await storageService.saveConversation(conversation); + + final updatedConversation = conversation.copyWith( + title: 'Updated Title', + lastUpdated: DateTime.now(), + ); + + expectLater( + storageService.conversationStream, + emits([updatedConversation]), + ); + + await storageService.updateConversation(updatedConversation); + }); + + test('should handle multiple rapid updates', () async { + final conversation1 = TestHelpers.createSampleConversation(id: 'conv1'); + final conversation2 = TestHelpers.createSampleConversation(id: 'conv2'); + final conversation3 = TestHelpers.createSampleConversation(id: 'conv3'); + + // Save multiple conversations rapidly + await storageService.saveConversation(conversation1); + await storageService.saveConversation(conversation2); + await storageService.saveConversation(conversation3); + + final allConversations = await storageService.getAllConversations(); + expect(allConversations, hasLength(3)); + }); + }); + + group('Error Handling', () { + test('should handle update of non-existent conversation gracefully', () async { + final conversation = TestHelpers.createSampleConversation(); + + // Should not throw error + await storageService.updateConversation(conversation); + + final retrieved = await storageService.getConversation(conversation.id); + expect(retrieved, isNull); + }); + + test('should handle delete of non-existent conversation gracefully', () async { + // Should not throw error + await storageService.deleteConversation('non-existent'); + + final allConversations = await storageService.getAllConversations(); + expect(allConversations, isEmpty); + }); + + test('should handle empty search query', () async { + final conversation = TestHelpers.createSampleConversation(); + await storageService.saveConversation(conversation); + + final results = await storageService.searchConversations(''); + expect(results, hasLength(1)); + }); + }); + + group('Logging', () { + test('should log save operations', () async { + final conversation = TestHelpers.createSampleConversation(); + + await storageService.saveConversation(conversation); + + verify(mockLogger.log( + 'InMemoryConversationStorageService', + 'Saving conversation: ${conversation.id}', + LogLevel.info, + )).called(1); + }); + + test('should log delete operations', () async { + const conversationId = 'test-id'; + + await storageService.deleteConversation(conversationId); + + verify(mockLogger.log( + 'InMemoryConversationStorageService', + 'Deleting conversation: $conversationId', + LogLevel.info, + )).called(1); + }); + + test('should log search operations', () async { + const query = 'test query'; + + await storageService.searchConversations(query); + + verify(mockLogger.log( + 'InMemoryConversationStorageService', + 'Searching conversations: $query', + LogLevel.debug, + )).called(1); + }); + }); + + group('Performance', () { + test('should handle large number of conversations efficiently', () async { + // Create 1000 conversations + final conversations = List.generate(1000, (index) => + TestHelpers.createSampleConversation(id: 'conv_$index'), + ); + + // Measure save time + final stopwatch = Stopwatch()..start(); + + for (final conversation in conversations) { + await storageService.saveConversation(conversation); + } + + stopwatch.stop(); + + // Should complete within reasonable time (adjust as needed) + expect(stopwatch.elapsedMilliseconds, lessThan(5000)); + + final allConversations = await storageService.getAllConversations(); + expect(allConversations, hasLength(1000)); + }); + + test('should handle search on large dataset efficiently', () async { + // Create 100 conversations with searchable content + final conversations = List.generate(100, (index) => + TestHelpers.createSampleConversation( + id: 'conv_$index', + title: index % 10 == 0 ? 'Special Meeting $index' : 'Regular Meeting $index', + ), + ); + + for (final conversation in conversations) { + await storageService.saveConversation(conversation); + } + + // Measure search time + final stopwatch = Stopwatch()..start(); + + final results = await storageService.searchConversations('Special'); + + stopwatch.stop(); + + // Should complete within reasonable time + expect(stopwatch.elapsedMilliseconds, lessThan(100)); + expect(results, hasLength(10)); // 10 special meetings + }); + }); + }); +} \ No newline at end of file diff --git a/test/unit/services/conversation_storage_service_test.mocks.dart b/test/unit/services/conversation_storage_service_test.mocks.dart new file mode 100644 index 0000000..4482452 --- /dev/null +++ b/test/unit/services/conversation_storage_service_test.mocks.dart @@ -0,0 +1,236 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in flutter_helix/test/unit/services/conversation_storage_service_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:flutter_helix/core/utils/logging_service.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [LoggingService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLoggingService extends _i1.Mock implements _i2.LoggingService { + MockLoggingService() { + _i1.throwOnMissingStub(this); + } + + @override + void setLogLevel(_i2.LogLevel? level) => super.noSuchMethod( + Invocation.method(#setLogLevel, [level]), + returnValueForMissingStub: null, + ); + + @override + void log(String? tag, String? message, _i2.LogLevel? level) => + super.noSuchMethod( + Invocation.method(#log, [tag, message, level]), + returnValueForMissingStub: null, + ); + + @override + void debug(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#debug, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void info(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#info, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void warning(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#warning, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void error( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#error, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + void critical( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#critical, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + List<_i2.LogEntry> getRecentLogs([int? limit]) => + (super.noSuchMethod( + Invocation.method(#getRecentLogs, [limit]), + returnValue: <_i2.LogEntry>[], + ) + as List<_i2.LogEntry>); + + @override + void clearLogs() => super.noSuchMethod( + Invocation.method(#clearLogs, []), + returnValueForMissingStub: null, + ); + + @override + _i3.Future enableFileLogging(String? filePath) => + (super.noSuchMethod( + Invocation.method(#enableFileLogging, [filePath]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) + as _i3.Future); + + @override + void disableFileLogging() => super.noSuchMethod( + Invocation.method(#disableFileLogging, []), + returnValueForMissingStub: null, + ); + + @override + void enablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#enablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void disablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#disablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void startPerformanceTimer(String? markerId) => super.noSuchMethod( + Invocation.method(#startPerformanceTimer, [markerId]), + returnValueForMissingStub: null, + ); + + @override + void endPerformanceTimer(String? markerId, [String? operation]) => + super.noSuchMethod( + Invocation.method(#endPerformanceTimer, [markerId, operation]), + returnValueForMissingStub: null, + ); + + @override + void addTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#addTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void removeTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#removeTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void clearTagFilters() => super.noSuchMethod( + Invocation.method(#clearTagFilters, []), + returnValueForMissingStub: null, + ); + + @override + void setMessageFilter(String? filter) => super.noSuchMethod( + Invocation.method(#setMessageFilter, [filter]), + returnValueForMissingStub: null, + ); + + @override + List<_i2.LogEntry> getFilteredLogs({ + _i2.LogLevel? minLevel, + String? tag, + DateTime? since, + int? limit, + }) => + (super.noSuchMethod( + Invocation.method(#getFilteredLogs, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + #limit: limit, + }), + returnValue: <_i2.LogEntry>[], + ) + as List<_i2.LogEntry>); + + @override + String exportLogsAsJson({ + _i2.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i4.dummyValue( + this, + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + String exportLogsAsText({ + _i2.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i4.dummyValue( + this, + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + Map getLoggingStats() => + (super.noSuchMethod( + Invocation.method(#getLoggingStats, []), + returnValue: {}, + ) + as Map); +} diff --git a/test/unit/services/glasses_service_test.dart b/test/unit/services/glasses_service_test.dart new file mode 100644 index 0000000..a6750ac --- /dev/null +++ b/test/unit/services/glasses_service_test.dart @@ -0,0 +1,103 @@ +// ABOUTME: Unit tests for GlassesService implementation +// ABOUTME: Tests basic functionality and error handling for smart glasses service + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; + +import 'package:flutter_helix/services/implementations/glasses_service_impl.dart'; +import 'package:flutter_helix/services/glasses_service.dart'; +import 'package:flutter_helix/models/glasses_connection_state.dart'; +import 'package:flutter_helix/core/utils/logging_service.dart'; + +// Generate mocks for this test +@GenerateMocks([LoggingService]) +import 'glasses_service_test.mocks.dart'; + +void main() { + group('GlassesService', () { + late GlassesServiceImpl glassesService; + late MockLoggingService mockLogger; + + setUp(() { + mockLogger = MockLoggingService(); + glassesService = GlassesServiceImpl(logger: mockLogger); + }); + + tearDown(() { + glassesService.dispose(); + }); + + group('Initialization', () { + test('should initialize with disconnected state', () { + expect(glassesService.connectionState, equals(ConnectionStatus.disconnected)); + expect(glassesService.isConnected, isFalse); + expect(glassesService.connectedDevice, isNull); + }); + + test('should check Bluetooth availability', () async { + final isAvailable = await glassesService.isBluetoothAvailable(); + expect(isAvailable, isA()); + }); + + test('should request Bluetooth permission', () async { + final hasPermission = await glassesService.requestBluetoothPermission(); + expect(hasPermission, isA()); + }); + }); + + group('Error Handling', () { + test('should handle service not initialized error', () async { + expect( + () async => await glassesService.startScanning(), + throwsA(isA()), + ); + }); + + test('should handle firmware update when not connected', () async { + expect( + () async => await glassesService.updateFirmware(), + throwsA(isA()), + ); + }); + + test('should handle HUD commands when not connected', () async { + expect( + () async => await glassesService.displayText('Test'), + throwsA(isA()), + ); + }); + + test('should handle disconnection', () async { + await glassesService.disconnect(); + expect(glassesService.connectionState, equals(ConnectionStatus.disconnected)); + }); + }); + + group('Streams', () { + test('should provide connection state stream', () { + expect(glassesService.connectionStateStream, isA>()); + }); + + test('should provide discovered devices stream', () { + expect(glassesService.discoveredDevicesStream, isA>>()); + }); + + test('should provide gesture stream', () { + expect(glassesService.gestureStream, isA>()); + }); + + test('should provide device status stream', () { + expect(glassesService.deviceStatusStream, isA>()); + }); + }); + + group('Resource Management', () { + test('should dispose resources properly', () async { + await glassesService.dispose(); + expect(glassesService.connectionState, equals(ConnectionStatus.disconnected)); + }); + }); + }); +} \ No newline at end of file diff --git a/test/unit/services/glasses_service_test.mocks.dart b/test/unit/services/glasses_service_test.mocks.dart new file mode 100644 index 0000000..6b148ad --- /dev/null +++ b/test/unit/services/glasses_service_test.mocks.dart @@ -0,0 +1,236 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in flutter_helix/test/unit/services/glasses_service_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:flutter_helix/core/utils/logging_service.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [LoggingService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLoggingService extends _i1.Mock implements _i2.LoggingService { + MockLoggingService() { + _i1.throwOnMissingStub(this); + } + + @override + void setLogLevel(_i2.LogLevel? level) => super.noSuchMethod( + Invocation.method(#setLogLevel, [level]), + returnValueForMissingStub: null, + ); + + @override + void log(String? tag, String? message, _i2.LogLevel? level) => + super.noSuchMethod( + Invocation.method(#log, [tag, message, level]), + returnValueForMissingStub: null, + ); + + @override + void debug(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#debug, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void info(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#info, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void warning(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#warning, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void error( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#error, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + void critical( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#critical, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + List<_i2.LogEntry> getRecentLogs([int? limit]) => + (super.noSuchMethod( + Invocation.method(#getRecentLogs, [limit]), + returnValue: <_i2.LogEntry>[], + ) + as List<_i2.LogEntry>); + + @override + void clearLogs() => super.noSuchMethod( + Invocation.method(#clearLogs, []), + returnValueForMissingStub: null, + ); + + @override + _i3.Future enableFileLogging(String? filePath) => + (super.noSuchMethod( + Invocation.method(#enableFileLogging, [filePath]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) + as _i3.Future); + + @override + void disableFileLogging() => super.noSuchMethod( + Invocation.method(#disableFileLogging, []), + returnValueForMissingStub: null, + ); + + @override + void enablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#enablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void disablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#disablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void startPerformanceTimer(String? markerId) => super.noSuchMethod( + Invocation.method(#startPerformanceTimer, [markerId]), + returnValueForMissingStub: null, + ); + + @override + void endPerformanceTimer(String? markerId, [String? operation]) => + super.noSuchMethod( + Invocation.method(#endPerformanceTimer, [markerId, operation]), + returnValueForMissingStub: null, + ); + + @override + void addTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#addTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void removeTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#removeTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void clearTagFilters() => super.noSuchMethod( + Invocation.method(#clearTagFilters, []), + returnValueForMissingStub: null, + ); + + @override + void setMessageFilter(String? filter) => super.noSuchMethod( + Invocation.method(#setMessageFilter, [filter]), + returnValueForMissingStub: null, + ); + + @override + List<_i2.LogEntry> getFilteredLogs({ + _i2.LogLevel? minLevel, + String? tag, + DateTime? since, + int? limit, + }) => + (super.noSuchMethod( + Invocation.method(#getFilteredLogs, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + #limit: limit, + }), + returnValue: <_i2.LogEntry>[], + ) + as List<_i2.LogEntry>); + + @override + String exportLogsAsJson({ + _i2.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i4.dummyValue( + this, + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + String exportLogsAsText({ + _i2.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i4.dummyValue( + this, + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + Map getLoggingStats() => + (super.noSuchMethod( + Invocation.method(#getLoggingStats, []), + returnValue: {}, + ) + as Map); +} diff --git a/test/unit/services/llm_service_test.dart b/test/unit/services/llm_service_test.dart new file mode 100644 index 0000000..33c7d0c --- /dev/null +++ b/test/unit/services/llm_service_test.dart @@ -0,0 +1,533 @@ +// ABOUTME: Unit tests for LLMService implementation +// ABOUTME: Tests AI analysis, fact-checking, sentiment analysis, and API integration + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:dio/dio.dart'; + +import 'package:flutter_helix/services/implementations/llm_service_impl.dart'; +import 'package:flutter_helix/services/llm_service.dart'; +import 'package:flutter_helix/models/analysis_result.dart'; +import 'package:flutter_helix/core/utils/exceptions.dart'; +import '../../test_helpers.dart'; + +// Mock Dio for API testing +class MockDio extends Mock implements Dio {} +class MockResponse extends Mock implements Response {} + +void main() { + group('LLMService', () { + late LLMServiceImpl llmService; + late MockDio mockDio; + + setUp(() { + mockDio = MockDio(); + llmService = LLMServiceImpl(dio: mockDio); + }); + + tearDown(() { + llmService.dispose(); + }); + + group('Initialization', () { + test('should initialize with default OpenAI provider', () { + expect(llmService.currentProvider, equals(LLMProvider.openai)); + expect(llmService.isInitialized, isTrue); + }); + + test('should switch between providers', () { + // Test OpenAI + llmService.setProvider(LLMProvider.openai); + expect(llmService.currentProvider, equals(LLMProvider.openai)); + + // Test Anthropic + llmService.setProvider(LLMProvider.anthropic); + expect(llmService.currentProvider, equals(LLMProvider.anthropic)); + }); + + test('should validate API keys for different providers', () { + // Valid OpenAI key + expect(llmService.isValidAPIKey(TestHelpers.testOpenAIKey, LLMProvider.openai), isTrue); + + // Valid Anthropic key + expect(llmService.isValidAPIKey(TestHelpers.testAnthropicKey, LLMProvider.anthropic), isTrue); + + // Invalid keys + expect(llmService.isValidAPIKey('invalid-key', LLMProvider.openai), isFalse); + expect(llmService.isValidAPIKey('wrong-prefix', LLMProvider.anthropic), isFalse); + }); + }); + + group('Conversation Analysis', () { + test('should analyze conversation with comprehensive analysis', () async { + // Arrange + const conversationText = 'We discussed the quarterly budget and decided to increase marketing spend by 20%.'; + + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{ + 'message': { + 'content': ''' + { + "summary": "Team discussed quarterly budget allocation", + "keyPoints": ["Budget discussion", "Marketing increase"], + "factChecks": [], + "actionItems": [], + "sentiment": "positive", + "confidence": 0.89 + } + ''' + } + }] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act + final result = await llmService.analyzeConversation( + conversationText, + type: AnalysisType.comprehensive, + ); + + // Assert + expect(result, isA()); + expect(result.summary, contains('budget')); + expect(result.confidence, greaterThan(0.8)); + }); + + test('should handle different analysis types', () async { + const conversationText = 'The product launch went well. Sales exceeded expectations.'; + + // Mock response for fact-checking only + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{ + 'message': {'content': '{"factChecks": [], "confidence": 0.85}'} + }] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Test fact-checking analysis + final factCheckResult = await llmService.analyzeConversation( + conversationText, + type: AnalysisType.factChecking, + ); + + expect(factCheckResult, isA()); + }); + + test('should cache analysis results for identical inputs', () async { + // Arrange + const conversationText = 'Test conversation for caching'; + + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{ + 'message': {'content': '{"summary": "Test", "confidence": 0.9}'} + }] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act - First call + final result1 = await llmService.analyzeConversation(conversationText); + + // Act - Second call (should use cache) + final result2 = await llmService.analyzeConversation(conversationText); + + // Assert + expect(result1.summary, equals(result2.summary)); + verify(mockDio.post(any, data: any, options: any)).called(1); // Only one API call + }); + }); + + group('Fact Checking', () { + test('should extract and verify factual claims', () async { + // Arrange + const conversationText = 'The iPhone was first released in 2007 and changed the smartphone industry.'; + + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{ + 'message': { + 'content': ''' + { + "factChecks": [{ + "claim": "The iPhone was first released in 2007", + "status": "verified", + "confidence": 0.98, + "sources": ["Apple Inc.", "Wikipedia"], + "explanation": "Apple announced the iPhone on January 9, 2007" + }], + "confidence": 0.95 + } + ''' + } + }] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act + final factChecks = await llmService.checkFacts(conversationText); + + // Assert + expect(factChecks, isNotEmpty); + expect(factChecks.first.claim, contains('iPhone')); + expect(factChecks.first.status, equals(FactCheckStatus.verified)); + expect(factChecks.first.confidence, greaterThan(0.9)); + }); + + test('should handle disputed claims', () async { + // Arrange + const conversationText = 'Electric cars produce zero emissions whatsoever.'; + + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{ + 'message': { + 'content': ''' + { + "factChecks": [{ + "claim": "Electric cars produce zero emissions whatsoever", + "status": "disputed", + "confidence": 0.82, + "sources": ["EPA", "Scientific studies"], + "explanation": "Electric cars produce no direct emissions but electricity generation may create emissions" + }] + } + ''' + } + }] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act + final factChecks = await llmService.checkFacts(conversationText); + + // Assert + expect(factChecks.first.status, equals(FactCheckStatus.disputed)); + expect(factChecks.first.explanation, isNotEmpty); + }); + }); + + group('Sentiment Analysis', () { + test('should analyze positive sentiment', () async { + // Arrange + const conversationText = 'I am extremely happy with the results! This is fantastic news.'; + + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{ + 'message': { + 'content': ''' + { + "sentiment": { + "overallSentiment": "positive", + "confidence": 0.94, + "emotions": { + "happiness": 0.9, + "excitement": 0.8, + "satisfaction": 0.85 + } + } + } + ''' + } + }] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act + final sentiment = await llmService.analyzeSentiment(conversationText); + + // Assert + expect(sentiment.overallSentiment, equals(SentimentType.positive)); + expect(sentiment.confidence, greaterThan(0.9)); + expect(sentiment.emotions['happiness'], greaterThan(0.8)); + }); + + test('should analyze negative sentiment', () async { + // Arrange + const conversationText = 'This is disappointing. I am very frustrated with these results.'; + + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{ + 'message': { + 'content': ''' + { + "sentiment": { + "overallSentiment": "negative", + "confidence": 0.88, + "emotions": { + "frustration": 0.85, + "disappointment": 0.9, + "anger": 0.4 + } + } + } + ''' + } + }] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act + final sentiment = await llmService.analyzeSentiment(conversationText); + + // Assert + expect(sentiment.overallSentiment, equals(SentimentType.negative)); + expect(sentiment.emotions['frustration'], greaterThan(0.8)); + }); + }); + + group('Action Item Extraction', () { + test('should extract action items with priorities and assignments', () async { + // Arrange + const conversationText = ''' + We need to review the budget by Friday. John should prepare the presentation for next week's board meeting. + Someone needs to follow up with the client about their requirements. + '''; + + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{ + 'message': { + 'content': ''' + { + "actionItems": [ + { + "id": "action-1", + "description": "Review the budget", + "dueDate": "2024-01-26T17:00:00Z", + "priority": "high", + "confidence": 0.92, + "status": "pending" + }, + { + "id": "action-2", + "description": "Prepare presentation for board meeting", + "assignee": "John", + "priority": "medium", + "confidence": 0.89, + "status": "pending" + } + ] + } + ''' + } + }] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act + final actionItems = await llmService.extractActionItems(conversationText); + + // Assert + expect(actionItems.length, equals(2)); + expect(actionItems.first.description, contains('budget')); + expect(actionItems.first.priority, equals(ActionItemPriority.high)); + expect(actionItems[1].assignee, equals('John')); + }); + }); + + group('API Error Handling', () { + test('should handle API rate limiting', () async { + // Arrange + when(mockDio.post(any, data: any, options: any)) + .thenThrow(DioException( + requestOptions: RequestOptions(path: '/api'), + response: Response( + statusCode: 429, + requestOptions: RequestOptions(path: '/api'), + data: {'error': 'Rate limit exceeded'}, + ), + )); + + // Act & Assert + expect( + () async => await llmService.analyzeConversation('test'), + throwsA(isA()), + ); + }); + + test('should handle invalid API key', () async { + // Arrange + when(mockDio.post(any, data: any, options: any)) + .thenThrow(DioException( + requestOptions: RequestOptions(path: '/api'), + response: Response( + statusCode: 401, + requestOptions: RequestOptions(path: '/api'), + data: {'error': 'Invalid API key'}, + ), + )); + + // Act & Assert + expect( + () async => await llmService.analyzeConversation('test'), + throwsA(isA()), + ); + }); + + test('should handle network connectivity issues', () async { + // Arrange + when(mockDio.post(any, data: any, options: any)) + .thenThrow(DioException( + requestOptions: RequestOptions(path: '/api'), + type: DioExceptionType.connectionTimeout, + message: 'Connection timeout', + )); + + // Act & Assert + expect( + () async => await llmService.analyzeConversation('test'), + throwsA(isA()), + ); + }); + + test('should handle malformed API responses', () async { + // Arrange + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({'invalid': 'response'}); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act & Assert + expect( + () async => await llmService.analyzeConversation('test'), + throwsA(isA()), + ); + }); + }); + + group('Performance Optimization', () { + test('should respect rate limiting', () async { + // Arrange + final startTime = DateTime.now(); + + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{'message': {'content': '{"summary": "test"}'}}] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act - Multiple rapid requests + final futures = List.generate(5, (index) => + llmService.analyzeConversation('test conversation $index') + ); + + await Future.wait(futures); + + final endTime = DateTime.now(); + final duration = endTime.difference(startTime); + + // Assert - Should take some time due to rate limiting + expect(duration.inMilliseconds, greaterThan(100)); + }); + + test('should handle large conversation texts efficiently', () async { + // Arrange + final largeText = List.generate(1000, (index) => 'Word $index').join(' '); + + final mockResponse = MockResponse(); + when(mockResponse.statusCode).thenReturn(200); + when(mockResponse.data).thenReturn({ + 'choices': [{'message': {'content': '{"summary": "Large text analysis"}'}}] + }); + + when(mockDio.post(any, data: any, options: any)) + .thenAnswer((_) async => mockResponse); + + // Act + final startTime = DateTime.now(); + final result = await llmService.analyzeConversation(largeText); + final endTime = DateTime.now(); + + // Assert + expect(result, isA()); + expect(endTime.difference(startTime).inSeconds, lessThan(30)); + }); + }); + + group('Configuration', () { + test('should configure analysis parameters', () { + // Test confidence threshold + llmService.setConfidenceThreshold(0.8); + expect(llmService.confidenceThreshold, equals(0.8)); + + // Test temperature setting + llmService.setTemperature(0.7); + expect(llmService.temperature, equals(0.7)); + + // Test max tokens + llmService.setMaxTokens(2000); + expect(llmService.maxTokens, equals(2000)); + }); + + test('should validate configuration parameters', () { + // Invalid confidence threshold + expect(() => llmService.setConfidenceThreshold(1.5), throwsArgumentError); + expect(() => llmService.setConfidenceThreshold(-0.1), throwsArgumentError); + + // Invalid temperature + expect(() => llmService.setTemperature(2.5), throwsArgumentError); + expect(() => llmService.setTemperature(-0.1), throwsArgumentError); + + // Invalid max tokens + expect(() => llmService.setMaxTokens(-100), throwsArgumentError); + }); + }); + + group('Resource Management', () { + test('should dispose resources properly', () { + // Arrange + llmService.analyzeConversation('test'); // Start some operation + + // Act + llmService.dispose(); + + // Assert + expect(llmService.isDisposed, isTrue); + }); + + test('should clear cache on demand', () { + // Arrange - Assume cache has entries (would be set by previous operations) + + // Act + llmService.clearCache(); + + // Assert - Cache should be empty (implementation-specific verification) + expect(llmService.cacheSize, equals(0)); + }); + }); + }); +} \ No newline at end of file diff --git a/test/unit/services/real_time_transcription_service_test.dart b/test/unit/services/real_time_transcription_service_test.dart new file mode 100644 index 0000000..c42845a --- /dev/null +++ b/test/unit/services/real_time_transcription_service_test.dart @@ -0,0 +1,522 @@ +// ABOUTME: Unit tests for RealTimeTranscriptionService implementation +// ABOUTME: Tests real-time transcription pipeline, performance monitoring, and memory management + +import 'dart:async'; +import 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mockito/annotations.dart'; + +import 'package:flutter_helix/services/real_time_transcription_service.dart'; +import 'package:flutter_helix/services/audio_service.dart'; +import 'package:flutter_helix/services/transcription_service.dart'; +import 'package:flutter_helix/models/transcription_segment.dart'; +import 'package:flutter_helix/core/utils/logging_service.dart'; + +@GenerateMocks([AudioService, TranscriptionService, LoggingService]) +import 'real_time_transcription_service_test.mocks.dart'; + +void main() { + group('RealTimeTranscriptionService', () { + late RealTimeTranscriptionServiceImpl service; + late MockAudioService mockAudioService; + late MockTranscriptionService mockTranscriptionService; + late MockLoggingService mockLoggingService; + + late StreamController audioStreamController; + late StreamController transcriptionStreamController; + + setUp(() { + mockAudioService = MockAudioService(); + mockTranscriptionService = MockTranscriptionService(); + mockLoggingService = MockLoggingService(); + + audioStreamController = StreamController.broadcast(); + transcriptionStreamController = StreamController.broadcast(); + + // Setup mock streams + when(mockAudioService.audioStream).thenAnswer((_) => audioStreamController.stream); + when(mockTranscriptionService.transcriptionStream).thenAnswer((_) => transcriptionStreamController.stream); + + // Setup mock properties + when(mockAudioService.hasPermission).thenReturn(true); + when(mockTranscriptionService.isInitialized).thenReturn(true); + + service = RealTimeTranscriptionServiceImpl( + logger: mockLoggingService, + audioService: mockAudioService, + transcriptionService: mockTranscriptionService, + ); + }); + + tearDown(() async { + await audioStreamController.close(); + await transcriptionStreamController.close(); + await service.dispose(); + }); + + group('Initialization', () { + test('should initialize with correct default state', () { + expect(service.state, equals(TranscriptionPipelineState.idle)); + expect(service.isActive, isFalse); + }); + + test('should initialize with custom configuration', () async { + const config = TranscriptionPipelineConfig( + audioChunkDurationMs: 50, + targetLatencyMs: 300, + enablePartialResults: true, + ); + + await service.initialize(config); + + expect(service.config.audioChunkDurationMs, equals(50)); + expect(service.config.targetLatencyMs, equals(300)); + expect(service.config.enablePartialResults, isTrue); + }); + + test('should fail initialization if audio permission denied', () async { + when(mockAudioService.hasPermission).thenReturn(false); + when(mockAudioService.requestPermission()).thenAnswer((_) async => false); + + const config = TranscriptionPipelineConfig(); + + expect( + () async => await service.initialize(config), + throwsA(isA()), + ); + }); + + test('should fail initialization if transcription service not available', () async { + when(mockTranscriptionService.isInitialized).thenReturn(false); + when(mockTranscriptionService.initialize()).thenThrow(Exception('Service not available')); + + const config = TranscriptionPipelineConfig(); + + expect( + () async => await service.initialize(config), + throwsA(isA()), + ); + }); + }); + + group('State Management', () { + test('should transition states correctly during transcription lifecycle', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + expect(service.state, equals(TranscriptionPipelineState.idle)); + + // Mock successful transcription start + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + expect(service.state, equals(TranscriptionPipelineState.active)); + expect(service.isActive, isTrue); + + // Mock successful transcription stop + when(mockTranscriptionService.stopTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.stopRecording()).thenAnswer((_) => Future.value()); + + await service.stopTranscription(); + expect(service.state, equals(TranscriptionPipelineState.idle)); + expect(service.isActive, isFalse); + }); + + test('should emit state changes via stream', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + final stateChanges = []; + final subscription = service.stateStream.listen((state) { + stateChanges.add(state); + }); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + expect(stateChanges, contains(TranscriptionPipelineState.active)); + + await subscription.cancel(); + }); + }); + + group('Transcription Processing', () { + test('should process final transcription segments', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + final receivedSegments = []; + final subscription = service.transcriptionStream.listen((segment) { + receivedSegments.add(segment); + }); + + // Simulate transcription result + final testSegment = TranscriptionSegment( + text: 'hello world', + startTime: DateTime.now(), + endTime: DateTime.now().add(const Duration(seconds: 1)), + confidence: 0.95, + isFinal: true, + ); + + transcriptionStreamController.add(testSegment); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(receivedSegments, hasLength(1)); + expect(receivedSegments.first.text, equals('Hello world.')); // Should be capitalized and punctuated + expect(receivedSegments.first.confidence, equals(0.95)); + + await subscription.cancel(); + }); + + test('should process partial transcription segments', () async { + const config = TranscriptionPipelineConfig(enablePartialResults: true); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + final receivedPartials = []; + final subscription = service.partialTranscriptionStream.listen((segment) { + receivedPartials.add(segment); + }); + + // Simulate partial transcription result + final partialSegment = TranscriptionSegment( + text: 'hello wor', + startTime: DateTime.now(), + endTime: DateTime.now().add(const Duration(milliseconds: 500)), + confidence: 0.7, + isFinal: false, + ); + + transcriptionStreamController.add(partialSegment); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(receivedPartials, hasLength(1)); + expect(receivedPartials.first.text, equals('Hello wor...')); // Should have ellipsis + expect(receivedPartials.first.isFinal, isFalse); + + await subscription.cancel(); + }); + + test('should enhance text with sentence completion and punctuation', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + final receivedSegments = []; + final subscription = service.transcriptionStream.listen((segment) { + receivedSegments.add(segment); + }); + + // Test various text processing scenarios + final testCases = [ + ('hello world', 'Hello world.'), + ('how are you', 'How are you.'), + ('good morning!', 'Good morning!'), // Already has punctuation + ('yes', 'Yes'), // Too short for period + ]; + + for (final (input, expected) in testCases) { + final segment = TranscriptionSegment( + text: input, + startTime: DateTime.now(), + endTime: DateTime.now().add(const Duration(seconds: 1)), + confidence: 0.9, + isFinal: true, + ); + + transcriptionStreamController.add(segment); + await Future.delayed(const Duration(milliseconds: 50)); + } + + expect(receivedSegments, hasLength(4)); + expect(receivedSegments[0].text, equals('Hello world.')); + expect(receivedSegments[1].text, equals('How are you.')); + expect(receivedSegments[2].text, equals('Good morning!')); + expect(receivedSegments[3].text, equals('Yes')); + + await subscription.cancel(); + }); + }); + + group('Performance Monitoring', () { + test('should track latency measurements', () async { + const config = TranscriptionPipelineConfig(targetLatencyMs: 500); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + final latencies = []; + final subscription = service.latencyStream.listen((latency) { + latencies.add(latency); + }); + + // Simulate audio chunk processing + audioStreamController.add(Uint8List.fromList([1, 2, 3, 4])); + await Future.delayed(const Duration(milliseconds: 100)); + + // Simulate transcription result + final segment = TranscriptionSegment( + text: 'test', + startTime: DateTime.now(), + endTime: DateTime.now(), + confidence: 0.8, + isFinal: true, + ); + + transcriptionStreamController.add(segment); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(latencies, isNotEmpty); + expect(latencies.first, greaterThan(0)); + + await subscription.cancel(); + }); + + test('should provide performance metrics', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + final metrics = service.getPerformanceMetrics(); + + expect(metrics, isA>()); + expect(metrics.keys, containsAll([ + 'sessionDurationMs', + 'processedChunks', + 'droppedChunks', + 'averageLatencyMs', + 'currentSegments', + 'processingRate', + 'totalWordsProcessed', + 'bufferedWords', + ])); + }); + }); + + group('Memory Management', () { + test('should manage segment buffer size', () async { + const config = TranscriptionPipelineConfig(maxBufferedSegments: 5); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + // Add more segments than the buffer limit + for (int i = 0; i < 10; i++) { + final segment = TranscriptionSegment( + text: 'segment $i', + startTime: DateTime.now(), + endTime: DateTime.now(), + confidence: 0.8, + isFinal: true, + ); + transcriptionStreamController.add(segment); + await Future.delayed(const Duration(milliseconds: 10)); + } + + await Future.delayed(const Duration(milliseconds: 100)); + + final currentSegments = service.getCurrentSegments(); + expect(currentSegments.length, lessThanOrEqualTo(5)); + }); + + test('should clear session data properly', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + // Add some segments + final segment = TranscriptionSegment( + text: 'test segment', + startTime: DateTime.now(), + endTime: DateTime.now(), + confidence: 0.8, + isFinal: true, + ); + transcriptionStreamController.add(segment); + await Future.delayed(const Duration(milliseconds: 50)); + + expect(service.getCurrentSegments(), isNotEmpty); + + await service.clearSession(); + + expect(service.getCurrentSegments(), isEmpty); + final metrics = service.getPerformanceMetrics(); + expect(metrics['totalWordsProcessed'], equals(0)); + }); + }); + + group('Audio Processing', () { + test('should handle audio stream data', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + // Simulate audio data + final audioData = Uint8List.fromList(List.generate(1024, (i) => i % 256)); + audioStreamController.add(audioData); + + await Future.delayed(const Duration(milliseconds: 50)); + + final metrics = service.getPerformanceMetrics(); + expect(metrics['processedChunks'], greaterThan(0)); + }); + + test('should handle audio stream errors gracefully', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + // Simulate audio stream error + audioStreamController.addError(Exception('Audio stream error')); + + await Future.delayed(const Duration(milliseconds: 50)); + + expect(service.state, equals(TranscriptionPipelineState.error)); + }); + }); + + group('Language and Backend Configuration', () { + test('should pass language configuration to transcription service', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription( + language: anyNamed('language'), + preferredBackend: anyNamed('preferredBackend'), + enableCapitalization: anyNamed('enableCapitalization'), + enablePunctuation: anyNamed('enablePunctuation'), + )).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription( + language: 'es-ES', + preferredBackend: TranscriptionBackend.whisper, + ); + + verify(mockTranscriptionService.startTranscription( + language: 'es-ES', + preferredBackend: TranscriptionBackend.whisper, + enableCapitalization: true, + enablePunctuation: true, + )).called(1); + }); + }); + + group('Error Handling', () { + test('should handle transcription service errors', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + // Simulate transcription error + transcriptionStreamController.addError(Exception('Transcription error')); + + await Future.delayed(const Duration(milliseconds: 50)); + + expect(service.state, equals(TranscriptionPipelineState.error)); + }); + + test('should handle service initialization failures', () async { + when(mockTranscriptionService.initialize()).thenThrow(Exception('Init failed')); + + const config = TranscriptionPipelineConfig(); + + expect( + () async => await service.initialize(config), + throwsA(isA()), + ); + }); + }); + + group('Resource Cleanup', () { + test('should dispose resources properly', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + + when(mockTranscriptionService.stopTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.stopRecording()).thenAnswer((_) => Future.value()); + + await service.dispose(); + + expect(service.state, equals(TranscriptionPipelineState.idle)); + }); + + test('should handle multiple dispose calls safely', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + // Should not throw + await service.dispose(); + await service.dispose(); + await service.dispose(); + }); + }); + + group('Pause and Resume', () { + test('should pause and resume transcription', () async { + const config = TranscriptionPipelineConfig(); + await service.initialize(config); + + when(mockTranscriptionService.startTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.startRecording()).thenAnswer((_) => Future.value()); + when(mockTranscriptionService.pauseTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.pauseRecording()).thenAnswer((_) => Future.value()); + when(mockTranscriptionService.resumeTranscription()).thenAnswer((_) => Future.value()); + when(mockAudioService.resumeRecording()).thenAnswer((_) => Future.value()); + + await service.startTranscription(); + expect(service.state, equals(TranscriptionPipelineState.active)); + + await service.pauseTranscription(); + expect(service.state, equals(TranscriptionPipelineState.paused)); + + await service.resumeTranscription(); + expect(service.state, equals(TranscriptionPipelineState.active)); + }); + }); + }); +} \ No newline at end of file diff --git a/test/unit/services/real_time_transcription_service_test.mocks.dart b/test/unit/services/real_time_transcription_service_test.mocks.dart new file mode 100644 index 0000000..36d3e3b --- /dev/null +++ b/test/unit/services/real_time_transcription_service_test.mocks.dart @@ -0,0 +1,693 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in flutter_helix/test/unit/services/real_time_transcription_service_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; +import 'dart:typed_data' as _i6; + +import 'package:flutter_helix/core/utils/logging_service.dart' as _i9; +import 'package:flutter_helix/models/audio_configuration.dart' as _i2; +import 'package:flutter_helix/models/transcription_segment.dart' as _i3; +import 'package:flutter_helix/services/audio_service.dart' as _i4; +import 'package:flutter_helix/services/transcription_service.dart' as _i8; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i7; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeAudioConfiguration_0 extends _i1.SmartFake + implements _i2.AudioConfiguration { + _FakeAudioConfiguration_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeTranscriptionSegment_1 extends _i1.SmartFake + implements _i3.TranscriptionSegment { + _FakeTranscriptionSegment_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [AudioService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockAudioService extends _i1.Mock implements _i4.AudioService { + MockAudioService() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.AudioConfiguration get configuration => + (super.noSuchMethod( + Invocation.getter(#configuration), + returnValue: _FakeAudioConfiguration_0( + this, + Invocation.getter(#configuration), + ), + ) + as _i2.AudioConfiguration); + + @override + bool get isRecording => + (super.noSuchMethod(Invocation.getter(#isRecording), returnValue: false) + as bool); + + @override + bool get hasPermission => + (super.noSuchMethod(Invocation.getter(#hasPermission), returnValue: false) + as bool); + + @override + _i5.Stream<_i6.Uint8List> get audioStream => + (super.noSuchMethod( + Invocation.getter(#audioStream), + returnValue: _i5.Stream<_i6.Uint8List>.empty(), + ) + as _i5.Stream<_i6.Uint8List>); + + @override + _i5.Stream get audioLevelStream => + (super.noSuchMethod( + Invocation.getter(#audioLevelStream), + returnValue: _i5.Stream.empty(), + ) + as _i5.Stream); + + @override + _i5.Stream get voiceActivityStream => + (super.noSuchMethod( + Invocation.getter(#voiceActivityStream), + returnValue: _i5.Stream.empty(), + ) + as _i5.Stream); + + @override + _i5.Stream get recordingDurationStream => + (super.noSuchMethod( + Invocation.getter(#recordingDurationStream), + returnValue: _i5.Stream.empty(), + ) + as _i5.Stream); + + @override + _i5.Future initialize(_i2.AudioConfiguration? config) => + (super.noSuchMethod( + Invocation.method(#initialize, [config]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future requestPermission() => + (super.noSuchMethod( + Invocation.method(#requestPermission, []), + returnValue: _i5.Future.value(false), + ) + as _i5.Future); + + @override + _i5.Future startRecording() => + (super.noSuchMethod( + Invocation.method(#startRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future stopRecording() => + (super.noSuchMethod( + Invocation.method(#stopRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future pauseRecording() => + (super.noSuchMethod( + Invocation.method(#pauseRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future resumeRecording() => + (super.noSuchMethod( + Invocation.method(#resumeRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future startConversationRecording(String? conversationId) => + (super.noSuchMethod( + Invocation.method(#startConversationRecording, [conversationId]), + returnValue: _i5.Future.value( + _i7.dummyValue( + this, + Invocation.method(#startConversationRecording, [ + conversationId, + ]), + ), + ), + ) + as _i5.Future); + + @override + _i5.Future stopConversationRecording() => + (super.noSuchMethod( + Invocation.method(#stopConversationRecording, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future> getInputDevices() => + (super.noSuchMethod( + Invocation.method(#getInputDevices, []), + returnValue: _i5.Future>.value( + <_i4.AudioInputDevice>[], + ), + ) + as _i5.Future>); + + @override + _i5.Future selectInputDevice(String? deviceId) => + (super.noSuchMethod( + Invocation.method(#selectInputDevice, [deviceId]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future configureAudioProcessing({ + bool? enableNoiseReduction = true, + bool? enableEchoCancellation = true, + double? gainLevel = 1.0, + }) => + (super.noSuchMethod( + Invocation.method(#configureAudioProcessing, [], { + #enableNoiseReduction: enableNoiseReduction, + #enableEchoCancellation: enableEchoCancellation, + #gainLevel: gainLevel, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future setVoiceActivityDetection(bool? enabled) => + (super.noSuchMethod( + Invocation.method(#setVoiceActivityDetection, [enabled]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future setAudioQuality(_i2.AudioQuality? quality) => + (super.noSuchMethod( + Invocation.method(#setAudioQuality, [quality]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future testAudioRecording() => + (super.noSuchMethod( + Invocation.method(#testAudioRecording, []), + returnValue: _i5.Future.value(false), + ) + as _i5.Future); + + @override + _i5.Future dispose() => + (super.noSuchMethod( + Invocation.method(#dispose, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); +} + +/// A class which mocks [TranscriptionService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTranscriptionService extends _i1.Mock + implements _i8.TranscriptionService { + MockTranscriptionService() { + _i1.throwOnMissingStub(this); + } + + @override + bool get isInitialized => + (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) + as bool); + + @override + bool get isTranscribing => + (super.noSuchMethod( + Invocation.getter(#isTranscribing), + returnValue: false, + ) + as bool); + + @override + bool get hasPermissions => + (super.noSuchMethod( + Invocation.getter(#hasPermissions), + returnValue: false, + ) + as bool); + + @override + bool get isAvailable => + (super.noSuchMethod(Invocation.getter(#isAvailable), returnValue: false) + as bool); + + @override + String get currentLanguage => + (super.noSuchMethod( + Invocation.getter(#currentLanguage), + returnValue: _i7.dummyValue( + this, + Invocation.getter(#currentLanguage), + ), + ) + as String); + + @override + _i8.TranscriptionBackend get currentBackend => + (super.noSuchMethod( + Invocation.getter(#currentBackend), + returnValue: _i8.TranscriptionBackend.device, + ) + as _i8.TranscriptionBackend); + + @override + _i8.TranscriptionQuality get currentQuality => + (super.noSuchMethod( + Invocation.getter(#currentQuality), + returnValue: _i8.TranscriptionQuality.low, + ) + as _i8.TranscriptionQuality); + + @override + double get vadSensitivity => + (super.noSuchMethod(Invocation.getter(#vadSensitivity), returnValue: 0.0) + as double); + + @override + _i5.Stream<_i3.TranscriptionSegment> get transcriptionStream => + (super.noSuchMethod( + Invocation.getter(#transcriptionStream), + returnValue: _i5.Stream<_i3.TranscriptionSegment>.empty(), + ) + as _i5.Stream<_i3.TranscriptionSegment>); + + @override + _i5.Stream get confidenceStream => + (super.noSuchMethod( + Invocation.getter(#confidenceStream), + returnValue: _i5.Stream.empty(), + ) + as _i5.Stream); + + @override + _i5.Future initialize() => + (super.noSuchMethod( + Invocation.method(#initialize, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future requestPermissions() => + (super.noSuchMethod( + Invocation.method(#requestPermissions, []), + returnValue: _i5.Future.value(false), + ) + as _i5.Future); + + @override + _i5.Future startTranscription({ + bool? enableCapitalization = true, + bool? enablePunctuation = true, + String? language, + _i8.TranscriptionBackend? preferredBackend, + }) => + (super.noSuchMethod( + Invocation.method(#startTranscription, [], { + #enableCapitalization: enableCapitalization, + #enablePunctuation: enablePunctuation, + #language: language, + #preferredBackend: preferredBackend, + }), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future stopTranscription() => + (super.noSuchMethod( + Invocation.method(#stopTranscription, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future pauseTranscription() => + (super.noSuchMethod( + Invocation.method(#pauseTranscription, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future resumeTranscription() => + (super.noSuchMethod( + Invocation.method(#resumeTranscription, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future setLanguage(String? languageCode) => + (super.noSuchMethod( + Invocation.method(#setLanguage, [languageCode]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future configureQuality(_i8.TranscriptionQuality? quality) => + (super.noSuchMethod( + Invocation.method(#configureQuality, [quality]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future configureBackend(_i8.TranscriptionBackend? backend) => + (super.noSuchMethod( + Invocation.method(#configureBackend, [backend]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future> getAvailableLanguages() => + (super.noSuchMethod( + Invocation.method(#getAvailableLanguages, []), + returnValue: _i5.Future>.value([]), + ) + as _i5.Future>); + + @override + double getLastConfidence() => + (super.noSuchMethod( + Invocation.method(#getLastConfidence, []), + returnValue: 0.0, + ) + as double); + + @override + _i5.Future<_i3.TranscriptionSegment> transcribeAudio(String? audioPath) => + (super.noSuchMethod( + Invocation.method(#transcribeAudio, [audioPath]), + returnValue: _i5.Future<_i3.TranscriptionSegment>.value( + _FakeTranscriptionSegment_1( + this, + Invocation.method(#transcribeAudio, [audioPath]), + ), + ), + ) + as _i5.Future<_i3.TranscriptionSegment>); + + @override + _i5.Future calibrateVoiceActivity() => + (super.noSuchMethod( + Invocation.method(#calibrateVoiceActivity, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future setVADSensitivity(double? sensitivity) => + (super.noSuchMethod( + Invocation.method(#setVADSensitivity, [sensitivity]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future dispose() => + (super.noSuchMethod( + Invocation.method(#dispose, []), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); +} + +/// A class which mocks [LoggingService]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockLoggingService extends _i1.Mock implements _i9.LoggingService { + MockLoggingService() { + _i1.throwOnMissingStub(this); + } + + @override + void setLogLevel(_i9.LogLevel? level) => super.noSuchMethod( + Invocation.method(#setLogLevel, [level]), + returnValueForMissingStub: null, + ); + + @override + void log(String? tag, String? message, _i9.LogLevel? level) => + super.noSuchMethod( + Invocation.method(#log, [tag, message, level]), + returnValueForMissingStub: null, + ); + + @override + void debug(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#debug, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void info(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#info, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void warning(String? tag, String? message) => super.noSuchMethod( + Invocation.method(#warning, [tag, message]), + returnValueForMissingStub: null, + ); + + @override + void error( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#error, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + void critical( + String? tag, + String? message, [ + Object? error, + StackTrace? stackTrace, + ]) => super.noSuchMethod( + Invocation.method(#critical, [tag, message, error, stackTrace]), + returnValueForMissingStub: null, + ); + + @override + List<_i9.LogEntry> getRecentLogs([int? limit]) => + (super.noSuchMethod( + Invocation.method(#getRecentLogs, [limit]), + returnValue: <_i9.LogEntry>[], + ) + as List<_i9.LogEntry>); + + @override + void clearLogs() => super.noSuchMethod( + Invocation.method(#clearLogs, []), + returnValueForMissingStub: null, + ); + + @override + _i5.Future enableFileLogging(String? filePath) => + (super.noSuchMethod( + Invocation.method(#enableFileLogging, [filePath]), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) + as _i5.Future); + + @override + void disableFileLogging() => super.noSuchMethod( + Invocation.method(#disableFileLogging, []), + returnValueForMissingStub: null, + ); + + @override + void enablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#enablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void disablePerformanceLogging() => super.noSuchMethod( + Invocation.method(#disablePerformanceLogging, []), + returnValueForMissingStub: null, + ); + + @override + void startPerformanceTimer(String? markerId) => super.noSuchMethod( + Invocation.method(#startPerformanceTimer, [markerId]), + returnValueForMissingStub: null, + ); + + @override + void endPerformanceTimer(String? markerId, [String? operation]) => + super.noSuchMethod( + Invocation.method(#endPerformanceTimer, [markerId, operation]), + returnValueForMissingStub: null, + ); + + @override + void addTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#addTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void removeTagFilter(String? tag) => super.noSuchMethod( + Invocation.method(#removeTagFilter, [tag]), + returnValueForMissingStub: null, + ); + + @override + void clearTagFilters() => super.noSuchMethod( + Invocation.method(#clearTagFilters, []), + returnValueForMissingStub: null, + ); + + @override + void setMessageFilter(String? filter) => super.noSuchMethod( + Invocation.method(#setMessageFilter, [filter]), + returnValueForMissingStub: null, + ); + + @override + List<_i9.LogEntry> getFilteredLogs({ + _i9.LogLevel? minLevel, + String? tag, + DateTime? since, + int? limit, + }) => + (super.noSuchMethod( + Invocation.method(#getFilteredLogs, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + #limit: limit, + }), + returnValue: <_i9.LogEntry>[], + ) + as List<_i9.LogEntry>); + + @override + String exportLogsAsJson({ + _i9.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i7.dummyValue( + this, + Invocation.method(#exportLogsAsJson, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + String exportLogsAsText({ + _i9.LogLevel? minLevel, + String? tag, + DateTime? since, + }) => + (super.noSuchMethod( + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + returnValue: _i7.dummyValue( + this, + Invocation.method(#exportLogsAsText, [], { + #minLevel: minLevel, + #tag: tag, + #since: since, + }), + ), + ) + as String); + + @override + Map getLoggingStats() => + (super.noSuchMethod( + Invocation.method(#getLoggingStats, []), + returnValue: {}, + ) + as Map); +} diff --git a/test/unit/services/transcription_service_test.dart b/test/unit/services/transcription_service_test.dart new file mode 100644 index 0000000..8760179 --- /dev/null +++ b/test/unit/services/transcription_service_test.dart @@ -0,0 +1,107 @@ +// ABOUTME: Simplified unit tests for TranscriptionService implementation +// ABOUTME: Tests basic initialization and service availability without platform dependencies + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:flutter_helix/services/implementations/transcription_service_impl.dart'; +import 'package:flutter_helix/services/transcription_service.dart'; +import 'package:flutter_helix/core/utils/logging_service.dart'; + +void main() { + group('TranscriptionService', () { + late TranscriptionServiceImpl transcriptionService; + + setUp(() { + transcriptionService = TranscriptionServiceImpl(logger: LoggingService.instance); + }); + + tearDown(() async { + await transcriptionService.dispose(); + }); + + group('Basic Properties', () { + test('should have correct initial state', () { + expect(transcriptionService.isTranscribing, isFalse); + expect(transcriptionService.currentLanguage, equals('en-US')); + expect(transcriptionService.currentBackend, equals(TranscriptionBackend.device)); + expect(transcriptionService.currentQuality, equals(TranscriptionQuality.standard)); + }); + + test('should provide streams', () { + expect(transcriptionService.transcriptionStream, isA()); + expect(transcriptionService.confidenceStream, isA()); + }); + }); + + group('Configuration', () { + test('should update quality setting', () async { + await transcriptionService.configureQuality(TranscriptionQuality.high); + expect(transcriptionService.currentQuality, equals(TranscriptionQuality.high)); + }); + + test('should update backend setting', () async { + await transcriptionService.configureBackend(TranscriptionBackend.whisper); + expect(transcriptionService.currentBackend, equals(TranscriptionBackend.whisper)); + }); + + test('should update VAD sensitivity', () async { + await transcriptionService.setVADSensitivity(0.8); + expect(transcriptionService.vadSensitivity, equals(0.8)); + }); + }); + + group('Initialization', () { + test('should initialize without throwing', () async { + // Initialization might fail in test environment due to platform dependencies + // but it should handle errors gracefully + try { + await transcriptionService.initialize(); + expect(transcriptionService.isInitialized, isA()); + } catch (e) { + // Expected in test environment without speech recognition services + expect(e, isA()); + } + }); + }); + + group('Language Support', () { + test('should return available languages list', () async { + try { + final languages = await transcriptionService.getAvailableLanguages(); + expect(languages, isA>()); + } catch (e) { + // Expected in test environment + expect(e, isA()); + } + }); + }); + + group('Transcription Control', () { + test('should handle start transcription gracefully', () async { + try { + await transcriptionService.startTranscription(); + // If successful, should be transcribing + expect(transcriptionService.isTranscribing, isA()); + } catch (e) { + // Expected in test environment without microphone access + expect(e, isA()); + } + }); + + test('should handle stop transcription', () async { + // Should not throw even if not started + await transcriptionService.stopTranscription(); + expect(transcriptionService.isTranscribing, isFalse); + }); + }); + + group('Resource Management', () { + test('should dispose properly', () async { + await transcriptionService.dispose(); + // Should not throw when called multiple times + await transcriptionService.dispose(); + }); + }); + }); +} \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..a18923c --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,17 @@ +// Basic Flutter widget test for the Helix app + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:flutter_helix/app.dart'; + +void main() { + testWidgets('Helix app launches successfully', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const HelixApp()); + + // Verify that our app launches without errors + expect(find.byType(BottomNavigationBar), findsOneWidget); + expect(find.byType(Scaffold), findsWidgets); + }); +} \ No newline at end of file diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..8574007 --- /dev/null +++ b/todo.md @@ -0,0 +1,296 @@ +# Helix Epic 1.2: ConversationTab Integration - TODO Tracker + +## Current Status +**Epic**: 1.2 - ConversationTab Integration (ART-10) +**Last Updated**: 2025-08-03 +**Overall Progress**: 0% (Ready to start implementation) +**Priority**: P0 (Urgent) + +--- + +## Epic 1.2 Implementation Chunks + +### ✅ Planning & Architecture (COMPLETE) +- [x] **Analyze current codebase structure** - Identified key files and integration points +- [x] **Create comprehensive TDD plan** - 8-chunk implementation with specific prompts +- [x] **Define success metrics** - Clear definition of done and quality gates +- [x] **Map integration points** - ConversationTab ↔ AudioService communication +- [x] **Establish testing strategy** - Widget, integration, and performance testing + +### ⏳ Chunk 1: Test Infrastructure Setup (2 hours) - READY +**Goal**: Establish comprehensive testing framework for UI-service integration +**Linear Issue**: Setup for ART-11 and ART-12 + +#### Tasks: +- [ ] Create comprehensive widget tests for ConversationTab +- [ ] Set up integration tests for complete recording workflow +- [ ] Enhance test helpers with UI testing utilities +- [ ] Establish baseline test coverage metrics + +#### Files to Create/Modify: +- `test/widget/conversation_tab_test.dart` (create) +- `test/integration/ui_audio_integration_test.dart` (create) +- `test/test_helpers.dart` (enhance) + +#### Success Criteria: +- [ ] Widget tests framework established +- [ ] Integration test infrastructure ready +- [ ] Test helpers for UI-AudioService mocking +- [ ] Baseline test coverage measurement + +--- + +### ⏳ Chunk 2: Recording Button State Management (3 hours) - PENDING +**Goal**: Ensure recording button accurately reflects AudioService state +**Linear Issue**: ART-11 (US 1.2.1: Connect UI to AudioService) + +#### Tasks: +- [ ] Fix recording button icon state synchronization +- [ ] Implement rapid tapping protection +- [ ] Add loading states during permission requests +- [ ] Implement graceful error handling with user feedback + +#### Files to Modify: +- `lib/ui/widgets/conversation_tab.dart` (state management) +- `test/widget/conversation_tab_test.dart` (add tests) + +#### Success Criteria: +- [ ] Recording button shows correct state always +- [ ] No duplicate recording calls from rapid tapping +- [ ] Loading states during async operations +- [ ] Graceful error handling and user feedback + +--- + +### ⏳ Chunk 3: Real-Time Timer Integration (2 hours) - PENDING +**Goal**: Connect timer display to AudioService duration stream +**Linear Issue**: ART-11 (US 1.2.1: Connect UI to AudioService) + +#### Tasks: +- [ ] Connect timer to AudioService duration stream +- [ ] Implement timer reset when recording stops +- [ ] Add stream error handling for timer +- [ ] Implement pause/resume timer functionality + +#### Files to Modify: +- `lib/ui/widgets/conversation_tab.dart` (timer logic) +- `test/widget/conversation_tab_test.dart` (timer tests) + +#### Success Criteria: +- [ ] Timer shows real elapsed recording time +- [ ] Timer resets to 00:00 when stopping +- [ ] Timer handles stream interruptions gracefully +- [ ] Timer works correctly with pause/resume + +--- + +### ⏳ Chunk 4: Waveform Performance Optimization (4 hours) - PENDING +**Goal**: Optimize ReactiveWaveform for smooth 30fps real-time updates +**Linear Issue**: ART-12 (US 1.2.2: Live Waveform Visualization) + +#### Tasks: +- [ ] Optimize waveform for 30fps rendering target +- [ ] Handle rapid audio level changes without jank +- [ ] Implement efficient memory management for history +- [ ] Fine-tune audio level mapping and visualization + +#### Files to Modify: +- `lib/ui/widgets/conversation_tab.dart` (ReactiveWaveform) +- `test/widget/waveform_performance_test.dart` (create) + +#### Success Criteria: +- [ ] Smooth 30fps waveform animation +- [ ] No UI jank during audio level updates +- [ ] Efficient memory usage for audio history +- [ ] Accurate visual representation of voice input + +--- + +### ⏳ Chunk 5: Stream Subscription Management (2 hours) - PENDING +**Goal**: Ensure proper lifecycle management of AudioService streams +**Linear Issue**: ART-11 (US 1.2.1: Connect UI to AudioService) + +#### Tasks: +- [ ] Implement proper stream subscription setup +- [ ] Add comprehensive disposal and cleanup +- [ ] Handle service reinitialization scenarios +- [ ] Implement robust stream error handling + +#### Files to Modify: +- `lib/ui/widgets/conversation_tab.dart` (subscription lifecycle) +- `test/widget/conversation_tab_test.dart` (lifecycle tests) + +#### Success Criteria: +- [ ] No memory leaks from uncancelled subscriptions +- [ ] Proper error handling for stream failures +- [ ] Clean initialization and disposal lifecycle +- [ ] Robust handling of service state changes + +--- + +### ⏳ Chunk 6: Permission Flow Integration (2 hours) - PENDING +**Goal**: Seamlessly integrate permission requests with recording workflow +**Linear Issue**: ART-11 (US 1.2.1: Connect UI to AudioService) + +#### Tasks: +- [ ] Implement seamless permission request flow +- [ ] Add automatic recording start after permission grant +- [ ] Implement proper error handling for permission denial +- [ ] Add settings dialog for permanently denied permissions + +#### Files to Modify: +- `lib/ui/widgets/conversation_tab.dart` (permission flow) +- `test/widget/conversation_tab_test.dart` (permission tests) + +#### Success Criteria: +- [ ] Smooth permission request flow +- [ ] Automatic recording start after permission grant +- [ ] Clear error messages for permission failures +- [ ] Easy path to app settings for denied permissions + +--- + +### ⏳ Chunk 7: End-to-End Integration Testing (3 hours) - PENDING +**Goal**: Comprehensive testing of complete recording workflow +**Linear Issue**: ART-11 and ART-12 (Integration validation) + +#### Tasks: +- [ ] Create comprehensive end-to-end workflow tests +- [ ] Test multiple recording session scenarios +- [ ] Validate conversation saving with real audio data +- [ ] Implement interruption and edge case handling + +#### Files to Create/Modify: +- `test/integration/complete_recording_workflow_test.dart` (create) +- Fix any remaining integration issues discovered + +#### Success Criteria: +- [ ] End-to-end recording workflow works perfectly +- [ ] Multiple recording sessions don't interfere +- [ ] Real audio files are saved correctly +- [ ] Graceful handling of interruptions and edge cases + +--- + +### ⏳ Chunk 8: Performance and Polish (2 hours) - PENDING +**Goal**: Final optimization and user experience polish +**Linear Issue**: ART-11 and ART-12 (Final polish) + +#### Tasks: +- [ ] Optimize UI responsiveness during heavy processing +- [ ] Implement memory usage optimization +- [ ] Add battery usage optimization for continuous recording +- [ ] Ensure all animations are smooth and jank-free + +#### Files to Modify: +- `lib/ui/widgets/conversation_tab.dart` (final optimizations) +- `test/performance/recording_performance_test.dart` (create) + +#### Success Criteria: +- [ ] Responsive UI during recording +- [ ] Optimized memory and battery usage +- [ ] Smooth animations and transitions +- [ ] Professional user experience + +--- + +## Epic 1.2 Success Metrics + +### Definition of Done ✅ +- [ ] Record button triggers actual recording +- [ ] UI reflects real recording state +- [ ] Live waveform shows actual voice input +- [ ] Timer displays real recording duration +- [ ] Smooth 30fps waveform animation +- [ ] No UI jank during recording +- [ ] >80% test coverage on UI-AudioService integration +- [ ] End-to-end recording workflow works perfectly + +### Quality Gates +1. **All tests pass** - 100% test success rate +2. **Performance targets met** - 30fps waveform, <100ms button response +3. **Memory efficiency** - No memory leaks, efficient audio history management +4. **User experience** - Smooth animations, clear feedback, graceful error handling + +### Integration Points Verified +- ConversationTab ↔ AudioService communication +- Real-time audio level visualization +- Recording state synchronization +- Permission flow integration +- Error handling and recovery +- Stream lifecycle management + +--- + +## Implementation Timeline + +### Week 1 (Epic 1.2 Kick-off): +**Target**: Complete Chunks 1-4 (Test setup through Waveform optimization) +**Expected Duration**: 11 hours total + +**Day 1-2**: Chunks 1-2 (Test Infrastructure + Button State) +**Day 3-4**: Chunk 3 (Timer Integration) +**Day 5**: Chunk 4 (Waveform Optimization) + +### Week 2 (Epic 1.2 Completion): +**Target**: Complete Chunks 5-8 (Lifecycle through Polish) +**Expected Duration**: 9 hours total + +**Day 1**: Chunks 5-6 (Stream Management + Permissions) +**Day 2-3**: Chunk 7 (Integration Testing) +**Day 4**: Chunk 8 (Performance Polish) +**Day 5**: Epic validation and handoff + +--- + +## Resources & References + +### Key Files for Epic 1.2: +- `lib/ui/widgets/conversation_tab.dart` - **Primary target** for integration +- `lib/services/implementations/audio_service_impl.dart` - **Working service** to integrate with +- `test/integration/recording_workflow_test.dart` - **Existing tests** to build upon + +### Linear Issues: +- **ART-10**: Epic 1.2: ConversationTab Integration +- **ART-11**: US 1.2.1: Connect UI to AudioService +- **ART-12**: US 1.2.2: Live Waveform Visualization + +### Code Generation Prompts: +Ready-to-use prompts for each chunk are available in `plan.md` sections 228-473 + +### Dependencies: +- Epic 1.1 (AudioService fixes) - **COMPLETED** ✅ +- Working AudioService implementation - **AVAILABLE** ✅ +- ConversationTab UI structure - **EXISTS** ✅ + +--- + +## Current State Assessment + +### What's Working ✅: +- AudioService has real functionality for recording, permissions, audio levels +- ConversationTab UI is visually complete and responsive +- Basic service subscription infrastructure exists +- Test framework is established + +### What Needs Work ❌: +- UI-Service integration gaps in state management +- Waveform performance optimization needed +- Stream subscription lifecycle needs robustness +- Permission flow user experience needs polish +- End-to-end workflow testing required + +### Ready to Start ✅: +Epic 1.2 is ready for immediate implementation. All dependencies are met and the comprehensive plan provides specific, actionable steps for TDD-driven development. + +--- + +**Epic 1.2 Status**: ✅ READY FOR IMPLEMENTATION +**Next Action**: Execute Chunk 1 (Test Infrastructure Setup) +**Estimated Completion**: End of Week 2 (2025-08-17) + +--- + +**Last Updated**: 2025-08-03 +**Next Review**: Daily during implementation +**Contact**: Doctor Art for questions or updates \ No newline at end of file diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..3da7ef9 --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + flutter_helix + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..9c793a1 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "flutter_helix", + "short_name": "flutter_helix", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..29e761e --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(flutter_helix LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_helix") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..48de52b --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..0e69e40 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + permission_handler_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..bd6ff8a --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.evenrealities" "\0" + VALUE "FileDescription", "flutter_helix" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "flutter_helix" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.evenrealities. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_helix.exe" "\0" + VALUE "ProductName", "flutter_helix" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..f46049d --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"flutter_helix", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_