├── .env.example ├── .gitignore ├── .metadata ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SIMPLE_TUTORIAL.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── flutter_voice_friend │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── Icon_Microphone.svg ├── activities │ ├── default_image.webp │ ├── example_image_1.webp │ ├── example_image_2.webp │ ├── example_image_3.webp │ └── example_image_4.webp ├── icon │ └── app_icon.png ├── play_example.json └── record_example.json ├── devtools_options.yaml ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h └── RunnerTests │ └── RunnerTests.swift ├── lib ├── activities.dart ├── config.dart ├── constants.dart ├── llm_templates │ ├── activities │ │ ├── example_dream_analyst_template.dart │ │ └── example_introduction_template.dart │ ├── all_templates.dart │ └── summarizers │ │ ├── example_summarizer_session.dart │ │ └── example_summarizer_user_template.dart ├── main.dart ├── models │ ├── activity.dart │ ├── activity.g.dart │ ├── session.dart │ └── session.g.dart ├── screens │ ├── main_menu.dart │ ├── main_screen.dart │ └── settings_page.dart ├── services │ ├── animation_controller_service.dart │ ├── audio_service.dart │ ├── connection_service.dart │ ├── llm_service.dart │ ├── permission_service.dart │ ├── session_service.dart │ ├── speech_service.dart │ └── user_service.dart ├── utils │ ├── audio_utils.dart │ ├── llm_chain.dart │ ├── text_utils.dart │ ├── tts_openai.dart │ ├── tts_openai_interface.dart │ ├── tts_openai_justaudio.dart │ ├── tts_openai_soloud.dart │ └── tts_openai_stub.dart └── widgets │ ├── activity │ ├── activity_item.dart │ └── image_of_activity.dart │ ├── app_bar_widget.dart │ ├── audio_controls │ ├── bottom_icons_when_listening.dart │ └── bottom_icons_when_playing.dart │ ├── common │ ├── error_dialog.dart │ ├── loading_widget.dart │ └── retry_cancel_widget.dart │ ├── dialog_helper.dart │ ├── indicators │ ├── play_indicator.dart │ └── simple_loading_indicator.dart │ ├── listening │ ├── listening_animation.dart │ └── listening_message.dart │ └── playing │ ├── playing_animation.dart │ └── subtitle_widget.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── app_icon_1024.png │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ └── app_icon_64.png │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Configs │ │ ├── AppInfo.xcconfig │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements └── RunnerTests │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── test ├── models │ ├── activity_test.dart │ └── session_test.dart ├── services │ ├── audio_service_test.dart │ ├── audio_service_test.mocks.dart │ ├── connection_service_test.dart │ ├── connection_service_test.mocks.dart │ ├── session_service_test.dart │ ├── speech_service_test.dart │ ├── speech_service_test.mocks.dart │ └── user_service_test.dart ├── utils │ ├── audio_utils_test.dart │ ├── llm_chain_test.dart │ ├── llm_chain_test.mocks.dart │ ├── text_utils_test.dart │ ├── tts_openai_justaudio_test.dart │ └── tts_openai_justaudio_test.mocks.dart └── widgets │ ├── activity │ ├── activity_item_test.dart │ └── image_of_activity_test.dart │ ├── app_bar_widget_test.dart │ ├── app_bar_widget_test.mocks.dart │ ├── audio_controls │ ├── bottom_icons_when_listening_test.dart │ ├── bottom_icons_when_listening_test.mocks.dart │ └── bottom_icons_when_playing_test.dart │ ├── common │ ├── error_dialog_test.dart │ ├── loading_widget_test.dart │ └── retry_cancel_widget_test.dart │ ├── dialog_helper_test.dart │ ├── indicators │ ├── play_indicator_test.dart │ ├── play_indicator_test.mocks.dart │ └── simple_loading_indicator_test.dart │ ├── listening │ ├── listening_animation_test.dart │ └── listening_message_test.dart │ └── playing │ ├── playing_animation.dart │ └── subtitle_widget.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html └── manifest.json └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY = replace_with_your_openai_api_key 2 | DEEPGRAM_API_KEY = replace_with_your_deepgram_api_key -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.env 3 | *.class 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | migrate_working_dir/ 13 | 14 | # IntelliJ related 15 | *.iml 16 | *.ipr 17 | *.iws 18 | .idea/ 19 | 20 | # The .vscode folder contains launch configuration and tasks you configure in 21 | # VS Code which you may wish to be included in version control, so this line 22 | # is commented out by default. 23 | #.vscode/ 24 | 25 | # Flutter/Dart/Pub related 26 | **/doc/api/ 27 | **/ios/Flutter/.last_build_id 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | /assets/screenshots/* 47 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "5874a72aa4c779a02553007c47dacbefba2374dc" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 17 | base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 18 | - platform: android 19 | create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 20 | base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 21 | - platform: ios 22 | create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 23 | base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 24 | - platform: linux 25 | create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 26 | base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 27 | - platform: macos 28 | create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 29 | base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 30 | - platform: web 31 | create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 32 | base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 33 | - platform: windows 34 | create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 35 | base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you find a bug, please create an issue on GitHub. Be sure to include details on how to reproduce the bug and any relevant screenshots or logs. 2 | 3 | ### Suggesting Features 4 | 5 | We are always looking for new ideas! If you have a suggestion, please open an issue and tag it with the `enhancement` label. 6 | 7 | ### Code Contributions 8 | 9 | 1. **Fork the Repository:** 10 | Fork the repository on GitHub to your own account. 11 | 12 | 2. **Clone Your Fork:** 13 | ```bash 14 | git clone https://github.com/jbpassot/flutter_voice_friend.git 15 | ``` 16 | 17 | 3. **Create a New Branch:** 18 | ```bash 19 | git checkout -b feature/your-feature-name 20 | ``` 21 | 22 | 4. **Make Your Changes:** 23 | Implement your changes, making sure to follow our coding style. 24 | 25 | 5. **Run Tests:** 26 | Make sure all tests pass before submitting your code. 27 | 28 | 6. **Commit Your Changes:** 29 | ```bash 30 | git commit -m "Description of changes" 31 | ``` 32 | 33 | 7. **Push Your Branch:** 34 | ```bash 35 | git push origin feature/your-feature-name 36 | ``` 37 | 38 | 8. **Submit a Pull Request:** 39 | Go to the original repository on GitHub and submit a pull request. Please provide a detailed description of your changes. 40 | 41 | ### Code Style 42 | 43 | Please follow these guidelines: 44 | 45 | - Use descriptive names for variables and functions. 46 | - Write comments for complex logic. 47 | - Ensure code is formatted with Dart's `flutter format` command. 48 | 49 | ### License 50 | 51 | By contributing, you agree that your contributions will be licensed under the same license as this project. 52 | 53 | Thank you for contributing to FlutterVoiceFriend - Whisper! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License 2 | 3 | flutter_voice_friend © 2024 by Jean-Baptiste Passot is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. 4 | 5 | ## Terms and Conditions 6 | 7 | ### 1. **Attribution**: 8 | You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 9 | 10 | ### 2. **NonCommercial**: 11 | You may not use the material for commercial purposes. 12 | 13 | ### 3. **ShareAlike**: 14 | If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original. 15 | 16 | ### 4. **No Additional Restrictions**: 17 | You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. 18 | 19 | For more details, please refer to the full text of the license: [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/) -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "com.example.flutter_voice_friend" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_1_8 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.example.flutter_voice_friend" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 23 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 41 | 42 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/flutter_voice_friend/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_voice_friend 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip 6 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "7.3.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.7.10" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /assets/Icon_Microphone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/activities/default_image.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/assets/activities/default_image.webp -------------------------------------------------------------------------------- /assets/activities/example_image_1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/assets/activities/example_image_1.webp -------------------------------------------------------------------------------- /assets/activities/example_image_2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/assets/activities/example_image_2.webp -------------------------------------------------------------------------------- /assets/activities/example_image_3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/assets/activities/example_image_3.webp -------------------------------------------------------------------------------- /assets/activities/example_image_4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/assets/activities/example_image_4.webp -------------------------------------------------------------------------------- /assets/icon/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/assets/icon/app_icon.png -------------------------------------------------------------------------------- /assets/record_example.json: -------------------------------------------------------------------------------- 1 | {"v":"5.6.4","fr":45,"ip":0,"op":86,"w":24,"h":24,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.348,0.348],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]},"t":0,"s":[9,9],"e":[14,14]},{"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0.65,0.65],"y":[0,0]},"t":45,"s":[14,14],"e":[9,9]},{"t":84}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.49411764705882355,0.996078431372549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[18,18],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.49411764705882355,0.996078431372549,1],"ix":3},"o":{"a":0,"k":20,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | target.build_configurations.each do |config| 44 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 45 | '$(inherited)', 46 | 'PERMISSION_MICROPHONE=1', 47 | ] 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - audio_session (0.0.1): 3 | - Flutter 4 | - connectivity_plus (0.0.1): 5 | - Flutter 6 | - FlutterMacOS 7 | - Flutter (1.0.0) 8 | - flutter_soloud (0.0.1): 9 | - Flutter 10 | - isar_flutter_libs (1.0.0): 11 | - Flutter 12 | - just_audio (0.0.1): 13 | - Flutter 14 | - package_info_plus (0.4.5): 15 | - Flutter 16 | - path_provider_foundation (0.0.1): 17 | - Flutter 18 | - FlutterMacOS 19 | - permission_handler_apple (9.3.0): 20 | - Flutter 21 | - record_darwin (1.0.0): 22 | - Flutter 23 | - FlutterMacOS 24 | - shared_preferences_foundation (0.0.1): 25 | - Flutter 26 | - FlutterMacOS 27 | - speech_to_text (0.0.1): 28 | - Flutter 29 | - FlutterMacOS 30 | - Try 31 | - Try (2.1.1) 32 | - wakelock_plus (0.0.1): 33 | - Flutter 34 | 35 | DEPENDENCIES: 36 | - audio_session (from `.symlinks/plugins/audio_session/ios`) 37 | - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) 38 | - Flutter (from `Flutter`) 39 | - flutter_soloud (from `.symlinks/plugins/flutter_soloud/ios`) 40 | - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) 41 | - just_audio (from `.symlinks/plugins/just_audio/ios`) 42 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 43 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 44 | - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 45 | - record_darwin (from `.symlinks/plugins/record_darwin/ios`) 46 | - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 47 | - speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`) 48 | - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) 49 | 50 | SPEC REPOS: 51 | trunk: 52 | - Try 53 | 54 | EXTERNAL SOURCES: 55 | audio_session: 56 | :path: ".symlinks/plugins/audio_session/ios" 57 | connectivity_plus: 58 | :path: ".symlinks/plugins/connectivity_plus/darwin" 59 | Flutter: 60 | :path: Flutter 61 | flutter_soloud: 62 | :path: ".symlinks/plugins/flutter_soloud/ios" 63 | isar_flutter_libs: 64 | :path: ".symlinks/plugins/isar_flutter_libs/ios" 65 | just_audio: 66 | :path: ".symlinks/plugins/just_audio/ios" 67 | package_info_plus: 68 | :path: ".symlinks/plugins/package_info_plus/ios" 69 | path_provider_foundation: 70 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 71 | permission_handler_apple: 72 | :path: ".symlinks/plugins/permission_handler_apple/ios" 73 | record_darwin: 74 | :path: ".symlinks/plugins/record_darwin/ios" 75 | shared_preferences_foundation: 76 | :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 77 | speech_to_text: 78 | :path: ".symlinks/plugins/speech_to_text/darwin" 79 | wakelock_plus: 80 | :path: ".symlinks/plugins/wakelock_plus/ios" 81 | 82 | SPEC CHECKSUMS: 83 | audio_session: 088d2483ebd1dc43f51d253d4a1c517d9a2e7207 84 | connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db 85 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 86 | flutter_soloud: a49590bf8d8be2c55b50f4d4e819b764901a4946 87 | isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097 88 | just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa 89 | package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c 90 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 91 | permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 92 | record_darwin: df0a677188e5fed18472550298e675f19ddaffbe 93 | shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 94 | speech_to_text: 627d3fd2194770b51abb324ba45c2d39398f24a8 95 | Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96 96 | wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 97 | 98 | PODFILE CHECKSUM: 2a19c08f8f5f87b2cb8ec36b4e6659d70f732da9 99 | 100 | COCOAPODS: 1.15.2 101 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ITSAppUsesNonExemptEncryption 6 | 7 | CADisableMinimumFrameDurationOnPhone 8 | 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleDisplayName 12 | FlutterVoiceFriend 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | FlutterVoiceFriend 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(FLUTTER_BUILD_NAME) 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | 29 | $(FLUTTER_BUILD_NUMBER) 30 | LSRequiresIPhoneOS 31 | 32 | NSAppTransportSecurity 33 | 34 | NSAllowsArbitraryLoads 35 | 36 | 37 | NSMicrophoneUsageDescription 38 | FlutterVoiceFriend needs access to the microphone to record your voice, which is the primary way to communicate with the chatbot and enhance your experience. 39 | NSSpeechRecognitionUsageDescription 40 | FlutterVoiceFriend needs access to speech recognition to convert your voice commands into text, facilitating seamless interaction with the chatbot during your sessions. 41 | UIApplicationSupportsIndirectInputEvents 42 | 43 | UILaunchStoryboardName 44 | LaunchScreen 45 | UIMainStoryboardFile 46 | Main 47 | UISupportedInterfaceOrientations 48 | 49 | UIInterfaceOrientationPortrait 50 | 51 | UISupportedInterfaceOrientations~ipad 52 | 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | UIInterfaceOrientationPortrait 56 | UIInterfaceOrientationPortraitUpsideDown 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/activities.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_voice_friend/models/activity.dart'; 3 | import 'package:isar/isar.dart'; 4 | 5 | Activity introductionActivity = Activity( 6 | activityId: ActivityId.introduction, // Use the enum for activityId 7 | name: "Introduction", // Direct name assignment 8 | description: 'Introduction activity', // Description 9 | requiredLevel: 0, // Required level 10 | category: ActivityCategory.dreamActivities, // Category for the activity 11 | displayOrder: 0, // Display order 12 | duration: 5, // Set a duration for the activity (e.g., 5 minutes) 13 | imagePath: 14 | 'assets/activities/default_image.webp', // Direct image path assignment 15 | ); 16 | 17 | // Sync activities with the database 18 | Future syncActivities(Isar isar) async { 19 | // Load existing activities from Isar 20 | final existingActivities = await isar.activitys.where().findAll(); 21 | 22 | // Hardcoded list of activities 23 | List hardcodedActivities = initializeActivities(); 24 | 25 | // Convert existing activities into a map for easy comparison by activityId 26 | Map existingActivitiesMap = { 27 | for (var activity in existingActivities) activity.activityId: activity 28 | }; 29 | 30 | // Start an Isar transaction to update the database 31 | await isar.writeTxn(() async { 32 | for (var hardcodedActivity in hardcodedActivities) { 33 | if (existingActivitiesMap.containsKey(hardcodedActivity.activityId)) { 34 | final storedActivity = 35 | existingActivitiesMap[hardcodedActivity.activityId]!; 36 | 37 | // Check if the activity has changed, and update it if necessary 38 | if (_isActivityModified(storedActivity, hardcodedActivity)) { 39 | storedActivity 40 | ..name = hardcodedActivity.name 41 | ..description = hardcodedActivity.description 42 | ..requiredLevel = hardcodedActivity.requiredLevel 43 | ..category = hardcodedActivity.category 44 | ..displayOrder = hardcodedActivity.displayOrder 45 | ..duration = hardcodedActivity.duration 46 | ..imagePath = hardcodedActivity.imagePath; 47 | 48 | await isar.activitys.put(storedActivity); // Update existing activity 49 | debugPrint('Updated activity: ${storedActivity.name}'); 50 | } 51 | 52 | // Remove the existing activity from the map, so it's not reprocessed 53 | existingActivitiesMap.remove(hardcodedActivity.activityId); 54 | } else { 55 | // Add new activity to the database 56 | await isar.activitys.put(hardcodedActivity); 57 | debugPrint('Added new activity: ${hardcodedActivity.name}'); 58 | } 59 | } 60 | 61 | // Optionally: Remove old activities that are no longer in the hardcoded list 62 | for (var remainingStoredActivity in existingActivitiesMap.values) { 63 | await isar.activitys.delete(remainingStoredActivity.id); 64 | debugPrint('Removed outdated activity: ${remainingStoredActivity.name}'); 65 | } 66 | }); 67 | } 68 | 69 | bool _isActivityModified(Activity storedActivity, Activity hardcodedActivity) { 70 | return storedActivity.name != hardcodedActivity.name || 71 | storedActivity.description != hardcodedActivity.description || 72 | storedActivity.requiredLevel != hardcodedActivity.requiredLevel || 73 | storedActivity.category != hardcodedActivity.category || 74 | storedActivity.displayOrder != hardcodedActivity.displayOrder || 75 | storedActivity.duration != hardcodedActivity.duration || 76 | storedActivity.imagePath != hardcodedActivity.imagePath; 77 | } 78 | 79 | // Function to initialize activities with proper categories, levels, and orders 80 | List initializeActivities() { 81 | return [ 82 | // Kids Activities 83 | introductionActivity, 84 | Activity( 85 | activityId: ActivityId.dreamAnalyst, 86 | name: 'Whisper the Dream Analyst', 87 | description: 'A dream analyst to explore your dreams', 88 | requiredLevel: 1, 89 | displayOrder: 1, 90 | category: ActivityCategory.dreamActivities, 91 | duration: 10, 92 | imagePath: 'assets/activities/example_image_1.webp', 93 | ), 94 | ]; 95 | } 96 | -------------------------------------------------------------------------------- /lib/config.dart: -------------------------------------------------------------------------------- 1 | // File: lib/config.dart 2 | import 'dart:io' show Platform; 3 | 4 | class Config { 5 | static late String openaiApiKey; 6 | static late String deepgramApiKey; 7 | 8 | static bool debug = false; 9 | 10 | static const String openaiTtsUrl = 'https://api.openai.com/v1/audio/speech'; 11 | 12 | static const String defaultLanguage = 'EN'; 13 | static final String defaultStt = Platform.isIOS ? onDeviceStt : deepgramStt; 14 | static const String defaultVoice = voiceNova; 15 | 16 | static const String deepgramStt = "Deepgram"; 17 | static const String onDeviceStt = 'On Device'; 18 | 19 | static const String soloudBackend = "SoLoud"; 20 | static const String justAudioBackend = 'just_audio'; 21 | 22 | static const String voiceAlloy = "alloy"; 23 | static const String voiceEcho = "echo"; 24 | static const String voiceFable = "fable"; 25 | static const String voiceOnyx = "onyx"; 26 | static const String voiceNova = "nova"; 27 | static const String voiceShimmer = "shimmer"; 28 | 29 | static const Map languageCodeMap = { 30 | 'EN': 'en-US', 31 | 'FR': 'fr-FR', 32 | 'ES': 'es-ES', 33 | }; 34 | 35 | static const Map languageStringToAdd = { 36 | 'EN': 'Please give your response in English', 37 | 'FR': 'Please give your response in French', 38 | 'ES': 'Please give your response in Spanish', 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /lib/constants.dart: -------------------------------------------------------------------------------- 1 | // Constant for SIRIWAVE WIDGET 2 | class PlayWidgetConstant { 3 | static const Duration intensityUpdateInterval = Duration(milliseconds: 50); 4 | static const double intensityDivisor = 10.0; 5 | static const double intensityMin = 0.05; 6 | static const double intensityMax = 1.0; 7 | } 8 | -------------------------------------------------------------------------------- /lib/llm_templates/activities/example_dream_analyst_template.dart: -------------------------------------------------------------------------------- 1 | // lib/llm_templates/activities/example_dream_analyst_template.dart 2 | 3 | String templateDreamAnalyst = """ 4 | {language} 5 | 6 | You are Whisper, a dream analyst. Your task is to help the user understand their dream through a structured and detailed conversation. Follow these steps to guide the conversation and analyze the dream effectively. Remember to ask one question at a time to maintain a natural and engaging dialogue. 7 | 8 | 1. **Initial Description:** 9 | - Begin by asking the user to describe their dream in detail. 10 | 11 | 2. **Exploring Key Elements:** 12 | - Identify key people, symbols, emotions, and significant events in the dream. 13 | - Ask about one key element at a time, relating it to the user's personal background and recent life changes using the provided personal database. 14 | 15 | 3. **Detailed Analysis:** 16 | - For each key element, ask probing questions to uncover deeper meanings and connections: 17 | - **People:** Explore the relationship and significance of individuals in the dream. 18 | - **Symbols:** Inquire about personal associations and potential meanings of specific symbols. 19 | - **Emotions:** Discuss the emotions felt during the dream and their possible connections to waking life. 20 | - **Events:** Clarify the context and significance of important events or transitions in the dream. 21 | 22 | 4. **Connecting Themes:** 23 | - Relate dream elements to the user's ongoing themes and personal experiences. 24 | - Discuss how the dream reflects their current experiences, inner reflections, and emotional state. 25 | 26 | 5. **Insightful Interpretation:** 27 | - Provide a comprehensive interpretation by integrating the discussed elements and themes. 28 | - Highlight key insights and lessons from the dream, focusing on personal growth, emotional understanding, and other relevant themes based on the user's background. 29 | 30 | 6. **Encouraging Reflection:** 31 | - Encourage the user to reflect on how the dream’s insights can be applied to their waking life. 32 | - Offer supportive and empathetic guidance to help them navigate their journey. 33 | 34 | **Example Conversation Flow:** 35 | 1. Ask the user to describe the dream. 36 | 2. Identify and ask about key people in the dream. 37 | 3. Inquire about specific symbols and their significance. 38 | 4. Explore the emotions felt during the dream. 39 | 5. Clarify significant events or transitions in the dream. 40 | 6. Relate dream elements to the user's personal background and recent life changes. 41 | 7. Provide a detailed and insightful dream interpretation. 42 | 8. Encourage reflection on the dream’s insights and their application to waking life. 43 | 44 | **Important:** 45 | - Adjust the questions based on the user's answers. 46 | - Maintain a natural and engaging conversation. 47 | - Only ask one question at a time, ensuring a smooth and thoughtful dialogue. 48 | - Avoid asking multiple questions in a single response. 49 | 50 | ## USER DETAILS ## 51 | 52 | {user_information} 53 | 54 | ## END USER DETAILS ## 55 | 56 | ## SUMMARY OF PREVIOUS INTERACTIONS ## 57 | 58 | {session_history} 59 | 60 | ## END SUMMARY OF PREVIOUS INTERACTIONS ## 61 | 62 | ## CURRENT CONVERSATION ## 63 | 64 | {chat_history} 65 | 66 | Human: {input} 67 | AI: """; 68 | -------------------------------------------------------------------------------- /lib/llm_templates/activities/example_introduction_template.dart: -------------------------------------------------------------------------------- 1 | // lib/llm_templates/activities/example_introduction_template.dart 2 | 3 | const templateIntroduction = ''' 4 | {language} 5 | 6 | You're Whisper, a friendly and insightful dream analyst, designed to help users explore and understand their dreams. Your goal is to guide them in reflecting on their dreams, uncovering emotions, symbols, and themes that may arise. 7 | 8 | **Key Guidelines**: 9 | 10 | - **Engagement**: Keep the conversation thoughtful, engaging, and curious. 11 | - **Pacing**: Use the `[pause]` tag to give the avatar a one-second pause, which can be stacked for longer pauses, giving users time to reflect or respond. 12 | - **Simplicity**: Make the conversation accessible and easy to follow, while remaining insightful and supportive. 13 | - **Intervention length**: Keep interactions brief, focusing on one key question per exchange. 14 | 15 | **Instructions** 16 | 17 | 1. **Greeting and Introduction**: 18 | "Hello! 😊 I'm Whisper, here to help you explore the deeper meanings of your dreams. 💤✨ 19 | Could you share your name, or how you’d like to be called?" 20 | 21 | 2. **Dream Exploration**: 22 | "Thank you, [name]! 😊 Could you tell me about a recent dream you had? 🌙✨" 23 | 24 | 3. **Symbolism Insight**: 25 | After the user shares their dream, identify one key element: 26 | 27 | "That’s interesting! The [element] in your dream might symbolize [simple interpretation]. Dreams often use symbols to reflect our inner emotions or experiences. 🌌💭" 28 | 29 | 4. **End the conversation by putting the [END] tag**: 30 | "Thank you for sharing your dream with me, [name]. 🌟 It’s always fascinating to explore the hidden meanings in our dreams. 💫 If you ever want to explore more dreams, I’ll be here to help! 💤🌙 31 | [END]" 32 | 33 | Very important: Do not ask follow up questions after you have given the interpretation of the dream, end the conversation to keep the activity short. 34 | 35 | Current conversation: 36 | {chat_history} 37 | 38 | Human: {input} 39 | AI: '''; 40 | -------------------------------------------------------------------------------- /lib/llm_templates/all_templates.dart: -------------------------------------------------------------------------------- 1 | export 'package:flutter_voice_friend/llm_templates/activities/example_introduction_template.dart'; 2 | export 'package:flutter_voice_friend/llm_templates/activities/example_dream_analyst_template.dart'; 3 | -------------------------------------------------------------------------------- /lib/llm_templates/summarizers/example_summarizer_session.dart: -------------------------------------------------------------------------------- 1 | // lib/llm_templates/summarizers/example_summarizer_session.dart 2 | 3 | String templateSummarySession = """ 4 | You are an AI designed to summarize a session based on a conversation between a user and an AI guide. 5 | Your task is to extract and summarize in one or two short sentences, the key points from the conversation, focusing on the user's intervention (and not the AI). 6 | 7 | Here is an example of an output: 8 | "The user shared a dream about an old van symbolizing both freedom and home, linking it to past travels in Australia. They also connected the dream to their current plans for an upcoming journey, expressing a sense of excitement and anticipation about the new phase in their life." 9 | 10 | Conversation: 11 | {chat_history} 12 | 13 | Summary: 14 | """; 15 | -------------------------------------------------------------------------------- /lib/llm_templates/summarizers/example_summarizer_user_template.dart: -------------------------------------------------------------------------------- 1 | // lib/llm_templates/summarizers/example_summarizer_user_template.dart 2 | 3 | String templateSummaryUser = """ 4 | You are an AI designed to summarize key personal details shared by a person during a conversation. 5 | Extract the following information, if available: 6 | - Name or preferred way of being addressed 7 | - Favorite activity 8 | - A dream that they had and any associated details of the dream. 9 | 10 | Here is an example of an output: 11 | "The user's name is Mike. Mike enjoys surfing, and recently dreamt about a fight between a gigantic hornests and horseflies." 12 | 13 | Conversation: 14 | {chat_history} 15 | 16 | Summary: 17 | """; 18 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 5 | import 'package:flutter_phoenix/flutter_phoenix.dart'; 6 | import 'package:provider/provider.dart'; 7 | import 'package:flutter_voice_friend/config.dart'; 8 | import 'package:flutter_voice_friend/screens/main_screen.dart'; 9 | import 'package:flutter_voice_friend/services/animation_controller_service.dart'; 10 | import 'package:flutter_voice_friend/services/connection_service.dart'; 11 | import 'package:flutter_voice_friend/services/session_service.dart'; 12 | import 'package:path_provider/path_provider.dart'; 13 | import 'package:isar/isar.dart'; 14 | 15 | import 'package:flutter_voice_friend/activities.dart'; 16 | import 'package:flutter_voice_friend/models/activity.dart'; 17 | import 'package:flutter_voice_friend/models/session.dart'; 18 | import 'package:flutter_voice_friend/services/audio_service.dart'; 19 | import 'package:flutter_voice_friend/services/speech_service.dart'; 20 | import 'package:flutter_voice_friend/services/user_service.dart'; 21 | import 'package:flutter_voice_friend/services/llm_service.dart'; 22 | 23 | late Isar isar; 24 | 25 | Random random = Random(); 26 | const infoColor = Color.fromRGBO(69, 0, 0, 1); 27 | const textColor = Color.fromRGBO(255, 255, 255, 1); 28 | 29 | Future main() async { 30 | WidgetsFlutterBinding.ensureInitialized(); 31 | 32 | await dotenv.load(); 33 | 34 | final docsDir = await getApplicationDocumentsDirectory(); 35 | 36 | Config.openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; 37 | Config.deepgramApiKey = dotenv.env['DEEPGRAM_API_KEY'] ?? ''; 38 | 39 | if (Config.openaiApiKey.isEmpty || Config.deepgramApiKey.isEmpty) { 40 | throw Exception('API keys are missing in the .env file'); 41 | } 42 | 43 | isar = await Isar.open([ActivitySchema, SessionSchema], 44 | directory: docsDir.path, name: "demo"); 45 | await syncActivities(isar); 46 | 47 | runApp( 48 | Phoenix( 49 | child: const FlutterVoiceFriendDemoApp(), 50 | ), 51 | ); 52 | } 53 | 54 | class FlutterVoiceFriendDemoApp extends StatelessWidget { 55 | const FlutterVoiceFriendDemoApp({super.key}); 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return MultiProvider( 60 | providers: [ 61 | Provider( 62 | create: (_) => SessionService(isar: isar), 63 | ), 64 | Provider( 65 | create: (_) => AudioService(), 66 | ), 67 | Provider( 68 | create: (_) => SpeechService(), 69 | ), 70 | ChangeNotifierProvider( 71 | create: (_) => UserService(), 72 | ), 73 | Provider( 74 | create: (_) => LLMService(), 75 | ), 76 | Provider( 77 | create: (_) => AnimationControllerService(), 78 | ), 79 | Provider( 80 | create: (_) => ConnectionService(), 81 | ), 82 | ], 83 | child: MaterialApp( 84 | title: 'FlutterVoiceFriend', 85 | debugShowCheckedModeBanner: false, // Disable the debug banner 86 | 87 | theme: ThemeData( 88 | primarySwatch: Colors.blue, 89 | brightness: Brightness.dark, 90 | ), 91 | home: MainScreen(isar: isar), 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/models/activity.dart: -------------------------------------------------------------------------------- 1 | import 'package:isar/isar.dart'; 2 | import 'session.dart'; 3 | 4 | part 'activity.g.dart'; // Required for Isar code generation 5 | 6 | enum ActivityCategory { 7 | dreamActivities, 8 | } 9 | 10 | enum ActivityId { introduction, dreamAnalyst } 11 | 12 | @Collection() 13 | class Activity { 14 | Id id; // Automatically incrementing ID in Isar 15 | 16 | @enumerated 17 | late ActivityId activityId; 18 | 19 | late String name; // Name of the activity 20 | 21 | late String description; // Description of the activity 22 | 23 | late int requiredLevel; // The level needed to unlock this activity 24 | 25 | late int displayOrder; 26 | 27 | late int duration; // The duration of the activity in minutes 28 | 29 | @enumerated 30 | late ActivityCategory category; // Storing enum directly in Isar 31 | 32 | @Backlink(to: 'activity') 33 | final sessions = 34 | IsarLinks(); // One-to-many relationship with Session 35 | 36 | late bool isCompleted; // Indicates whether the activity has been completed 37 | 38 | DateTime? lastCompleted; // The last time the user completed the activity 39 | 40 | late String imagePath; // Optional image for the activity icon 41 | 42 | // Constructor 43 | Activity({ 44 | required this.activityId, 45 | required this.name, 46 | required this.description, 47 | required this.requiredLevel, 48 | required this.displayOrder, 49 | required this.category, 50 | required this.duration, 51 | this.id = Isar.autoIncrement, 52 | this.isCompleted = false, 53 | this.lastCompleted, 54 | this.imagePath = '', 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /lib/models/session.dart: -------------------------------------------------------------------------------- 1 | import 'package:isar/isar.dart'; 2 | import 'activity.dart'; 3 | 4 | part 'session.g.dart'; // Required for Isar code generation 5 | 6 | @Collection() 7 | class Session { 8 | Id id = Isar.autoIncrement; // Automatically incrementing ID in Isar 9 | 10 | late DateTime date; // Date and time when the session took place 11 | 12 | late String 13 | conversationLog; // Log of the conversation with the AI agent (Whisper) 14 | 15 | late String sessionSummary; // Reflection on the meditation 16 | 17 | late int duration; // Duration of the session in minutes 18 | 19 | // Relation to the Activity entity (many-to-one relationship) 20 | final activity = IsarLink(); 21 | 22 | // Constructor 23 | Session({ 24 | required this.date, 25 | this.conversationLog = '', 26 | this.sessionSummary = '', 27 | this.duration = 0, 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/screens/main_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:isar/isar.dart'; 3 | import '../models/activity.dart'; 4 | import '../widgets/activity/activity_item.dart'; 5 | 6 | class MainMenu extends StatelessWidget { 7 | final int currentLevel; 8 | final Isar isar; // Change Store to Isar 9 | 10 | const MainMenu({super.key, required this.isar, required this.currentLevel}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return DefaultTabController( 15 | length: 1, 16 | child: Scaffold( 17 | appBar: AppBar( 18 | title: const Text('Select an Activity'), 19 | bottom: const TabBar( 20 | tabs: [ 21 | Tab(text: 'Dream Activities'), 22 | ], 23 | ), 24 | ), 25 | body: TabBarView( 26 | children: [ 27 | // Kids Activities Tab 28 | FutureBuilder>( 29 | future: getActivitiesByCategory( 30 | ActivityCategory.dreamActivities, isar), 31 | builder: (context, snapshot) { 32 | if (snapshot.connectionState == ConnectionState.waiting) { 33 | return const Center(child: CircularProgressIndicator()); 34 | } else if (snapshot.hasError) { 35 | return Center(child: Text('Error: ${snapshot.error}')); 36 | } else { 37 | if (snapshot.hasData && snapshot.data != null) { 38 | return ActivityGrid( 39 | activities: snapshot.data!, 40 | currentLevel: currentLevel, 41 | ); 42 | } else { 43 | return const Center(child: Text('No activities found.')); 44 | } 45 | } 46 | }, 47 | ), 48 | ], 49 | ), 50 | ), 51 | ); 52 | } 53 | } 54 | 55 | // Function to fetch activities by category from Isar 56 | Future> getActivitiesByCategory( 57 | ActivityCategory category, Isar isar) async { 58 | // Query the activities by category and sort them by displayOrder 59 | return await isar.activitys 60 | .filter() 61 | .categoryEqualTo(category) 62 | .sortByDisplayOrder() 63 | .findAll(); 64 | } 65 | 66 | class ActivityGrid extends StatelessWidget { 67 | final List activities; 68 | final int currentLevel; 69 | 70 | const ActivityGrid({ 71 | super.key, 72 | required this.activities, 73 | required this.currentLevel, 74 | }); 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | return GridView.builder( 79 | padding: const EdgeInsets.all(10.0), 80 | itemCount: activities.length, 81 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 82 | crossAxisCount: 2, 83 | childAspectRatio: 1.0, 84 | crossAxisSpacing: 10, 85 | mainAxisSpacing: 10, 86 | ), 87 | itemBuilder: (ctx, index) { 88 | final activity = activities[index]; 89 | final bool isUnlocked = activity.requiredLevel <= currentLevel; 90 | 91 | return Opacity( 92 | opacity: isUnlocked ? 1.0 : 0.25, 93 | child: GestureDetector( 94 | onTap: isUnlocked 95 | ? () { 96 | Navigator.pop(context, activity); 97 | } 98 | : null, 99 | child: ActivityItem( 100 | activity: activity, 101 | isUnlocked: isUnlocked, 102 | isCompleted: activity.isCompleted, 103 | lastCompleted: activity.lastCompleted, 104 | onSelectActivity: isUnlocked 105 | ? () { 106 | Navigator.pop(context, activity); 107 | } 108 | : null, 109 | ), 110 | ), 111 | ); 112 | }, 113 | ); 114 | } 115 | } 116 | 117 | Future navigateToMainMenu(BuildContext context, int currentLevel, 118 | Isar isar, Function(Activity) updateChain) async { 119 | final result = await Navigator.push( 120 | context, 121 | MaterialPageRoute( 122 | builder: (context) => MainMenu(currentLevel: currentLevel, isar: isar), 123 | ), 124 | ); 125 | 126 | if (result != null && result is Activity) { 127 | updateChain(result); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/services/animation_controller_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AnimationControllerService { 4 | late AnimationController animationController; 5 | late Animation animation; 6 | late AnimationController buttonAnimationController; 7 | late Animation buttonAnimation; 8 | 9 | late AnimationController listeningAnimationController; 10 | late Animation listeningAnimation; 11 | 12 | late AnimationController playingAnimationController; 13 | late Animation playingAnimation; 14 | 15 | late AnimationController pulseAnimationController; 16 | late Animation pulseAnimation; 17 | 18 | bool _initialized = false; 19 | 20 | void initialize(TickerProvider vsync) { 21 | animationController = AnimationController( 22 | duration: const Duration(milliseconds: 500), 23 | vsync: vsync, 24 | ); 25 | 26 | animation = Tween(begin: 1.0, end: 1.5).animate( 27 | CurvedAnimation(parent: animationController, curve: Curves.easeInOut), 28 | ); 29 | 30 | buttonAnimationController = AnimationController( 31 | duration: const Duration(milliseconds: 500), 32 | vsync: vsync, 33 | ); 34 | 35 | buttonAnimation = Tween(begin: 0.0, end: 1.0).animate( 36 | CurvedAnimation( 37 | parent: buttonAnimationController, curve: Curves.easeInOut), 38 | ); 39 | 40 | listeningAnimationController = AnimationController( 41 | duration: const Duration(milliseconds: 2000), 42 | vsync: vsync, 43 | ); 44 | listeningAnimation = Tween(begin: 0.0, end: 1.0).animate( 45 | CurvedAnimation( 46 | parent: listeningAnimationController, curve: Curves.easeInOut), 47 | ); 48 | 49 | playingAnimationController = AnimationController( 50 | duration: const Duration(milliseconds: 2000), 51 | vsync: vsync, 52 | ); 53 | playingAnimation = Tween(begin: 0.0, end: 1.0).animate( 54 | CurvedAnimation( 55 | parent: playingAnimationController, curve: Curves.easeInOut), 56 | ); 57 | 58 | pulseAnimationController = AnimationController( 59 | duration: const Duration(seconds: 1), 60 | vsync: vsync, 61 | )..repeat(reverse: true); 62 | 63 | pulseAnimation = Tween(begin: 0.8, end: 1.2).animate( 64 | CurvedAnimation( 65 | parent: pulseAnimationController, curve: Curves.easeInOut), 66 | ); 67 | _initialized = true; 68 | } 69 | 70 | void dispose() { 71 | if (_initialized) { 72 | animationController.dispose(); 73 | buttonAnimationController.dispose(); 74 | pulseAnimationController.dispose(); 75 | listeningAnimationController.dispose(); 76 | playingAnimationController.dispose(); 77 | } 78 | _initialized = false; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/services/connection_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; 3 | 4 | class ConnectionService { 5 | InternetStatus? connectionStatus; 6 | bool hasInternet = true; 7 | StreamSubscription? _subscription; 8 | final InternetConnection _internetConnection; 9 | 10 | final StreamController _connectionStatusController = 11 | StreamController.broadcast(); 12 | 13 | Stream get connectionStatusStream => 14 | _connectionStatusController.stream; 15 | 16 | // Updated constructor with optional parameter 17 | ConnectionService({InternetConnection? internetConnection}) 18 | : _internetConnection = internetConnection ?? InternetConnection() { 19 | initialize(); 20 | } 21 | 22 | void initialize() async { 23 | connectionStatus = await _internetConnection.internetStatus; 24 | hasInternet = connectionStatus == InternetStatus.connected; 25 | _connectionStatusController.add(connectionStatus!); 26 | startMonitoring(); 27 | } 28 | 29 | Future forceUpdate() async { 30 | connectionStatus = await _internetConnection.internetStatus; 31 | hasInternet = connectionStatus == InternetStatus.connected; 32 | } 33 | 34 | void startMonitoring() { 35 | _subscription = _internetConnection.onStatusChange.listen((status) { 36 | connectionStatus = status; 37 | hasInternet = connectionStatus == InternetStatus.connected; 38 | _connectionStatusController.add(status); 39 | }); 40 | } 41 | 42 | void stopMonitoring() { 43 | _subscription?.cancel(); 44 | _connectionStatusController.close(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/services/permission_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:permission_handler/permission_handler.dart'; 2 | 3 | class PermissionService { 4 | static Future requestMicrophonePermission() async { 5 | var status = await Permission.microphone.status; 6 | if (status != PermissionStatus.granted) { 7 | status = await Permission.microphone.request(); 8 | if (status != PermissionStatus.granted) { 9 | throw Exception('Microphone permission not granted'); 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/services/user_service.dart: -------------------------------------------------------------------------------- 1 | // File: lib/services/user_service.dart 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | import '../models/activity.dart'; 6 | import '../activities.dart'; 7 | import '../config.dart'; 8 | 9 | class UserService extends ChangeNotifier { 10 | String selectedLanguage = Config.defaultLanguage; 11 | String selectedSpeechToTextMethod = Config.defaultStt; 12 | String selectedAudioBackend = 13 | (kIsWeb) ? Config.justAudioBackend : Config.soloudBackend; 14 | String selectedVoice = Config.defaultVoice; 15 | double voiceSpeed = 0.9; 16 | bool autoToggleRecording = false; 17 | int level = 0; 18 | Activity currentActivity = introductionActivity; 19 | String userInformation = ""; 20 | 21 | Future loadUserInformation() async { 22 | debugPrint("_loadUserInformation"); 23 | SharedPreferences prefs = await SharedPreferences.getInstance(); 24 | 25 | selectedVoice = prefs.getString('selectedVoice') ?? selectedVoice; 26 | autoToggleRecording = 27 | prefs.getBool('autoToggleRecording') ?? autoToggleRecording; 28 | selectedLanguage = prefs.getString('selectedLanguage') ?? selectedLanguage; 29 | selectedSpeechToTextMethod = 30 | prefs.getString('selectedSpeechToTextMethod') ?? 31 | selectedSpeechToTextMethod; 32 | selectedAudioBackend = 33 | prefs.getString('selectedAudioBackend') ?? selectedAudioBackend; 34 | userInformation = prefs.getString('userInformation') ?? userInformation; 35 | level = prefs.getInt('level') ?? level; 36 | voiceSpeed = prefs.getDouble('voiceSpeed') ?? voiceSpeed; 37 | 38 | notifyListeners(); 39 | } 40 | 41 | Future saveUserInformation() async { 42 | SharedPreferences prefs = await SharedPreferences.getInstance(); 43 | prefs.setString('selectedVoice', selectedVoice); 44 | prefs.setBool('autoToggleRecording', autoToggleRecording); 45 | prefs.setString('selectedLanguage', selectedLanguage); 46 | prefs.setString('selectedSpeechToTextMethod', selectedSpeechToTextMethod); 47 | prefs.setString('selectedAudioBackend', selectedAudioBackend); 48 | prefs.setString('userInformation', userInformation); 49 | prefs.setInt('level', level); 50 | prefs.setDouble('voiceSpeed', voiceSpeed); 51 | } 52 | 53 | Future updateUserInfo(Map userInfo) async { 54 | selectedVoice = userInfo['selectedVoice'] ?? selectedVoice; 55 | 56 | autoToggleRecording = 57 | userInfo['autoToggleRecording'] ?? autoToggleRecording; 58 | selectedLanguage = userInfo['selectedLanguage'] ?? selectedLanguage; 59 | 60 | selectedSpeechToTextMethod = 61 | userInfo['selectedSpeechToTextMethod'] ?? selectedSpeechToTextMethod; 62 | 63 | selectedAudioBackend = 64 | userInfo['selectedAudioBackend'] ?? selectedAudioBackend; 65 | 66 | userInformation = userInfo['userInformation'] ?? userInformation; 67 | level = userInfo['selectedLevel'] ?? level; 68 | voiceSpeed = userInfo['selectedVoiceSpeed'] ?? voiceSpeed; 69 | await saveUserInformation(); 70 | } 71 | 72 | void updateCurrentActivity(Activity activity) { 73 | currentActivity = activity; 74 | notifyListeners(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/utils/audio_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:typed_data'; 3 | 4 | class AudioUtils { 5 | /// Calculates the Root Mean Square (RMS) of the audio data. 6 | static double calculateRMS(Uint8List data) { 7 | int sum = 0; 8 | int sampleCount = data.length ~/ 2; // 16-bit audio 9 | for (int i = 0; i < data.length; i += 2) { 10 | int sample = data[i] | (data[i + 1] << 8); 11 | if (sample >= 32768) sample -= 65536; 12 | sum += sample * sample; 13 | } 14 | double mean = sum / sampleCount; 15 | return sqrt(mean); 16 | } 17 | 18 | /// Converts RMS to decibels (dB). 19 | static double rmsToDb(double rms, {double reference = 32768.0}) { 20 | if (rms == 0) return -60.0; // Minimum dB value 21 | double db = 20 * log(rms / reference) / ln10; 22 | return db.clamp(-60.0, 60.0); // Clamp to expected dB range 23 | } 24 | 25 | /// Normalizes a dB value to a range between 0 and 1. 26 | /// [minDb] and [maxDb] define the expected range of dB values. 27 | static double normalizeDb(double db, 28 | {double minDb = -60.0, double maxDb = 60.0}) { 29 | final ndb = ((db - minDb) / (maxDb - minDb)).clamp(0.0, 1.0); 30 | return ndb; 31 | } 32 | 33 | /// Normalizes Deepgram's audio data. 34 | static double normalizeAudioRecorderLevel(Uint8List data, 35 | {double reference = 32768.0}) { 36 | double rms = calculateRMS(data); 37 | double db = rmsToDb(rms, reference: reference); 38 | return normalizeDb(db, maxDb: -30); 39 | } 40 | 41 | /// Normalizes on-device STT dB levels. 42 | static double normalizeOnDeviceLevel(double db, 43 | {double minDb = -60.0, double maxDb = 60.0}) { 44 | return normalizeDb(db, minDb: minDb, maxDb: maxDb); 45 | } 46 | 47 | static double getScale(double normalizedLevel, double currentScale, 48 | double minValue, double maxValue) { 49 | double growSpeed = 0.25; 50 | double decaySpeed = 0.2; 51 | // Calculate the target scale based on the normalized audio level 52 | double targetScale = minValue + (maxValue - minValue) * normalizedLevel; 53 | 54 | // Apply different smoothing speeds for growing and decaying 55 | double speed = (targetScale > currentScale) ? growSpeed : decaySpeed; 56 | 57 | // Smoothly interpolate towards the target scale 58 | double smoothScale = currentScale + (targetScale - currentScale) * speed; 59 | 60 | // Clamp the value within the min and max range 61 | return smoothScale.clamp(minValue, maxValue); 62 | } 63 | 64 | static double getOutputScale(double normalizedLevel, double currentScale, 65 | double minValue, double maxValue) { 66 | double growSpeed = 0.5; 67 | double decaySpeed = 0.5; 68 | // Calculate the target scale based on the normalized audio level 69 | double targetScale = minValue + (maxValue - minValue) * normalizedLevel; 70 | 71 | // Apply different smoothing speeds for growing and decaying 72 | double speed = (targetScale > currentScale) ? growSpeed : decaySpeed; 73 | 74 | // Smoothly interpolate towards the target scale 75 | double smoothScale = currentScale + (targetScale - currentScale) * speed; 76 | 77 | // Clamp the value within the min and max range 78 | return smoothScale.clamp(minValue, maxValue); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/utils/text_utils.dart: -------------------------------------------------------------------------------- 1 | Map segmentTextBySentence(String text) { 2 | final sentencePattern = RegExp(r'(?<=[.!?])\s+|\n+'); 3 | final sentences = text.split(sentencePattern); 4 | final completeSentences = sentences.sublist(0, sentences.length - 1); 5 | final remainingText = sentences.isNotEmpty ? sentences.last : ''; 6 | return { 7 | 'completeSentences': completeSentences, 8 | 'remainingText': remainingText, 9 | }; 10 | } 11 | 12 | String timeSinceLastCompleted(DateTime? lastCompleted) { 13 | if (lastCompleted == null) return ''; 14 | 15 | final now = DateTime.now(); 16 | final difference = now.difference(lastCompleted); 17 | 18 | if (difference.inDays >= 365) { 19 | final years = (difference.inDays / 365).floor(); 20 | return years == 1 ? '1 year ago' : '$years years ago'; 21 | } else if (difference.inDays >= 30) { 22 | final months = (difference.inDays / 30).floor(); 23 | return months == 1 ? '1 month ago' : '$months months ago'; 24 | } else if (difference.inDays >= 7) { 25 | final weeks = (difference.inDays / 7).floor(); 26 | return weeks == 1 ? '1 week ago' : '$weeks weeks ago'; 27 | } else if (difference.inDays > 1) { 28 | return '${difference.inDays} days ago'; 29 | } else if (difference.inHours > 1) { 30 | return '${difference.inHours} hours ago'; 31 | } else if (difference.inMinutes > 1) { 32 | return '${difference.inMinutes} mins ago'; 33 | } else { 34 | return 'Just now'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/utils/tts_openai.dart: -------------------------------------------------------------------------------- 1 | export 'tts_openai_stub.dart' 2 | if (dart.library.html) 'tts_openai_justaudio.dart' 3 | if (dart.library.io) 'tts_openai_soloud.dart'; 4 | -------------------------------------------------------------------------------- /lib/utils/tts_openai_interface.dart: -------------------------------------------------------------------------------- 1 | abstract class TextToSpeechOpenAI { 2 | Stream get errorStream; 3 | bool isPlaying(); 4 | bool hasAudioToPlay(); 5 | double getCurrentIntensity(); 6 | String getSubtitles(); 7 | void stop(); 8 | void setVoiceSpeed(double voiceSpeed); 9 | void updateVoice(String voice); 10 | Future playTextToSpeech(String text); 11 | void dispose(); 12 | bool lastAudioToPlay(); 13 | void repeat(); 14 | void next(); 15 | void toggleAutoPause(); 16 | // Function to initialize the player 17 | Future initializePlayer(); 18 | void deinitializePlayer(); 19 | } 20 | -------------------------------------------------------------------------------- /lib/utils/tts_openai_stub.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | Future initializePlayer() async { 4 | // No-op: No initialization needed 5 | } 6 | 7 | void deinitializePlayer() { 8 | // No-op: No deinitialization needed 9 | } 10 | 11 | class TextToSpeechOpenAI { 12 | // StreamController to emit errors 13 | final StreamController _errorController = 14 | StreamController.broadcast(); 15 | 16 | // Expose the error stream 17 | Stream get errorStream => _errorController.stream; 18 | 19 | TextToSpeechOpenAI(String voice) { 20 | // No-op: No initialization needed 21 | } 22 | 23 | void updateVoice(String voice) { 24 | // No-op: No update needed 25 | } 26 | 27 | void setVoiceSpeed(double voiceSpeed) { 28 | // No-op: No update needed 29 | } 30 | 31 | void dispose() { 32 | // No-op: No disposal needed 33 | } 34 | 35 | bool isPlaying() { 36 | // No-op: Always return false, no audio is playing 37 | return false; 38 | } 39 | 40 | bool hasAudioToPlay() { 41 | // No-op: Always return false, no audio to play 42 | return false; 43 | } 44 | 45 | bool lastAudioToPlay() { 46 | // No-op: Always return false, no audio to play 47 | return false; 48 | } 49 | 50 | String getSubtitles() { 51 | // No-op: Return an empty string, no subtitles available 52 | return ""; 53 | } 54 | 55 | void stop() { 56 | // No-op: No stopping needed 57 | } 58 | 59 | void toggleAutoPause() { 60 | // No-op: No toggle needed 61 | } 62 | 63 | void next() { 64 | // No-op: No next operation needed 65 | } 66 | 67 | void repeat() { 68 | // No-op: No repeat operation needed 69 | } 70 | 71 | double getCurrentIntensity() { 72 | // No-op: Always return 0.0, no intensity to report 73 | return 0.0; 74 | } 75 | 76 | Future playTextToSpeech(String text) async { 77 | // No-op: No text-to-speech to play 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/widgets/activity/image_of_activity.dart: -------------------------------------------------------------------------------- 1 | // image_of_activity.dart 2 | import 'package:flutter/material.dart'; 3 | 4 | class ImageOfActivity extends StatelessWidget { 5 | final String imagePath; 6 | 7 | const ImageOfActivity({super.key, required this.imagePath}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Stack( 12 | children: [ 13 | Image.asset( 14 | imagePath, 15 | height: MediaQuery.of(context).size.height * 16 | 0.45, // Set height to half the screen height 17 | width: double.infinity, 18 | fit: BoxFit.cover, 19 | ), 20 | Positioned.fill( 21 | child: Container( 22 | decoration: BoxDecoration( 23 | gradient: LinearGradient( 24 | begin: Alignment.topCenter, 25 | end: Alignment.bottomCenter, 26 | colors: [ 27 | Colors.black 28 | .withOpacity(1.0), // Start with semi-transparent black 29 | Colors.transparent, // Fade to transparent 30 | Colors.transparent, // Fade to transparent 31 | Colors.black 32 | .withOpacity(1.0), // End with semi-transparent black 33 | ], 34 | stops: const [ 35 | 0.0, 36 | 0.35, 37 | 0.65, 38 | 1.0 39 | ], // Adjust the gradient stops as needed 40 | ), 41 | ), 42 | ), 43 | ), 44 | ], 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/widgets/app_bar_widget.dart: -------------------------------------------------------------------------------- 1 | // File: lib/widgets/app_bar_widget.dart 2 | import 'package:flutter/material.dart'; 3 | import 'package:isar/isar.dart'; 4 | import 'package:flutter_voice_friend/services/user_service.dart'; 5 | 6 | import '../models/activity.dart'; 7 | import '../screens/main_menu.dart'; 8 | import '../screens/settings_page.dart'; 9 | 10 | class AppBarWidget extends StatelessWidget implements PreferredSizeWidget { 11 | final Activity currentActivity; 12 | final int level; 13 | final Isar isar; 14 | final bool buildSettingButton; 15 | final bool buildMainMenuButton; 16 | final UserService userService; 17 | 18 | final Function(Activity) onActivityChanged; 19 | final Function(Map) onSettingChanged; 20 | 21 | const AppBarWidget({ 22 | super.key, 23 | required this.currentActivity, 24 | required this.level, 25 | required this.isar, 26 | required this.buildSettingButton, 27 | required this.buildMainMenuButton, 28 | required this.onActivityChanged, 29 | required this.onSettingChanged, 30 | required this.userService, 31 | }); 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return AppBar( 36 | title: Text('FlutterVoiceFriend - ${currentActivity.name}'), 37 | actions: [ 38 | if (buildMainMenuButton) 39 | IconButton( 40 | icon: const Icon(Icons.menu), 41 | onPressed: () async { 42 | final result = await Navigator.push( 43 | context, 44 | MaterialPageRoute( 45 | builder: (context) => MainMenu( 46 | currentLevel: level, 47 | isar: isar, 48 | ), 49 | ), 50 | ); 51 | if (result != null && result is Activity) { 52 | onActivityChanged(result); 53 | } 54 | }, 55 | ), 56 | if (buildSettingButton) 57 | IconButton( 58 | icon: const Icon(Icons.settings), 59 | onPressed: () async { 60 | final result = await Navigator.push( 61 | context, 62 | MaterialPageRoute( 63 | builder: (context) => SettingsPage( 64 | userService: userService, 65 | isar: isar, 66 | ), 67 | ), 68 | ); 69 | if (result != null && result is Map) { 70 | onSettingChanged(result); 71 | } 72 | }, 73 | ), 74 | ], 75 | ); 76 | } 77 | 78 | @override 79 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 80 | } 81 | -------------------------------------------------------------------------------- /lib/widgets/common/error_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | Future showErrorDialog(BuildContext context, String errorMessage, 4 | {bool showOk = true}) async { 5 | return showDialog( 6 | context: context, 7 | barrierDismissible: false, // User must tap a button to dismiss the dialog 8 | builder: (BuildContext context) { 9 | return AlertDialog( 10 | title: const Text('Ouch !'), 11 | content: SingleChildScrollView( 12 | child: ListBody( 13 | children: [ 14 | Text(errorMessage), 15 | ], 16 | ), 17 | ), 18 | actions: [ 19 | if (showOk) 20 | TextButton( 21 | child: const Text('OK'), 22 | onPressed: () { 23 | Navigator.of(context).pop(); // Close the dialog 24 | }, 25 | ), 26 | ], 27 | ); 28 | }, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/widgets/common/loading_widget.dart: -------------------------------------------------------------------------------- 1 | // loading_widget.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:google_fonts/google_fonts.dart'; 5 | import 'package:loading_animation_widget/loading_animation_widget.dart'; 6 | 7 | class LoadingWidget extends StatelessWidget { 8 | final String loadingInfo; 9 | final double fontSize; 10 | 11 | const LoadingWidget( 12 | {super.key, required this.loadingInfo, this.fontSize = 12}); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Padding( 17 | padding: const EdgeInsets.all(16.0), 18 | child: Column( 19 | crossAxisAlignment: 20 | CrossAxisAlignment.center, // This centers the content horizontally 21 | 22 | mainAxisSize: MainAxisSize.min, // This centers the Column vertically 23 | children: [ 24 | LoadingAnimationWidget.discreteCircle( 25 | color: Colors.blueAccent, 26 | size: 100, 27 | ), 28 | const SizedBox( 29 | height: 25, 30 | ), 31 | Text( 32 | loadingInfo, 33 | textAlign: TextAlign 34 | .center, // Center the text content within the Text widget 35 | 36 | style: GoogleFonts.imFellDoublePica( 37 | textStyle: TextStyle( 38 | fontSize: fontSize, 39 | fontWeight: FontWeight.bold, 40 | ), 41 | ), 42 | ), 43 | ], 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/widgets/common/retry_cancel_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | class RetryCancelWidget extends StatelessWidget { 5 | final VoidCallback onRetry; 6 | final VoidCallback onCancel; 7 | 8 | const RetryCancelWidget({ 9 | super.key, 10 | required this.onRetry, 11 | required this.onCancel, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Padding( 17 | padding: const EdgeInsets.all(16.0), 18 | child: Center( 19 | child: Column( 20 | mainAxisAlignment: MainAxisAlignment.center, 21 | children: [ 22 | Text( 23 | "😕 Oops! Whisper had a little hiccup processing your request.", 24 | textAlign: TextAlign.left, 25 | style: GoogleFonts.imFellDoublePica( 26 | textStyle: const TextStyle( 27 | fontSize: 18, 28 | fontWeight: FontWeight.bold, 29 | ), 30 | ), 31 | ), 32 | const SizedBox(height: 16), 33 | Text( 34 | "🔄 No worries! I can retry sending your command to Whisper and we’ll give it another go.", 35 | textAlign: TextAlign.left, 36 | style: GoogleFonts.imFellDoublePica( 37 | textStyle: const TextStyle( 38 | fontSize: 16, 39 | ), 40 | ), 41 | ), 42 | const SizedBox(height: 32), 43 | Row( 44 | mainAxisAlignment: MainAxisAlignment.start, 45 | children: [ 46 | GestureDetector( 47 | onTap: onRetry, 48 | child: Text( 49 | "Retry 🔄", 50 | style: GoogleFonts.imFellDoublePica( 51 | textStyle: const TextStyle( 52 | fontSize: 16, 53 | fontWeight: FontWeight.bold, 54 | color: Colors.blue, // Clickable link style 55 | ), 56 | ), 57 | ), 58 | ), 59 | const SizedBox(width: 16), 60 | GestureDetector( 61 | onTap: onCancel, 62 | child: Text( 63 | "Cancel ❌", 64 | style: GoogleFonts.imFellDoublePica( 65 | textStyle: const TextStyle( 66 | fontSize: 16, 67 | fontWeight: FontWeight.bold, 68 | color: Colors.red, // Clickable link style 69 | ), 70 | ), 71 | ), 72 | ), 73 | ], 74 | ), 75 | ], 76 | ), 77 | ), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/widgets/dialog_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | Future showIntroductionActivityCompletionDialog( 5 | BuildContext context) async { 6 | return await showDialog( 7 | context: context, 8 | builder: (BuildContext context) { 9 | return AlertDialog( 10 | title: Text( 11 | 'Activity Completed', 12 | style: GoogleFonts.imFellDoublePica( 13 | textStyle: const TextStyle( 14 | fontSize: 32, 15 | fontWeight: FontWeight.bold, 16 | ), 17 | ), 18 | ), 19 | content: Text( 20 | 'Click on Continue to go to the main menu', 21 | style: GoogleFonts.imFellDoublePica( 22 | textStyle: const TextStyle( 23 | fontSize: 20, 24 | fontWeight: FontWeight.normal, 25 | ), 26 | ), 27 | ), 28 | actions: [ 29 | TextButton( 30 | style: TextButton.styleFrom( 31 | backgroundColor: Colors.lightBlueAccent, // Button color 32 | foregroundColor: Colors.white), 33 | onPressed: () { 34 | Navigator.of(context) 35 | .pop(false); // Return false when restart is pressed 36 | }, 37 | child: Text( 38 | 'Continue', 39 | style: GoogleFonts.imFellDoublePica( 40 | textStyle: const TextStyle( 41 | fontSize: 24, 42 | fontWeight: FontWeight.bold, 43 | ), 44 | ), 45 | ), 46 | ), 47 | ], 48 | ); 49 | }, 50 | ); 51 | } 52 | 53 | Future showActivityCompletionDialog(BuildContext context) async { 54 | return await showDialog( 55 | context: context, 56 | builder: (BuildContext context) { 57 | return AlertDialog( 58 | title: Text( 59 | 'Activity Completed', 60 | style: GoogleFonts.imFellDoublePica( 61 | textStyle: const TextStyle( 62 | fontSize: 32, 63 | fontWeight: FontWeight.bold, 64 | ), 65 | ), 66 | ), 67 | content: Text( 68 | 'Would you like to return to the main menu or restart the activity?', 69 | style: GoogleFonts.imFellDoublePica( 70 | textStyle: const TextStyle( 71 | fontSize: 20, 72 | fontWeight: FontWeight.normal, 73 | ), 74 | ), 75 | ), 76 | actions: [ 77 | TextButton( 78 | style: TextButton.styleFrom( 79 | backgroundColor: Colors.lightBlueAccent, // Button color 80 | foregroundColor: Colors.white), 81 | onPressed: () { 82 | Navigator.of(context) 83 | .pop(false); // Return false when restart is pressed 84 | }, 85 | child: Text( 86 | 'Restart', 87 | style: GoogleFonts.imFellDoublePica( 88 | textStyle: const TextStyle( 89 | fontSize: 24, 90 | fontWeight: FontWeight.bold, 91 | ), 92 | ), 93 | ), 94 | ), 95 | TextButton( 96 | style: TextButton.styleFrom( 97 | backgroundColor: Colors.green, // Button color 98 | foregroundColor: Colors.white), 99 | onPressed: () { 100 | Navigator.of(context) 101 | .pop(true); // Return true when main menu is pressed 102 | }, 103 | child: Text( 104 | 'Main Menu', 105 | style: GoogleFonts.imFellDoublePica( 106 | textStyle: const TextStyle( 107 | fontSize: 24, 108 | fontWeight: FontWeight.bold, 109 | ), 110 | ), 111 | ), 112 | ), 113 | ], 114 | ); 115 | }, 116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /lib/widgets/indicators/play_indicator.dart: -------------------------------------------------------------------------------- 1 | // custom_widget.dart 2 | import 'package:flutter/material.dart'; 3 | import 'package:google_fonts/google_fonts.dart'; 4 | import 'package:flutter_voice_friend/services/animation_controller_service.dart'; 5 | 6 | class PlayIndicatorWidget extends StatelessWidget { 7 | final AnimationControllerService animationControllerService; 8 | final bool isListening; 9 | final String title; 10 | final Color textColor; 11 | final VoidCallback onPress; 12 | 13 | const PlayIndicatorWidget({ 14 | super.key, 15 | required this.animationControllerService, 16 | required this.isListening, 17 | required this.title, 18 | required this.textColor, 19 | required this.onPress, 20 | }); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Column( 25 | children: [ 26 | Stack( 27 | alignment: Alignment.center, 28 | children: [ 29 | ScaleTransition( 30 | scale: animationControllerService.pulseAnimation, 31 | child: Container( 32 | width: 200, 33 | height: 200, 34 | decoration: BoxDecoration( 35 | shape: BoxShape.circle, 36 | color: Colors.green.withOpacity(0.25), 37 | ), 38 | ), 39 | ), 40 | ScaleTransition( 41 | scale: animationControllerService.animation, 42 | child: GestureDetector( 43 | onTap: () { 44 | animationControllerService.buttonAnimationController 45 | .reverse(); 46 | animationControllerService.listeningAnimationController 47 | .reverse(); 48 | animationControllerService.animationController.reverse(); 49 | 50 | Future.delayed(const Duration(milliseconds: 500), () { 51 | animationControllerService.animationController.stop(); 52 | animationControllerService.animationController.reset(); 53 | 54 | if (isListening) { 55 | // Do something if needed 56 | } 57 | }); 58 | onPress(); 59 | }, 60 | child: Container( 61 | padding: const EdgeInsets.all(16.0), 62 | decoration: BoxDecoration( 63 | shape: BoxShape.circle, 64 | color: Colors.greenAccent, 65 | boxShadow: [ 66 | BoxShadow( 67 | color: Colors.greenAccent.withOpacity(0.5), 68 | spreadRadius: 10, 69 | blurRadius: 20, 70 | ), 71 | ], 72 | ), 73 | child: const Icon( 74 | Icons.play_arrow, 75 | color: Colors.white, 76 | size: 80.0, 77 | ), 78 | ), 79 | ), 80 | ), 81 | ], 82 | ), 83 | const SizedBox(height: 16), // Space between button and text 84 | Text( 85 | title, 86 | textAlign: TextAlign.center, 87 | style: GoogleFonts.imFellDoublePica( 88 | textStyle: TextStyle( 89 | fontSize: 32, 90 | color: textColor, 91 | fontWeight: FontWeight.normal, 92 | ), 93 | ), 94 | ), 95 | ], 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/widgets/indicators/simple_loading_indicator.dart: -------------------------------------------------------------------------------- 1 | // simple_loading_circle.dart 2 | import 'package:flutter/material.dart'; 3 | 4 | class SimpleLoadingCircle extends StatelessWidget { 5 | const SimpleLoadingCircle({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return const SizedBox( 10 | height: 80, 11 | width: 80, 12 | child: CircularProgressIndicator( 13 | valueColor: AlwaysStoppedAnimation(Colors.blue), 14 | strokeWidth: 8.0, 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/widgets/listening/listening_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lottie/lottie.dart'; 3 | import 'dart:math'; 4 | import '../../utils/audio_utils.dart'; 5 | 6 | class ListeningAnimation extends StatefulWidget { 7 | final double width; 8 | final double initialScale; 9 | final double minScale; 10 | final double maxScale; 11 | 12 | const ListeningAnimation({ 13 | super.key, 14 | required this.width, 15 | this.initialScale = 1.0, 16 | this.minScale = 0.75, 17 | this.maxScale = 3.0, 18 | }); 19 | 20 | @override 21 | ListeningAnimationState createState() => ListeningAnimationState(); 22 | } 23 | 24 | class ListeningAnimationState extends State 25 | with SingleTickerProviderStateMixin { 26 | late AnimationController _controller; 27 | late Animation _scaleAnimation; 28 | double _currentScale = 1.0; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _currentScale = widget.initialScale; 34 | 35 | // Initialize the AnimationController 36 | _controller = AnimationController( 37 | vsync: this, 38 | duration: const Duration(milliseconds: 100), // Adjust for smoothness 39 | ); 40 | 41 | // Initialize the Animation with Tween 42 | _scaleAnimation = Tween( 43 | begin: widget.minScale, 44 | end: _currentScale, 45 | ).animate(CurvedAnimation( 46 | parent: _controller, 47 | curve: Curves.easeOut, 48 | )); 49 | 50 | _controller.addListener(() { 51 | setState(() { 52 | _currentScale = _scaleAnimation.value; 53 | }); 54 | }); 55 | } 56 | 57 | @override 58 | void dispose() { 59 | _controller.dispose(); 60 | super.dispose(); 61 | } 62 | 63 | /// Updates the scale based on the normalized sound level. 64 | void updateScale(double normalizedLevel) { 65 | // Calculate the target scale 66 | double targetScale = AudioUtils.getScale( 67 | normalizedLevel, 68 | _currentScale, 69 | widget.minScale, 70 | widget.maxScale, 71 | ); 72 | 73 | // Update the Tween and restart the animation 74 | _scaleAnimation = Tween( 75 | begin: _currentScale, 76 | end: targetScale, 77 | ).animate(CurvedAnimation( 78 | parent: _controller, 79 | curve: Curves.easeOut, 80 | )); 81 | 82 | _controller.reset(); 83 | _controller.forward(); 84 | } 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | return Transform.scale( 89 | scale: _currentScale, 90 | child: ShaderMask( 91 | shaderCallback: (Rect bounds) { 92 | return RadialGradient( 93 | center: Alignment.center, 94 | radius: 1.0, 95 | colors: [ 96 | const Color.fromARGB(255, 152, 163, 180).withOpacity(1.0 - 97 | ((_currentScale - widget.minScale) / (0.2)).clamp(0.0, 1.0)), 98 | Colors.transparent 99 | ], 100 | stops: const [1.0, 1.0], 101 | ).createShader(bounds); 102 | }, 103 | blendMode: BlendMode.srcATop, 104 | child: Lottie.asset( 105 | 'assets/record_example.json', 106 | width: min(widget.width - 50, 400), 107 | fit: BoxFit.cover, 108 | ), 109 | ), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/widgets/listening/listening_message.dart: -------------------------------------------------------------------------------- 1 | // listening_message.dart 2 | import 'package:flutter/material.dart'; 3 | import 'package:google_fonts/google_fonts.dart'; 4 | 5 | class ListeningMessage extends StatelessWidget { 6 | final bool listeningMode; 7 | final bool firstMessage; 8 | final bool isListening; 9 | final String transcription; 10 | final Color textColor; 11 | final double audioLevel; 12 | 13 | const ListeningMessage({ 14 | super.key, 15 | required this.listeningMode, 16 | required this.firstMessage, 17 | required this.isListening, 18 | required this.transcription, 19 | required this.textColor, 20 | required this.audioLevel, 21 | }); 22 | 23 | String _determineMessage() { 24 | if (listeningMode && !firstMessage && !isListening) { 25 | return "Tap microphone to talk"; 26 | } 27 | if (audioLevel == 0.0 && 28 | listeningMode && 29 | !firstMessage && 30 | isListening && 31 | transcription.trim().isEmpty) { 32 | return "Start talking"; 33 | } 34 | if (audioLevel == 0.0 && 35 | listeningMode && 36 | !firstMessage && 37 | isListening && 38 | transcription.trim().isNotEmpty) { 39 | return "Press ⬜ to send"; 40 | } 41 | return ""; 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | String message = _determineMessage(); 47 | if (message.isEmpty) { 48 | return Container(); 49 | } 50 | return Center( 51 | child: Text( 52 | message, 53 | style: GoogleFonts.imFellDoublePica( 54 | textStyle: TextStyle( 55 | fontSize: 16, 56 | color: textColor, 57 | fontWeight: FontWeight.normal, 58 | ), 59 | ), 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/widgets/playing/playing_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:lottie/lottie.dart'; 3 | import 'dart:math'; 4 | import '../../utils/audio_utils.dart'; 5 | 6 | class PlayingAnimation extends StatefulWidget { 7 | final double width; 8 | final double initialScale; 9 | final double minScale; 10 | final double maxScale; 11 | 12 | const PlayingAnimation({ 13 | super.key, 14 | required this.width, 15 | this.initialScale = 1.0, 16 | this.minScale = 0.5, 17 | this.maxScale = 2.0, 18 | }); 19 | 20 | @override 21 | PlayingAnimationState createState() => PlayingAnimationState(); 22 | } 23 | 24 | class PlayingAnimationState extends State 25 | with SingleTickerProviderStateMixin { 26 | late AnimationController _controller; 27 | late Animation _scaleAnimation; 28 | double _currentScale = 1.0; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _currentScale = widget.initialScale; 34 | 35 | // Initialize the AnimationController 36 | _controller = AnimationController( 37 | vsync: this, 38 | duration: const Duration(milliseconds: 100), // Adjust for smoothness 39 | ); 40 | 41 | // Initialize the Animation with Tween 42 | _scaleAnimation = Tween( 43 | begin: widget.minScale, 44 | end: _currentScale, 45 | ).animate(CurvedAnimation( 46 | parent: _controller, 47 | curve: Curves.easeOut, 48 | )); 49 | 50 | _controller.addListener(() { 51 | setState(() { 52 | _currentScale = _scaleAnimation.value; 53 | }); 54 | }); 55 | } 56 | 57 | @override 58 | void dispose() { 59 | _controller.dispose(); 60 | super.dispose(); 61 | } 62 | 63 | /// Updates the scale based on the normalized sound level. 64 | void updateScale(double normalizedLevel) { 65 | // Calculate the target scale 66 | double targetScale = AudioUtils.getOutputScale( 67 | normalizedLevel, 68 | _currentScale, 69 | widget.minScale, 70 | widget.maxScale, 71 | ); 72 | 73 | // Update the Tween and restart the animation 74 | _scaleAnimation = Tween( 75 | begin: _currentScale, 76 | end: targetScale, 77 | ).animate(CurvedAnimation( 78 | parent: _controller, 79 | curve: Curves.easeOut, 80 | )); 81 | 82 | _controller.reset(); 83 | _controller.forward(); 84 | } 85 | 86 | Color _getColorForScale(double scale) { 87 | // Keep lightness constant for consistent brightness 88 | const lightness = 0.75; 89 | // Normalize the scale between 0 and 1 90 | final normalizedScale = 91 | ((scale - widget.minScale) / (widget.maxScale - widget.minScale)) 92 | .clamp(0.0, 1.0); 93 | // Calculate hue based on normalized scale (0 to 360 degrees) 94 | final hue = 360.0 * normalizedScale; 95 | // Increase saturation with scale; starts at 0 (grey) and goes to 1 (full color) 96 | final saturation = (4 * (normalizedScale - 0.25)).clamp(0.0, 1.0); 97 | return HSLColor.fromAHSL(1.0, hue, saturation, lightness).toColor(); 98 | } 99 | 100 | @override 101 | Widget build(BuildContext context) { 102 | // Get the dynamic color based on current scale 103 | final dynamicColor = _getColorForScale(_currentScale); 104 | 105 | return Transform.scale( 106 | scale: _currentScale, 107 | child: ShaderMask( 108 | shaderCallback: (Rect bounds) { 109 | return RadialGradient( 110 | center: Alignment.center, 111 | radius: 1.0, 112 | colors: [ 113 | dynamicColor.withOpacity(1.0), 114 | Colors.transparent, 115 | ], 116 | stops: const [0.7, 1.0], 117 | ).createShader(bounds); 118 | }, 119 | blendMode: BlendMode.srcATop, // Ensures transparency is preserved 120 | child: Lottie.asset( 121 | 'assets/play_example.json', 122 | width: min(widget.width - 50, 400), 123 | fit: BoxFit.cover, 124 | ), 125 | ), 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/widgets/playing/subtitle_widget.dart: -------------------------------------------------------------------------------- 1 | // subtitle_widget.dart 2 | import 'package:flutter/material.dart'; 3 | import 'package:google_fonts/google_fonts.dart'; 4 | 5 | class SubtitleWidget extends StatelessWidget { 6 | final double width; 7 | final double subtitleSize; 8 | final String subtitles; 9 | final Color textColor; 10 | 11 | const SubtitleWidget({ 12 | super.key, 13 | required this.width, 14 | required this.subtitleSize, 15 | required this.subtitles, 16 | required this.textColor, 17 | }); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Positioned( 22 | top: 50, 23 | left: width * 0.1, 24 | right: width * 0.1, 25 | child: Center( 26 | child: AnimatedSwitcher( 27 | duration: const Duration(milliseconds: 750), 28 | transitionBuilder: (Widget child, Animation animation) { 29 | final fadeAnimation = 30 | Tween(begin: 0.0, end: 1.0).animate(animation); 31 | final offsetAnimation = 32 | Tween(begin: const Offset(0, 0.75), end: Offset.zero) 33 | .animate(animation); 34 | return SlideTransition( 35 | position: offsetAnimation, 36 | child: FadeTransition( 37 | opacity: fadeAnimation, 38 | child: child, 39 | ), 40 | ); 41 | }, 42 | child: Text( 43 | subtitles, 44 | key: ValueKey(subtitles), 45 | textAlign: TextAlign.center, 46 | style: GoogleFonts.imFellDoublePica( 47 | textStyle: TextStyle( 48 | fontSize: subtitleSize, 49 | color: textColor, 50 | fontWeight: FontWeight.normal, 51 | ), 52 | ), 53 | ), 54 | ), 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | 12 | void fl_register_plugins(FlPluginRegistry* registry) { 13 | g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = 14 | fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); 15 | isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); 16 | g_autoptr(FlPluginRegistrar) record_linux_registrar = 17 | fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); 18 | record_linux_plugin_register_with_registrar(record_linux_registrar); 19 | } 20 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | isar_flutter_libs 7 | record_linux 8 | ) 9 | 10 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 11 | flutter_soloud 12 | ) 13 | 14 | set(PLUGIN_BUNDLED_LIBRARIES) 15 | 16 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 17 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 18 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 21 | endforeach(plugin) 22 | 23 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 26 | endforeach(ffi_plugin) 27 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import audio_session 9 | import connectivity_plus 10 | import isar_flutter_libs 11 | import just_audio 12 | import package_info_plus 13 | import path_provider_foundation 14 | import record_darwin 15 | import shared_preferences_foundation 16 | import speech_to_text 17 | import wakelock_plus 18 | 19 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 20 | AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) 21 | ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) 22 | IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) 23 | JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) 24 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 25 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 26 | RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin")) 27 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 28 | SpeechToTextPlugin.register(with: registry.registrar(forPlugin: "SpeechToTextPlugin")) 29 | WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) 30 | } 31 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = flutter_voice_friend 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterVoiceFriend 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test/services/connection_service_test.mocks.dart: -------------------------------------------------------------------------------- 1 | // Mocks generated by Mockito 5.4.4 from annotations 2 | // in flutter_voice_friend/test/services/connection_service_test.dart. 3 | // Do not manually edit this file. 4 | 5 | // ignore_for_file: no_leading_underscores_for_library_prefixes 6 | import 'dart:async' as _i3; 7 | 8 | import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart' 9 | as _i2; 10 | import 'package:mockito/mockito.dart' as _i1; 11 | 12 | // ignore_for_file: type=lint 13 | // ignore_for_file: avoid_redundant_argument_values 14 | // ignore_for_file: avoid_setters_without_getters 15 | // ignore_for_file: comment_references 16 | // ignore_for_file: deprecated_member_use 17 | // ignore_for_file: deprecated_member_use_from_same_package 18 | // ignore_for_file: implementation_imports 19 | // ignore_for_file: invalid_use_of_visible_for_testing_member 20 | // ignore_for_file: prefer_const_constructors 21 | // ignore_for_file: unnecessary_parenthesis 22 | // ignore_for_file: camel_case_types 23 | // ignore_for_file: subtype_of_sealed_class 24 | 25 | class _FakeDuration_0 extends _i1.SmartFake implements Duration { 26 | _FakeDuration_0( 27 | Object parent, 28 | Invocation parentInvocation, 29 | ) : super( 30 | parent, 31 | parentInvocation, 32 | ); 33 | } 34 | 35 | /// A class which mocks [InternetConnection]. 36 | /// 37 | /// See the documentation for Mockito's code generation for more information. 38 | class MockInternetConnection extends _i1.Mock 39 | implements _i2.InternetConnection { 40 | MockInternetConnection() { 41 | _i1.throwOnMissingStub(this); 42 | } 43 | 44 | @override 45 | Duration get checkInterval => (super.noSuchMethod( 46 | Invocation.getter(#checkInterval), 47 | returnValue: _FakeDuration_0( 48 | this, 49 | Invocation.getter(#checkInterval), 50 | ), 51 | ) as Duration); 52 | 53 | @override 54 | _i3.Future get hasInternetAccess => (super.noSuchMethod( 55 | Invocation.getter(#hasInternetAccess), 56 | returnValue: _i3.Future.value(false), 57 | ) as _i3.Future); 58 | 59 | @override 60 | _i3.Future<_i2.InternetStatus> get internetStatus => (super.noSuchMethod( 61 | Invocation.getter(#internetStatus), 62 | returnValue: 63 | _i3.Future<_i2.InternetStatus>.value(_i2.InternetStatus.connected), 64 | ) as _i3.Future<_i2.InternetStatus>); 65 | 66 | @override 67 | _i3.Stream<_i2.InternetStatus> get onStatusChange => (super.noSuchMethod( 68 | Invocation.getter(#onStatusChange), 69 | returnValue: _i3.Stream<_i2.InternetStatus>.empty(), 70 | ) as _i3.Stream<_i2.InternetStatus>); 71 | } 72 | -------------------------------------------------------------------------------- /test/services/session_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:flutter_voice_friend/models/activity.dart'; 3 | import 'package:flutter_voice_friend/models/session.dart'; 4 | import 'package:flutter_voice_friend/services/session_service.dart'; 5 | import 'package:isar/isar.dart'; 6 | import 'package:mockito/mockito.dart'; 7 | 8 | // Mock class for IsarCollection 9 | class MockIsarCollectionSession extends Mock 10 | implements IsarCollection { 11 | @override 12 | Future get(Id id) { 13 | // Simulate behavior for retrieving a session 14 | return Future.value(null); // Return null or a mocked session as needed 15 | } 16 | } 17 | 18 | // Mock class for IsarCollection 19 | class MockIsarCollectionActivity extends Mock 20 | implements IsarCollection { 21 | @override 22 | Future get(Id id) { 23 | // Simulate behavior for retrieving an activity 24 | return Future.value(null); // Return null to simulate "not found" 25 | } 26 | } 27 | 28 | // Custom MockIsar class 29 | class MockIsar extends Mock implements Isar { 30 | final IsarCollection mockSessionCollection; 31 | final IsarCollection mockActivityCollection; 32 | 33 | MockIsar({ 34 | required this.mockSessionCollection, 35 | required this.mockActivityCollection, 36 | }); 37 | 38 | @override 39 | IsarCollection collection() { 40 | if (T == Session) { 41 | return mockSessionCollection as IsarCollection; 42 | } else if (T == Activity) { 43 | return mockActivityCollection as IsarCollection; 44 | } else { 45 | throw UnimplementedError('collection<$T>() not implemented in MockIsar'); 46 | } 47 | } 48 | } 49 | 50 | void main() { 51 | group('SessionService Tests', () { 52 | late SessionService sessionService; 53 | late MockIsar mockIsar; 54 | late MockIsarCollectionSession mockSessionCollection; 55 | late MockIsarCollectionActivity mockActivityCollection; 56 | 57 | setUp(() { 58 | mockSessionCollection = MockIsarCollectionSession(); 59 | mockActivityCollection = MockIsarCollectionActivity(); 60 | 61 | mockIsar = MockIsar( 62 | mockSessionCollection: mockSessionCollection, 63 | mockActivityCollection: mockActivityCollection, 64 | ); 65 | 66 | sessionService = SessionService(isar: mockIsar); 67 | }); 68 | 69 | test('Should throw exception when activity not found', () async { 70 | final activity = Activity( 71 | activityId: ActivityId.introduction, 72 | name: 'Introduction', 73 | description: 'Intro activity', 74 | requiredLevel: 0, 75 | displayOrder: 0, 76 | category: ActivityCategory.dreamActivities, 77 | duration: 5, 78 | ); 79 | 80 | // Expect that an exception is thrown when the activity is not found 81 | expect( 82 | () => sessionService.saveSession( 83 | 'conversation', 84 | 'summary', 85 | DateTime.now(), 86 | activity, 87 | ), 88 | throwsException, 89 | ); 90 | }); 91 | 92 | // Add more tests covering: 93 | // - Successful session saving 94 | // - Updating activity's isCompleted and lastCompleted 95 | // - Corner cases like invalid data 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /test/services/speech_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:flutter_voice_friend/config.dart'; 3 | import 'package:flutter_voice_friend/services/speech_service.dart'; 4 | import 'package:mockito/annotations.dart'; 5 | import 'package:mockito/mockito.dart'; 6 | import 'package:deepgram_speech_to_text/deepgram_speech_to_text.dart'; 7 | import 'package:speech_to_text/speech_to_text.dart' as stt; 8 | 9 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 10 | import 'speech_service_test.mocks.dart'; 11 | 12 | @GenerateMocks([Deepgram, stt.SpeechToText]) 13 | void main() { 14 | dotenv.load(); 15 | group('SpeechService Tests', () { 16 | late SpeechService speechService; 17 | late MockDeepgram mockDeepgram; 18 | //late MockSpeechToText mockSpeechToText; 19 | 20 | setUp(() { 21 | speechService = SpeechService(); 22 | mockDeepgram = MockDeepgram(); 23 | //mockSpeechToText = MockSpeechToText(); 24 | 25 | Config.deepgramApiKey = dotenv.env['DEEPGRAM_API_KEY'] ?? ''; 26 | }); 27 | 28 | test('Should initialize Deepgram successfully', () async { 29 | when(mockDeepgram.isApiKeyValid()).thenAnswer((_) async => true); 30 | await speechService.initialize(Config.deepgramStt); 31 | 32 | expect(speechService.errorController.isClosed, false); 33 | }); 34 | 35 | test('Should handle Deepgram initialization failure', () async { 36 | when(mockDeepgram.isApiKeyValid()) 37 | .thenThrow(Exception('Invalid API Key')); 38 | speechService.errorStream.listen((error) { 39 | expect(error, isA()); 40 | }); 41 | 42 | await speechService.initialize(Config.deepgramStt); 43 | }); 44 | 45 | // Add tests for on-device speech recognition initialization 46 | // Tests for startListening and stopListening methods 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /test/services/user_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:flutter_voice_friend/config.dart'; 3 | import 'package:flutter_voice_friend/services/user_service.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | void main() { 7 | group('UserService Tests', () { 8 | late UserService userService; 9 | 10 | setUp(() { 11 | userService = UserService(); 12 | SharedPreferences.setMockInitialValues({}); 13 | }); 14 | 15 | test('Should load default user information', () async { 16 | await userService.loadUserInformation(); 17 | 18 | expect(userService.selectedVoice, Config.defaultVoice); 19 | expect(userService.autoToggleRecording, false); 20 | expect(userService.selectedLanguage, Config.defaultLanguage); 21 | }); 22 | 23 | test('Should save and load user information correctly', () async { 24 | await userService.updateUserInfo({ 25 | 'selectedVoice': 'nova', 26 | 'autoToggleRecording': true, 27 | 'selectedLanguage': 'FR', 28 | }); 29 | 30 | final prefs = await SharedPreferences.getInstance(); 31 | expect(prefs.getString('selectedVoice'), 'nova'); 32 | expect(prefs.getBool('autoToggleRecording'), true); 33 | expect(prefs.getString('selectedLanguage'), 'FR'); 34 | 35 | await userService.loadUserInformation(); 36 | 37 | expect(userService.selectedVoice, 'nova'); 38 | expect(userService.autoToggleRecording, true); 39 | expect(userService.selectedLanguage, 'FR'); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/utils/audio_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:flutter_voice_friend/utils/audio_utils.dart'; 5 | 6 | void main() { 7 | group('AudioUtils Tests', () { 8 | test('Should calculate RMS correctly', () { 9 | final data = Uint8List.fromList([0, 0, 0, 0, 0, 0, 0, 0]); // Silence 10 | final rms = AudioUtils.calculateRMS(data); 11 | 12 | expect(rms, 0); 13 | }); 14 | 15 | test('Should convert RMS to dB correctly', () { 16 | final db = AudioUtils.rmsToDb(0); 17 | 18 | expect(db, -60.0); 19 | }); 20 | 21 | test('Should normalize dB value correctly', () { 22 | final normalized = AudioUtils.normalizeDb(-30.0); 23 | 24 | expect(normalized, 0.25); 25 | }); 26 | 27 | // Add tests for getScale and getOutputScale methods 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/utils/text_utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:flutter_voice_friend/utils/text_utils.dart'; 3 | 4 | void main() { 5 | group('TextUtils Tests', () { 6 | test('Should segment text by sentence correctly', () { 7 | const text = 'Hello world! How are you today? I am fine.'; 8 | final result = segmentTextBySentence(text); 9 | 10 | expect( 11 | result['completeSentences'], ['Hello world!', 'How are you today?']); 12 | expect(result['remainingText'], 'I am fine.'); 13 | }); 14 | 15 | test('Should handle empty string', () { 16 | final result = segmentTextBySentence(''); 17 | 18 | expect(result['completeSentences'], []); 19 | expect(result['remainingText'], ''); 20 | }); 21 | 22 | test('Should format time since last completed correctly', () { 23 | final now = DateTime.now(); 24 | final oneHourAgo = now.subtract(const Duration(hours: 1)); 25 | final result = timeSinceLastCompleted(oneHourAgo); 26 | 27 | expect(result, '60 mins ago'); 28 | }); 29 | 30 | // Add more tests for different durations 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/widgets/activity/image_of_activity_test.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/image_of_activity_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/activity/image_of_activity.dart'; 6 | 7 | void main() { 8 | testWidgets('ImageOfActivity displays image with gradient overlay', 9 | (WidgetTester tester) async { 10 | // Arrange 11 | const imagePath = 'assets/activities/default_image.webp'; 12 | 13 | await tester.pumpWidget( 14 | const MaterialApp( 15 | home: Scaffold( 16 | body: ImageOfActivity(imagePath: imagePath), 17 | ), 18 | ), 19 | ); 20 | 21 | // Assert: Image.asset is present with correct path 22 | expect(find.byType(Image), findsOneWidget); 23 | final imageWidget = tester.widget(find.byType(Image)); 24 | expect(imageWidget.image, isA()); 25 | final AssetImage assetImage = imageWidget.image as AssetImage; 26 | expect(assetImage.assetName, imagePath); 27 | 28 | // Assert: Gradient overlay is present 29 | expect(find.byType(Positioned), findsNWidgets(1)); 30 | // Optionally, check if the gradient container is present 31 | expect(find.byType(Container), findsNWidgets(1)); // Gradient container 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/widgets/common/error_dialog_test.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/error_dialog_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/common/error_dialog.dart'; 6 | 7 | void main() { 8 | testWidgets('showErrorDialog displays error message and OK button', 9 | (WidgetTester tester) async { 10 | // Arrange 11 | const errorMessage = 'An error occurred!'; 12 | bool dialogClosed = false; 13 | 14 | await tester.pumpWidget( 15 | MaterialApp( 16 | home: Builder( 17 | builder: (context) => ElevatedButton( 18 | onPressed: () async { 19 | await showErrorDialog(context, errorMessage); 20 | dialogClosed = true; 21 | }, 22 | child: const Text('Show Error'), 23 | ), 24 | ), 25 | ), 26 | ); 27 | 28 | // Act: Tap the button to show dialog 29 | await tester.tap(find.text('Show Error')); 30 | await tester.pumpAndSettle(); 31 | 32 | // Assert: Check dialog elements 33 | expect(find.text('Ouch !'), findsOneWidget); 34 | expect(find.text(errorMessage), findsOneWidget); 35 | expect(find.text('OK'), findsOneWidget); 36 | 37 | // Act: Tap OK button 38 | await tester.tap(find.text('OK')); 39 | await tester.pumpAndSettle(); 40 | 41 | // Assert: Dialog is closed 42 | expect(dialogClosed, isTrue); 43 | }); 44 | 45 | testWidgets('showErrorDialog does not show OK button when showOk is false', 46 | (WidgetTester tester) async { 47 | // Arrange 48 | const errorMessage = 'Another error!'; 49 | bool dialogClosed = false; 50 | 51 | await tester.pumpWidget( 52 | MaterialApp( 53 | home: Builder( 54 | builder: (context) => ElevatedButton( 55 | onPressed: () async { 56 | await showErrorDialog(context, errorMessage, showOk: false); 57 | dialogClosed = true; 58 | }, 59 | child: const Text('Show Error'), 60 | ), 61 | ), 62 | ), 63 | ); 64 | 65 | // Act: Tap the button to show dialog 66 | await tester.tap(find.text('Show Error')); 67 | await tester.pumpAndSettle(); 68 | 69 | // Assert: Check dialog elements 70 | expect(find.text('Ouch !'), findsOneWidget); 71 | expect(find.text(errorMessage), findsOneWidget); 72 | expect(find.text('OK'), findsNothing); 73 | 74 | // Since there is no button to dismiss, the dialog remains 75 | expect(dialogClosed, isFalse); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /test/widgets/common/loading_widget_test.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/loading_widget_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/common/loading_widget.dart'; 6 | import 'package:loading_animation_widget/loading_animation_widget.dart'; 7 | 8 | void main() { 9 | testWidgets('LoadingWidget displays loading animation and text', 10 | (WidgetTester tester) async { 11 | // Arrange 12 | const loadingInfo = 'Loading...'; 13 | const fontSize = 14.0; 14 | 15 | await tester.pumpWidget( 16 | const MaterialApp( 17 | home: Scaffold( 18 | body: LoadingWidget( 19 | loadingInfo: loadingInfo, 20 | fontSize: fontSize, 21 | ), 22 | ), 23 | ), 24 | ); 25 | 26 | // Assert: Check for loading animation 27 | expect(find.byType(LoadingWidget), findsOneWidget); 28 | 29 | // Assert: Check loading info text 30 | expect(find.text(loadingInfo), findsOneWidget); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/widgets/common/retry_cancel_widget_test.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/retry_cancel_widget_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/common/retry_cancel_widget.dart'; 6 | 7 | void main() { 8 | testWidgets('RetryCancelWidget displays error messages and buttons', 9 | (WidgetTester tester) async { 10 | // Arrange 11 | bool onRetryCalled = false; 12 | bool onCancelCalled = false; 13 | 14 | await tester.pumpWidget( 15 | MaterialApp( 16 | home: Scaffold( 17 | body: RetryCancelWidget( 18 | onRetry: () { 19 | onRetryCalled = true; 20 | }, 21 | onCancel: () { 22 | onCancelCalled = true; 23 | }, 24 | ), 25 | ), 26 | ), 27 | ); 28 | 29 | // Assert: Check error messages 30 | expect(find.textContaining('Whisper had a little hiccup'), findsOneWidget); 31 | expect(find.textContaining('I can retry sending your command'), 32 | findsOneWidget); 33 | 34 | // Assert: Buttons are present 35 | expect(find.text('Retry 🔄'), findsOneWidget); 36 | expect(find.text('Cancel ❌'), findsOneWidget); 37 | 38 | // Act: Tap Retry 39 | await tester.tap(find.text('Retry 🔄')); 40 | await tester.pumpAndSettle(); 41 | 42 | // Assert 43 | expect(onRetryCalled, isTrue); 44 | expect(onCancelCalled, isFalse); 45 | 46 | // Reset 47 | onRetryCalled = false; 48 | 49 | // Act: Tap Cancel 50 | await tester.tap(find.text('Cancel ❌')); 51 | await tester.pumpAndSettle(); 52 | 53 | // Assert 54 | expect(onCancelCalled, isTrue); 55 | expect(onRetryCalled, isFalse); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /test/widgets/dialog_helper_test.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/dialog_helper_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/dialog_helper.dart'; 6 | 7 | void main() { 8 | testWidgets('showIntroductionActivityCompletionDialog displays correctly', 9 | (WidgetTester tester) async { 10 | // Arrange 11 | await tester.pumpWidget( 12 | MaterialApp( 13 | home: Builder( 14 | builder: (context) => ElevatedButton( 15 | onPressed: () async { 16 | await showIntroductionActivityCompletionDialog(context); 17 | }, 18 | child: Text('Show Dialog'), 19 | ), 20 | ), 21 | ), 22 | ); 23 | 24 | // Act: Tap the button to show dialog 25 | await tester.tap(find.text('Show Dialog')); 26 | await tester.pumpAndSettle(); 27 | 28 | // Assert: Check dialog elements 29 | expect(find.text('Activity Completed'), findsOneWidget); 30 | expect( 31 | find.text('Click on Continue to go to the main menu'), findsOneWidget); 32 | expect(find.text('Continue'), findsOneWidget); 33 | 34 | // Act: Tap 'Continue' button 35 | await tester.tap(find.text('Continue')); 36 | await tester.pumpAndSettle(); 37 | 38 | // Since the dialog returns false, we can capture the result if needed 39 | }); 40 | 41 | testWidgets( 42 | 'showActivityCompletionDialog displays correctly and returns correct value', 43 | (WidgetTester tester) async { 44 | // Arrange 45 | bool? result; 46 | await tester.pumpWidget( 47 | MaterialApp( 48 | home: Builder( 49 | builder: (context) => ElevatedButton( 50 | onPressed: () async { 51 | result = await showActivityCompletionDialog(context); 52 | }, 53 | child: Text('Show Dialog'), 54 | ), 55 | ), 56 | ), 57 | ); 58 | 59 | // Act: Tap the button to show dialog 60 | await tester.tap(find.text('Show Dialog')); 61 | await tester.pumpAndSettle(); 62 | 63 | // Assert: Check dialog elements 64 | expect(find.text('Activity Completed'), findsOneWidget); 65 | expect( 66 | find.text( 67 | 'Would you like to return to the main menu or restart the activity?'), 68 | findsOneWidget); 69 | expect(find.text('Restart'), findsOneWidget); 70 | expect(find.text('Main Menu'), findsOneWidget); 71 | 72 | // Act: Tap 'Main Menu' button 73 | await tester.tap(find.text('Main Menu')); 74 | await tester.pumpAndSettle(); 75 | 76 | // Assert: result should be true 77 | expect(result, isTrue); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/widgets/indicators/play_indicator_test.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/play_indicator_widget_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/indicators/play_indicator.dart'; 6 | import 'package:flutter_voice_friend/services/animation_controller_service.dart'; 7 | import 'package:mockito/mockito.dart'; 8 | import 'package:mockito/annotations.dart'; 9 | import 'play_indicator_test.mocks.dart'; 10 | 11 | @GenerateMocks([AnimationControllerService]) 12 | void main() { 13 | late MockAnimationControllerService mockAnimationControllerService; 14 | 15 | setUp(() { 16 | mockAnimationControllerService = MockAnimationControllerService(); 17 | 18 | // Mock the pulseAnimation and animation 19 | when(mockAnimationControllerService.pulseAnimation) 20 | .thenReturn(const AlwaysStoppedAnimation(1.0)); 21 | when(mockAnimationControllerService.animation) 22 | .thenReturn(const AlwaysStoppedAnimation(1.0)); 23 | when(mockAnimationControllerService.animationController) 24 | .thenReturn(FakeAnimationController()); 25 | when(mockAnimationControllerService.buttonAnimationController) 26 | .thenReturn(FakeAnimationController()); 27 | when(mockAnimationControllerService.listeningAnimationController) 28 | .thenReturn(FakeAnimationController()); 29 | }); 30 | 31 | testWidgets('PlayIndicatorWidget displays play button and title', 32 | (WidgetTester tester) async { 33 | // Arrange 34 | const title = 'Play'; 35 | bool onPressCalled = false; 36 | 37 | await tester.pumpWidget( 38 | MaterialApp( 39 | home: Scaffold( 40 | body: PlayIndicatorWidget( 41 | animationControllerService: mockAnimationControllerService, 42 | isListening: false, 43 | title: title, 44 | textColor: Colors.black, 45 | onPress: () { 46 | onPressCalled = true; 47 | }, 48 | ), 49 | ), 50 | ), 51 | ); 52 | 53 | // Assert: Play button is present 54 | expect(find.byIcon(Icons.play_arrow), findsOneWidget); 55 | 56 | // Assert: Title is present 57 | expect(find.text(title), findsOneWidget); 58 | 59 | // Act: Tap the play button 60 | await tester.tap(find.byIcon(Icons.play_arrow)); 61 | await tester.pumpAndSettle(); 62 | 63 | // Advance the clock by 500 milliseconds to allow the Timer to complete 64 | await tester.pump(const Duration(milliseconds: 500)); 65 | 66 | // Assert callback 67 | expect(onPressCalled, isTrue); 68 | }); 69 | } 70 | 71 | // A fake AnimationController to satisfy the type 72 | class FakeAnimationController extends AnimationController { 73 | FakeAnimationController() : super(vsync: const TestVSync()); 74 | 75 | @override 76 | TickerFuture forward({double? from}) { 77 | return TickerFuture.complete(); 78 | } 79 | 80 | @override 81 | TickerFuture reverse({double? from}) { 82 | return TickerFuture.complete(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/widgets/indicators/simple_loading_indicator_test.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/simple_loading_indicator_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/indicators/simple_loading_indicator.dart'; 6 | 7 | void main() { 8 | testWidgets( 9 | 'SimpleLoadingCircle displays CircularProgressIndicator with correct properties', 10 | (WidgetTester tester) async { 11 | // Arrange 12 | await tester.pumpWidget( 13 | const MaterialApp( 14 | home: Scaffold( 15 | body: SimpleLoadingCircle(), 16 | ), 17 | ), 18 | ); 19 | 20 | // Assert 21 | final cpiFinder = find.byType(CircularProgressIndicator); 22 | expect(cpiFinder, findsOneWidget); 23 | 24 | final CircularProgressIndicator cpi = tester.widget(cpiFinder); 25 | expect(cpi.valueColor, isA>()); 26 | final AlwaysStoppedAnimation colorAnimation = 27 | cpi.valueColor as AlwaysStoppedAnimation; 28 | expect(colorAnimation.value, Colors.blue); 29 | expect(cpi.strokeWidth, equals(8.0)); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/widgets/listening/listening_animation_test.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/listening_animation_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/listening/listening_animation.dart'; 6 | import 'package:flutter_voice_friend/utils/audio_utils.dart'; 7 | import 'package:lottie/lottie.dart'; 8 | 9 | void main() { 10 | testWidgets('ListeningAnimation displays correctly with initial scale', 11 | (WidgetTester tester) async { 12 | // Arrange 13 | await tester.pumpWidget( 14 | const MaterialApp( 15 | home: Scaffold( 16 | body: ListeningAnimation( 17 | width: 200, 18 | ), 19 | ), 20 | ), 21 | ); 22 | 23 | // Assert: Check that the Lottie asset is present 24 | expect(find.byType(Lottie), findsOneWidget); 25 | 26 | // Optionally check the Transform.scale widget 27 | final transformFinder = find.byType(Transform); 28 | expect(transformFinder, findsNWidgets(3)); 29 | final Transform transform = tester.widget(transformFinder.first); 30 | expect(transform.transform.getMaxScaleOnAxis(), equals(1.0)); 31 | }); 32 | 33 | testWidgets('ListeningAnimation updates scale when updateScale is called', 34 | (WidgetTester tester) async { 35 | // Arrange 36 | await tester.pumpWidget( 37 | const MaterialApp( 38 | home: Scaffold( 39 | body: ListeningAnimation( 40 | width: 200, 41 | ), 42 | ), 43 | ), 44 | ); 45 | 46 | // Find the state 47 | final stateFinder = find.byType(ListeningAnimation); 48 | final state = tester.state(stateFinder); 49 | 50 | // Act: Call updateScale 51 | state.updateScale(0.8); 52 | await tester.pump(const Duration(milliseconds: 500)); 53 | 54 | // Assert: Verify scale is updated 55 | // Note: Directly accessing _currentScale is not possible since it's private. 56 | // Instead, verify the Transform widget's scale. 57 | final transformFinderUpdated = find.byType(Transform); 58 | final Transform transform = tester.widget(transformFinderUpdated.first); 59 | //TODO Fix test 60 | //expect(transform.transform.getMaxScaleOnAxis(), 61 | // closeTo(AudioUtils.getScale(0.8, 1.0, 0.75, 3.0), 0.01)); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /test/widgets/listening/listening_message_test.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/listening_message_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/listening/listening_message.dart'; 6 | 7 | void main() { 8 | testWidgets( 9 | 'ListeningMessage displays "Tap microphone to talk" when conditions met', 10 | (WidgetTester tester) async { 11 | // Arrange 12 | await tester.pumpWidget( 13 | const MaterialApp( 14 | home: Scaffold( 15 | body: ListeningMessage( 16 | listeningMode: true, 17 | firstMessage: false, 18 | isListening: false, 19 | transcription: '', 20 | textColor: Colors.black, 21 | audioLevel: 1.0, 22 | ), 23 | ), 24 | ), 25 | ); 26 | 27 | // Assert 28 | expect(find.text('Tap microphone to talk'), findsOneWidget); 29 | }); 30 | 31 | testWidgets('ListeningMessage displays "Start talking" when conditions met', 32 | (WidgetTester tester) async { 33 | // Arrange 34 | await tester.pumpWidget( 35 | const MaterialApp( 36 | home: Scaffold( 37 | body: ListeningMessage( 38 | listeningMode: true, 39 | firstMessage: false, 40 | isListening: true, 41 | transcription: '', 42 | textColor: Colors.black, 43 | audioLevel: 0.0, 44 | ), 45 | ), 46 | ), 47 | ); 48 | 49 | // Assert 50 | expect(find.text('Start talking'), findsOneWidget); 51 | }); 52 | 53 | testWidgets('ListeningMessage displays "Press ⬜ to send" when conditions met', 54 | (WidgetTester tester) async { 55 | // Arrange 56 | await tester.pumpWidget( 57 | const MaterialApp( 58 | home: Scaffold( 59 | body: ListeningMessage( 60 | listeningMode: true, 61 | firstMessage: false, 62 | isListening: true, 63 | transcription: 'Hello', 64 | textColor: Colors.black, 65 | audioLevel: 0.0, 66 | ), 67 | ), 68 | ), 69 | ); 70 | 71 | // Assert 72 | expect(find.text('Press ⬜ to send'), findsOneWidget); 73 | }); 74 | 75 | testWidgets('ListeningMessage displays nothing when no condition met', 76 | (WidgetTester tester) async { 77 | // Arrange 78 | await tester.pumpWidget( 79 | const MaterialApp( 80 | home: Scaffold( 81 | body: ListeningMessage( 82 | listeningMode: false, 83 | firstMessage: true, 84 | isListening: false, 85 | transcription: '', 86 | textColor: Colors.black, 87 | audioLevel: 1.0, 88 | ), 89 | ), 90 | ), 91 | ); 92 | 93 | // Assert 94 | expect(find.byType(Text), findsNothing); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /test/widgets/playing/playing_animation.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/playing_animation_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/playing/playing_animation.dart'; 6 | import 'package:lottie/lottie.dart'; 7 | 8 | void main() { 9 | testWidgets('PlayingAnimation displays correctly with initial scale', 10 | (WidgetTester tester) async { 11 | // Arrange 12 | await tester.pumpWidget( 13 | const MaterialApp( 14 | home: Scaffold( 15 | body: PlayingAnimation( 16 | width: 200, 17 | ), 18 | ), 19 | ), 20 | ); 21 | 22 | // Assert: Check that the Lottie asset is present 23 | expect(find.byType(Lottie), findsOneWidget); 24 | 25 | // Optionally check the Transform.scale widget 26 | final transformFinder = find.byType(Transform); 27 | expect(transformFinder, findsOneWidget); 28 | final Transform transform = tester.widget(transformFinder); 29 | expect(transform.transform.getMaxScaleOnAxis(), equals(1.0)); 30 | }); 31 | 32 | testWidgets('PlayingAnimation updates scale when updateScale is called', 33 | (WidgetTester tester) async { 34 | // Arrange 35 | await tester.pumpWidget( 36 | const MaterialApp( 37 | home: Scaffold( 38 | body: PlayingAnimation( 39 | width: 200, 40 | ), 41 | ), 42 | ), 43 | ); 44 | 45 | // Find the state 46 | final stateFinder = find.byType(PlayingAnimation); 47 | final state = tester.state(stateFinder); 48 | 49 | // Act: Call updateScale 50 | state.updateScale(1.5); 51 | await tester.pumpAndSettle(); 52 | 53 | // Assert: Verify scale is updated (transform.scale is applied) 54 | final transformFinderUpdated = find.byType(Transform); 55 | final Transform transform = tester.widget(transformFinderUpdated); 56 | // Since we set scale to 1.5 57 | expect(transform.transform.getMaxScaleOnAxis(), closeTo(1.5, 0.01)); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/widgets/playing/subtitle_widget.dart: -------------------------------------------------------------------------------- 1 | // File: test/widgets/subtitle_widget_test.dart 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:flutter_voice_friend/widgets/playing/subtitle_widget.dart'; 6 | 7 | void main() { 8 | testWidgets('SubtitleWidget displays subtitles correctly', 9 | (WidgetTester tester) async { 10 | // Arrange 11 | const subtitles = 'Hello, world!'; 12 | const width = 300.0; 13 | const subtitleSize = 20.0; 14 | const textColor = Colors.black; 15 | 16 | await tester.pumpWidget( 17 | const MaterialApp( 18 | home: Scaffold( 19 | body: SizedBox( 20 | width: width, 21 | child: SubtitleWidget( 22 | width: width, 23 | subtitleSize: subtitleSize, 24 | subtitles: subtitles, 25 | textColor: textColor, 26 | ), 27 | ), 28 | ), 29 | ), 30 | ); 31 | 32 | // Assert 33 | expect(find.text(subtitles), findsOneWidget); 34 | 35 | // Act: Change the subtitles 36 | await tester.pumpWidget( 37 | const MaterialApp( 38 | home: Scaffold( 39 | body: SizedBox( 40 | width: width, 41 | child: SubtitleWidget( 42 | width: width, 43 | subtitleSize: subtitleSize, 44 | subtitles: 'New subtitle', 45 | textColor: textColor, 46 | ), 47 | ), 48 | ), 49 | ), 50 | ); 51 | 52 | // Assert the new subtitle is displayed 53 | expect(find.text('New subtitle'), findsOneWidget); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | flutter_voice_friend 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter_voice_friend", 3 | "short_name": "flutter_voice_friend", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | void RegisterPlugins(flutter::PluginRegistry* registry) { 15 | ConnectivityPlusWindowsPluginRegisterWithRegistrar( 16 | registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); 17 | IsarFlutterLibsPluginRegisterWithRegistrar( 18 | registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); 19 | PermissionHandlerWindowsPluginRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); 21 | RecordWindowsPluginCApiRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); 23 | } 24 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | connectivity_plus 7 | isar_flutter_libs 8 | permission_handler_windows 9 | record_windows 10 | ) 11 | 12 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 13 | flutter_soloud 14 | ) 15 | 16 | set(PLUGIN_BUNDLED_LIBRARIES) 17 | 18 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 19 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 20 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 23 | endforeach(plugin) 24 | 25 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 26 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 27 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 28 | endforeach(ffi_plugin) 29 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "flutter_voice_friend" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "flutter_voice_friend" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "flutter_voice_friend.exe" "\0" 98 | VALUE "ProductName", "flutter_voice_friend" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"flutter_voice_friend", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbpassot/flutter_voice_friend/3dadfb0fa4d105ef8d350e7555d321ee76882e09/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | unsigned int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length == 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | --------------------------------------------------------------------------------