├── .github ├── FUNDING.yml └── workflows │ ├── build_android.yml │ ├── prerelease_android.yml │ └── prerelease_ios.yml ├── .gitignore ├── .metadata ├── .run └── release.run.xml ├── CONTRIBUTING.md ├── COPYING ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── app │ ├── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── chat │ │ │ │ └── saga │ │ │ │ └── voice_outliner │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launcher_icon.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── fastlane │ ├── Appfile │ ├── Fastfile │ └── README.md ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── icon │ └── icon.png ├── onboarding │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── 5.png └── screenshots │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 5inch-iPhone │ ├── 1.png │ ├── 2.png │ └── 3.png │ ├── android │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png │ └── ipad │ ├── 1.png │ ├── 2.png │ └── 3.png ├── deploy_android.sh ├── deploy_ios.sh ├── fonts ├── WorkSans-Medium.ttf ├── WorkSans-Regular.ttf └── WorkSans-SemiBold.ttf ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Gemfile ├── Gemfile.lock ├── 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 │ ├── GoogleService-Info.plist │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ └── Runner.entitlements └── fastlane │ ├── Appfile │ ├── Fastfile │ ├── Matchfile │ ├── README.md │ ├── metadata │ ├── copyright.txt │ ├── en-US │ │ ├── apple_tv_privacy_policy.txt │ │ ├── description.txt │ │ ├── keywords.txt │ │ ├── marketing_url.txt │ │ ├── name.txt │ │ ├── privacy_url.txt │ │ ├── promotional_text.txt │ │ ├── release_notes.txt │ │ ├── subtitle.txt │ │ └── support_url.txt │ ├── primary_category.txt │ ├── primary_first_sub_category.txt │ ├── primary_second_sub_category.txt │ ├── review_information │ │ ├── demo_password.txt │ │ ├── demo_user.txt │ │ ├── email_address.txt │ │ ├── first_name.txt │ │ ├── last_name.txt │ │ ├── notes.txt │ │ └── phone_number.txt │ ├── secondary_category.txt │ ├── secondary_first_sub_category.txt │ └── secondary_second_sub_category.txt │ └── screenshots │ └── en-US │ ├── 0_APP_IPAD_PRO_129_0.png │ ├── 0_APP_IPAD_PRO_3GEN_129_0.png │ ├── 0_APP_IPHONE_55_0.png │ ├── 0_APP_IPHONE_65_0.png │ ├── 1_APP_IPAD_PRO_129_1.png │ ├── 1_APP_IPAD_PRO_3GEN_129_1.png │ ├── 1_APP_IPHONE_55_1.png │ ├── 1_APP_IPHONE_65_1.png │ ├── 2_APP_IPAD_PRO_129_2.png │ ├── 2_APP_IPAD_PRO_3GEN_129_2.png │ ├── 2_APP_IPHONE_55_2.png │ └── 2_APP_IPHONE_65_2.png ├── lib ├── consts.dart ├── data │ ├── note.dart │ └── outline.dart ├── globals.dart ├── main.dart ├── repositories │ ├── db_repository.dart │ ├── drive_backup.dart │ ├── ios_speech_recognizer.dart │ └── vosk_speech_recognizer.dart ├── state │ ├── notes_state.dart │ ├── outline_state.dart │ └── player_state.dart ├── views │ ├── drive_settings_view.dart │ ├── ios_transcription_setup_view.dart │ ├── map_view.dart │ ├── notes_view.dart │ ├── onboarding_view.dart │ ├── outlines_view.dart │ ├── settings_view.dart │ ├── timeline_view.dart │ └── vosk_transcription_setup_view.dart └── widgets │ ├── ios_locale_selector.dart │ ├── markdown_exporter.dart │ ├── note_item.dart │ ├── note_wizard.dart │ ├── outline_wizard.dart │ ├── outlines_list.dart │ ├── record_button.dart │ ├── result_note.dart │ └── search_results_list.dart ├── pubspec.lock ├── pubspec.yaml ├── submit_ios.sh └── website ├── .gitignore ├── .parcelrc ├── README.md ├── package.json ├── src ├── 1.png ├── 2.png ├── 3.png ├── app_store.svg ├── apple-app-store-badge.svg ├── icon-w.png ├── icon.png ├── index.css ├── index.html └── play_store.svg ├── static ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── card.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── favicon.png └── site.webmanifest └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: maxkrieger 2 | -------------------------------------------------------------------------------- /.github/workflows/build_android.yml: -------------------------------------------------------------------------------- 1 | name: build_android 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build_android: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup ruby 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: 2.7.2 18 | bundler-cache: true 19 | 20 | - name: Bundle install 21 | run: cd ./android && bundle install 22 | 23 | - name: Setup JDK 24 | uses: actions/setup-java@v1 25 | with: 26 | java-version: "11.x" 27 | 28 | - name: Dump Secrets 29 | run: | 30 | echo "SENTRY_DSN='${{secrets.SENTRY_DSN}}'" > .env 31 | cd android/ 32 | echo "${{secrets.KEYSTORE}}" > keystore.b64 33 | base64 -d -i keystore.b64 > keystore.jks 34 | echo "${{secrets.KEY_PROPERTIES}}" > key.properties 35 | 36 | - name: Setup flutter 37 | uses: subosito/flutter-action@v1 38 | with: 39 | channel: "stable" 40 | 41 | - name: Install tools 42 | run: | 43 | flutter pub get 44 | 45 | - name: Flutter build release 46 | run: | 47 | unset ANDROID_NDK_HOME 48 | flutter build appbundle -------------------------------------------------------------------------------- /.github/workflows/prerelease_android.yml: -------------------------------------------------------------------------------- 1 | name: prerelease_android 2 | 3 | on: 4 | release: 5 | types: 6 | - prereleased 7 | 8 | jobs: 9 | deploy_android: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup ruby 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: 2.7.2 18 | bundler-cache: true 19 | 20 | - name: Bundle install 21 | run: cd ./android && bundle install 22 | 23 | - name: Setup JDK 24 | uses: actions/setup-java@v1 25 | with: 26 | java-version: "11.x" 27 | 28 | - name: Dump Secrets 29 | run: | 30 | echo "SENTRY_DSN='${{secrets.SENTRY_DSN}}'" > .env 31 | cd android/ 32 | echo "${{secrets.KEYSTORE}}" > keystore.b64 33 | base64 -d -i keystore.b64 > keystore.jks 34 | echo "${{secrets.KEY_PROPERTIES}}" > key.properties 35 | echo "${{secrets.GOOGLE_PLAY_JSON_CONTENT}}" > google_play.b64 36 | base64 -d -i google_play.b64 > google_play.json 37 | 38 | - name: Setup flutter 39 | uses: subosito/flutter-action@v1 40 | with: 41 | channel: "stable" 42 | 43 | - name: Install tools 44 | run: | 45 | flutter pub get 46 | 47 | - name: Flutter build release 48 | run: | 49 | unset ANDROID_NDK_HOME 50 | flutter build appbundle 51 | 52 | - name: Deploy beta to Play Store 53 | run: | 54 | cd ./android && bundle exec fastlane beta 55 | env: 56 | PLAY_APP_IDENTIFIER: ${{ secrets.PLAY_APP_IDENTIFIER }} 57 | -------------------------------------------------------------------------------- /.github/workflows/prerelease_ios.yml: -------------------------------------------------------------------------------- 1 | name: prerelease_ios 2 | 3 | on: 4 | release: 5 | types: 6 | - prereleased 7 | 8 | jobs: 9 | deploy_ios: 10 | runs-on: macos-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Select Xcode Version 15 | uses: maxim-lobanov/setup-xcode@v1 16 | with: 17 | xcode-version: latest-stable 18 | 19 | - name: Setup ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: 2.7.2 23 | bundler-cache: true 24 | 25 | - name: Bundle install 26 | run: cd ./ios && bundle install 27 | 28 | - name: Setup JDK 29 | uses: actions/setup-java@v1 30 | with: 31 | java-version: "12.x" 32 | 33 | - name: Setup flutter 34 | uses: subosito/flutter-action@v2 35 | with: 36 | channel: "stable" 37 | architecture: x64 38 | 39 | - uses: webfactory/ssh-agent@v0.5.3 40 | with: 41 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 42 | 43 | - name: Install tools 44 | run: | 45 | flutter pub get 46 | cd ./ios && pod install 47 | 48 | - name: Flutter build release 49 | run: | 50 | flutter build ios --release --no-codesign 51 | 52 | - name: Dump Secrets 53 | run: | 54 | echo "SENTRY_DSN='${{secrets.SENTRY_DSN}}'" > .env 55 | cd ios/ 56 | echo "${{secrets.CONNECT_KEY}}" > ci.p8 57 | 58 | - name: Deploy to TestFlight 59 | run: | 60 | cd ./ios && bundle exec fastlane beta_ci 61 | env: 62 | MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} 63 | MATCH_GIT: ${{ secrets.MATCH_GIT }} 64 | CONNECT_KEY_ID: ${{ secrets.CONNECT_KEY_ID }} 65 | CONNECT_ISSUER_ID: ${{ secrets.CONNECT_ISSUER_ID }} 66 | ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }} 67 | TEAM_ID: ${{ secrets.TEAM_ID }} 68 | APPLE_ID: ${{ secrets.APPLE_ID }} 69 | APP_IDENTIFIER: ${{ secrets.APP_IDENTIFIER }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/local.p8 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | ios/build/ 35 | 36 | # Web related 37 | lib/generated_plugin_registrant.dart 38 | 39 | # Symbolication related 40 | app.*.symbols 41 | 42 | # Obfuscation related 43 | app.*.map.json 44 | 45 | # Android Studio will place build artifacts here 46 | /android/app/debug 47 | /android/app/profile 48 | /android/app/release 49 | ios/fastlane/report.xml 50 | assets/speech_acct.json 51 | local.properties 52 | android/fastlane/report.xml 53 | key.properties 54 | *.p8 55 | ios/fastlane/local.p8 56 | android/google_play.json 57 | 58 | ios/Runner.ipa 59 | ios/Runner.app.dSYM.zip 60 | 61 | *.env 62 | ios/fastlane/Preview.html 63 | -------------------------------------------------------------------------------- /.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: 4cc385b4b84ac2f816d939a49ea1f328c4e0b48e 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /.run/release.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome. Note that while your code is under the AGPLv3 license, this app will also be distributed on the iOS App Store. -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | As additional permission under section 7, you are allowed to distribute the software through an app store, even if that store has restrictive terms and conditions that are incompatible with the GPL, provided that the source is also available under the GPL with or without this permission through a channel without those restrictive terms and conditions. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Voiceliner 2 | 3 | A voice memos-like for Android and iOS. Written in Flutter. Transcription on iOS uses the native transcription APIs (mostly on-device) and on Android, uses [Vosk](https://github.com/alphacep/vosk-api). 4 | The codebase is still quite messy, but contributions welcome! 5 | 6 | ## Screenshots 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | ## Contributing & License 17 | 18 | This project is AGPLv3 but with an exception for the App Store. [Learn More](CONTRIBUTING.md) 19 | 20 | ## Building 21 | 22 | - Install flutter 23 | - `flutter run lib/main.dart` 24 | 25 | ## Rebuilding Icons 26 | 27 | Place a 1024x1024 `icon.png` in `assets/icon/icon.png` and run 28 | 29 | ``` 30 | flutter pub run flutter_launcher_icons:main 31 | ``` 32 | 33 | ## Deploying 34 | 35 | - `android/key.properties`: 36 | 37 | ``` 38 | storePassword=keystore password 39 | keyPassword=key password 40 | keyAlias=key alias 41 | storeFile=/keystore/location 42 | ``` 43 | 44 | For continuous integration: 45 | 46 | | Env Var | Value | 47 | |----------------------------------------------|---------------------------------------------------| 48 | | APPLE_ID | apple account email" | 49 | | APP_IDENTIFIER | ios com.blabla.blabla | 50 | | PLAY_APP_IDENTIFIER | android com.blablabla.bla | 51 | | ITC_TEAM_ID | documented in fastlane | 52 | | TEAM_ID | documented in fastlane | 53 | | MATCH_GIT | github SSH URI for fastlane match | 54 | | MATCH_PASSWORD | documented in fastlane | 55 | | FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD | app specific password for fastlane | 56 | | FASTLANE_USER | documented in fastlane | 57 | | FASTLANE_PASSWORD | documented in fastlane | 58 | | CONNECT_KEY | app store connect .p8 file contents | 59 | | CONNECT_KEY_ID | app store connect key id | 60 | | CONNECT_ISSUER_ID | app store connect key issuer id | 61 | | GOOGLE_PLAY_JSON_CONTENT | base64 encoded json keys for google play fastlane | 62 | | KEYSTORE | base64 encoded keystore.jks | 63 | | KEY_PROPERTIES | key.properties seen above | 64 | | SSH_PRIVATE_KEY | for github access | 65 | | SENTRY_DSN | for sentry logging | 66 | 67 | For local deployment, populate the following `.env` files: 68 | 69 | `ios/fastlane/.env`: 70 | 71 | ``` 72 | MATCH_GIT=... 73 | APP_IDENTIFIER=... 74 | CONNECT_KEY_ID=... 75 | CONNECT_ISSUER_ID=... 76 | APPLE_ID=... 77 | FIRST_NAME=... 78 | LAST_NAME=... 79 | PHONE_NUMBER=... 80 | EMAIL_ADDRESS=... 81 | ``` 82 | 83 | `android/fastlane/.env`: 84 | 85 | ``` 86 | PLAY_APP_IDENTIFIER=... 87 | ``` 88 | 89 | `.env`: 90 | 91 | ``` 92 | SENTRY_DSN=... 93 | ``` 94 | 95 | You can then use `./deploy_ios.sh` and `./deploy_android.sh` to deploy to the app stores. 96 | 97 | 98 | ## Upgrading fastlane 99 | 100 | ``` 101 | ios/$ bundle update fastlane 102 | android/$ bundle update fastlane 103 | ``` 104 | 105 | ## Fastlane Match Notes 106 | 107 | When running `fastlane match development --generate_apple_certs`, make sure to specify `*` for the bundle id, so that it can make provisioning profiles both for the `.debug` bundle identifier and the main one. -------------------------------------------------------------------------------- /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 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /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/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /android/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | addressable (2.8.5) 7 | public_suffix (>= 2.0.2, < 6.0) 8 | artifactory (3.0.15) 9 | atomos (0.1.3) 10 | aws-eventstream (1.2.0) 11 | aws-partitions (1.813.0) 12 | aws-sdk-core (3.181.0) 13 | aws-eventstream (~> 1, >= 1.0.2) 14 | aws-partitions (~> 1, >= 1.651.0) 15 | aws-sigv4 (~> 1.5) 16 | jmespath (~> 1, >= 1.6.1) 17 | aws-sdk-kms (1.71.0) 18 | aws-sdk-core (~> 3, >= 3.177.0) 19 | aws-sigv4 (~> 1.1) 20 | aws-sdk-s3 (1.134.0) 21 | aws-sdk-core (~> 3, >= 3.181.0) 22 | aws-sdk-kms (~> 1) 23 | aws-sigv4 (~> 1.6) 24 | aws-sigv4 (1.6.0) 25 | aws-eventstream (~> 1, >= 1.0.2) 26 | babosa (1.0.4) 27 | claide (1.1.0) 28 | colored (1.2) 29 | colored2 (3.1.2) 30 | commander (4.6.0) 31 | highline (~> 2.0.0) 32 | declarative (0.0.20) 33 | digest-crc (0.6.5) 34 | rake (>= 12.0.0, < 14.0.0) 35 | domain_name (0.5.20190701) 36 | unf (>= 0.0.5, < 1.0.0) 37 | dotenv (2.8.1) 38 | emoji_regex (3.2.3) 39 | excon (0.102.0) 40 | faraday (1.10.3) 41 | faraday-em_http (~> 1.0) 42 | faraday-em_synchrony (~> 1.0) 43 | faraday-excon (~> 1.1) 44 | faraday-httpclient (~> 1.0) 45 | faraday-multipart (~> 1.0) 46 | faraday-net_http (~> 1.0) 47 | faraday-net_http_persistent (~> 1.0) 48 | faraday-patron (~> 1.0) 49 | faraday-rack (~> 1.0) 50 | faraday-retry (~> 1.0) 51 | ruby2_keywords (>= 0.0.4) 52 | faraday-cookie_jar (0.0.7) 53 | faraday (>= 0.8.0) 54 | http-cookie (~> 1.0.0) 55 | faraday-em_http (1.0.0) 56 | faraday-em_synchrony (1.0.0) 57 | faraday-excon (1.1.0) 58 | faraday-httpclient (1.0.1) 59 | faraday-multipart (1.0.4) 60 | multipart-post (~> 2) 61 | faraday-net_http (1.0.1) 62 | faraday-net_http_persistent (1.2.0) 63 | faraday-patron (1.0.0) 64 | faraday-rack (1.0.0) 65 | faraday-retry (1.0.3) 66 | faraday_middleware (1.2.0) 67 | faraday (~> 1.0) 68 | fastimage (2.2.7) 69 | fastlane (2.214.0) 70 | CFPropertyList (>= 2.3, < 4.0.0) 71 | addressable (>= 2.8, < 3.0.0) 72 | artifactory (~> 3.0) 73 | aws-sdk-s3 (~> 1.0) 74 | babosa (>= 1.0.3, < 2.0.0) 75 | bundler (>= 1.12.0, < 3.0.0) 76 | colored 77 | commander (~> 4.6) 78 | dotenv (>= 2.1.1, < 3.0.0) 79 | emoji_regex (>= 0.1, < 4.0) 80 | excon (>= 0.71.0, < 1.0.0) 81 | faraday (~> 1.0) 82 | faraday-cookie_jar (~> 0.0.6) 83 | faraday_middleware (~> 1.0) 84 | fastimage (>= 2.1.0, < 3.0.0) 85 | gh_inspector (>= 1.1.2, < 2.0.0) 86 | google-apis-androidpublisher_v3 (~> 0.3) 87 | google-apis-playcustomapp_v1 (~> 0.1) 88 | google-cloud-storage (~> 1.31) 89 | highline (~> 2.0) 90 | json (< 3.0.0) 91 | jwt (>= 2.1.0, < 3) 92 | mini_magick (>= 4.9.4, < 5.0.0) 93 | multipart-post (>= 2.0.0, < 3.0.0) 94 | naturally (~> 2.2) 95 | optparse (~> 0.1.1) 96 | plist (>= 3.1.0, < 4.0.0) 97 | rubyzip (>= 2.0.0, < 3.0.0) 98 | security (= 0.1.3) 99 | simctl (~> 1.6.3) 100 | terminal-notifier (>= 2.0.0, < 3.0.0) 101 | terminal-table (>= 1.4.5, < 2.0.0) 102 | tty-screen (>= 0.6.3, < 1.0.0) 103 | tty-spinner (>= 0.8.0, < 1.0.0) 104 | word_wrap (~> 1.0.0) 105 | xcodeproj (>= 1.13.0, < 2.0.0) 106 | xcpretty (~> 0.3.0) 107 | xcpretty-travis-formatter (>= 0.0.3) 108 | gh_inspector (1.1.3) 109 | google-apis-androidpublisher_v3 (0.49.0) 110 | google-apis-core (>= 0.11.0, < 2.a) 111 | google-apis-core (0.11.1) 112 | addressable (~> 2.5, >= 2.5.1) 113 | googleauth (>= 0.16.2, < 2.a) 114 | httpclient (>= 2.8.1, < 3.a) 115 | mini_mime (~> 1.0) 116 | representable (~> 3.0) 117 | retriable (>= 2.0, < 4.a) 118 | rexml 119 | webrick 120 | google-apis-iamcredentials_v1 (0.17.0) 121 | google-apis-core (>= 0.11.0, < 2.a) 122 | google-apis-playcustomapp_v1 (0.13.0) 123 | google-apis-core (>= 0.11.0, < 2.a) 124 | google-apis-storage_v1 (0.19.0) 125 | google-apis-core (>= 0.9.0, < 2.a) 126 | google-cloud-core (1.6.0) 127 | google-cloud-env (~> 1.0) 128 | google-cloud-errors (~> 1.0) 129 | google-cloud-env (1.6.0) 130 | faraday (>= 0.17.3, < 3.0) 131 | google-cloud-errors (1.3.1) 132 | google-cloud-storage (1.44.0) 133 | addressable (~> 2.8) 134 | digest-crc (~> 0.4) 135 | google-apis-iamcredentials_v1 (~> 0.1) 136 | google-apis-storage_v1 (~> 0.19.0) 137 | google-cloud-core (~> 1.6) 138 | googleauth (>= 0.16.2, < 2.a) 139 | mini_mime (~> 1.0) 140 | googleauth (1.7.0) 141 | faraday (>= 0.17.3, < 3.a) 142 | jwt (>= 1.4, < 3.0) 143 | memoist (~> 0.16) 144 | multi_json (~> 1.11) 145 | os (>= 0.9, < 2.0) 146 | signet (>= 0.16, < 2.a) 147 | highline (2.0.3) 148 | http-cookie (1.0.5) 149 | domain_name (~> 0.5) 150 | httpclient (2.8.3) 151 | jmespath (1.6.2) 152 | json (2.6.3) 153 | jwt (2.7.1) 154 | memoist (0.16.2) 155 | mini_magick (4.12.0) 156 | mini_mime (1.1.5) 157 | multi_json (1.15.0) 158 | multipart-post (2.3.0) 159 | nanaimo (0.3.0) 160 | naturally (2.2.1) 161 | optparse (0.1.1) 162 | os (1.1.4) 163 | plist (3.7.0) 164 | public_suffix (5.0.3) 165 | rake (13.0.6) 166 | representable (3.2.0) 167 | declarative (< 0.1.0) 168 | trailblazer-option (>= 0.1.1, < 0.2.0) 169 | uber (< 0.2.0) 170 | retriable (3.1.2) 171 | rexml (3.2.6) 172 | rouge (2.0.7) 173 | ruby2_keywords (0.0.5) 174 | rubyzip (2.3.2) 175 | security (0.1.3) 176 | signet (0.17.0) 177 | addressable (~> 2.8) 178 | faraday (>= 0.17.5, < 3.a) 179 | jwt (>= 1.5, < 3.0) 180 | multi_json (~> 1.10) 181 | simctl (1.6.10) 182 | CFPropertyList 183 | naturally 184 | terminal-notifier (2.0.0) 185 | terminal-table (1.8.0) 186 | unicode-display_width (~> 1.1, >= 1.1.1) 187 | trailblazer-option (0.1.2) 188 | tty-cursor (0.7.1) 189 | tty-screen (0.8.1) 190 | tty-spinner (0.9.3) 191 | tty-cursor (~> 0.7) 192 | uber (0.1.0) 193 | unf (0.1.4) 194 | unf_ext 195 | unf_ext (0.0.8.2) 196 | unicode-display_width (1.8.0) 197 | webrick (1.8.1) 198 | word_wrap (1.0.0) 199 | xcodeproj (1.22.0) 200 | CFPropertyList (>= 2.3.3, < 4.0) 201 | atomos (~> 0.1.3) 202 | claide (>= 1.0.2, < 2.0) 203 | colored2 (~> 3.1) 204 | nanaimo (~> 0.3.0) 205 | rexml (~> 3.2.4) 206 | xcpretty (0.3.0) 207 | rouge (~> 2.0.7) 208 | xcpretty-travis-formatter (1.0.1) 209 | xcpretty (~> 0.2, >= 0.0.7) 210 | 211 | PLATFORMS 212 | universal-darwin-21 213 | universal-darwin-22 214 | universal-darwin-23 215 | x86_64-darwin-19 216 | 217 | DEPENDENCIES 218 | fastlane 219 | 220 | BUNDLED WITH 221 | 2.2.32 222 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | def keystoreProperties = new Properties() 29 | def keystorePropertiesFile = rootProject.file('key.properties') 30 | if (keystorePropertiesFile.exists()) { 31 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 32 | } 33 | 34 | android { 35 | compileSdkVersion 33 36 | 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = '1.8' 44 | } 45 | 46 | sourceSets { 47 | main.java.srcDirs += 'src/main/kotlin' 48 | } 49 | 50 | defaultConfig { 51 | applicationId "chat.saga.voice_outliner" 52 | multiDexEnabled true 53 | minSdkVersion 21 54 | targetSdkVersion 33 55 | versionCode flutterVersionCode.toInteger() 56 | versionName flutterVersionName 57 | } 58 | 59 | signingConfigs { 60 | release { 61 | keyAlias keystoreProperties['keyAlias'] 62 | keyPassword keystoreProperties['keyPassword'] 63 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 64 | storePassword keystoreProperties['storePassword'] 65 | } 66 | } 67 | buildTypes { 68 | release { 69 | signingConfig signingConfigs.release 70 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 71 | } 72 | } 73 | } 74 | 75 | flutter { 76 | source '../..' 77 | } 78 | 79 | dependencies { 80 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 81 | def appcompat_version = "1.6.1" 82 | implementation "androidx.appcompat:appcompat:$appcompat_version" 83 | api 'net.java.dev.jna:jna:5.13.0@aar' 84 | implementation (group: 'com.alphacephei', name: 'vosk-android', version: '0.3.45') { 85 | exclude group: 'net.java.dev.jna', module: 'jna' 86 | } 87 | } 88 | 89 | task wrapper(type: Wrapper) { 90 | gradleVersion = '7.3' 91 | } 92 | // HACK https://stackoverflow.com/a/64418715/10833799 93 | task prepareKotlinBuildScriptModel { 94 | 95 | } -------------------------------------------------------------------------------- /android/app/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /android/app/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class com.sun.jna.* { *; } 2 | -keepclassmembers class * extends com.sun.jna.* { public *; } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 14 | 18 | 22 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/chat/saga/voice_outliner/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package chat.saga.voice_outliner 2 | 3 | import androidx.annotation.NonNull 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugin.common.MethodCall 7 | import io.flutter.plugin.common.MethodChannel 8 | import org.json.JSONObject 9 | import org.vosk.Recognizer 10 | import org.vosk.Model 11 | import org.vosk.android.RecognitionListener 12 | import org.vosk.android.SpeechStreamService 13 | import java.io.File 14 | import java.io.FileInputStream 15 | 16 | class VOListener(val onRes: (String?) -> Unit, val onFinalRes: (String?) -> Unit) : 17 | RecognitionListener { 18 | public override fun onTimeout() { 19 | } 20 | 21 | public override fun onError(exception: java.lang.Exception?) { 22 | onFinalResult(null) 23 | } 24 | 25 | public override fun onFinalResult(hypothesis: String?) { 26 | if (hypothesis != null) { 27 | val text = JSONObject(hypothesis).getString("text").toString() 28 | onFinalRes(text) 29 | } else onFinalRes(null) 30 | } 31 | 32 | public override fun onPartialResult(hypothesis: String?) { 33 | } 34 | 35 | public override fun onResult(hypothesis: String?) { 36 | if (hypothesis != null) { 37 | val text = JSONObject(hypothesis).getString("text").toString() 38 | onRes(text) 39 | } else onRes(null) 40 | } 41 | } 42 | 43 | class MainActivity : FlutterActivity() { 44 | private val CHANNEL = "voiceoutliner.saga.chat/androidtx" 45 | private var model: Model? = null; 46 | 47 | /** 48 | * @param path 49 | * Unzipped model path 50 | */ 51 | private fun initModel(call: MethodCall, result: MethodChannel.Result) { 52 | try { 53 | val path = call.argument("path") 54 | if (path == null) { 55 | result.error("Null path provided", "Cannot transcribe null path", null) 56 | return 57 | } 58 | this.model = Model(path) 59 | result.success(null); 60 | 61 | } catch (e: Exception) { 62 | result.error("Couldn't initialize model", e.toString(), null); 63 | } 64 | } 65 | 66 | private fun transcribe(call: MethodCall, result: MethodChannel.Result) { 67 | try { 68 | val path = call.argument("path") 69 | if (path == null) { 70 | result.error("Null path provided", "Cannot transcribe null path", null) 71 | return 72 | } 73 | if (this.model == null) { 74 | result.error("Model uninitialized", null, null) 75 | return 76 | } 77 | val file = File(path) 78 | val inputStream = FileInputStream(file) 79 | val recognizer = Recognizer(this.model, 44100F); 80 | val speechStreamService = SpeechStreamService(recognizer, inputStream, 44100F) 81 | val results = arrayListOf() 82 | speechStreamService.start(VOListener({ res -> 83 | if (res != null) { 84 | results.add(res) 85 | } 86 | }, { res -> 87 | if (res != null) { 88 | results.add(res) 89 | } 90 | if (results.isEmpty() || results.joinToString("").isBlank() ) { 91 | result.success(null) 92 | } else { 93 | result.success(results.joinToString(" ")) 94 | } 95 | speechStreamService.stop() 96 | })) 97 | } catch (e: Exception) { 98 | result.error("Couldn't transcribe", e.toString(), null) 99 | } 100 | } 101 | 102 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 103 | super.configureFlutterEngine(flutterEngine) 104 | MethodChannel( 105 | flutterEngine.dartExecutor.binaryMessenger, 106 | CHANNEL 107 | ).setMethodCallHandler { call, result -> 108 | when (call.method) { 109 | "transcribe" -> transcribe(call, result) 110 | "initModel" -> initModel(call, result) 111 | else -> result.notImplemented() 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.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 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.21' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | maven { 19 | url 'https://alphacephei.com/maven/' 20 | } 21 | } 22 | } 23 | 24 | rootProject.buildDir = '../build' 25 | subprojects { 26 | project.buildDir = "${rootProject.buildDir}/${project.name}" 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | tasks.register("clean", Delete) { 31 | delete rootProject.buildDir 32 | } 33 | 34 | -------------------------------------------------------------------------------- /android/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | json_key_file("google_play.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one 2 | package_name(ENV["PLAY_APP_IDENTIFIER"]) # e.g. com.krausefx.app 3 | -------------------------------------------------------------------------------- /android/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | before_all do 15 | update_fastlane 16 | end 17 | 18 | default_platform(:android) 19 | 20 | platform :android do 21 | desc "Runs all the tests" 22 | lane :test do 23 | gradle(task: "test") 24 | end 25 | 26 | desc "Deploy a new beta version to the Google Play" 27 | lane :beta do 28 | upload_to_play_store(track: "beta", aab: "../build/app/outputs/bundle/release/app-release.aab") 29 | end 30 | 31 | lane :release do 32 | upload_to_play_store(track: "production", aab: "../build/app/outputs/bundle/release/app-release.aab") 33 | end 34 | 35 | # TODO https://docs.fastlane.tools/actions/upload_to_play_store/ 36 | desc "Deploy a production version to Google Play" 37 | lane :release_submit do 38 | upload_to_play_store(track_promote_to: "production", skip_upload_aab: true, skip_upload_apk: true, skip_upload_metadata: true) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /android/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## Android 17 | 18 | ### android test 19 | 20 | ```sh 21 | [bundle exec] fastlane android test 22 | ``` 23 | 24 | Runs all the tests 25 | 26 | ### android beta 27 | 28 | ```sh 29 | [bundle exec] fastlane android beta 30 | ``` 31 | 32 | Deploy a new beta version to the Google Play 33 | 34 | ### android release 35 | 36 | ```sh 37 | [bundle exec] fastlane android release 38 | ``` 39 | 40 | 41 | 42 | ### android release_submit 43 | 44 | ```sh 45 | [bundle exec] fastlane android release_submit 46 | ``` 47 | 48 | Deploy a production version to Google Play 49 | 50 | ---- 51 | 52 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 53 | 54 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 55 | 56 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 57 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jan 30 19:47:35 PST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/icon/icon.png -------------------------------------------------------------------------------- /assets/onboarding/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/onboarding/1.png -------------------------------------------------------------------------------- /assets/onboarding/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/onboarding/2.png -------------------------------------------------------------------------------- /assets/onboarding/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/onboarding/3.png -------------------------------------------------------------------------------- /assets/onboarding/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/onboarding/4.png -------------------------------------------------------------------------------- /assets/onboarding/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/onboarding/5.png -------------------------------------------------------------------------------- /assets/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/1.png -------------------------------------------------------------------------------- /assets/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/2.png -------------------------------------------------------------------------------- /assets/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/3.png -------------------------------------------------------------------------------- /assets/screenshots/5inch-iPhone/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/5inch-iPhone/1.png -------------------------------------------------------------------------------- /assets/screenshots/5inch-iPhone/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/5inch-iPhone/2.png -------------------------------------------------------------------------------- /assets/screenshots/5inch-iPhone/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/5inch-iPhone/3.png -------------------------------------------------------------------------------- /assets/screenshots/android/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/android/1.png -------------------------------------------------------------------------------- /assets/screenshots/android/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/android/2.png -------------------------------------------------------------------------------- /assets/screenshots/android/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/android/3.png -------------------------------------------------------------------------------- /assets/screenshots/android/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/android/4.png -------------------------------------------------------------------------------- /assets/screenshots/ipad/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/ipad/1.png -------------------------------------------------------------------------------- /assets/screenshots/ipad/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/ipad/2.png -------------------------------------------------------------------------------- /assets/screenshots/ipad/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/assets/screenshots/ipad/3.png -------------------------------------------------------------------------------- /deploy_android.sh: -------------------------------------------------------------------------------- 1 | # Put a .env in android/fastlane with relevant variables described in README.md 2 | flutter build appbundle 3 | cd android 4 | bundle exec fastlane release -------------------------------------------------------------------------------- /deploy_ios.sh: -------------------------------------------------------------------------------- 1 | # Put a .env in ios/fastlane with relevant variables described in README.md 2 | flutter build ios --release --no-codesign 3 | cd ios 4 | bundle exec fastlane beta_local -------------------------------------------------------------------------------- /fonts/WorkSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/fonts/WorkSans-Medium.ttf -------------------------------------------------------------------------------- /fonts/WorkSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/fonts/WorkSans-Regular.ttf -------------------------------------------------------------------------------- /fonts/WorkSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/fonts/WorkSans-SemiBold.ttf -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /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/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | -------------------------------------------------------------------------------- /ios/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | addressable (2.8.5) 7 | public_suffix (>= 2.0.2, < 6.0) 8 | artifactory (3.0.15) 9 | atomos (0.1.3) 10 | aws-eventstream (1.2.0) 11 | aws-partitions (1.832.0) 12 | aws-sdk-core (3.185.0) 13 | aws-eventstream (~> 1, >= 1.0.2) 14 | aws-partitions (~> 1, >= 1.651.0) 15 | aws-sigv4 (~> 1.5) 16 | jmespath (~> 1, >= 1.6.1) 17 | aws-sdk-kms (1.72.0) 18 | aws-sdk-core (~> 3, >= 3.184.0) 19 | aws-sigv4 (~> 1.1) 20 | aws-sdk-s3 (1.136.0) 21 | aws-sdk-core (~> 3, >= 3.181.0) 22 | aws-sdk-kms (~> 1) 23 | aws-sigv4 (~> 1.6) 24 | aws-sigv4 (1.6.0) 25 | aws-eventstream (~> 1, >= 1.0.2) 26 | babosa (1.0.4) 27 | claide (1.1.0) 28 | colored (1.2) 29 | colored2 (3.1.2) 30 | commander (4.6.0) 31 | highline (~> 2.0.0) 32 | declarative (0.0.20) 33 | digest-crc (0.6.5) 34 | rake (>= 12.0.0, < 14.0.0) 35 | domain_name (0.5.20190701) 36 | unf (>= 0.0.5, < 1.0.0) 37 | dotenv (2.8.1) 38 | emoji_regex (3.2.3) 39 | excon (0.104.0) 40 | faraday (1.10.3) 41 | faraday-em_http (~> 1.0) 42 | faraday-em_synchrony (~> 1.0) 43 | faraday-excon (~> 1.1) 44 | faraday-httpclient (~> 1.0) 45 | faraday-multipart (~> 1.0) 46 | faraday-net_http (~> 1.0) 47 | faraday-net_http_persistent (~> 1.0) 48 | faraday-patron (~> 1.0) 49 | faraday-rack (~> 1.0) 50 | faraday-retry (~> 1.0) 51 | ruby2_keywords (>= 0.0.4) 52 | faraday-cookie_jar (0.0.7) 53 | faraday (>= 0.8.0) 54 | http-cookie (~> 1.0.0) 55 | faraday-em_http (1.0.0) 56 | faraday-em_synchrony (1.0.0) 57 | faraday-excon (1.1.0) 58 | faraday-httpclient (1.0.1) 59 | faraday-multipart (1.0.4) 60 | multipart-post (~> 2) 61 | faraday-net_http (1.0.1) 62 | faraday-net_http_persistent (1.2.0) 63 | faraday-patron (1.0.0) 64 | faraday-rack (1.0.0) 65 | faraday-retry (1.0.3) 66 | faraday_middleware (1.2.0) 67 | faraday (~> 1.0) 68 | fastimage (2.2.7) 69 | fastlane (2.216.0) 70 | CFPropertyList (>= 2.3, < 4.0.0) 71 | addressable (>= 2.8, < 3.0.0) 72 | artifactory (~> 3.0) 73 | aws-sdk-s3 (~> 1.0) 74 | babosa (>= 1.0.3, < 2.0.0) 75 | bundler (>= 1.12.0, < 3.0.0) 76 | colored 77 | commander (~> 4.6) 78 | dotenv (>= 2.1.1, < 3.0.0) 79 | emoji_regex (>= 0.1, < 4.0) 80 | excon (>= 0.71.0, < 1.0.0) 81 | faraday (~> 1.0) 82 | faraday-cookie_jar (~> 0.0.6) 83 | faraday_middleware (~> 1.0) 84 | fastimage (>= 2.1.0, < 3.0.0) 85 | gh_inspector (>= 1.1.2, < 2.0.0) 86 | google-apis-androidpublisher_v3 (~> 0.3) 87 | google-apis-playcustomapp_v1 (~> 0.1) 88 | google-cloud-storage (~> 1.31) 89 | highline (~> 2.0) 90 | http-cookie (~> 1.0.5) 91 | json (< 3.0.0) 92 | jwt (>= 2.1.0, < 3) 93 | mini_magick (>= 4.9.4, < 5.0.0) 94 | multipart-post (>= 2.0.0, < 3.0.0) 95 | naturally (~> 2.2) 96 | optparse (~> 0.1.1) 97 | plist (>= 3.1.0, < 4.0.0) 98 | rubyzip (>= 2.0.0, < 3.0.0) 99 | security (= 0.1.3) 100 | simctl (~> 1.6.3) 101 | terminal-notifier (>= 2.0.0, < 3.0.0) 102 | terminal-table (~> 3) 103 | tty-screen (>= 0.6.3, < 1.0.0) 104 | tty-spinner (>= 0.8.0, < 1.0.0) 105 | word_wrap (~> 1.0.0) 106 | xcodeproj (>= 1.13.0, < 2.0.0) 107 | xcpretty (~> 0.3.0) 108 | xcpretty-travis-formatter (>= 0.0.3) 109 | gh_inspector (1.1.3) 110 | google-apis-androidpublisher_v3 (0.50.0) 111 | google-apis-core (>= 0.11.0, < 2.a) 112 | google-apis-core (0.11.1) 113 | addressable (~> 2.5, >= 2.5.1) 114 | googleauth (>= 0.16.2, < 2.a) 115 | httpclient (>= 2.8.1, < 3.a) 116 | mini_mime (~> 1.0) 117 | representable (~> 3.0) 118 | retriable (>= 2.0, < 4.a) 119 | rexml 120 | webrick 121 | google-apis-iamcredentials_v1 (0.17.0) 122 | google-apis-core (>= 0.11.0, < 2.a) 123 | google-apis-playcustomapp_v1 (0.13.0) 124 | google-apis-core (>= 0.11.0, < 2.a) 125 | google-apis-storage_v1 (0.19.0) 126 | google-apis-core (>= 0.9.0, < 2.a) 127 | google-cloud-core (1.6.0) 128 | google-cloud-env (~> 1.0) 129 | google-cloud-errors (~> 1.0) 130 | google-cloud-env (1.6.0) 131 | faraday (>= 0.17.3, < 3.0) 132 | google-cloud-errors (1.3.1) 133 | google-cloud-storage (1.44.0) 134 | addressable (~> 2.8) 135 | digest-crc (~> 0.4) 136 | google-apis-iamcredentials_v1 (~> 0.1) 137 | google-apis-storage_v1 (~> 0.19.0) 138 | google-cloud-core (~> 1.6) 139 | googleauth (>= 0.16.2, < 2.a) 140 | mini_mime (~> 1.0) 141 | googleauth (1.8.1) 142 | faraday (>= 0.17.3, < 3.a) 143 | jwt (>= 1.4, < 3.0) 144 | multi_json (~> 1.11) 145 | os (>= 0.9, < 2.0) 146 | signet (>= 0.16, < 2.a) 147 | highline (2.0.3) 148 | http-cookie (1.0.5) 149 | domain_name (~> 0.5) 150 | httpclient (2.8.3) 151 | jmespath (1.6.2) 152 | json (2.6.3) 153 | jwt (2.7.1) 154 | mini_magick (4.12.0) 155 | mini_mime (1.1.5) 156 | multi_json (1.15.0) 157 | multipart-post (2.3.0) 158 | nanaimo (0.3.0) 159 | naturally (2.2.1) 160 | optparse (0.1.1) 161 | os (1.1.4) 162 | plist (3.7.0) 163 | public_suffix (5.0.3) 164 | rake (13.0.6) 165 | representable (3.2.0) 166 | declarative (< 0.1.0) 167 | trailblazer-option (>= 0.1.1, < 0.2.0) 168 | uber (< 0.2.0) 169 | retriable (3.1.2) 170 | rexml (3.2.6) 171 | rouge (2.0.7) 172 | ruby2_keywords (0.0.5) 173 | rubyzip (2.3.2) 174 | security (0.1.3) 175 | signet (0.18.0) 176 | addressable (~> 2.8) 177 | faraday (>= 0.17.5, < 3.a) 178 | jwt (>= 1.5, < 3.0) 179 | multi_json (~> 1.10) 180 | simctl (1.6.10) 181 | CFPropertyList 182 | naturally 183 | terminal-notifier (2.0.0) 184 | terminal-table (3.0.2) 185 | unicode-display_width (>= 1.1.1, < 3) 186 | trailblazer-option (0.1.2) 187 | tty-cursor (0.7.1) 188 | tty-screen (0.8.1) 189 | tty-spinner (0.9.3) 190 | tty-cursor (~> 0.7) 191 | uber (0.1.0) 192 | unf (0.1.4) 193 | unf_ext 194 | unf_ext (0.0.8.2) 195 | unicode-display_width (2.5.0) 196 | webrick (1.8.1) 197 | word_wrap (1.0.0) 198 | xcodeproj (1.23.0) 199 | CFPropertyList (>= 2.3.3, < 4.0) 200 | atomos (~> 0.1.3) 201 | claide (>= 1.0.2, < 2.0) 202 | colored2 (~> 3.1) 203 | nanaimo (~> 0.3.0) 204 | rexml (~> 3.2.4) 205 | xcpretty (0.3.0) 206 | rouge (~> 2.0.7) 207 | xcpretty-travis-formatter (1.0.1) 208 | xcpretty (~> 0.2, >= 0.0.7) 209 | 210 | PLATFORMS 211 | universal-darwin-21 212 | universal-darwin-22 213 | x86_64-darwin-19 214 | 215 | DEPENDENCIES 216 | fastlane 217 | 218 | BUNDLED WITH 219 | 2.2.32 220 | -------------------------------------------------------------------------------- /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 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | target.build_configurations.each do |config| 41 | config.build_settings["ONLY_ACTIVE_ARCH"] = "YES" 42 | config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64" 43 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' 44 | # Here are some configurations automatically generated by flutter 45 | 46 | # You can enable the permissions needed here. For example to enable camera 47 | # permission, just remove the `#` character in front so it looks like this: 48 | # 49 | # ## dart: PermissionGroup.camera 50 | # 'PERMISSION_CAMERA=1' 51 | # 52 | # Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h 53 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 54 | '$(inherited)', 55 | 56 | ## dart: PermissionGroup.calendar 57 | # 'PERMISSION_EVENTS=1', 58 | 59 | ## dart: PermissionGroup.reminders 60 | # 'PERMISSION_REMINDERS=1', 61 | 62 | ## dart: PermissionGroup.contacts 63 | # 'PERMISSION_CONTACTS=1', 64 | 65 | ## dart: PermissionGroup.camera 66 | # 'PERMISSION_CAMERA=1', 67 | 68 | ## dart: PermissionGroup.microphone 69 | 'PERMISSION_MICROPHONE=1', 70 | 71 | ## dart: PermissionGroup.speech 72 | 'PERMISSION_SPEECH_RECOGNIZER=1', 73 | 74 | ## dart: PermissionGroup.photos 75 | # 'PERMISSION_PHOTOS=1', 76 | 77 | ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] 78 | 'PERMISSION_LOCATION=1', 79 | 80 | ## dart: PermissionGroup.notification 81 | # 'PERMISSION_NOTIFICATIONS=1', 82 | 83 | ## dart: PermissionGroup.mediaLibrary 84 | # 'PERMISSION_MEDIA_LIBRARY=1', 85 | 86 | ## dart: PermissionGroup.sensors 87 | # 'PERMISSION_SENSORS=1', 88 | 89 | ## dart: PermissionGroup.bluetooth 90 | # 'PERMISSION_BLUETOOTH=1', 91 | 92 | ## dart: PermissionGroup.appTrackingTransparency 93 | # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1', 94 | 95 | ## dart: PermissionGroup.criticalAlerts 96 | # 'PERMISSION_CRITICAL_ALERTS=1' 97 | ] 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AppAuth (1.6.2): 3 | - AppAuth/Core (= 1.6.2) 4 | - AppAuth/ExternalUserAgent (= 1.6.2) 5 | - AppAuth/Core (1.6.2) 6 | - AppAuth/ExternalUserAgent (1.6.2): 7 | - AppAuth/Core 8 | - audio_session (0.0.1): 9 | - Flutter 10 | - connectivity_plus (0.0.1): 11 | - Flutter 12 | - ReachabilitySwift 13 | - emoji_picker_flutter (0.0.1): 14 | - Flutter 15 | - Flutter (1.0.0) 16 | - flutter_archive (0.0.1): 17 | - Flutter 18 | - ZIPFoundation (= 0.9.13) 19 | - flutter_keyboard_visibility (0.0.1): 20 | - Flutter 21 | - flutter_sound (9.2.13): 22 | - Flutter 23 | - flutter_sound_core (= 9.2.13) 24 | - flutter_sound_core (9.2.13) 25 | - flutter_vibrate (0.0.1): 26 | - Flutter 27 | - FMDB (2.7.5): 28 | - FMDB/standard (= 2.7.5) 29 | - FMDB/standard (2.7.5) 30 | - google_sign_in_ios (0.0.1): 31 | - Flutter 32 | - GoogleSignIn (~> 6.2) 33 | - GoogleSignIn (6.2.4): 34 | - AppAuth (~> 1.5) 35 | - GTMAppAuth (~> 1.3) 36 | - GTMSessionFetcher/Core (< 3.0, >= 1.1) 37 | - GTMAppAuth (1.3.1): 38 | - AppAuth/Core (~> 1.6) 39 | - GTMSessionFetcher/Core (< 3.0, >= 1.5) 40 | - GTMSessionFetcher/Core (2.3.0) 41 | - location (0.0.1): 42 | - Flutter 43 | - maps_launcher (0.0.1): 44 | - Flutter 45 | - package_info_plus (0.4.5): 46 | - Flutter 47 | - path_provider_foundation (0.0.1): 48 | - Flutter 49 | - FlutterMacOS 50 | - permission_handler_apple (9.1.1): 51 | - Flutter 52 | - ReachabilitySwift (5.0.0) 53 | - Sentry/HybridSDK (8.9.1): 54 | - SentryPrivate (= 8.9.1) 55 | - sentry_flutter (0.0.1): 56 | - Flutter 57 | - FlutterMacOS 58 | - Sentry/HybridSDK (= 8.9.1) 59 | - SentryPrivate (8.9.1) 60 | - share_plus (0.0.1): 61 | - Flutter 62 | - shared_preferences_foundation (0.0.1): 63 | - Flutter 64 | - FlutterMacOS 65 | - sqflite (0.0.3): 66 | - Flutter 67 | - FMDB (>= 2.7.5) 68 | - url_launcher_ios (0.0.1): 69 | - Flutter 70 | - ZIPFoundation (0.9.13) 71 | 72 | DEPENDENCIES: 73 | - audio_session (from `.symlinks/plugins/audio_session/ios`) 74 | - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) 75 | - emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`) 76 | - Flutter (from `Flutter`) 77 | - flutter_archive (from `.symlinks/plugins/flutter_archive/ios`) 78 | - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) 79 | - flutter_sound (from `.symlinks/plugins/flutter_sound/ios`) 80 | - flutter_vibrate (from `.symlinks/plugins/flutter_vibrate/ios`) 81 | - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/ios`) 82 | - location (from `.symlinks/plugins/location/ios`) 83 | - maps_launcher (from `.symlinks/plugins/maps_launcher/ios`) 84 | - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) 85 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 86 | - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 87 | - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) 88 | - share_plus (from `.symlinks/plugins/share_plus/ios`) 89 | - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 90 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 91 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 92 | 93 | SPEC REPOS: 94 | trunk: 95 | - AppAuth 96 | - flutter_sound_core 97 | - FMDB 98 | - GoogleSignIn 99 | - GTMAppAuth 100 | - GTMSessionFetcher 101 | - ReachabilitySwift 102 | - Sentry 103 | - SentryPrivate 104 | - ZIPFoundation 105 | 106 | EXTERNAL SOURCES: 107 | audio_session: 108 | :path: ".symlinks/plugins/audio_session/ios" 109 | connectivity_plus: 110 | :path: ".symlinks/plugins/connectivity_plus/ios" 111 | emoji_picker_flutter: 112 | :path: ".symlinks/plugins/emoji_picker_flutter/ios" 113 | Flutter: 114 | :path: Flutter 115 | flutter_archive: 116 | :path: ".symlinks/plugins/flutter_archive/ios" 117 | flutter_keyboard_visibility: 118 | :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" 119 | flutter_sound: 120 | :path: ".symlinks/plugins/flutter_sound/ios" 121 | flutter_vibrate: 122 | :path: ".symlinks/plugins/flutter_vibrate/ios" 123 | google_sign_in_ios: 124 | :path: ".symlinks/plugins/google_sign_in_ios/ios" 125 | location: 126 | :path: ".symlinks/plugins/location/ios" 127 | maps_launcher: 128 | :path: ".symlinks/plugins/maps_launcher/ios" 129 | package_info_plus: 130 | :path: ".symlinks/plugins/package_info_plus/ios" 131 | path_provider_foundation: 132 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 133 | permission_handler_apple: 134 | :path: ".symlinks/plugins/permission_handler_apple/ios" 135 | sentry_flutter: 136 | :path: ".symlinks/plugins/sentry_flutter/ios" 137 | share_plus: 138 | :path: ".symlinks/plugins/share_plus/ios" 139 | shared_preferences_foundation: 140 | :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 141 | sqflite: 142 | :path: ".symlinks/plugins/sqflite/ios" 143 | url_launcher_ios: 144 | :path: ".symlinks/plugins/url_launcher_ios/ios" 145 | 146 | SPEC CHECKSUMS: 147 | AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570 148 | audio_session: 4f3e461722055d21515cf3261b64c973c062f345 149 | connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a 150 | emoji_picker_flutter: df19dac03a2b39ac667dc8d1da939ef3a9e21347 151 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 152 | flutter_archive: 1805fdd3a695fd284b43edb53dc35ca843fb761c 153 | flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 154 | flutter_sound: c60effa2a350fb977885f0db2fbc4c1ad5160900 155 | flutter_sound_core: 26c10e5832e76aaacfae252d8925232281c486ae 156 | flutter_vibrate: 9f4c2ab57008965f78969472367c329dd77eb801 157 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 158 | google_sign_in_ios: 1256ff9d941db546373826966720b0c24804bcdd 159 | GoogleSignIn: 5651ce3a61e56ca864160e79b484cd9ed3f49b7a 160 | GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd 161 | GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 162 | location: d5cf8598915965547c3f36761ae9cc4f4e87d22e 163 | maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203 164 | package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 165 | path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 166 | permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 167 | ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 168 | Sentry: e3203780941722a1fcfee99e351de14244c7f806 169 | sentry_flutter: 8f0ffd53088e6a4d50c095852c5cad9e4405025c 170 | SentryPrivate: 5e3683390f66611fc7c6215e27645873adb55d13 171 | share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 172 | shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 173 | sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a 174 | url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 175 | ZIPFoundation: ae5b4b813d216d3bf0a148773267fff14bd51d37 176 | 177 | PODFILE CHECKSUM: 289b2c470abb1cc51612511117581a1ad35195ef 178 | 179 | COCOAPODS: 1.12.0 180 | -------------------------------------------------------------------------------- /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 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /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 Speech 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | @objc class AppDelegate: FlutterAppDelegate { 7 | private func receiveTxRequest(call: FlutterMethodCall, result: @escaping FlutterResult) { 8 | let uri = (call.arguments as! [String: Any])["path"] as! String 9 | let localeId = (call.arguments as! [String: Any])["locale"] as! String 10 | let url = URL(fileURLWithPath: uri) 11 | guard let myRecognizer = SFSpeechRecognizer(locale: Locale.init(identifier: localeId)) else { 12 | // A recognizer is not supported for the current locale 13 | result(FlutterError(code: "FAILED_REC", message: "unsupported locale", details: nil)) 14 | return 15 | } 16 | 17 | if !myRecognizer.isAvailable { 18 | result(FlutterError(code: "FAILED_REC", message: "unavailable recognizer", details: nil)) 19 | return 20 | } 21 | 22 | let request = SFSpeechURLRecognitionRequest(url: url) 23 | myRecognizer.recognitionTask(with: request) { (res, error) in 24 | guard let res = res else { 25 | // Recognition failed, so check error for details and handle it 26 | result(FlutterError(code: "FAILED_REC", message: error.debugDescription, details: nil)) 27 | return 28 | } 29 | 30 | // Print the speech that has been recognized so far 31 | if res.isFinal { 32 | result(String(res.bestTranscription.formattedString)) 33 | } 34 | } 35 | } 36 | private func requestTxPermission(result: @escaping FlutterResult) { 37 | let status = SFSpeechRecognizer.authorizationStatus() 38 | switch status { 39 | case .notDetermined: 40 | SFSpeechRecognizer.requestAuthorization({ (status) -> Void in 41 | result(Bool(status == SFSpeechRecognizerAuthorizationStatus.authorized)) 42 | }) 43 | case .denied: 44 | result(Bool(false)) 45 | case .restricted: 46 | result(Bool(false)) 47 | case .authorized: 48 | result(Bool(true)) 49 | default: 50 | result(Bool(true)) 51 | } 52 | } 53 | private func getLocaleOptions(result: @escaping FlutterResult) { 54 | let locales = SFSpeechRecognizer.supportedLocales() 55 | var localeOptions: [String: String] = [:] 56 | for locale in locales { 57 | localeOptions[locale.identifier] = locale.localizedString(forIdentifier: locale.identifier) ?? locale.identifier 58 | } 59 | result(localeOptions) 60 | } 61 | override func application( 62 | _ application: UIApplication, 63 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 64 | ) -> Bool { 65 | let controller: FlutterViewController = window?.rootViewController as! FlutterViewController 66 | let transcribeChannel = FlutterMethodChannel( 67 | name: "voiceoutliner.saga.chat/iostx", binaryMessenger: controller.binaryMessenger) 68 | transcribeChannel.setMethodCallHandler({ 69 | (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in 70 | switch call.method { 71 | case "transcribe": 72 | self.receiveTxRequest(call: call, result: result) 73 | case "requestPermission": 74 | self.requestTxPermission(result: result) 75 | case "getLocaleOptions": 76 | self.getLocaleOptions(result: result) 77 | default: 78 | result(FlutterMethodNotImplemented) 79 | } 80 | }) 81 | GeneratedPluginRegistrant.register(with: self) 82 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/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/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 658946540988-ak572264ge5odag4o8euqe5ev6bf354l.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.658946540988-ak572264ge5odag4o8euqe5ev6bf354l 9 | PLIST_VERSION 10 | 1 11 | BUNDLE_ID 12 | chat.saga.voice-outliner 13 | 14 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Voiceliner 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | voice_outliner 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleURLTypes 24 | 25 | 26 | CFBundleTypeRole 27 | Editor 28 | CFBundleURLSchemes 29 | 30 | com.googleusercontent.apps.658946540988-ak572264ge5odag4o8euqe5ev6bf354l 31 | 32 | 33 | 34 | CFBundleVersion 35 | $(FLUTTER_BUILD_NUMBER) 36 | ITSAppUsesNonExemptEncryption 37 | 38 | LSApplicationQueriesSchemes 39 | 40 | https 41 | http 42 | 43 | LSRequiresIPhoneOS 44 | 45 | NSLocationUsageDescription 46 | This app can add your location to your recordings to help you remember them. 47 | NSLocationWhenInUseUsageDescription 48 | This app can add your location to your recordings to help you remember them. 49 | NSMicrophoneUsageDescription 50 | This app records your voice so that it can create voice notes. 51 | NSSpeechRecognitionUsageDescription 52 | This app transcribes recordings so that you can more easily find them. 53 | UIFileSharingEnabled 54 | 55 | UILaunchStoryboardName 56 | LaunchScreen 57 | UIMainStoryboardFile 58 | Main 59 | UISupportedInterfaceOrientations 60 | 61 | UIInterfaceOrientationPortrait 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | UISupportedInterfaceOrientations~ipad 66 | 67 | UIInterfaceOrientationPortrait 68 | UIInterfaceOrientationPortraitUpsideDown 69 | UIInterfaceOrientationLandscapeLeft 70 | UIInterfaceOrientationLandscapeRight 71 | 72 | UISupportsDocumentBrowser 73 | 74 | UIViewControllerBasedStatusBarAppearance 75 | 76 | CADisableMinimumFrameDurationOnPhone 77 | 78 | UIApplicationSupportsIndirectInputEvents 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ios/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier(ENV["APP_IDENTIFIER"]) # The bundle identifier of your app 2 | apple_id(ENV["APPLE_ID"]) # Your Apple email address 3 | 4 | itc_team_id(ENV["ITC_TEAM_ID"]) # App Store Connect Team ID 5 | team_id(ENV["TEAM_ID"]) # Developer Portal Team ID 6 | 7 | # For more information about the Appfile, see: 8 | # https://docs.fastlane.tools/advanced/#appfile 9 | -------------------------------------------------------------------------------- /ios/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | before_all do 15 | update_fastlane 16 | end 17 | 18 | default_platform(:ios) 19 | 20 | platform :ios do 21 | desc "Push a new beta build to TestFlight on CI" 22 | lane :beta_ci do 23 | setup_ci 24 | 25 | sync_code_signing( 26 | type: "appstore", 27 | readonly: is_ci 28 | ) 29 | 30 | build_app(workspace: "Runner.xcworkspace", scheme: "Runner") 31 | api_key = app_store_connect_api_key( 32 | key_id: ENV["CONNECT_KEY_ID"], 33 | issuer_id: ENV["CONNECT_ISSUER_ID"], 34 | key_filepath: "./ci.p8", 35 | ) 36 | 37 | upload_to_testflight(skip_waiting_for_build_processing: true, api_key: api_key) 38 | end 39 | 40 | desc "Push a new beta build to TestFlight on CI" 41 | lane :beta_local do 42 | build_app(workspace: "Runner.xcworkspace", scheme: "Runner") 43 | api_key = app_store_connect_api_key( 44 | key_id: ENV["CONNECT_KEY_ID"], 45 | issuer_id: ENV["CONNECT_ISSUER_ID"], 46 | key_filepath: "./local.p8", 47 | ) 48 | 49 | upload_to_testflight(api_key: api_key) 50 | end 51 | 52 | lane :release_local do 53 | api_key = app_store_connect_api_key( 54 | key_id: ENV["CONNECT_KEY_ID"], 55 | issuer_id: ENV["CONNECT_ISSUER_ID"], 56 | key_filepath: "./local.p8", 57 | ) 58 | upload_to_app_store( 59 | api_key: api_key, 60 | submit_for_review: true, 61 | automatic_release: true, 62 | app_review_information: { 63 | first_name: ENV["FIRST_NAME"], 64 | last_name: ENV["LAST_NAME"], 65 | phone_number: ENV["PHONE_NUMBER"], 66 | email_address: ENV["EMAIL_ADDRESS"], 67 | }, 68 | submission_information: { 69 | add_id_info_uses_idfa: false, 70 | }, 71 | username: ENV["APPLE_ID"], 72 | skip_binary_upload: true, 73 | precheck_include_in_app_purchases: false 74 | ) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /ios/fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | git_url(ENV["MATCH_GIT"]) 2 | 3 | storage_mode("git") 4 | 5 | type("development") # The default type, can be: appstore, adhoc, enterprise or development 6 | 7 | # app_identifier(["tools.fastlane.app", "tools.fastlane.app2"]) 8 | # username("user@fastlane.tools") # Your Apple Developer Portal username 9 | 10 | # For all available options run `fastlane match --help` 11 | # Remove the # in the beginning of the line to enable the other options 12 | 13 | # The docs are available on https://docs.fastlane.tools/actions/match 14 | -------------------------------------------------------------------------------- /ios/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## iOS 17 | 18 | ### ios beta_ci 19 | 20 | ```sh 21 | [bundle exec] fastlane ios beta_ci 22 | ``` 23 | 24 | Push a new beta build to TestFlight on CI 25 | 26 | ### ios beta_local 27 | 28 | ```sh 29 | [bundle exec] fastlane ios beta_local 30 | ``` 31 | 32 | Push a new beta build to TestFlight on CI 33 | 34 | ### ios release_local 35 | 36 | ```sh 37 | [bundle exec] fastlane ios release_local 38 | ``` 39 | 40 | 41 | 42 | ---- 43 | 44 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 45 | 46 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 47 | 48 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 49 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/copyright.txt: -------------------------------------------------------------------------------- 1 | 2022 Max Krieger 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/apple_tv_privacy_policy.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/description.txt: -------------------------------------------------------------------------------- 1 | The fastest way to capture and structure your thoughts: through voice. 2 | 3 | Hold record, say what you want, and release. Just like you'd expect from an outliner, create hierarchies and rearrange. Notes are auto-transcribed and searchable, but you can always play back the audio. 4 | 5 | Automatically attach location to notes. Remember walks you took, and which places sparked what ideas. 6 | 7 | This app is open source and available on GitHub. Everything is local to your device. You can back up to Google Drive and iCloud. 8 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/keywords.txt: -------------------------------------------------------------------------------- 1 | voice,memos,voice memos,notes,outline,transcription 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/marketing_url.txt: -------------------------------------------------------------------------------- 1 | https://a9.io/voiceliner/ 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/name.txt: -------------------------------------------------------------------------------- 1 | Voiceliner 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/privacy_url.txt: -------------------------------------------------------------------------------- 1 | https://gist.github.com/maxkrieger/301352ae9b7a9e51f49d843fb851d823 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/promotional_text.txt: -------------------------------------------------------------------------------- 1 | Braindump better. 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/release_notes.txt: -------------------------------------------------------------------------------- 1 | - Tap the date stamp in a memo to see the full date! 2 | - Fix locales on iOS -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/subtitle.txt: -------------------------------------------------------------------------------- 1 | Braindump better. 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/en-US/support_url.txt: -------------------------------------------------------------------------------- 1 | https://github.com/maxkrieger/voiceliner/issues 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/primary_category.txt: -------------------------------------------------------------------------------- 1 | PRODUCTIVITY 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/primary_first_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/primary_second_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/review_information/demo_password.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/review_information/demo_user.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/review_information/email_address.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/review_information/first_name.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/metadata/review_information/first_name.txt -------------------------------------------------------------------------------- /ios/fastlane/metadata/review_information/last_name.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/metadata/review_information/last_name.txt -------------------------------------------------------------------------------- /ios/fastlane/metadata/review_information/notes.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/review_information/phone_number.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/secondary_category.txt: -------------------------------------------------------------------------------- 1 | UTILITIES 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/secondary_first_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/metadata/secondary_second_sub_category.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/0_APP_IPAD_PRO_129_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/0_APP_IPAD_PRO_129_0.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/0_APP_IPAD_PRO_3GEN_129_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/0_APP_IPAD_PRO_3GEN_129_0.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/0_APP_IPHONE_55_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/0_APP_IPHONE_55_0.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/0_APP_IPHONE_65_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/0_APP_IPHONE_65_0.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/1_APP_IPAD_PRO_129_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/1_APP_IPAD_PRO_129_1.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/1_APP_IPAD_PRO_3GEN_129_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/1_APP_IPAD_PRO_3GEN_129_1.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/1_APP_IPHONE_55_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/1_APP_IPHONE_55_1.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/1_APP_IPHONE_65_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/1_APP_IPHONE_65_1.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/2_APP_IPAD_PRO_129_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/2_APP_IPAD_PRO_129_2.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/2_APP_IPAD_PRO_3GEN_129_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/2_APP_IPAD_PRO_3GEN_129_2.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/2_APP_IPHONE_55_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/2_APP_IPHONE_55_2.png -------------------------------------------------------------------------------- /ios/fastlane/screenshots/en-US/2_APP_IPHONE_65_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/ios/fastlane/screenshots/en-US/2_APP_IPHONE_65_2.png -------------------------------------------------------------------------------- /lib/consts.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:location/location.dart'; 3 | 4 | const driveEnabledKey = "drive_enabled"; 5 | const lastBackupKey = "last_backed_up"; 6 | const shouldTranscribeKey = "should_transcribe"; 7 | const shouldLocateKey = "should_locate"; 8 | const showCompletedKey = "show_completed"; 9 | const showArchivedKey = "show_archived"; 10 | const allowRetranscriptionKey = "allow_retranscription"; 11 | const lastRouteKey = "last_route"; 12 | const lastOutlineKey = "last_outline"; 13 | const modelDirKey = "model_dir"; 14 | const modelLanguageKey = "model_language"; 15 | const localeKey = "ios_locale"; 16 | const autoDeleteOldBackupsKey = "auto_delete_old_backups"; 17 | 18 | const classicPurple = Color.fromRGBO(169, 129, 234, 1.0); 19 | const basePurple = Color.fromRGBO(163, 95, 255, 1); 20 | const warmRed = Color.fromRGBO(241, 52, 125, 1.0); 21 | const warningRed = Color.fromRGBO(255, 112, 112, 1.0); 22 | final locationInstance = Location(); 23 | const defaultEmoji = "🔮"; 24 | -------------------------------------------------------------------------------- /lib/data/note.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:intl/intl.dart'; 4 | 5 | class Note extends LinkedListEntry { 6 | final String id; 7 | String? filePath; 8 | DateTime dateCreated; 9 | bool isComplete; 10 | bool isCollapsed; 11 | bool transcribed = false; 12 | bool backedUp = false; 13 | int? color; 14 | double? latitude; 15 | double? longitude; 16 | Duration? duration; 17 | String? transcript; 18 | String? parentNoteId; 19 | String outlineId; 20 | Note( 21 | {required this.id, 22 | required this.dateCreated, 23 | required this.outlineId, 24 | this.filePath, 25 | this.parentNoteId, 26 | this.isComplete = false, 27 | this.transcribed = false, 28 | this.color, 29 | this.transcript, 30 | this.duration, 31 | this.latitude, 32 | this.longitude, 33 | required this.isCollapsed}); 34 | 35 | Note.fromMap(Map map) 36 | : id = map["id"], 37 | filePath = map["file_path"], 38 | dateCreated = DateTime.fromMillisecondsSinceEpoch(map["date_created"], 39 | isUtc: true), 40 | outlineId = map["outline_id"], 41 | transcript = map["transcript"], 42 | latitude = map["latitude"], 43 | longitude = map["longitude"], 44 | color = map["color"], 45 | parentNoteId = map["parent_note_id"], 46 | isComplete = map["is_complete"] == 1, 47 | isCollapsed = map["is_collapsed"] == 1, 48 | backedUp = map["backed_up"] == 1, 49 | transcribed = map["transcribed"] == 1 { 50 | if (map["duration"] != null) { 51 | duration = Duration(milliseconds: map["duration"]); 52 | } 53 | } 54 | 55 | String get infoString => 56 | "Recording at ${DateFormat.yMd().add_jm().format(dateCreated.toLocal())}"; 57 | 58 | String? get predecessorNoteId => previous?.id; 59 | 60 | Map get map { 61 | return { 62 | "id": id, 63 | "file_path": filePath, 64 | "date_created": dateCreated.toUtc().millisecondsSinceEpoch, 65 | "outline_id": outlineId, 66 | "transcript": transcript, 67 | "parent_note_id": parentNoteId, 68 | "predecessor_note_id": predecessorNoteId, 69 | "duration": duration != null ? duration!.inMilliseconds : null, 70 | "is_complete": isComplete ? 1 : 0, 71 | "is_collapsed": isCollapsed ? 1 : 0, 72 | "transcribed": transcribed ? 1 : 0, 73 | "backed_up": backedUp ? 1 : 0, 74 | "color": color, 75 | "latitude": latitude, 76 | "longitude": longitude 77 | }; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/data/outline.dart: -------------------------------------------------------------------------------- 1 | class Outline { 2 | Outline( 3 | {required this.name, 4 | required this.id, 5 | required this.dateCreated, 6 | required this.dateUpdated, 7 | required this.archived, 8 | required this.emoji}); 9 | String name; 10 | String emoji; 11 | final String id; 12 | bool archived; 13 | final DateTime dateCreated; 14 | final DateTime dateUpdated; 15 | 16 | Outline.fromMap(Map map) 17 | : id = map["id"], 18 | name = map["name"], 19 | emoji = map["emoji"], 20 | archived = map["archived"] == 1, 21 | dateCreated = DateTime.fromMillisecondsSinceEpoch(map["date_created"], 22 | isUtc: true), 23 | dateUpdated = DateTime.fromMillisecondsSinceEpoch(map["date_updated"], 24 | isUtc: true); 25 | 26 | Map get map { 27 | return { 28 | "id": id, 29 | "name": name, 30 | "emoji": emoji, 31 | "date_created": dateCreated.toUtc().millisecondsSinceEpoch, 32 | "date_updated": dateCreated.toUtc().millisecondsSinceEpoch, 33 | "archived": archived ? 1 : 0, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/globals.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | final GlobalKey snackbarKey = 4 | GlobalKey(); 5 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_dotenv/flutter_dotenv.dart'; 6 | import 'package:provider/provider.dart'; 7 | import 'package:sentry_flutter/sentry_flutter.dart' as sentry; 8 | import 'package:shared_preferences/shared_preferences.dart'; 9 | import 'package:voice_outliner/repositories/db_repository.dart'; 10 | import 'package:voice_outliner/repositories/drive_backup.dart'; 11 | import 'package:voice_outliner/repositories/vosk_speech_recognizer.dart'; 12 | import 'package:voice_outliner/state/outline_state.dart'; 13 | import 'package:voice_outliner/state/player_state.dart'; 14 | import 'package:voice_outliner/views/ios_transcription_setup_view.dart'; 15 | import 'package:voice_outliner/views/notes_view.dart'; 16 | import 'package:voice_outliner/views/onboarding_view.dart'; 17 | import 'package:voice_outliner/views/outlines_view.dart'; 18 | import 'package:voice_outliner/views/vosk_transcription_setup_view.dart'; 19 | 20 | import 'consts.dart'; 21 | import 'globals.dart'; 22 | 23 | final routes = { 24 | "/": const OutlinesView(), 25 | "/notes": const NotesView(), 26 | "/onboarding": const OnboardingView(), 27 | "/transcription_setup_vosk": const VoskTranscriptionSetupView(), 28 | "/transcription_setup_ios": const IOSTranscriptionSetupView() 29 | }; 30 | 31 | const generalAppBar = 32 | AppBarTheme(elevation: 0.4, centerTitle: false, titleSpacing: 20); 33 | 34 | final theme = ThemeData( 35 | fontFamily: "Work Sans", 36 | appBarTheme: generalAppBar.copyWith( 37 | backgroundColor: Colors.white, 38 | foregroundColor: Colors.black87, 39 | titleTextStyle: const TextStyle( 40 | fontFamily: "Work Sans", 41 | fontSize: 24, 42 | fontWeight: FontWeight.w700, 43 | color: Colors.black)), 44 | focusColor: classicPurple, 45 | primarySwatch: Colors.deepPurple, 46 | primaryColor: classicPurple, 47 | ); 48 | 49 | Future main() async { 50 | await dotenv.load(fileName: ".env"); 51 | 52 | final SharedPreferences sharedPrefs = await SharedPreferences.getInstance(); 53 | if (sharedPrefs.getBool(shouldTranscribeKey) == null) { 54 | sharedPrefs.setBool(shouldTranscribeKey, true); 55 | } 56 | if (sharedPrefs.getBool(driveEnabledKey) ?? false) { 57 | await googleSignIn.signInSilently(); 58 | } 59 | if (Platform.isAndroid) { 60 | final modelDir = sharedPrefs.getString(modelDirKey); 61 | if (modelDir != null) { 62 | await voskInitModel(modelDir); 63 | } 64 | } 65 | void appRunner() => runApp(MultiProvider( 66 | providers: [ 67 | ChangeNotifierProvider( 68 | lazy: false, create: (_) => PlayerModel()..load()), 69 | ChangeNotifierProvider( 70 | lazy: false, 71 | create: (_) => DBRepository()..load(), 72 | ), 73 | ChangeNotifierProxyProvider2( 75 | create: (_) => OutlinesModel(), 76 | update: (_, d, p, o) => (o ?? OutlinesModel())..load(p, d)) 77 | ], 78 | child: VoiceOutlinerApp( 79 | sharedPreferences: sharedPrefs, 80 | ))); 81 | if (kReleaseMode) { 82 | await sentry.SentryFlutter.init((config) { 83 | config.dsn = dotenv.env["SENTRY_DSN"]; 84 | config.diagnosticLevel = sentry.SentryLevel.error; 85 | }, appRunner: appRunner); 86 | } else { 87 | print("debug mode!"); 88 | appRunner(); 89 | } 90 | } 91 | 92 | class VoiceOutlinerApp extends StatefulWidget { 93 | final SharedPreferences sharedPreferences; 94 | const VoiceOutlinerApp({Key? key, required this.sharedPreferences}) 95 | : super(key: key); 96 | 97 | @override 98 | _VoiceOutlinerAppState createState() => _VoiceOutlinerAppState(); 99 | } 100 | 101 | class _VoiceOutlinerAppState extends State { 102 | String? lastOutline; 103 | 104 | @override 105 | void initState() { 106 | super.initState(); 107 | setState(() { 108 | lastOutline = widget.sharedPreferences.getString(lastOutlineKey); 109 | }); 110 | } 111 | 112 | Future saveRoute(RouteSettings route) async { 113 | await widget.sharedPreferences.setString("last_route", route.name!); 114 | if (route.arguments != null) { 115 | await widget.sharedPreferences.setString( 116 | "last_outline", (route.arguments as NotesViewArgs).outlineId); 117 | } 118 | } 119 | 120 | String _initialRoute() { 121 | final lastRoute = widget.sharedPreferences.getString(lastRouteKey); 122 | // If you've onboarded but don't have language set up on android 123 | if (Platform.isAndroid && 124 | widget.sharedPreferences.getString(modelDirKey) == null && 125 | (widget.sharedPreferences.getBool(shouldTranscribeKey) ?? true) && 126 | lastRoute != null) { 127 | return "/transcription_setup_vosk"; 128 | } 129 | // If you've onboarded but don't have language set up on iOS 130 | if (Platform.isIOS && 131 | widget.sharedPreferences.getString(localeKey) == null && 132 | (widget.sharedPreferences.getBool(shouldTranscribeKey) ?? true) && 133 | lastRoute != null) { 134 | return "/transcription_setup_ios"; 135 | } 136 | if (lastRoute != null) { 137 | return lastRoute; 138 | } 139 | return "/onboarding"; 140 | } 141 | 142 | @override 143 | Widget build(BuildContext context) { 144 | bool loading = context.select((m) => m.isReady); 145 | if (!loading) { 146 | return const Center(child: CircularProgressIndicator()); 147 | } 148 | return MaterialApp( 149 | title: 'Voiceliner', 150 | scaffoldMessengerKey: snackbarKey, 151 | debugShowCheckedModeBanner: false, 152 | onGenerateRoute: (RouteSettings route) { 153 | saveRoute(route); 154 | final rte = routes[route.name]; 155 | if (rte == null) { 156 | throw ("Route null"); 157 | } 158 | return PageRouteBuilder( 159 | pageBuilder: (c, a, aa) => rte, 160 | transitionsBuilder: (c, an, an2, child) => Align( 161 | child: SlideTransition( 162 | position: Tween( 163 | begin: Offset(route.name == "/" ? -1 : 1, 0), 164 | end: Offset.zero, 165 | ).animate(an), 166 | child: child)), 167 | transitionDuration: const Duration(milliseconds: 200), 168 | settings: RouteSettings( 169 | name: route.name, 170 | arguments: route.arguments ?? 171 | (lastOutline != null 172 | ? NotesViewArgs(lastOutline!) 173 | : null))); 174 | }, 175 | initialRoute: _initialRoute(), 176 | themeMode: ThemeMode.system, 177 | theme: theme, 178 | darkTheme: ThemeData( 179 | fontFamily: "Work Sans", 180 | appBarTheme: generalAppBar.copyWith( 181 | titleTextStyle: const TextStyle( 182 | fontFamily: "Work Sans", 183 | fontSize: 24, 184 | fontWeight: FontWeight.w700, 185 | color: Colors.white)), 186 | brightness: Brightness.dark, 187 | primaryColor: classicPurple, 188 | primarySwatch: Colors.deepPurple), 189 | color: classicPurple, 190 | ); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /lib/repositories/ios_speech_recognizer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:sentry_flutter/sentry_flutter.dart'; 6 | 7 | import '../globals.dart'; 8 | 9 | const iosPlatform = MethodChannel("voiceoutliner.saga.chat/iostx"); 10 | 11 | Future recognizeNoteIOS(String path, String locale) async { 12 | try { 13 | final platformRes = await iosPlatform 14 | .invokeMethod("transcribe", {"path": path, "locale": locale}); 15 | if (platformRes is String) { 16 | return platformRes; 17 | } else { 18 | snackbarKey.currentState 19 | ?.showSnackBar(const SnackBar(content: Text("Could not get speech"))); 20 | return null; 21 | } 22 | } catch (err, tr) { 23 | print(err); 24 | snackbarKey.currentState 25 | ?.showSnackBar(const SnackBar(content: Text("Could not get speech"))); 26 | return null; 27 | } 28 | } 29 | 30 | Future tryTxPermissionIOS() async { 31 | if (!Platform.isIOS) { 32 | print("Not IOS"); 33 | return false; 34 | } 35 | try { 36 | final res = await iosPlatform.invokeMethod("requestPermission"); 37 | return res; 38 | } catch (err) { 39 | print(err); 40 | return false; 41 | } 42 | } 43 | 44 | /// Returns a map {"en-US": "English (US)"} 45 | Future> getLocaleOptions() async { 46 | if (!Platform.isIOS) { 47 | print("Not IOS"); 48 | return {}; 49 | } 50 | try { 51 | final res = await iosPlatform.invokeMethod("getLocaleOptions"); 52 | return Map.from(res); 53 | } catch (err, tr) { 54 | snackbarKey.currentState?.showSnackBar( 55 | SnackBar(content: Text("Could not get locales: ${err.toString()}"))); 56 | Sentry.captureException(err, stackTrace: tr); 57 | return {}; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/repositories/vosk_speech_recognizer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_archive/flutter_archive.dart'; 7 | import 'package:http/http.dart' as http; 8 | import 'package:path_provider/path_provider.dart'; 9 | import 'package:sentry_flutter/sentry_flutter.dart'; 10 | import 'package:shared_preferences/shared_preferences.dart'; 11 | import 'package:voice_outliner/consts.dart'; 12 | 13 | import '../globals.dart'; 14 | 15 | const androidPlatform = MethodChannel("voiceoutliner.saga.chat/androidtx"); 16 | 17 | const voskModelsURL = "https://alphacephei.com/vosk/models/model-list.json"; 18 | 19 | class VoskModel { 20 | String url; 21 | String sizeText; 22 | String languageText; 23 | String languageCode; 24 | VoskModel(this.url, this.sizeText, this.languageText, this.languageCode); 25 | } 26 | 27 | Future> retrieveVoskModels() async { 28 | try { 29 | final res = await http.get(Uri.parse(voskModelsURL)); 30 | if (res.statusCode == 200) { 31 | final decoded = jsonDecode(res.body); 32 | if (decoded is List) { 33 | final parsed = decoded 34 | .where((element) => 35 | element["type"] == "small" && element["obsolete"] == "false") 36 | .map((element) => VoskModel(element["url"], element["size_text"], 37 | element["lang_text"], element["lang"])) 38 | .toList(growable: false); 39 | parsed.sort((a, b) => a.languageCode.compareTo(b.languageCode)); 40 | return parsed; 41 | } 42 | } 43 | print("Status code is ${res.statusCode}"); 44 | return []; 45 | } catch (err, tr) { 46 | print(err); 47 | Sentry.captureException(err, stackTrace: tr); 48 | snackbarKey.currentState?.showSnackBar(SnackBar( 49 | content: Text("Could not retrieve models: ${err.toString()}"))); 50 | return []; 51 | } 52 | } 53 | 54 | Future voskSpeechRecognize(String path) async { 55 | if (!Platform.isAndroid) { 56 | print("Not Android"); 57 | return null; 58 | } 59 | try { 60 | final result = 61 | await androidPlatform.invokeMethod("transcribe", {"path": path}); 62 | return result; 63 | } catch (err, tr) { 64 | snackbarKey.currentState?.showSnackBar( 65 | SnackBar(content: Text("Could not recognize: ${err.toString()}"))); 66 | print(err); 67 | return null; 68 | } 69 | } 70 | 71 | /// Returns null if success 72 | Future voskInitModel(String path) async { 73 | if (!Platform.isAndroid) { 74 | return "Not Android"; 75 | } 76 | try { 77 | // TODO: is this fallible to the sandbox path changing? 78 | final res = await androidPlatform.invokeMethod("initModel", {"path": path}); 79 | if (res != null) { 80 | return res.toString(); 81 | } 82 | return null; 83 | } catch (err, tr) { 84 | snackbarKey.currentState?.showSnackBar( 85 | SnackBar(content: Text("Could not init model: ${err.toString()}"))); 86 | return err.toString(); 87 | } 88 | } 89 | 90 | /// returns null if success, error message if not 91 | Future voskDownloadAndInitModel(String url) async { 92 | if (!Platform.isAndroid) { 93 | return "Not Android"; 94 | } 95 | try { 96 | final cacheDirs = await getExternalCacheDirectories(); 97 | if (cacheDirs == null || cacheDirs.isEmpty) { 98 | return "Couldn't get cache"; 99 | } 100 | final uri = Uri.parse(url); 101 | final cacheDir = cacheDirs.first; 102 | final modelsDir = await Directory("${cacheDir.path}/vosk_models").create(); 103 | final tmpDir = await getTemporaryDirectory(); 104 | final req = await HttpClient().getUrl(uri); 105 | final res = await req.close(); 106 | final zipFile = File("${tmpDir.path}/downloaded.zip"); 107 | await res.pipe(zipFile.openWrite()); 108 | await ZipFile.extractToDirectory( 109 | zipFile: zipFile, destinationDir: modelsDir); 110 | final folderName = uri.pathSegments.last.replaceAll(".zip", ""); 111 | final modelDir = Directory("${modelsDir.path}/$folderName"); 112 | final initResult = await androidPlatform 113 | .invokeMethod("initModel", {"path": modelDir.path}); 114 | if (initResult != null) { 115 | return initResult.toString(); 116 | } 117 | final sharedPreferences = await SharedPreferences.getInstance(); 118 | await sharedPreferences.setString(modelDirKey, modelDir.path); 119 | return null; 120 | } catch (err, tr) { 121 | snackbarKey.currentState?.showSnackBar( 122 | SnackBar(content: Text("Could not init model: ${err.toString()}"))); 123 | return err.toString(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/state/outline_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:sentry_flutter/sentry_flutter.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | import 'package:voice_outliner/consts.dart'; 7 | import 'package:voice_outliner/data/outline.dart'; 8 | import 'package:voice_outliner/repositories/db_repository.dart'; 9 | import 'package:voice_outliner/repositories/drive_backup.dart'; 10 | import 'package:voice_outliner/state/player_state.dart'; 11 | 12 | final defaultOutline = Outline( 13 | name: "", 14 | emoji: "📓", 15 | id: "", 16 | dateCreated: DateTime.now(), 17 | dateUpdated: DateTime.now(), 18 | archived: false); 19 | 20 | class OutlinesModel extends ChangeNotifier { 21 | OutlinesModel(); 22 | 23 | final List outlines = []; 24 | 25 | late DBRepository _dbRepository; 26 | late PlayerModel _playerModel; 27 | late SharedPreferences prefs; 28 | bool isReady = false; 29 | bool showArchived = false; 30 | bool showCompleted = true; 31 | // Not sure where else to put this state 32 | bool allowRetranscription = false; 33 | 34 | Future createOutline(String name, String emoji) async { 35 | final now = DateTime.now(); 36 | final outline = Outline( 37 | name: name, 38 | emoji: emoji, 39 | id: uuid.v4(), 40 | dateCreated: now.toUtc(), 41 | dateUpdated: now.toUtc(), 42 | archived: false); 43 | outlines.insert(0, outline); 44 | notifyListeners(); 45 | await _dbRepository.addOutline(outline); 46 | return outline; 47 | } 48 | 49 | Future deleteOutline(Outline outline) async { 50 | final notes = await _dbRepository.getNotesForOutlineId(outline.id); 51 | for (final n in notes) { 52 | final path = _playerModel.getPathFromFilename(n["file_path"]); 53 | final exists = await File(path).exists(); 54 | if (exists) { 55 | await File(path).delete(); 56 | } 57 | } 58 | await _dbRepository.deleteOutline(outline); 59 | outlines.removeWhere((element) => element.id == outline.id); 60 | notifyListeners(); 61 | } 62 | 63 | Future renameOutline(Outline outline, String name, String emoji) async { 64 | outline.name = name; 65 | outline.emoji = emoji; 66 | notifyListeners(); 67 | await _dbRepository.renameOutline(outline); 68 | } 69 | 70 | Future toggleArchive(Outline outline) async { 71 | outline.archived = !outline.archived; 72 | notifyListeners(); 73 | await _dbRepository.archiveToggleOutline(outline); 74 | } 75 | 76 | Outline getOutlineFromId(String outlineId) => 77 | outlines.firstWhere((element) => element.id == outlineId); 78 | 79 | Future loadOutlines() async { 80 | final outlineResults = await _dbRepository.getOutlines(); 81 | outlines.clear(); 82 | outlines.addAll( 83 | outlineResults.map((Map res) => Outline.fromMap(res))); 84 | outlines.sort((a, b) => b.dateUpdated.compareTo(a.dateUpdated)); 85 | isReady = true; 86 | notifyListeners(); 87 | } 88 | 89 | void toggleShowArchived() { 90 | showArchived = !showArchived; 91 | prefs.setBool(showArchivedKey, showArchived); 92 | notifyListeners(); 93 | } 94 | 95 | void setShowCompleted(bool show) { 96 | showCompleted = show; 97 | prefs.setBool(showCompletedKey, showCompleted); 98 | notifyListeners(); 99 | } 100 | 101 | void setAllowRetranscription(bool allow) { 102 | allowRetranscription = allow; 103 | prefs.setBool(allowRetranscriptionKey, allowRetranscription); 104 | notifyListeners(); 105 | } 106 | 107 | Future load(PlayerModel playerModel, DBRepository db) async { 108 | if (db.ready && !isReady) { 109 | Sentry.addBreadcrumb( 110 | Breadcrumb(message: "Load outlines", timestamp: DateTime.now())); 111 | _dbRepository = db; 112 | _playerModel = playerModel; 113 | prefs = await SharedPreferences.getInstance(); 114 | showArchived = prefs.getBool(showArchivedKey) ?? false; 115 | showCompleted = prefs.getBool(showCompletedKey) ?? true; 116 | allowRetranscription = prefs.getBool(allowRetranscriptionKey) ?? false; 117 | await loadOutlines(); 118 | await tryBackup(); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/state/player_state.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:audio_session/audio_session.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_sound/flutter_sound.dart'; 6 | import 'package:logger/logger.dart'; 7 | import 'package:path_provider/path_provider.dart'; 8 | import 'package:permission_handler/permission_handler.dart'; 9 | import 'package:sentry_flutter/sentry_flutter.dart' as sentry; 10 | import 'package:sentry_flutter/sentry_flutter.dart'; 11 | import 'package:voice_outliner/data/note.dart'; 12 | import 'package:voice_outliner/repositories/ios_speech_recognizer.dart'; 13 | 14 | enum PlayerState { 15 | notLoaded, 16 | noPermission, 17 | notReady, 18 | error, 19 | ready, 20 | playing, 21 | recording, 22 | recordingContinuously 23 | } 24 | 25 | Future getRecordingsDir() async { 26 | final docsDirectory = await getApplicationDocumentsDirectory(); 27 | return Directory("${docsDirectory.path}/recordings"); 28 | } 29 | 30 | class PlayerModel extends ChangeNotifier { 31 | final _player = FlutterSoundPlayer(logLevel: Level.warning); 32 | final _recorder = FlutterSoundRecorder(logLevel: Level.warning); 33 | late Directory recordingsDirectory; 34 | PlayerState _playerState = PlayerState.notLoaded; 35 | Duration currentDuration = const Duration(milliseconds: 0); 36 | AudioSession? _session; 37 | 38 | PlayerState get playerState => _playerState; 39 | set playerState(PlayerState state) { 40 | _playerState = state; 41 | notifyListeners(); 42 | } 43 | 44 | @override 45 | void dispose() { 46 | _recorder.closeRecorder(); 47 | _player.closePlayer(); 48 | super.dispose(); 49 | } 50 | 51 | Future playNote(Note note, onDone) async { 52 | if (note.filePath != null) { 53 | playerState = PlayerState.playing; 54 | await _player.startPlayer( 55 | codec: 56 | note.filePath!.endsWith("aac") ? Codec.aacADTS : Codec.pcm16WAV, 57 | fromURI: getPathFromFilename(note.filePath!), 58 | sampleRate: 44100, 59 | whenFinished: () { 60 | playerState = PlayerState.ready; 61 | onDone(); 62 | }); 63 | } 64 | } 65 | 66 | Future stopPlaying() async { 67 | await _player.stopPlayer(); 68 | playerState = PlayerState.ready; 69 | } 70 | 71 | String getPathFromFilename(String fileName) { 72 | final path = "${recordingsDirectory.path}/$fileName"; 73 | return path; 74 | } 75 | 76 | Future startRecording(Note note) async { 77 | if (note.filePath != null && playerState != PlayerState.recording) { 78 | await _session!.setActive(true); 79 | await _recorder.startRecorder( 80 | codec: Platform.isIOS ? Codec.aacADTS : Codec.pcm16WAV, 81 | toFile: getPathFromFilename(note.filePath!), 82 | sampleRate: 44100, 83 | bitRate: 128000); 84 | // If not already flagged as continuous 85 | if (playerState != PlayerState.recordingContinuously) { 86 | playerState = PlayerState.recording; 87 | } 88 | notifyListeners(); 89 | } 90 | } 91 | 92 | void setContinuousRecording() async { 93 | playerState = PlayerState.recordingContinuously; 94 | notifyListeners(); 95 | } 96 | 97 | Future stopRecording() async { 98 | playerState = PlayerState.ready; 99 | notifyListeners(); 100 | await _recorder.stopRecorder(); 101 | await _session!.setActive(false); 102 | } 103 | 104 | Future tryPermission() async { 105 | final status = await Permission.microphone.request(); 106 | if (status != PermissionStatus.granted) { 107 | return; 108 | } 109 | if (Platform.isIOS) { 110 | final txStatus = await tryTxPermissionIOS(); 111 | if (!txStatus) { 112 | return; 113 | } 114 | } 115 | await load(); 116 | } 117 | 118 | // Attempt to warm up cache. Takes a while to start recorder otherwise. 119 | Future makeDummyRecording() async { 120 | await _session!.setActive(true); 121 | final tempDir = await getTemporaryDirectory(); 122 | await _recorder.startRecorder( 123 | codec: Codec.aacADTS, 124 | toFile: "${tempDir.path}/tmp.aac", 125 | sampleRate: 44100, 126 | bitRate: 128000); 127 | await Future.delayed(const Duration(milliseconds: 200)); 128 | await _recorder.stopRecorder(); 129 | await _session!.setActive(false); 130 | } 131 | 132 | Future load() async { 133 | Sentry.addBreadcrumb( 134 | Breadcrumb(message: "Load player", timestamp: DateTime.now())); 135 | recordingsDirectory = await getRecordingsDir(); 136 | recordingsDirectory.create(recursive: true); 137 | if (playerState == PlayerState.notLoaded) { 138 | playerState = PlayerState.noPermission; 139 | notifyListeners(); 140 | } 141 | final granted = await Permission.microphone.isGranted; 142 | if (granted) { 143 | playerState = PlayerState.notReady; 144 | try { 145 | // Initing recorder before player means bluetooth works properly 146 | await _recorder.openRecorder(); 147 | _recorder.setSubscriptionDuration(const Duration(milliseconds: 100)); 148 | _recorder.onProgress?.listen((event) { 149 | currentDuration = event.duration; 150 | }); 151 | await _player.openPlayer(); 152 | 153 | _session = await AudioSession.instance; 154 | // https://github.com/Canardoux/flutter_sound/issues/868#issuecomment-1081063748 155 | await _session!.configure(AudioSessionConfiguration( 156 | avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, 157 | avAudioSessionCategoryOptions: 158 | AVAudioSessionCategoryOptions.allowBluetooth | 159 | AVAudioSessionCategoryOptions.defaultToSpeaker, 160 | avAudioSessionMode: AVAudioSessionMode.spokenAudio, 161 | avAudioSessionRouteSharingPolicy: 162 | AVAudioSessionRouteSharingPolicy.defaultPolicy, 163 | avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, 164 | androidAudioAttributes: const AndroidAudioAttributes( 165 | contentType: AndroidAudioContentType.speech, 166 | flags: AndroidAudioFlags.none, 167 | usage: AndroidAudioUsage.voiceCommunication, 168 | ), 169 | androidAudioFocusGainType: AndroidAudioFocusGainType.gain, 170 | androidWillPauseWhenDucked: true, 171 | )); 172 | playerState = PlayerState.ready; 173 | notifyListeners(); 174 | await makeDummyRecording(); 175 | } catch (e, st) { 176 | playerState = PlayerState.error; 177 | notifyListeners(); 178 | await sentry.Sentry.captureException(e, stackTrace: st); 179 | } 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /lib/views/ios_transcription_setup_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:voice_outliner/widgets/ios_locale_selector.dart'; 3 | 4 | class IOSTranscriptionSetupView extends StatelessWidget { 5 | const IOSTranscriptionSetupView({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Scaffold( 10 | appBar: AppBar( 11 | title: const Text("Setup"), 12 | automaticallyImplyLeading: false, 13 | ), 14 | body: Padding( 15 | padding: const EdgeInsets.all(20.0), 16 | child: Center( 17 | child: Column(children: [ 18 | const Text( 19 | "Select transcription language", 20 | style: TextStyle(fontSize: 18.0), 21 | ), 22 | const SizedBox(height: 20), 23 | const IOSLocaleSelector(), 24 | const SizedBox(height: 20), 25 | ElevatedButton( 26 | onPressed: () => Navigator.pushNamedAndRemoveUntil( 27 | context, "/", (route) => false), 28 | child: const Text("continue")) 29 | ]))), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/views/map_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_map/flutter_map.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:latlong2/latlong.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | import 'package:voice_outliner/repositories/db_repository.dart'; 8 | import 'package:voice_outliner/views/settings_view.dart'; 9 | 10 | import '../consts.dart'; 11 | import 'notes_view.dart'; 12 | 13 | class MapView extends StatefulWidget { 14 | final String? outlineId; 15 | const MapView({Key? key, this.outlineId}) : super(key: key); 16 | 17 | @override 18 | _MapViewState createState() => _MapViewState(); 19 | } 20 | 21 | class Pin { 22 | final String id; 23 | final String outlineId; 24 | final String label; 25 | final LatLng point; 26 | const Pin( 27 | {required this.outlineId, 28 | required this.label, 29 | required this.point, 30 | required this.id}); 31 | } 32 | 33 | class _MapViewState extends State { 34 | bool loading = true; 35 | List notes = []; 36 | LatLngBounds bounds = LatLngBounds(LatLng(0, 0), LatLng(0, 0)); 37 | LatLng? currentLoc; 38 | bool fitAll = true; 39 | final controller = MapController(); 40 | SharedPreferences? sharedPreferences; 41 | @override 42 | void initState() { 43 | super.initState(); 44 | Future.delayed(Duration.zero, () => loadPins()); 45 | } 46 | 47 | void pushOutline(BuildContext ctx, String outlineId, String noteId) { 48 | Navigator.pushNamedAndRemoveUntil(ctx, "/notes", (_) => false, 49 | arguments: NotesViewArgs(outlineId, scrollToNoteId: noteId)); 50 | } 51 | 52 | Future loadPins() async { 53 | sharedPreferences = await SharedPreferences.getInstance(); 54 | List> results = []; 55 | if (widget.outlineId != null) { 56 | results.addAll(await context 57 | .read() 58 | .getNotesForOutlineId(widget.outlineId!, requireUncomplete: true)); 59 | } else { 60 | results.addAll(await context 61 | .read() 62 | .getAllNotes(requireUncomplete: true)); 63 | } 64 | final filtered = results 65 | .where((element) => element["latitude"] != null) 66 | .map((e) => Pin( 67 | outlineId: e["outline_id"], 68 | id: e["id"], 69 | label: e["transcript"] ?? 70 | DateFormat.yMd().format(DateTime.fromMillisecondsSinceEpoch( 71 | e["date_created"], 72 | isUtc: true)), 73 | point: LatLng(e["latitude"], e["longitude"]))) 74 | .toList() 75 | .reversed 76 | .take(500) 77 | .toList() 78 | .reversed 79 | .toList(); 80 | if (filtered.isNotEmpty) { 81 | setState(() { 82 | notes = filtered; 83 | bounds = LatLngBounds.fromPoints(filtered.map((e) => e.point).toList()); 84 | }); 85 | } 86 | 87 | if (sharedPreferences?.getBool(shouldLocateKey) ?? false) { 88 | final loc = await locationInstance.getLocation(); 89 | final ll = LatLng(loc.latitude!, loc.longitude!); 90 | if (loc.latitude != null && loc.longitude != null) { 91 | setState(() { 92 | currentLoc = ll; 93 | }); 94 | } 95 | } 96 | setState(() { 97 | loading = false; 98 | }); 99 | } 100 | 101 | void toggleFit() { 102 | if (currentLoc == null) { 103 | ScaffoldMessenger.of(context).showSnackBar(const SnackBar( 104 | content: Text("Location unavailable, enable it in settings"))); 105 | } 106 | if (fitAll && currentLoc != null) { 107 | controller.move(currentLoc!, 15.0); 108 | } else { 109 | final cz = controller.centerZoomFitBounds(bounds); 110 | controller.move(cz.center, cz.zoom); 111 | } 112 | setState(() { 113 | fitAll = !fitAll; 114 | }); 115 | } 116 | 117 | void _openSettings() { 118 | Navigator.pop(context); 119 | Navigator.push( 120 | context, 121 | MaterialPageRoute(builder: (_) => const SettingsView()), 122 | ); 123 | } 124 | 125 | @override 126 | Widget build(BuildContext context) { 127 | return Scaffold( 128 | appBar: AppBar( 129 | title: const Text("Map"), 130 | actions: [ 131 | IconButton( 132 | tooltip: fitAll ? "go to current location" : "see all notes", 133 | onPressed: toggleFit, 134 | icon: fitAll 135 | ? const Icon(Icons.gps_fixed) 136 | : const Icon(Icons.map_outlined)) 137 | ], 138 | ), 139 | body: !loading 140 | ? (notes.isNotEmpty 141 | ? FlutterMap( 142 | mapController: controller, 143 | options: MapOptions( 144 | bounds: bounds, 145 | boundsOptions: 146 | const FitBoundsOptions(padding: EdgeInsets.all(8.0)), 147 | interactiveFlags: 148 | InteractiveFlag.all - InteractiveFlag.rotate, 149 | ), 150 | children: [ 151 | TileLayer( 152 | urlTemplate: 153 | "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 154 | subdomains: ['a', 'b', 'c']), 155 | MarkerLayer( 156 | markers: notes 157 | .map((Pin note) => Marker( 158 | point: note.point, 159 | width: 130, 160 | builder: (ctx) => ElevatedButton( 161 | style: ElevatedButton.styleFrom( 162 | backgroundColor: Colors.deepPurpleAccent 163 | .withOpacity(0.5)), 164 | onPressed: () => pushOutline( 165 | ctx, note.outlineId, note.id), 166 | child: Text( 167 | note.label, 168 | overflow: TextOverflow.fade, 169 | )), 170 | key: Key(note.id))) 171 | .toList()) 172 | ], 173 | ) 174 | : (sharedPreferences?.getBool(shouldLocateKey) ?? false 175 | ? const Center(child: Text("No notes have locations")) 176 | : Center( 177 | child: Column( 178 | mainAxisAlignment: MainAxisAlignment.center, 179 | children: [ 180 | const Text("Location attachment is off"), 181 | ElevatedButton( 182 | onPressed: _openSettings, 183 | child: const Text("open settings")) 184 | ], 185 | )))) 186 | : const Center(child: CircularProgressIndicator()), 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /lib/views/onboarding_view.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:introduction_screen/introduction_screen.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | import 'package:voice_outliner/consts.dart'; 8 | import 'package:voice_outliner/state/player_state.dart'; 9 | 10 | import '../globals.dart'; 11 | 12 | class OnboardingView extends StatefulWidget { 13 | const OnboardingView({Key? key}) : super(key: key); 14 | 15 | @override 16 | _OnboardingViewState createState() => _OnboardingViewState(); 17 | } 18 | 19 | class _OnboardingViewState extends State { 20 | SharedPreferences? sharedPreferences; 21 | @override 22 | void initState() { 23 | super.initState(); 24 | Future.delayed(Duration.zero, () async { 25 | sharedPreferences = await SharedPreferences.getInstance(); 26 | setState(() {}); 27 | }); 28 | } 29 | 30 | Future enableLocation() async { 31 | try { 32 | bool permissioned = await locationInstance.requestService(); 33 | if (!permissioned) { 34 | ScaffoldMessenger.of(context).showSnackBar( 35 | const SnackBar(content: Text("Couldn't get location"))); 36 | return; 37 | } 38 | final testLoc = await locationInstance.getLocation(); 39 | if (testLoc.latitude == null) { 40 | ScaffoldMessenger.of(context).showSnackBar( 41 | const SnackBar(content: Text("Couldn't get location"))); 42 | return; 43 | } 44 | setState(() { 45 | sharedPreferences?.setBool(shouldLocateKey, true); 46 | }); 47 | } catch (e) { 48 | snackbarKey.currentState?.showSnackBar( 49 | SnackBar(content: Text("Could not get location: ${e.toString()}"))); 50 | print(e); 51 | ScaffoldMessenger.of(context) 52 | .showSnackBar(const SnackBar(content: Text("Couldn't get location"))); 53 | } 54 | } 55 | 56 | void onDone() { 57 | Navigator.pushNamedAndRemoveUntil( 58 | context, 59 | Platform.isIOS 60 | ? "/transcription_setup_ios" 61 | : "/transcription_setup_vosk", 62 | (_) => false); 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | bool playerReady = context.select( 68 | (value) => value.playerState == PlayerState.ready); 69 | bool locationOn = sharedPreferences?.getBool(shouldLocateKey) ?? false; 70 | return IntroductionScreen( 71 | dotsDecorator: const DotsDecorator(activeColor: classicPurple), 72 | showDoneButton: true, 73 | showNextButton: true, 74 | next: Text( 75 | "Next", 76 | style: TextStyle(color: Theme.of(context).colorScheme.onSurface), 77 | ), 78 | done: Text( 79 | "Done", 80 | style: TextStyle(color: Theme.of(context).colorScheme.onSurface), 81 | ), 82 | onDone: onDone, 83 | pages: [ 84 | PageViewModel( 85 | title: "Welcome to Voiceliner", 86 | bodyWidget: Column(children: [ 87 | const Text( 88 | "Record your voice as structured notes.", 89 | style: TextStyle(fontSize: 18), 90 | textAlign: TextAlign.center, 91 | ), 92 | const SizedBox(height: 15), 93 | const Text( 94 | "Auto-transcribe them into an outline.", 95 | style: TextStyle(fontSize: 18), 96 | textAlign: TextAlign.center, 97 | ), 98 | const SizedBox(height: 15), 99 | ElevatedButton( 100 | onPressed: playerReady 101 | ? null 102 | : () { 103 | context.read().tryPermission(); 104 | }, 105 | child: Text( 106 | playerReady ? "all set!" : "grant microphone access")) 107 | ]), 108 | image: Center( 109 | child: Image.asset( 110 | "assets/onboarding/1.png", 111 | ))), 112 | PageViewModel( 113 | image: Center( 114 | child: Image.asset( 115 | "assets/onboarding/2.png", 116 | )), 117 | title: "Tap and hold the microphone to record notes", 118 | bodyWidget: Column(children: const [ 119 | Text( 120 | "While holding, drag up to change the temperature (color) of the note.", 121 | textAlign: TextAlign.center, 122 | style: TextStyle(fontSize: 18), 123 | ), 124 | SizedBox(height: 15), 125 | Text( 126 | "Tap and hold anywhere else to create a text-only note.", 127 | textAlign: TextAlign.center, 128 | style: TextStyle(fontSize: 18), 129 | ), 130 | ]), 131 | ), 132 | PageViewModel( 133 | title: "Swipe left or right to indent notes", 134 | body: "Drag them to reorder.", 135 | image: Center( 136 | child: Image.asset( 137 | "assets/onboarding/3.png", 138 | ))), 139 | PageViewModel( 140 | title: "See where you took notes", 141 | bodyWidget: Column(children: [ 142 | const Text( 143 | "Attach your location, situate memories.", 144 | textAlign: TextAlign.center, 145 | style: TextStyle(fontSize: 18), 146 | ), 147 | const SizedBox(height: 15), 148 | ElevatedButton( 149 | onPressed: locationOn ? null : enableLocation, 150 | child: Text(locationOn ? "all set!" : "enable location")) 151 | ]), 152 | image: Center( 153 | child: Image.asset( 154 | "assets/onboarding/4.png", 155 | ))), 156 | PageViewModel( 157 | title: "Organize your notes into outlines", 158 | bodyWidget: Column(children: [ 159 | const Text( 160 | "Set an emoji for each one.", 161 | textAlign: TextAlign.center, 162 | style: TextStyle(fontSize: 18), 163 | ), 164 | const SizedBox(height: 10), 165 | ElevatedButton(onPressed: onDone, child: const Text("Let's go!")) 166 | ]), 167 | image: Center( 168 | child: Image.asset( 169 | "assets/onboarding/5.png", 170 | ))) 171 | ], 172 | ); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/views/timeline_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:voice_outliner/data/note.dart'; 5 | import 'package:voice_outliner/repositories/db_repository.dart'; 6 | import 'package:voice_outliner/state/notes_state.dart'; 7 | import 'package:voice_outliner/views/map_view.dart'; 8 | import 'package:voice_outliner/widgets/result_note.dart'; 9 | 10 | class TimelineView extends StatefulWidget { 11 | const TimelineView({Key? key}) : super(key: key); 12 | 13 | @override 14 | _TimelineViewState createState() => _TimelineViewState(); 15 | } 16 | 17 | class _TimelineViewState extends State { 18 | int? numNotes; 19 | @override 20 | void initState() { 21 | super.initState(); 22 | Future.delayed(Duration.zero, _init); 23 | } 24 | 25 | Future _init() async { 26 | final count = await context.read().getNotesCount(); 27 | setState(() { 28 | numNotes = count; 29 | }); 30 | } 31 | 32 | Future _retrieveNote(int index) async { 33 | return await context.read().getNoteAt(index); 34 | } 35 | 36 | Widget _renderItem(BuildContext ctx, Note note) { 37 | return Container( 38 | margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 10), 39 | child: Column( 40 | crossAxisAlignment: CrossAxisAlignment.start, 41 | children: [ 42 | Container( 43 | padding: const EdgeInsets.only(bottom: 5, left: 10), 44 | child: Text( 45 | DateFormat.yMd().add_jm().format(note.dateCreated.toLocal()), 46 | style: 47 | TextStyle(fontSize: 14, color: Theme.of(ctx).hintColor), 48 | )), 49 | ResultNote( 50 | note: note, 51 | truncate: true, 52 | ) 53 | ], 54 | )); 55 | } 56 | 57 | Widget _buildItem(BuildContext context, int index) { 58 | return FutureBuilder( 59 | key: Key("i-$index"), 60 | builder: (ctx, snapshot) { 61 | if (snapshot.hasData && snapshot.data is Note) { 62 | final data = snapshot.data as Note; 63 | return _renderItem(ctx, data); 64 | } else { 65 | return _renderItem(ctx, defaultNote); 66 | } 67 | }, 68 | future: _retrieveNote(index), 69 | ); 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return Scaffold( 75 | appBar: AppBar( 76 | title: const Text("Timeline"), 77 | actions: [ 78 | IconButton( 79 | tooltip: "map", 80 | icon: const Icon(Icons.map), 81 | onPressed: () => Navigator.push( 82 | context, MaterialPageRoute(builder: (_) => const MapView())), 83 | ), 84 | ], 85 | ), 86 | body: numNotes == null 87 | ? Center( 88 | child: Text( 89 | "loading...", 90 | style: 91 | TextStyle(fontSize: 24, color: Theme.of(context).hintColor), 92 | )) 93 | : numNotes == 0 94 | ? Center( 95 | child: Text( 96 | "no notes yet!", 97 | style: TextStyle( 98 | fontSize: 24, color: Theme.of(context).hintColor), 99 | )) 100 | : Scrollbar( 101 | child: ListView.builder( 102 | padding: const EdgeInsets.only(bottom: 50, top: 20), 103 | reverse: true, 104 | shrinkWrap: true, 105 | prototypeItem: _renderItem(context, defaultNote), 106 | itemCount: numNotes, 107 | itemBuilder: _buildItem))); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/widgets/ios_locale_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | import 'package:voice_outliner/consts.dart'; 4 | import 'package:voice_outliner/repositories/ios_speech_recognizer.dart'; 5 | 6 | class IOSLocaleSelector extends StatefulWidget { 7 | const IOSLocaleSelector({Key? key}) : super(key: key); 8 | 9 | @override 10 | _IOSLocaleSelectorState createState() => _IOSLocaleSelectorState(); 11 | } 12 | 13 | class _IOSLocaleSelectorState extends State { 14 | late SharedPreferences sharedPreferences; 15 | bool isInited = false; 16 | Map locales = {}; 17 | @override 18 | void initState() { 19 | super.initState(); 20 | init(); 21 | } 22 | 23 | Future init() async { 24 | sharedPreferences = await SharedPreferences.getInstance(); 25 | locales = await getLocaleOptions(); 26 | setState(() { 27 | // Initiate side effect so if the user doesn't touch the dropdown, it's saved as en-US 28 | if (sharedPreferences.getString(localeKey) == null) { 29 | sharedPreferences.setString(localeKey, "en-US"); 30 | } 31 | isInited = true; 32 | }); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | if (!isInited) { 38 | return const Text("loading locales..."); 39 | } 40 | final items = locales.entries 41 | .map((entry) => 42 | DropdownMenuItem(value: entry.key, child: Text(entry.value))) 43 | .toList(growable: false); 44 | items.sort((a, b) => a.value!.compareTo(b.value!)); 45 | return DropdownButton( 46 | icon: const Icon(Icons.language), 47 | menuMaxHeight: 300, 48 | isExpanded: true, 49 | items: items, 50 | value: sharedPreferences.getString(localeKey) ?? "en-US", 51 | onChanged: (v) { 52 | setState(() { 53 | sharedPreferences.setString(localeKey, v!); 54 | }); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/widgets/markdown_exporter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:intl/intl.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | import 'package:provider/provider.dart'; 7 | import 'package:share_plus/share_plus.dart'; 8 | import 'package:voice_outliner/data/outline.dart'; 9 | import 'package:voice_outliner/state/notes_state.dart'; 10 | import 'package:voice_outliner/state/player_state.dart'; 11 | 12 | class _MarkdownExporter extends StatefulWidget { 13 | final Outline outline; 14 | final NotesModel notesModel; 15 | const _MarkdownExporter( 16 | {Key? key, required this.outline, required this.notesModel}) 17 | : super(key: key); 18 | 19 | @override 20 | _MarkdownExporterState createState() => _MarkdownExporterState(); 21 | } 22 | 23 | class _MarkdownExporterState extends State<_MarkdownExporter> { 24 | bool isReady = false; 25 | bool toFile = false; 26 | bool exportAudio = false; 27 | bool linkAudio = false; 28 | bool includeCheckboxes = false; 29 | bool includeDate = false; 30 | late PlayerModel _playerModel; 31 | 32 | Future _export() async { 33 | final tempDir = await getTemporaryDirectory(); 34 | var contents = "# ${widget.outline.name} \n"; 35 | final notesModel = widget.notesModel; 36 | final List filePaths = []; 37 | final List mimeTypes = []; 38 | for (var n in notesModel.notes) { 39 | var line = "- "; 40 | if (includeCheckboxes) { 41 | line += (n.isComplete ? "[x] " : "[ ] "); 42 | } 43 | 44 | if (linkAudio) { 45 | line += 46 | "[${n.transcript ?? n.infoString}](./${n.id}.${Platform.isIOS ? 'aac' : 'wav'})"; 47 | } else { 48 | line += n.transcript ?? n.infoString; 49 | } 50 | if (includeDate && n.transcript != null) { 51 | line += 52 | " (${DateFormat.yMd().add_jm().format(n.dateCreated.toLocal())})"; 53 | } 54 | line += "\n"; 55 | line = line.padLeft(line.length + 4 * notesModel.getDepth(n), " "); 56 | contents += line; 57 | } 58 | if (toFile) { 59 | final mdFile = File( 60 | "${tempDir.path}/${Uri.encodeFull(widget.outline.name.replaceAll("/", "-"))}.md"); 61 | await mdFile.writeAsString(contents); 62 | 63 | filePaths.add(mdFile.path); 64 | mimeTypes.add('text/markdown'); 65 | 66 | if (exportAudio) { 67 | for (var n in notesModel.notes) { 68 | if (n.filePath != null) { 69 | final filePath = 70 | _playerModel.getPathFromFilename(n.filePath as String); 71 | 72 | filePaths.add(filePath); 73 | mimeTypes.add("audio/aac"); 74 | } 75 | } 76 | } 77 | 78 | await Share.shareFiles( 79 | filePaths, 80 | mimeTypes: mimeTypes, 81 | ); 82 | } else { 83 | Share.share(contents, subject: widget.outline.name); 84 | } 85 | } 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | _playerModel = Provider.of(context); 90 | return AlertDialog( 91 | title: Text("Export ${widget.outline.name}"), 92 | content: Column( 93 | mainAxisSize: MainAxisSize.min, 94 | children: [ 95 | CheckboxListTile( 96 | value: toFile, 97 | title: const Text("as .md file"), 98 | onChanged: (v) => setState(() { 99 | toFile = v ?? false; 100 | if (toFile == false) { 101 | exportAudio = false; 102 | linkAudio = false; 103 | } 104 | })), 105 | CheckboxListTile( 106 | value: includeCheckboxes, 107 | title: const Text("with checkboxes"), 108 | onChanged: (v) => setState(() { 109 | includeCheckboxes = v ?? false; 110 | })), 111 | CheckboxListTile( 112 | value: includeDate, 113 | title: const Text("with datestamps"), 114 | onChanged: (v) => setState(() { 115 | includeDate = v ?? false; 116 | })), 117 | Visibility( 118 | visible: toFile, 119 | child: (CheckboxListTile( 120 | value: exportAudio, 121 | title: const Text("include audio files"), 122 | onChanged: (v) => setState(() { 123 | exportAudio = v ?? false; 124 | if (exportAudio == false) { 125 | linkAudio = false; 126 | } 127 | })))), 128 | Visibility( 129 | visible: exportAudio, 130 | child: (CheckboxListTile( 131 | value: linkAudio, 132 | title: const Text("link audio files in .md"), 133 | onChanged: (v) => setState(() { 134 | linkAudio = v ?? false; 135 | if (linkAudio == true) { 136 | exportAudio = true; 137 | toFile = true; 138 | } 139 | })))) 140 | ], 141 | ), 142 | actions: [ 143 | TextButton( 144 | onPressed: () async { 145 | Navigator.of(context).pop(); 146 | }, 147 | child: Text( 148 | "cancel", 149 | style: 150 | TextStyle(color: Theme.of(context).colorScheme.onSurface), 151 | )), 152 | TextButton( 153 | onPressed: () async { 154 | Navigator.of(context).pop(); 155 | await _export(); 156 | }, 157 | child: Text( 158 | "export", 159 | style: 160 | TextStyle(color: Theme.of(context).colorScheme.onSurface), 161 | )) 162 | ]); 163 | } 164 | 165 | Future load(PlayerModel playerModel) async { 166 | if (!isReady) { 167 | _playerModel = playerModel; 168 | isReady = true; 169 | } 170 | } 171 | } 172 | 173 | Future exportMarkdown( 174 | BuildContext context, Outline outline, NotesModel notesModel) async { 175 | await showDialog( 176 | barrierDismissible: false, 177 | context: context, 178 | builder: (_) => 179 | _MarkdownExporter(outline: outline, notesModel: notesModel)); 180 | } 181 | -------------------------------------------------------------------------------- /lib/widgets/note_wizard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:voice_outliner/widgets/note_item.dart'; 3 | 4 | class NoteWizard extends StatefulWidget { 5 | final String initialTranscript; 6 | final int initialColor; 7 | final Function(String transcript, int color) onSubmit; 8 | final String title; 9 | const NoteWizard( 10 | {Key? key, 11 | required this.initialColor, 12 | required this.initialTranscript, 13 | required this.onSubmit, 14 | this.title = "Edit Note"}) 15 | : super(key: key); 16 | 17 | @override 18 | _NoteWizardState createState() => _NoteWizardState(); 19 | } 20 | 21 | class _NoteWizardState extends State { 22 | final TextEditingController _textController = TextEditingController(); 23 | double color = 0; 24 | Future _onSubmitted() async { 25 | if (_textController.value.text.isNotEmpty) { 26 | widget.onSubmit(_textController.value.text, color.toInt()); 27 | Navigator.of(context, rootNavigator: true).pop(); 28 | } else { 29 | ScaffoldMessenger.of(context) 30 | .showSnackBar(const SnackBar(content: Text("Note is empty"))); 31 | } 32 | } 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | color = widget.initialColor.toDouble().abs(); 38 | _textController.text = widget.initialTranscript; 39 | _textController.selection = TextSelection( 40 | baseOffset: 0, extentOffset: _textController.value.text.length); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | _textController.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | return AlertDialog( 52 | title: Text(widget.title), 53 | content: Column( 54 | mainAxisSize: MainAxisSize.min, 55 | children: [ 56 | TextField( 57 | maxLines: 10, 58 | minLines: 1, 59 | decoration: const InputDecoration(hintText: "Transcript"), 60 | controller: _textController, 61 | autofocus: true, 62 | autocorrect: true, 63 | onSubmitted: (_) => _onSubmitted(), 64 | textCapitalization: TextCapitalization.sentences), 65 | const SizedBox(height: 15), 66 | Slider( 67 | label: "temperature", 68 | semanticFormatterCallback: (c) => "temperature $c/100", 69 | min: 0, 70 | max: 100, 71 | divisions: 100, 72 | activeColor: computeColor(color.toInt()), 73 | value: color, 74 | onChanged: (v) { 75 | setState(() { 76 | color = v; 77 | }); 78 | }) 79 | ], 80 | ), 81 | actions: [ 82 | TextButton( 83 | child: Text( 84 | "cancel", 85 | style: 86 | TextStyle(color: Theme.of(context).colorScheme.onSurface), 87 | ), 88 | onPressed: () { 89 | Navigator.of(context, rootNavigator: true).pop(); 90 | }), 91 | TextButton( 92 | child: Text( 93 | "save", 94 | style: 95 | TextStyle(color: Theme.of(context).colorScheme.onSurface), 96 | ), 97 | onPressed: () => _onSubmitted()) 98 | ]); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/widgets/outline_wizard.dart: -------------------------------------------------------------------------------- 1 | import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class OutlineWizard extends StatefulWidget { 5 | final String name; 6 | final String emoji; 7 | final String confirm; 8 | final bool autofocus; 9 | final String title; 10 | final Function(String name, String emoji) onSubmit; 11 | const OutlineWizard( 12 | {Key? key, 13 | required this.name, 14 | required this.emoji, 15 | required this.confirm, 16 | required this.onSubmit, 17 | this.autofocus = false, 18 | this.title = "Create Outline"}) 19 | : super(key: key); 20 | 21 | @override 22 | _OutlineWizardState createState() => _OutlineWizardState(); 23 | } 24 | 25 | class _OutlineWizardState extends State { 26 | final _renameController = TextEditingController(); 27 | final _emojiController = TextEditingController(); 28 | String emoji = ""; 29 | bool showEmojiEditor = false; 30 | @override 31 | void dispose() { 32 | super.dispose(); 33 | _renameController.dispose(); 34 | } 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | _renameController.text = widget.name; 40 | if (widget.autofocus) { 41 | _renameController.selection = TextSelection( 42 | baseOffset: 0, extentOffset: _renameController.value.text.length); 43 | } 44 | setState(() { 45 | emoji = widget.emoji; 46 | }); 47 | } 48 | 49 | Future _onSubmitted(BuildContext ctx) async { 50 | if (_renameController.value.text.isNotEmpty) { 51 | widget.onSubmit(_renameController.value.text, emoji); 52 | Navigator.of(ctx, rootNavigator: true).pop(); 53 | } else { 54 | ScaffoldMessenger.of(ctx) 55 | .showSnackBar(const SnackBar(content: Text("Note is empty"))); 56 | } 57 | } 58 | 59 | _selectEmoji(Category? c, Emoji e) { 60 | setState(() { 61 | emoji = e.emoji; 62 | showEmojiEditor = false; 63 | }); 64 | } 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | return AlertDialog( 69 | title: Text(widget.title), 70 | content: showEmojiEditor 71 | ? Column(mainAxisSize: MainAxisSize.min, children: [ 72 | SizedBox( 73 | height: 250, 74 | width: 300, 75 | child: EmojiPicker( 76 | onEmojiSelected: _selectEmoji, 77 | config: Config(bgColor: Theme.of(context).cardColor), 78 | )) 79 | ]) 80 | : Column(mainAxisSize: MainAxisSize.min, children: [ 81 | IconButton( 82 | iconSize: 40, 83 | onPressed: () => setState(() { 84 | showEmojiEditor = true; 85 | }), 86 | icon: Text( 87 | emoji, 88 | style: const TextStyle(fontSize: 30), 89 | )), 90 | TextField( 91 | decoration: 92 | const InputDecoration(hintText: "Outline Title"), 93 | controller: _renameController, 94 | autofocus: widget.autofocus, 95 | autocorrect: true, 96 | onSubmitted: (_) => _onSubmitted(context), 97 | textCapitalization: TextCapitalization.words) 98 | ]), 99 | actions: [ 100 | TextButton( 101 | child: Text("cancel", 102 | style: TextStyle( 103 | color: Theme.of(context).colorScheme.onSurface)), 104 | onPressed: () { 105 | Navigator.of(context, rootNavigator: true).pop(); 106 | }), 107 | TextButton( 108 | child: Text(widget.confirm, 109 | style: TextStyle( 110 | color: Theme.of(context).colorScheme.onSurface)), 111 | onPressed: () => _onSubmitted(context)) 112 | ]); 113 | } 114 | } 115 | 116 | Future launchOutlineWizard( 117 | String name, 118 | String emoji, 119 | BuildContext context, 120 | String confirm, 121 | String title, 122 | Function(String name, String emoji) onSubmit, 123 | {bool autofocus = false}) async { 124 | await showDialog( 125 | barrierDismissible: false, 126 | context: context, 127 | builder: (dialogCtx) => OutlineWizard( 128 | name: name, 129 | emoji: emoji, 130 | confirm: confirm, 131 | onSubmit: onSubmit, 132 | autofocus: autofocus, 133 | title: title, 134 | )); 135 | } 136 | -------------------------------------------------------------------------------- /lib/widgets/outlines_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:timeago_flutter/timeago_flutter.dart'; 4 | import 'package:voice_outliner/data/outline.dart'; 5 | import 'package:voice_outliner/state/outline_state.dart'; 6 | 7 | import '../consts.dart'; 8 | 9 | class OutlinesList extends StatelessWidget { 10 | final Function(String) onTap; 11 | final bool showArchived; 12 | final String? excludeItem; 13 | const OutlinesList( 14 | {Key? key, 15 | required this.onTap, 16 | this.showArchived = false, 17 | this.excludeItem}) 18 | : super(key: key); 19 | 20 | Widget _buildOutline(BuildContext ctx, int num) { 21 | return Builder(builder: (ct) { 22 | final outline = ct.select((value) => 23 | value.outlines.length > num ? value.outlines[num] : defaultOutline); 24 | if (outline.id == excludeItem || (!showArchived && outline.archived)) { 25 | return const SizedBox(height: 0); 26 | } 27 | return ListTile( 28 | contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 17), 29 | key: Key("outline-$num"), 30 | leading: outline.archived 31 | ? const Icon(Icons.archive) 32 | : CircleAvatar( 33 | child: Text( 34 | outline.emoji, 35 | textScaleFactor: 1.5, 36 | ), 37 | backgroundColor: classicPurple.withOpacity(0.2), 38 | ), 39 | tileColor: 40 | outline.archived ? const Color.fromRGBO(175, 175, 175, 0.1) : null, 41 | trailing: const Icon(Icons.chevron_right), 42 | title: Text(outline.name), 43 | subtitle: 44 | Timeago(builder: (_, t) => Text(t), date: outline.dateUpdated), 45 | onTap: () { 46 | onTap(outline.id); 47 | }, 48 | ); 49 | }); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | final numOutlines = 55 | context.select((value) => value.outlines.length); 56 | return Scrollbar( 57 | child: ListView.builder( 58 | padding: EdgeInsets.zero, 59 | shrinkWrap: true, 60 | itemCount: numOutlines, 61 | itemBuilder: _buildOutline)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/widgets/record_button.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_vibrate/flutter_vibrate.dart'; 6 | import 'package:provider/provider.dart'; 7 | import 'package:voice_outliner/consts.dart'; 8 | import 'package:voice_outliner/state/notes_state.dart'; 9 | import 'package:voice_outliner/state/player_state.dart'; 10 | 11 | /// How long does the user have to hold down for it to be considered a "hold"? 12 | const holdThreshold = 500; 13 | 14 | class RecordButton extends StatefulWidget { 15 | const RecordButton({Key? key}) : super(key: key); 16 | 17 | @override 18 | _RecordButtonState createState() => _RecordButtonState(); 19 | } 20 | 21 | class _RecordButtonState extends State { 22 | Offset offset = const Offset(0, 0); 23 | bool inCancelZone = false; 24 | 25 | Color computeShadowColor(double dy) { 26 | Color a = const Color.fromRGBO(169, 129, 234, 0.6); 27 | Color b = const Color.fromRGBO(248, 82, 150, 0.6); 28 | double t = (dy.abs() / MediaQuery.of(context).size.height); 29 | return Color.lerp(a, b, t)!; 30 | } 31 | 32 | _stopRecord(_) async { 33 | if (inCancelZone) { 34 | context.read().cancelRecording(); 35 | setState(() { 36 | inCancelZone = false; 37 | }); 38 | } else { 39 | int magnitude = max( 40 | ((-1 * offset.dy / MediaQuery.of(context).size.height) * 100).toInt(), 41 | 0); 42 | await context.read().stopRecording(magnitude); 43 | } 44 | } 45 | 46 | _stopRecord0() { 47 | _stopRecord(null); 48 | } 49 | 50 | // Treat record button as stop/start 51 | _tappedUp(_) { 52 | final playerState = context.read().playerState; 53 | if (playerState == PlayerState.recordingContinuously) { 54 | context.read().stopRecording(0); 55 | } else { 56 | context.read().setContinuousRecording(); 57 | } 58 | } 59 | 60 | _startRecord(_) async { 61 | final playerState = context.read().playerState; 62 | if (playerState == PlayerState.ready) { 63 | await context.read().startRecording(); 64 | } 65 | } 66 | 67 | _playEffect(LongPressDownDetails d) { 68 | Vibrate.feedback(FeedbackType.impact); 69 | setState(() { 70 | offset = d.localPosition; 71 | }); 72 | } 73 | 74 | _updateOffset(Offset localPosition, Offset globalPosition) { 75 | final screenWidth = MediaQuery.of(context).size.width; 76 | final deletionMargin = screenWidth * 0.05; 77 | setState(() { 78 | offset = localPosition; 79 | inCancelZone = globalPosition.dx < deletionMargin || 80 | globalPosition.dx > screenWidth - deletionMargin; 81 | }); 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | final playerState = 87 | context.select((p) => p.playerState); 88 | return GestureDetector( 89 | // For both taps and holds 90 | onTapDown: _startRecord, 91 | // For taps to do continuous recording 92 | onTapUp: _tappedUp, 93 | // Lets you play the vibrate, for some reason can't do it on tap down 94 | onLongPressDown: _playEffect, 95 | // For slow releases 96 | onLongPressUp: _stopRecord0, 97 | // For release of fast swipes 98 | onPanEnd: _stopRecord, 99 | // For slow swipes up 100 | onLongPressMoveUpdate: (LongPressMoveUpdateDetails d) { 101 | _updateOffset(d.localPosition, d.globalPosition); 102 | }, 103 | // For fast swipes up 104 | onPanUpdate: (DragUpdateDetails d) { 105 | _updateOffset(d.localPosition, d.globalPosition); 106 | }, 107 | child: AnimatedOpacity( 108 | duration: const Duration(milliseconds: 100), 109 | opacity: playerState == PlayerState.ready || 110 | playerState == PlayerState.recording || 111 | playerState == PlayerState.recordingContinuously 112 | ? 1.0 113 | : 0.0, 114 | child: AnimatedContainer( 115 | width: 200, 116 | height: 75, 117 | curve: Curves.bounceInOut, 118 | duration: const Duration(milliseconds: 100), 119 | decoration: BoxDecoration( 120 | boxShadow: [ 121 | BoxShadow( 122 | color: inCancelZone 123 | ? warningRed 124 | : playerState == PlayerState.recordingContinuously 125 | ? warmRed.withOpacity(0.5) 126 | : const Color.fromRGBO(156, 103, 241, .36), 127 | blurRadius: 18.0, 128 | spreadRadius: 0.0, 129 | offset: const Offset(0, 7)), 130 | if (playerState == PlayerState.recording) 131 | BoxShadow( 132 | color: inCancelZone 133 | ? warningRed 134 | : computeShadowColor(offset.dy), 135 | blurRadius: 120.0, 136 | spreadRadius: 120.0, 137 | offset: offset + const Offset(-100, -95)) 138 | ], 139 | borderRadius: BorderRadius.circular(100.0), 140 | color: inCancelZone 141 | ? warningRed 142 | : playerState == PlayerState.recordingContinuously 143 | ? warmRed 144 | : classicPurple, 145 | ), 146 | child: Center( 147 | child: Row( 148 | mainAxisAlignment: MainAxisAlignment.center, 149 | children: [ 150 | if (playerState != PlayerState.recording) ...[ 151 | Icon( 152 | playerState == PlayerState.recordingContinuously 153 | ? Icons.stop 154 | : Icons.mic, 155 | color: Colors.white), 156 | const SizedBox( 157 | width: 10.0, 158 | ) 159 | ], 160 | Text( 161 | playerState == PlayerState.recordingContinuously 162 | ? "tap to stop" 163 | : playerState == PlayerState.recording 164 | ? (inCancelZone 165 | ? "release to cancel" 166 | : "recording") 167 | : "hold to record", 168 | style: 169 | const TextStyle(color: Colors.white, fontSize: 15.0), 170 | ) 171 | ]))), 172 | )); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/widgets/result_note.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/src/provider.dart'; 3 | import 'package:voice_outliner/data/note.dart'; 4 | import 'package:voice_outliner/state/player_state.dart'; 5 | import 'package:voice_outliner/views/notes_view.dart'; 6 | 7 | import '../consts.dart'; 8 | import 'note_item.dart'; 9 | 10 | class ResultNote extends StatefulWidget { 11 | final Note note; 12 | final bool truncate; 13 | const ResultNote({Key? key, required this.note, this.truncate = false}) 14 | : super(key: key); 15 | 16 | @override 17 | _ResultNoteState createState() => _ResultNoteState(); 18 | } 19 | 20 | class _ResultNoteState extends State { 21 | bool playing = false; 22 | @override 23 | Widget build(BuildContext context) { 24 | return Card( 25 | elevation: 0, 26 | clipBehavior: Clip.hardEdge, 27 | shape: 28 | RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)), 29 | color: widget.note.isComplete 30 | ? const Color.fromRGBO(229, 229, 229, 0.3) 31 | : computeColor(widget.note.color).withOpacity(0.2), 32 | margin: const EdgeInsets.all(5.0), 33 | child: ListTile( 34 | leading: widget.note.filePath != null 35 | ? IconButton( 36 | tooltip: "play note", 37 | padding: EdgeInsets.zero, 38 | constraints: const BoxConstraints(), 39 | onPressed: () { 40 | final playerModel = context.read(); 41 | if (playing) { 42 | setState(() { 43 | playing = false; 44 | }); 45 | playerModel.stopPlaying(); 46 | } else { 47 | setState(() { 48 | playing = true; 49 | }); 50 | playerModel.playNote(widget.note, () { 51 | setState(() { 52 | playing = false; 53 | }); 54 | }); 55 | } 56 | }, 57 | color: classicPurple, 58 | icon: playing 59 | ? const Icon(Icons.stop_circle_outlined) 60 | : const Icon(Icons.play_circle)) 61 | : const Icon( 62 | Icons.text_fields, 63 | semanticLabel: "text note", 64 | color: classicPurple, 65 | ), 66 | onTap: () => Navigator.pushNamed(context, "/notes", 67 | arguments: NotesViewArgs(widget.note.outlineId, 68 | scrollToNoteId: widget.note.id)), 69 | title: Text( 70 | widget.note.transcript ?? widget.note.infoString, 71 | overflow: TextOverflow.ellipsis, 72 | style: TextStyle( 73 | color: Theme.of(context).colorScheme.onSurface, 74 | decoration: widget.note.isComplete 75 | ? TextDecoration.lineThrough 76 | : null), 77 | ))); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/widgets/search_results_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:voice_outliner/data/note.dart'; 3 | import 'package:voice_outliner/data/outline.dart'; 4 | import 'package:voice_outliner/views/notes_view.dart'; 5 | import 'package:voice_outliner/widgets/result_note.dart'; 6 | 7 | class GroupedResult { 8 | final List notes; 9 | final Outline outline; 10 | GroupedResult(this.outline, this.notes); 11 | } 12 | 13 | class ResultGroup extends StatelessWidget { 14 | final GroupedResult groupedResult; 15 | const ResultGroup({Key? key, required this.groupedResult}) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Card( 20 | margin: const EdgeInsets.all(10), 21 | child: ListTile( 22 | onTap: () => Navigator.pushNamed(context, "/notes", 23 | arguments: NotesViewArgs(groupedResult.outline.id)), 24 | title: Text(groupedResult.outline.name), 25 | subtitle: Column( 26 | children: groupedResult.notes 27 | .map((e) => ResultNote(note: e)) 28 | .toList(growable: false))), 29 | ); 30 | } 31 | } 32 | 33 | class SearchResultsList extends StatelessWidget { 34 | final List searchResults; 35 | // instead do search results 36 | const SearchResultsList({Key? key, required this.searchResults}) 37 | : super(key: key); 38 | 39 | void _removeFocus(BuildContext context) { 40 | final currentFocus = FocusScope.of(context); 41 | if (!currentFocus.hasPrimaryFocus) { 42 | currentFocus.unfocus(); 43 | } 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | //FutureProvider thing - await result of query and show spinner 49 | if (searchResults.isEmpty) { 50 | return Center( 51 | child: Text( 52 | "no results", 53 | style: TextStyle(fontSize: 24, color: Theme.of(context).hintColor), 54 | )); 55 | } 56 | // HACK to hide keyboard https://stackoverflow.com/questions/51652897/how-to-hide-soft-input-keyboard-on-flutter-after-clicking-outside-textfield-anyw 57 | // TODO: make scrollbar work with this 58 | return GestureDetector( 59 | behavior: HitTestBehavior.opaque, 60 | onPanStart: (_) => _removeFocus(context), 61 | onTapDown: (_) => _removeFocus(context), 62 | child: ListView( 63 | shrinkWrap: true, 64 | children: searchResults 65 | .map((e) => ResultGroup(groupedResult: e)) 66 | .toList(growable: false), 67 | )); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: voice_outliner 2 | description: Braindump better. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.27.4+114 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | 34 | # The following adds the Cupertino Icons font to your application. 35 | # Use with the CupertinoIcons class for iOS style icons. 36 | cupertino_icons: ^1.0.2 37 | permission_handler: ^10.0.0 38 | flutter_sound: ^9.2.13 39 | sqflite: ^2.0.0+4 40 | path_provider: ^2.0.3 41 | uuid: ^3.0.4 42 | intl: ^0.18.1 43 | timeago_flutter: ^3.5.0 44 | shared_preferences: ^2.0.7 45 | sentry_flutter: ^7.9.0 46 | url_launcher: ^6.0.12 47 | share_plus: ^7.1.0 48 | provider: ^6.0.1 49 | tuple: ^2.0.0 50 | google_sign_in: ^6.1.4 51 | googleapis: ^11.3.0 52 | extension_google_sign_in_as_googleapis_auth: ^2.0.4 53 | filesize: ^2.0.1 54 | connectivity_plus: ^4.0.2 55 | flutter_archive: ^5.0.0 56 | location: ^5.0.3 57 | maps_launcher: ^2.0.1 58 | flutter_map: ^4.0.0 59 | scroll_to_index: ^3.0.1 60 | flutter_vibrate: ^1.3.0 61 | http: ^0.13.4 62 | introduction_screen: ^3.0.2 63 | sticky_headers: ^0.3.0+2 64 | audio_session: ^0.1.10 65 | flutter_dotenv: ^5.0.2 66 | settings_ui: ^2.0.2 67 | emoji_picker_flutter: ^1.5.0 68 | 69 | dev_dependencies: 70 | flutter_launcher_icons: ^0.13.1 71 | flutter_test: 72 | sdk: flutter 73 | # The "flutter_lints" package below contains a set of recommended lints to 74 | # encourage good coding practices. The lint set provided by the package is 75 | # activated in the `analysis_options.yaml` file located at the root of your 76 | # package. See that file for information about deactivating specific lint 77 | # rules and activating additional ones. 78 | flutter_lints: ^2.0.1 79 | 80 | flutter_icons: 81 | android: "launcher_icon" 82 | ios: true 83 | remove_alpha_ios: true 84 | image_path: "assets/icon/icon.png" 85 | 86 | # For information on the generic Dart part of this file, see the 87 | # following page: https://dart.dev/tools/pub/pubspec 88 | 89 | # The following section is specific to Flutter. 90 | flutter: 91 | 92 | # The following line ensures that the Material Icons font is 93 | # included with your application, so that you can use the icons in 94 | # the material Icons class. 95 | uses-material-design: true 96 | 97 | # To add assets to your application, add an assets section, like this: 98 | assets: 99 | - assets/onboarding/ 100 | - .env 101 | # - images/a_dot_burr.jpeg 102 | # - images/a_dot_ham.jpeg 103 | 104 | # An image asset can refer to one or more resolution-specific "variants", see 105 | # https://flutter.dev/assets-and-images/#resolution-aware. 106 | 107 | # For details regarding adding assets from package dependencies, see 108 | # https://flutter.dev/assets-and-images/#from-packages 109 | 110 | # To add custom fonts to your application, add a fonts section here, 111 | # in this "flutter" section. Each entry in this list should have a 112 | # "family" key with the font family name, and a "fonts" key with a 113 | # list giving the asset and other descriptors for the font. For 114 | # example: 115 | fonts: 116 | - family: Work Sans 117 | fonts: 118 | - asset: fonts/WorkSans-Medium.ttf 119 | - asset: fonts/WorkSans-Regular.ttf 120 | - asset: fonts/WorkSans-SemiBold.ttf 121 | # - family: Trajan Pro 122 | # fonts: 123 | # - asset: fonts/TrajanPro.ttf 124 | # - asset: fonts/TrajanPro_Bold.ttf 125 | # weight: 700 126 | # 127 | # For details regarding fonts from package dependencies, 128 | # see https://flutter.dev/custom-fonts/#from-packages 129 | -------------------------------------------------------------------------------- /submit_ios.sh: -------------------------------------------------------------------------------- 1 | cd ios 2 | bundle exec fastlane release_local 3 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .parcel-cache/ 3 | dist/ -------------------------------------------------------------------------------- /website/.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@parcel/config-default"], 3 | "reporters": ["...", "parcel-reporter-static-files-copy"], 4 | "resolvers": ["parcel-resolver-ignore", "..."] 5 | } -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | ## Developing 4 | 5 | ``` 6 | yarn start 7 | ``` 8 | 9 | ## Deploying 10 | 11 | ``` 12 | yarn run deploy 13 | ``` 14 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "devDependencies": { 6 | "gh-pages": "^3.2.3", 7 | "parcel": "^2.0.1", 8 | "parcel-reporter-static-files-copy": "^1.3.4", 9 | "parcel-resolver-ignore": "^2.0.0" 10 | }, 11 | "scripts": { 12 | "start": "rm -rf dist && parcel src/index.html --public-url=/", 13 | "build": "parcel build src/index.html --public-url=/voiceliner", 14 | "predeploy": "yarn run build", 15 | "deploy": "gh-pages -d dist" 16 | }, 17 | "parcelIgnore": [ 18 | "favicon.ico", 19 | "apple-touch-icon.png", 20 | "site.webmanifest", 21 | "android-chrome-192x192.png", 22 | "android-chrome-512x512.png", 23 | "favicon-16x16.png", 24 | "favicon-32x32.png", 25 | "card.png" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /website/src/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/src/1.png -------------------------------------------------------------------------------- /website/src/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/src/2.png -------------------------------------------------------------------------------- /website/src/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/src/3.png -------------------------------------------------------------------------------- /website/src/icon-w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/src/icon-w.png -------------------------------------------------------------------------------- /website/src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/src/icon.png -------------------------------------------------------------------------------- /website/src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Work+Sans:wght@300;400;500;700&display=swap"); 2 | 3 | :root { 4 | --shadow-color: 252deg 7% 66%; 5 | --shadow-elevation-low: 0.3px 0.5px 0.7px hsl(var(--shadow-color) / 0.21), 6 | 0.4px 0.8px 1.1px -0.9px hsl(var(--shadow-color) / 0.25), 7 | 0.9px 1.8px 2.4px -1.9px hsl(var(--shadow-color) / 0.3); 8 | --shadow-elevation-medium: 0.3px 0.5px 0.7px hsl(var(--shadow-color) / 0.22), 9 | 0.9px 1.7px 2.3px -0.6px hsl(var(--shadow-color) / 0.26), 10 | 2px 3.9px 5.3px -1.3px hsl(var(--shadow-color) / 0.29), 11 | 4.5px 9.1px 12.3px -1.9px hsl(var(--shadow-color) / 0.33); 12 | --shadow-elevation-high: 0.3px 0.5px 0.7px hsl(var(--shadow-color) / 0.2), 13 | 1.4px 2.9px 3.9px -0.3px hsl(var(--shadow-color) / 0.22), 14 | 2.6px 5.2px 7.1px -0.5px hsl(var(--shadow-color) / 0.24), 15 | 4.1px 8.2px 11.1px -0.8px hsl(var(--shadow-color) / 0.25), 16 | 6.3px 12.5px 17px -1.1px hsl(var(--shadow-color) / 0.27), 17 | 9.4px 18.9px 25.6px -1.4px hsl(var(--shadow-color) / 0.29), 18 | 14px 28px 38px -1.6px hsl(var(--shadow-color) / 0.3), 19 | 20.3px 40.5px 55px -1.9px hsl(var(--shadow-color) / 0.32); 20 | } 21 | 22 | html, 23 | body { 24 | font-family: "Work Sans", sans-serif; 25 | } 26 | body { 27 | max-width: 1200px; 28 | margin: 0 auto; 29 | padding: 1em; 30 | background-color: #f2f1f6; 31 | } 32 | 33 | .fork { 34 | width: 100%; 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: right; 38 | align-items: center; 39 | flex-wrap: wrap; 40 | } 41 | 42 | header { 43 | text-align: center; 44 | margin-top: 30px; 45 | } 46 | 47 | h1 { 48 | color: #824bdd; 49 | font-size: 56px; 50 | margin: 0; 51 | } 52 | 53 | .name { 54 | display: flex; 55 | justify-content: center; 56 | align-items: center; 57 | } 58 | 59 | .name img { 60 | border-radius: 10px; 61 | box-shadow: var(--shadow-elevation-low); 62 | margin-right: 20px; 63 | } 64 | 65 | h2 { 66 | font-weight: 400; 67 | } 68 | 69 | .images { 70 | justify-content: center; 71 | display: flex; 72 | flex-direction: row; 73 | flex-wrap: wrap; 74 | } 75 | 76 | .images img { 77 | margin: 20px; 78 | box-shadow: var(--shadow-elevation-medium); 79 | border-radius: 10px; 80 | } 81 | 82 | .store { 83 | display: flex; 84 | padding: 10px; 85 | justify-content: center; 86 | flex-wrap: wrap; 87 | align-items: center; 88 | } 89 | .store > a > img { 90 | margin: 10px; 91 | } 92 | 93 | .descs { 94 | display: flex; 95 | flex-direction: column; 96 | align-items: center; 97 | } 98 | .description { 99 | font-size: 1.5em; 100 | box-shadow: var(--shadow-elevation-medium); 101 | border-radius: 10px; 102 | background-color: #ffffff; 103 | color: #2a1055; 104 | max-width: 700px; 105 | margin: 20px auto; 106 | padding: 30px; 107 | } 108 | 109 | a:link { 110 | color: #824bdd; 111 | } 112 | .githubs span { 113 | float: right; 114 | padding-right: 10px; 115 | margin-top: 10px; 116 | } 117 | 118 | footer { 119 | text-align: center; 120 | padding: 40px; 121 | } 122 | -------------------------------------------------------------------------------- /website/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Voiceliner 24 | 25 | 26 |
27 | 28 |
29 | Sponsor 37 | Star 45 | Issue 53 |
54 |
55 |
56 |
57 | icon 58 |

Voiceliner

59 |
60 |

Braindump better.

61 |
62 | Get it on Google Play 66 | Download on App Store 69 |
70 |
71 |
72 | First screenshot 73 | Second screenshot 74 | Third screenshot 75 |
76 |
77 |

78 | The fastest way to capture and structure your thoughts: through voice. 79 |

80 | Hold record, say what you want, and release. Just like you'd expect from 81 | an outliner, create hierarchies and rearrange. Notes are 82 | auto-transcribed and searchable, but you can always play back the audio. 83 |
84 |
85 | Automatically attach location to notes. Remember walks you took, and 86 | which places sparked what ideas. 87 |

88 |

89 | This app is AGPLv3 open source and 90 | available on GitHub. Everything is local to your device. Transcription on iOS is native, 93 | and on Android, uses Vosk locally 94 | (transcription is togglable). You can back up to Google Drive and 95 | iCloud. 96 |

97 |
98 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /website/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /website/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/static/apple-touch-icon.png -------------------------------------------------------------------------------- /website/static/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/static/card.png -------------------------------------------------------------------------------- /website/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/static/favicon-16x16.png -------------------------------------------------------------------------------- /website/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/static/favicon-32x32.png -------------------------------------------------------------------------------- /website/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/static/favicon.ico -------------------------------------------------------------------------------- /website/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxkrieger/voiceliner/238bb0ed46e71ca1148139cafe03c1d9c5a6eb65/website/static/favicon.png -------------------------------------------------------------------------------- /website/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} --------------------------------------------------------------------------------