├── .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 |
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 |
--------------------------------------------------------------------------------