├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── flutter │ │ │ │ └── reply │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ ├── launch_background.xml │ │ │ └── launch_color.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── app.dart ├── bottom_drawer.dart ├── colors.dart ├── compose_page.dart ├── custom_transition_page.dart ├── home.dart ├── inbox.dart ├── mail_card_preview.dart ├── mail_view_page.dart ├── mail_view_router.dart ├── main.dart ├── model │ ├── email_model.dart │ ├── email_store.dart │ └── router_provider.dart ├── profile_avatar.dart ├── router.dart ├── search_page.dart ├── settings_bottom_sheet.dart └── waterfall_notched_rectangle.dart ├── pubspec.lock ├── pubspec.yaml ├── screenshots ├── reply-transitions-android.gif └── reply-transitions-iOS.gif ├── test └── widget_test.dart └── web ├── favicon.png ├── icons ├── Icon-192.png └── Icon-512.png ├── index.html └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | .pub/ 4 | .idea 5 | .atom 6 | .vscode 7 | .flutter-plugins 8 | .flutter-plugins-dependencies 9 | .packages 10 | build/ 11 | *.iml 12 | **/ephemeral -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 8 | channel: master 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 17 | base_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 18 | - platform: android 19 | create_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 20 | base_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 21 | - platform: ios 22 | create_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 23 | base_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 24 | - platform: linux 25 | create_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 26 | base_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 27 | - platform: macos 28 | create_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 29 | base_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 30 | - platform: web 31 | create_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 32 | base_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 33 | - platform: windows 34 | create_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 35 | base_revision: 07c7d2e454c8126dbe6498c51399c6ca8073f4b6 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codelab: Building Beautiful Transitions with Material Motion for Flutter 2 | 3 | The Material motion system for Flutter is a set of transition patterns within 4 | the [Animations package](https://pub.dev/packages/animations) that can help 5 | users understand and navigate an app, as described in the 6 | [Material Design guidelines](https://material.io/design/motion/the-motion-system.html). 7 | 8 | This repo houses the source for the 9 | [Material motion system codelab](https://codelabs.developers.google.com/codelabs/material-motion-flutter), 10 | during which you will build Material transitions into an example email app 11 | called Reply. 12 | 13 | The starter code is available on the default `starter` branch, and the complete 14 | code is available on the `complete` branch, which can you can checkout by 15 | running `git checkout complete`. 16 | 17 | | Android | iOS | 18 | |----|----| 19 | |![Reply transitions for Android](screenshots/reply-transitions-android.gif) |![Reply transitions for iOS](screenshots/reply-transitions-iOS.gif)| 20 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml -------------------------------------------------------------------------------- /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/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 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | // ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.flutter.reply" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/flutter/reply/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.flutter.reply 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /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/drawable/launch_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.3' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 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 | -------------------------------------------------------------------------------- /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/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.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 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider_ios (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | path_provider_ios: 14 | :path: ".symlinks/plugins/path_provider_ios/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a 18 | path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 19 | 20 | PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c 21 | 22 | COCOAPODS: 1.11.2 23 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | C17A20C865AC42F3C8631633 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 151B81D07344E024C42EC367 /* Pods_Runner.framework */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 021D377D0B7C746C102071DB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 34 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 35 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 36 | 151B81D07344E024C42EC367 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 2FE079A7CCDA80B97BCDE461 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 38 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 39 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 40 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 41 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 42 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 43 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 44 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | B0F154022B32EADAB0E7E595 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | C17A20C865AC42F3C8631633 /* Pods_Runner.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 30011DC4E50D16F01B73E0ED /* Frameworks */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 151B81D07344E024C42EC367 /* Pods_Runner.framework */, 68 | ); 69 | name = Frameworks; 70 | sourceTree = ""; 71 | }; 72 | 9740EEB11CF90186004384FC /* Flutter */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 76 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 77 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 78 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 79 | ); 80 | name = Flutter; 81 | sourceTree = ""; 82 | }; 83 | 97C146E51CF9000F007C117D = { 84 | isa = PBXGroup; 85 | children = ( 86 | 9740EEB11CF90186004384FC /* Flutter */, 87 | 97C146F01CF9000F007C117D /* Runner */, 88 | 97C146EF1CF9000F007C117D /* Products */, 89 | C56CAF54698517D229601CA8 /* Pods */, 90 | 30011DC4E50D16F01B73E0ED /* Frameworks */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | 97C146EF1CF9000F007C117D /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 97C146EE1CF9000F007C117D /* Runner.app */, 98 | ); 99 | name = Products; 100 | sourceTree = ""; 101 | }; 102 | 97C146F01CF9000F007C117D /* Runner */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 106 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 107 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 108 | 97C147021CF9000F007C117D /* Info.plist */, 109 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 110 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 111 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 112 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 113 | ); 114 | path = Runner; 115 | sourceTree = ""; 116 | }; 117 | C56CAF54698517D229601CA8 /* Pods */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | B0F154022B32EADAB0E7E595 /* Pods-Runner.debug.xcconfig */, 121 | 021D377D0B7C746C102071DB /* Pods-Runner.release.xcconfig */, 122 | 2FE079A7CCDA80B97BCDE461 /* Pods-Runner.profile.xcconfig */, 123 | ); 124 | name = Pods; 125 | path = Pods; 126 | sourceTree = ""; 127 | }; 128 | /* End PBXGroup section */ 129 | 130 | /* Begin PBXNativeTarget section */ 131 | 97C146ED1CF9000F007C117D /* Runner */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 134 | buildPhases = ( 135 | 4756D50EFAC2FE7AA3D4F4AB /* [CP] Check Pods Manifest.lock */, 136 | 9740EEB61CF901F6004384FC /* Run Script */, 137 | 97C146EA1CF9000F007C117D /* Sources */, 138 | 97C146EB1CF9000F007C117D /* Frameworks */, 139 | 97C146EC1CF9000F007C117D /* Resources */, 140 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 141 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 142 | 8830A2EE7037C23B92D68813 /* [CP] Embed Pods Frameworks */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | ); 148 | name = Runner; 149 | productName = Runner; 150 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 151 | productType = "com.apple.product-type.application"; 152 | }; 153 | /* End PBXNativeTarget section */ 154 | 155 | /* Begin PBXProject section */ 156 | 97C146E61CF9000F007C117D /* Project object */ = { 157 | isa = PBXProject; 158 | attributes = { 159 | LastUpgradeCheck = 1300; 160 | ORGANIZATIONNAME = ""; 161 | TargetAttributes = { 162 | 97C146ED1CF9000F007C117D = { 163 | CreatedOnToolsVersion = 7.3.1; 164 | LastSwiftMigration = 1100; 165 | }; 166 | }; 167 | }; 168 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 169 | compatibilityVersion = "Xcode 9.3"; 170 | developmentRegion = en; 171 | hasScannedForEncodings = 0; 172 | knownRegions = ( 173 | en, 174 | Base, 175 | ); 176 | mainGroup = 97C146E51CF9000F007C117D; 177 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 97C146ED1CF9000F007C117D /* Runner */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 97C146EC1CF9000F007C117D /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 192 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 193 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 194 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXResourcesBuildPhase section */ 199 | 200 | /* Begin PBXShellScriptBuildPhase section */ 201 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 202 | isa = PBXShellScriptBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | ); 206 | inputPaths = ( 207 | ); 208 | name = "Thin Binary"; 209 | outputPaths = ( 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | shellPath = /bin/sh; 213 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 214 | }; 215 | 4756D50EFAC2FE7AA3D4F4AB /* [CP] Check Pods Manifest.lock */ = { 216 | isa = PBXShellScriptBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | ); 220 | inputFileListPaths = ( 221 | ); 222 | inputPaths = ( 223 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 224 | "${PODS_ROOT}/Manifest.lock", 225 | ); 226 | name = "[CP] Check Pods Manifest.lock"; 227 | outputFileListPaths = ( 228 | ); 229 | outputPaths = ( 230 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | shellPath = /bin/sh; 234 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 235 | showEnvVarsInLog = 0; 236 | }; 237 | 8830A2EE7037C23B92D68813 /* [CP] Embed Pods Frameworks */ = { 238 | isa = PBXShellScriptBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | ); 242 | inputFileListPaths = ( 243 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 244 | ); 245 | name = "[CP] Embed Pods Frameworks"; 246 | outputFileListPaths = ( 247 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | shellPath = /bin/sh; 251 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 252 | showEnvVarsInLog = 0; 253 | }; 254 | 9740EEB61CF901F6004384FC /* Run Script */ = { 255 | isa = PBXShellScriptBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | ); 259 | inputPaths = ( 260 | ); 261 | name = "Run Script"; 262 | outputPaths = ( 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | shellPath = /bin/sh; 266 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 267 | }; 268 | /* End PBXShellScriptBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | 97C146EA1CF9000F007C117D /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 276 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 277 | ); 278 | runOnlyForDeploymentPostprocessing = 0; 279 | }; 280 | /* End PBXSourcesBuildPhase section */ 281 | 282 | /* Begin PBXVariantGroup section */ 283 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 284 | isa = PBXVariantGroup; 285 | children = ( 286 | 97C146FB1CF9000F007C117D /* Base */, 287 | ); 288 | name = Main.storyboard; 289 | sourceTree = ""; 290 | }; 291 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 292 | isa = PBXVariantGroup; 293 | children = ( 294 | 97C147001CF9000F007C117D /* Base */, 295 | ); 296 | name = LaunchScreen.storyboard; 297 | sourceTree = ""; 298 | }; 299 | /* End PBXVariantGroup section */ 300 | 301 | /* Begin XCBuildConfiguration section */ 302 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 326 | CLANG_WARN_STRICT_PROTOTYPES = YES; 327 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 333 | ENABLE_NS_ASSERTIONS = NO; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu99; 336 | GCC_NO_COMMON_BLOCKS = YES; 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 344 | MTL_ENABLE_DEBUG_INFO = NO; 345 | SDKROOT = iphoneos; 346 | SUPPORTED_PLATFORMS = iphoneos; 347 | TARGETED_DEVICE_FAMILY = "1,2"; 348 | VALIDATE_PRODUCT = YES; 349 | }; 350 | name = Profile; 351 | }; 352 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 353 | isa = XCBuildConfiguration; 354 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 355 | buildSettings = { 356 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 357 | CLANG_ENABLE_MODULES = YES; 358 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 359 | ENABLE_BITCODE = NO; 360 | FRAMEWORK_SEARCH_PATHS = ( 361 | "$(inherited)", 362 | "$(PROJECT_DIR)/Flutter", 363 | ); 364 | INFOPLIST_FILE = Runner/Info.plist; 365 | LD_RUNPATH_SEARCH_PATHS = ( 366 | "$(inherited)", 367 | "@executable_path/Frameworks", 368 | ); 369 | LIBRARY_SEARCH_PATHS = ( 370 | "$(inherited)", 371 | "$(PROJECT_DIR)/Flutter", 372 | ); 373 | PRODUCT_BUNDLE_IDENTIFIER = com.flutter.reply; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 376 | SWIFT_VERSION = 5.0; 377 | VERSIONING_SYSTEM = "apple-generic"; 378 | }; 379 | name = Profile; 380 | }; 381 | 97C147031CF9000F007C117D /* Debug */ = { 382 | isa = XCBuildConfiguration; 383 | buildSettings = { 384 | ALWAYS_SEARCH_USER_PATHS = NO; 385 | CLANG_ANALYZER_NONNULL = YES; 386 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 387 | CLANG_CXX_LIBRARY = "libc++"; 388 | CLANG_ENABLE_MODULES = YES; 389 | CLANG_ENABLE_OBJC_ARC = YES; 390 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 391 | CLANG_WARN_BOOL_CONVERSION = YES; 392 | CLANG_WARN_COMMA = YES; 393 | CLANG_WARN_CONSTANT_CONVERSION = YES; 394 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 395 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 396 | CLANG_WARN_EMPTY_BODY = YES; 397 | CLANG_WARN_ENUM_CONVERSION = YES; 398 | CLANG_WARN_INFINITE_RECURSION = YES; 399 | CLANG_WARN_INT_CONVERSION = YES; 400 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 401 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 402 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 403 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 404 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 405 | CLANG_WARN_STRICT_PROTOTYPES = YES; 406 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 407 | CLANG_WARN_UNREACHABLE_CODE = YES; 408 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 409 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 410 | COPY_PHASE_STRIP = NO; 411 | DEBUG_INFORMATION_FORMAT = dwarf; 412 | ENABLE_STRICT_OBJC_MSGSEND = YES; 413 | ENABLE_TESTABILITY = YES; 414 | GCC_C_LANGUAGE_STANDARD = gnu99; 415 | GCC_DYNAMIC_NO_PIC = NO; 416 | GCC_NO_COMMON_BLOCKS = YES; 417 | GCC_OPTIMIZATION_LEVEL = 0; 418 | GCC_PREPROCESSOR_DEFINITIONS = ( 419 | "DEBUG=1", 420 | "$(inherited)", 421 | ); 422 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 423 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 424 | GCC_WARN_UNDECLARED_SELECTOR = YES; 425 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 426 | GCC_WARN_UNUSED_FUNCTION = YES; 427 | GCC_WARN_UNUSED_VARIABLE = YES; 428 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 429 | MTL_ENABLE_DEBUG_INFO = YES; 430 | ONLY_ACTIVE_ARCH = YES; 431 | SDKROOT = iphoneos; 432 | TARGETED_DEVICE_FAMILY = "1,2"; 433 | }; 434 | name = Debug; 435 | }; 436 | 97C147041CF9000F007C117D /* Release */ = { 437 | isa = XCBuildConfiguration; 438 | buildSettings = { 439 | ALWAYS_SEARCH_USER_PATHS = NO; 440 | CLANG_ANALYZER_NONNULL = YES; 441 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 442 | CLANG_CXX_LIBRARY = "libc++"; 443 | CLANG_ENABLE_MODULES = YES; 444 | CLANG_ENABLE_OBJC_ARC = YES; 445 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 446 | CLANG_WARN_BOOL_CONVERSION = YES; 447 | CLANG_WARN_COMMA = YES; 448 | CLANG_WARN_CONSTANT_CONVERSION = YES; 449 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 450 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 451 | CLANG_WARN_EMPTY_BODY = YES; 452 | CLANG_WARN_ENUM_CONVERSION = YES; 453 | CLANG_WARN_INFINITE_RECURSION = YES; 454 | CLANG_WARN_INT_CONVERSION = YES; 455 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 456 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 457 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 458 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 459 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 460 | CLANG_WARN_STRICT_PROTOTYPES = YES; 461 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 462 | CLANG_WARN_UNREACHABLE_CODE = YES; 463 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 464 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 465 | COPY_PHASE_STRIP = NO; 466 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 467 | ENABLE_NS_ASSERTIONS = NO; 468 | ENABLE_STRICT_OBJC_MSGSEND = YES; 469 | GCC_C_LANGUAGE_STANDARD = gnu99; 470 | GCC_NO_COMMON_BLOCKS = YES; 471 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 472 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 473 | GCC_WARN_UNDECLARED_SELECTOR = YES; 474 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 475 | GCC_WARN_UNUSED_FUNCTION = YES; 476 | GCC_WARN_UNUSED_VARIABLE = YES; 477 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 478 | MTL_ENABLE_DEBUG_INFO = NO; 479 | SDKROOT = iphoneos; 480 | SUPPORTED_PLATFORMS = iphoneos; 481 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 482 | TARGETED_DEVICE_FAMILY = "1,2"; 483 | VALIDATE_PRODUCT = YES; 484 | }; 485 | name = Release; 486 | }; 487 | 97C147061CF9000F007C117D /* Debug */ = { 488 | isa = XCBuildConfiguration; 489 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 490 | buildSettings = { 491 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 492 | CLANG_ENABLE_MODULES = YES; 493 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 494 | ENABLE_BITCODE = NO; 495 | FRAMEWORK_SEARCH_PATHS = ( 496 | "$(inherited)", 497 | "$(PROJECT_DIR)/Flutter", 498 | ); 499 | INFOPLIST_FILE = Runner/Info.plist; 500 | LD_RUNPATH_SEARCH_PATHS = ( 501 | "$(inherited)", 502 | "@executable_path/Frameworks", 503 | ); 504 | LIBRARY_SEARCH_PATHS = ( 505 | "$(inherited)", 506 | "$(PROJECT_DIR)/Flutter", 507 | ); 508 | PRODUCT_BUNDLE_IDENTIFIER = com.flutter.reply; 509 | PRODUCT_NAME = "$(TARGET_NAME)"; 510 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 511 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 512 | SWIFT_VERSION = 5.0; 513 | VERSIONING_SYSTEM = "apple-generic"; 514 | }; 515 | name = Debug; 516 | }; 517 | 97C147071CF9000F007C117D /* Release */ = { 518 | isa = XCBuildConfiguration; 519 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 520 | buildSettings = { 521 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 522 | CLANG_ENABLE_MODULES = YES; 523 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 524 | ENABLE_BITCODE = NO; 525 | FRAMEWORK_SEARCH_PATHS = ( 526 | "$(inherited)", 527 | "$(PROJECT_DIR)/Flutter", 528 | ); 529 | INFOPLIST_FILE = Runner/Info.plist; 530 | LD_RUNPATH_SEARCH_PATHS = ( 531 | "$(inherited)", 532 | "@executable_path/Frameworks", 533 | ); 534 | LIBRARY_SEARCH_PATHS = ( 535 | "$(inherited)", 536 | "$(PROJECT_DIR)/Flutter", 537 | ); 538 | PRODUCT_BUNDLE_IDENTIFIER = com.flutter.reply; 539 | PRODUCT_NAME = "$(TARGET_NAME)"; 540 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 541 | SWIFT_VERSION = 5.0; 542 | VERSIONING_SYSTEM = "apple-generic"; 543 | }; 544 | name = Release; 545 | }; 546 | /* End XCBuildConfiguration section */ 547 | 548 | /* Begin XCConfigurationList section */ 549 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 550 | isa = XCConfigurationList; 551 | buildConfigurations = ( 552 | 97C147031CF9000F007C117D /* Debug */, 553 | 97C147041CF9000F007C117D /* Release */, 554 | 249021D3217E4FDB00AE95B9 /* Profile */, 555 | ); 556 | defaultConfigurationIsVisible = 0; 557 | defaultConfigurationName = Release; 558 | }; 559 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 560 | isa = XCConfigurationList; 561 | buildConfigurations = ( 562 | 97C147061CF9000F007C117D /* Debug */, 563 | 97C147071CF9000F007C117D /* Release */, 564 | 249021D4217E4FDB00AE95B9 /* Profile */, 565 | ); 566 | defaultConfigurationIsVisible = 0; 567 | defaultConfigurationName = Release; 568 | }; 569 | /* End XCConfigurationList section */ 570 | }; 571 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 572 | } 573 | -------------------------------------------------------------------------------- /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 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /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 UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/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/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Reply 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:reply/router.dart'; 5 | 6 | import 'colors.dart'; 7 | import 'model/email_store.dart'; 8 | import 'model/router_provider.dart'; 9 | 10 | class ReplyApp extends StatefulWidget { 11 | const ReplyApp({Key? key}) : super(key: key); 12 | 13 | @override 14 | ReplyAppState createState() => ReplyAppState(); 15 | } 16 | 17 | class ReplyAppState extends State { 18 | final RouterProvider _replyState = RouterProvider(const ReplyHomePath()); 19 | final ReplyRouteInformationParser _routeInformationParser = 20 | ReplyRouteInformationParser(); 21 | late final ReplyRouterDelegate _routerDelegate; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | _routerDelegate = ReplyRouterDelegate(replyState: _replyState); 27 | } 28 | 29 | @override 30 | void dispose() { 31 | _routerDelegate.dispose(); 32 | super.dispose(); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return MultiProvider( 38 | providers: [ 39 | ChangeNotifierProvider.value(value: EmailStore()), 40 | ], 41 | child: Selector( 42 | selector: (context, emailStore) => emailStore.themeMode, 43 | builder: (context, themeMode, child) { 44 | return MaterialApp.router( 45 | routeInformationParser: _routeInformationParser, 46 | routerDelegate: _routerDelegate, 47 | themeMode: themeMode, 48 | title: 'Reply', 49 | darkTheme: _buildReplyDarkTheme(context), 50 | theme: _buildReplyLightTheme(context), 51 | ); 52 | }), 53 | ); 54 | } 55 | } 56 | 57 | ThemeData _buildReplyLightTheme(BuildContext context) { 58 | final base = ThemeData.light(); 59 | return base.copyWith( 60 | bottomSheetTheme: BottomSheetThemeData( 61 | backgroundColor: ReplyColors.blue700, 62 | modalBackgroundColor: Colors.white.withOpacity(0.7), 63 | ), 64 | cardColor: ReplyColors.white50, 65 | chipTheme: _buildChipTheme( 66 | ReplyColors.blue700, 67 | ReplyColors.lightChipBackground, 68 | Brightness.light, 69 | ), 70 | colorScheme: const ColorScheme.light( 71 | primary: ReplyColors.blue700, 72 | secondary: ReplyColors.orange500, 73 | surface: ReplyColors.white50, 74 | error: ReplyColors.red400, 75 | onPrimary: ReplyColors.white50, 76 | onSecondary: ReplyColors.black900, 77 | onBackground: ReplyColors.black900, 78 | onSurface: ReplyColors.black900, 79 | onError: ReplyColors.black900, 80 | background: ReplyColors.blue50, 81 | ), 82 | textTheme: _buildReplyLightTextTheme(base.textTheme), 83 | scaffoldBackgroundColor: ReplyColors.blue50, 84 | bottomAppBarTheme: const BottomAppBarTheme( 85 | color: ReplyColors.blue700, 86 | ), 87 | ); 88 | } 89 | 90 | ThemeData _buildReplyDarkTheme(BuildContext context) { 91 | final base = ThemeData.dark(); 92 | return base.copyWith( 93 | bottomSheetTheme: BottomSheetThemeData( 94 | backgroundColor: ReplyColors.darkDrawerBackground, 95 | modalBackgroundColor: Colors.black.withOpacity(0.7), 96 | ), 97 | cardColor: ReplyColors.darkCardBackground, 98 | chipTheme: _buildChipTheme( 99 | ReplyColors.blue200, 100 | ReplyColors.darkChipBackground, 101 | Brightness.dark, 102 | ), 103 | colorScheme: const ColorScheme.dark( 104 | primary: ReplyColors.blue200, 105 | secondary: ReplyColors.orange300, 106 | surface: ReplyColors.black800, 107 | error: ReplyColors.red200, 108 | onPrimary: ReplyColors.black900, 109 | onSecondary: ReplyColors.black900, 110 | onBackground: ReplyColors.white50, 111 | onSurface: ReplyColors.white50, 112 | onError: ReplyColors.black900, 113 | background: ReplyColors.black900, 114 | ), 115 | textTheme: _buildReplyDarkTextTheme(base.textTheme), 116 | scaffoldBackgroundColor: ReplyColors.black900, 117 | bottomAppBarTheme: const BottomAppBarTheme( 118 | color: ReplyColors.darkBottomAppBarBackground, 119 | ), 120 | ); 121 | } 122 | 123 | ChipThemeData _buildChipTheme( 124 | Color primaryColor, 125 | Color chipBackground, 126 | Brightness brightness, 127 | ) { 128 | return ChipThemeData( 129 | backgroundColor: primaryColor.withOpacity(0.12), 130 | disabledColor: primaryColor.withOpacity(0.87), 131 | selectedColor: primaryColor.withOpacity(0.05), 132 | secondarySelectedColor: chipBackground, 133 | padding: const EdgeInsets.all(4), 134 | shape: const StadiumBorder(), 135 | labelStyle: GoogleFonts.workSansTextTheme().bodyMedium!.copyWith( 136 | color: brightness == Brightness.dark 137 | ? ReplyColors.white50 138 | : ReplyColors.black900, 139 | ), 140 | secondaryLabelStyle: GoogleFonts.workSansTextTheme().bodyMedium!, 141 | brightness: brightness, 142 | ); 143 | } 144 | 145 | TextTheme _buildReplyLightTextTheme(TextTheme base) { 146 | return base.copyWith( 147 | headlineMedium: GoogleFonts.workSans( 148 | fontWeight: FontWeight.w600, 149 | fontSize: 34, 150 | letterSpacing: 0.4, 151 | height: 0.9, 152 | color: ReplyColors.black900, 153 | ), 154 | headlineSmall: GoogleFonts.workSans( 155 | fontWeight: FontWeight.bold, 156 | fontSize: 24, 157 | letterSpacing: 0.27, 158 | color: ReplyColors.black900, 159 | ), 160 | titleLarge: GoogleFonts.workSans( 161 | fontWeight: FontWeight.w600, 162 | fontSize: 20, 163 | letterSpacing: 0.18, 164 | color: ReplyColors.black900, 165 | ), 166 | titleSmall: GoogleFonts.workSans( 167 | fontWeight: FontWeight.w600, 168 | fontSize: 14, 169 | letterSpacing: -0.04, 170 | color: ReplyColors.black900, 171 | ), 172 | bodyLarge: GoogleFonts.workSans( 173 | fontWeight: FontWeight.normal, 174 | fontSize: 18, 175 | letterSpacing: 0.2, 176 | color: ReplyColors.black900, 177 | ), 178 | bodyMedium: GoogleFonts.workSans( 179 | fontWeight: FontWeight.normal, 180 | fontSize: 14, 181 | letterSpacing: -0.05, 182 | color: ReplyColors.black900, 183 | ), 184 | bodySmall: GoogleFonts.workSans( 185 | fontWeight: FontWeight.normal, 186 | fontSize: 12, 187 | letterSpacing: 0.2, 188 | color: ReplyColors.black900, 189 | ), 190 | ); 191 | } 192 | 193 | TextTheme _buildReplyDarkTextTheme(TextTheme base) { 194 | return base.copyWith( 195 | headlineMedium: GoogleFonts.workSans( 196 | fontWeight: FontWeight.w600, 197 | fontSize: 34, 198 | letterSpacing: 0.4, 199 | height: 0.9, 200 | color: ReplyColors.white50, 201 | ), 202 | headlineSmall: GoogleFonts.workSans( 203 | fontWeight: FontWeight.bold, 204 | fontSize: 24, 205 | letterSpacing: 0.27, 206 | color: ReplyColors.white50, 207 | ), 208 | titleLarge: GoogleFonts.workSans( 209 | fontWeight: FontWeight.w600, 210 | fontSize: 20, 211 | letterSpacing: 0.18, 212 | color: ReplyColors.white50, 213 | ), 214 | titleSmall: GoogleFonts.workSans( 215 | fontWeight: FontWeight.w600, 216 | fontSize: 14, 217 | letterSpacing: -0.04, 218 | color: ReplyColors.white50, 219 | ), 220 | bodyLarge: GoogleFonts.workSans( 221 | fontWeight: FontWeight.normal, 222 | fontSize: 18, 223 | letterSpacing: 0.2, 224 | color: ReplyColors.white50, 225 | ), 226 | bodyMedium: GoogleFonts.workSans( 227 | fontWeight: FontWeight.normal, 228 | fontSize: 14, 229 | letterSpacing: -0.05, 230 | color: ReplyColors.white50, 231 | ), 232 | bodySmall: GoogleFonts.workSans( 233 | fontWeight: FontWeight.normal, 234 | fontSize: 12, 235 | letterSpacing: 0.2, 236 | color: ReplyColors.white50, 237 | ), 238 | ); 239 | } 240 | -------------------------------------------------------------------------------- /lib/bottom_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'colors.dart'; 4 | 5 | class BottomDrawer extends StatelessWidget { 6 | const BottomDrawer({ 7 | Key? key, 8 | required this.onVerticalDragUpdate, 9 | required this.onVerticalDragEnd, 10 | required this.leading, 11 | required this.trailing, 12 | }) : super(key: key); 13 | 14 | final GestureDragUpdateCallback onVerticalDragUpdate; 15 | final GestureDragEndCallback onVerticalDragEnd; 16 | final Widget leading; 17 | final Widget trailing; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final theme = Theme.of(context); 22 | 23 | return GestureDetector( 24 | behavior: HitTestBehavior.opaque, 25 | onVerticalDragUpdate: onVerticalDragUpdate, 26 | onVerticalDragEnd: onVerticalDragEnd, 27 | child: Material( 28 | color: theme.bottomSheetTheme.backgroundColor, 29 | borderRadius: const BorderRadius.only( 30 | topLeft: Radius.circular(12), 31 | topRight: Radius.circular(12), 32 | ), 33 | child: ListView( 34 | padding: const EdgeInsets.all(12), 35 | physics: const NeverScrollableScrollPhysics(), 36 | children: [ 37 | const SizedBox(height: 28), 38 | leading, 39 | const SizedBox(height: 8), 40 | const Divider( 41 | color: ReplyColors.blue200, 42 | thickness: 0.25, 43 | indent: 18, 44 | endIndent: 160, 45 | ), 46 | const SizedBox(height: 16), 47 | Padding( 48 | padding: const EdgeInsetsDirectional.only(start: 18), 49 | child: Text( 50 | 'FOLDERS', 51 | style: theme.textTheme.bodySmall!.copyWith( 52 | color: ReplyColors.white50.withOpacity(0.64), 53 | ), 54 | ), 55 | ), 56 | const SizedBox(height: 4), 57 | trailing, 58 | ], 59 | ), 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ReplyColors { 4 | static const Color white50 = Color(0xFFFFFFFF); 5 | 6 | static const Color black800 = Color(0xFF121212); 7 | static const Color black900 = Color(0xFF000000); 8 | 9 | static const Color blue50 = Color(0xFFEEF0F2); 10 | static const Color blue100 = Color(0xFFD2DBE0); 11 | static const Color blue200 = Color(0xFFADBBC4); 12 | static const Color blue300 = Color(0xFF8CA2AE); 13 | static const Color blue600 = Color(0xFF4A6572); 14 | static const Color blue700 = Color(0xFF344955); 15 | static const Color blue800 = Color(0xFF232F34); 16 | 17 | static const Color orange300 = Color(0xFFFBD790); 18 | static const Color orange400 = Color(0xFFF9BE64); 19 | static const Color orange500 = Color(0xFFF9AA33); 20 | 21 | static const Color red200 = Color(0xFFCF7779); 22 | static const Color red400 = Color(0xFFFF4C5D); 23 | 24 | static const Color white50Alpha060 = Color(0x99FFFFFF); 25 | 26 | static const Color blue50Alpha060 = Color(0x99EEF0F2); 27 | 28 | static const Color black900Alpha020 = Color(0x33000000); 29 | static const Color black900Alpha087 = Color(0xDE000000); 30 | static const Color black900Alpha060 = Color(0x99000000); 31 | 32 | static const Color greyLabel = Color(0xFFAEAEAE); 33 | static const Color darkBottomAppBarBackground = Color(0xFF2D2D2D); 34 | static const Color darkDrawerBackground = Color(0xFF353535); 35 | static const Color darkCardBackground = Color(0xFF1E1E1E); 36 | static const Color darkChipBackground = Color(0xFF2A2A2A); 37 | static const Color lightChipBackground = Color(0xFFE5E5E5); 38 | } 39 | -------------------------------------------------------------------------------- /lib/compose_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'model/email_store.dart'; 5 | 6 | class ComposePage extends StatelessWidget { 7 | const ComposePage({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | var senderEmail = 'flutterfan@gmail.com'; 12 | var subject = ''; 13 | var recipient = 'Recipient'; 14 | var recipientAvatar = 'reply/avatars/avatar_0.jpg'; 15 | 16 | final emailStore = Provider.of(context); 17 | 18 | if (emailStore.currentlySelectedEmailId >= 0) { 19 | final currentEmail = emailStore.emails[emailStore.currentlySelectedInbox]! 20 | .elementAt(emailStore.currentlySelectedEmailId); 21 | subject = currentEmail.subject; 22 | recipient = currentEmail.sender; 23 | recipientAvatar = currentEmail.avatar; 24 | } 25 | 26 | return Scaffold( 27 | body: SafeArea( 28 | bottom: false, 29 | child: SizedBox( 30 | height: double.infinity, 31 | child: Material( 32 | color: Theme.of(context).cardColor, 33 | child: SingleChildScrollView( 34 | child: Column( 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | mainAxisSize: MainAxisSize.min, 37 | children: [ 38 | _SubjectRow( 39 | subject: subject, 40 | ), 41 | const _SectionDivider(), 42 | _SenderAddressRow( 43 | senderEmail: senderEmail, 44 | ), 45 | const _SectionDivider(), 46 | _RecipientsRow( 47 | recipients: recipient, 48 | avatar: recipientAvatar, 49 | ), 50 | const _SectionDivider(), 51 | Padding( 52 | padding: const EdgeInsets.all(12), 53 | child: TextField( 54 | minLines: 6, 55 | maxLines: 20, 56 | decoration: const InputDecoration.collapsed( 57 | hintText: 'New Message...', 58 | ), 59 | autofocus: false, 60 | style: Theme.of(context).textTheme.bodyMedium, 61 | ), 62 | ), 63 | ], 64 | ), 65 | ), 66 | ), 67 | ), 68 | ), 69 | ); 70 | } 71 | } 72 | 73 | class _SubjectRow extends StatefulWidget { 74 | const _SubjectRow({required this.subject}); 75 | 76 | final String subject; 77 | @override 78 | _SubjectRowState createState() => _SubjectRowState(); 79 | } 80 | 81 | class _SubjectRowState extends State<_SubjectRow> { 82 | late final TextEditingController _subjectController; 83 | 84 | @override 85 | void initState() { 86 | super.initState(); 87 | _subjectController = TextEditingController(text: widget.subject); 88 | } 89 | 90 | @override 91 | void dispose() { 92 | _subjectController.dispose(); 93 | super.dispose(); 94 | } 95 | 96 | @override 97 | Widget build(BuildContext context) { 98 | final theme = Theme.of(context); 99 | final colorScheme = theme.colorScheme; 100 | 101 | return Padding( 102 | padding: const EdgeInsets.only(top: 8), 103 | child: Row( 104 | crossAxisAlignment: CrossAxisAlignment.center, 105 | children: [ 106 | IconButton( 107 | onPressed: () => Navigator.of(context).pop(), 108 | icon: Icon( 109 | Icons.close, 110 | color: colorScheme.onSurface, 111 | ), 112 | ), 113 | Expanded( 114 | child: TextField( 115 | controller: _subjectController, 116 | maxLines: 1, 117 | autofocus: false, 118 | style: theme.textTheme.titleLarge, 119 | decoration: InputDecoration.collapsed( 120 | hintText: 'Subject', 121 | hintStyle: theme.textTheme.titleLarge!.copyWith( 122 | color: theme.colorScheme.primary.withOpacity(0.5), 123 | ), 124 | ), 125 | ), 126 | ), 127 | IconButton( 128 | onPressed: () => Navigator.of(context).pop(), 129 | icon: IconButton( 130 | icon: ImageIcon( 131 | const AssetImage( 132 | 'reply/icons/twotone_send.png', 133 | package: 'flutter_gallery_assets', 134 | ), 135 | color: colorScheme.onSurface, 136 | ), 137 | onPressed: () => Navigator.of(context).pop(), 138 | ), 139 | ), 140 | ], 141 | ), 142 | ); 143 | } 144 | } 145 | 146 | class _SenderAddressRow extends StatefulWidget { 147 | const _SenderAddressRow({required this.senderEmail}); 148 | 149 | final String senderEmail; 150 | 151 | @override 152 | __SenderAddressRowState createState() => __SenderAddressRowState(); 153 | } 154 | 155 | class __SenderAddressRowState extends State<_SenderAddressRow> { 156 | late String senderEmail; 157 | 158 | @override 159 | void initState() { 160 | super.initState(); 161 | senderEmail = widget.senderEmail; 162 | } 163 | 164 | @override 165 | Widget build(BuildContext context) { 166 | final theme = Theme.of(context); 167 | final textTheme = theme.textTheme; 168 | final accounts = [ 169 | 'flutterfan@gmail.com', 170 | 'materialfan@gmail.com', 171 | ]; 172 | 173 | return PopupMenuButton( 174 | padding: EdgeInsets.zero, 175 | onSelected: (email) { 176 | setState(() { 177 | senderEmail = email; 178 | }); 179 | }, 180 | itemBuilder: (context) => >[ 181 | PopupMenuItem( 182 | value: accounts[0], 183 | child: Text( 184 | accounts[0], 185 | style: textTheme.bodyMedium, 186 | ), 187 | ), 188 | PopupMenuItem( 189 | value: accounts[1], 190 | child: Text( 191 | accounts[1], 192 | style: textTheme.bodyMedium, 193 | ), 194 | ), 195 | ], 196 | child: Padding( 197 | padding: const EdgeInsets.only( 198 | left: 12, 199 | top: 16, 200 | right: 10, 201 | bottom: 10, 202 | ), 203 | child: Row( 204 | crossAxisAlignment: CrossAxisAlignment.center, 205 | children: [ 206 | Expanded( 207 | child: Text( 208 | senderEmail, 209 | style: textTheme.bodyMedium, 210 | ), 211 | ), 212 | Icon( 213 | Icons.arrow_drop_down, 214 | color: theme.colorScheme.onSurface, 215 | ), 216 | ], 217 | ), 218 | ), 219 | ); 220 | } 221 | } 222 | 223 | class _RecipientsRow extends StatelessWidget { 224 | const _RecipientsRow({ 225 | required this.recipients, 226 | required this.avatar, 227 | }); 228 | 229 | final String recipients; 230 | final String avatar; 231 | 232 | @override 233 | Widget build(BuildContext context) { 234 | return Padding( 235 | padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), 236 | child: Row( 237 | crossAxisAlignment: CrossAxisAlignment.center, 238 | children: [ 239 | Expanded( 240 | child: Wrap( 241 | children: [ 242 | Chip( 243 | backgroundColor: 244 | Theme.of(context).chipTheme.secondarySelectedColor, 245 | padding: EdgeInsets.zero, 246 | avatar: CircleAvatar( 247 | backgroundImage: AssetImage( 248 | avatar, 249 | package: 'flutter_gallery_assets', 250 | ), 251 | ), 252 | label: Text( 253 | recipients, 254 | ), 255 | ), 256 | ], 257 | ), 258 | ), 259 | InkResponse( 260 | customBorder: const CircleBorder(), 261 | onTap: () {}, 262 | radius: 24, 263 | child: const Icon(Icons.add_circle_outline), 264 | ), 265 | ], 266 | ), 267 | ); 268 | } 269 | } 270 | 271 | class _SectionDivider extends StatelessWidget { 272 | const _SectionDivider(); 273 | 274 | @override 275 | Widget build(BuildContext context) { 276 | return const Divider( 277 | thickness: 1.1, 278 | indent: 10, 279 | endIndent: 10, 280 | ); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /lib/custom_transition_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class CustomTransitionPage extends Page { 4 | final Widget screen; 5 | final ValueKey transitionKey; 6 | 7 | const CustomTransitionPage( 8 | {required this.screen, required this.transitionKey}) 9 | : super(key: transitionKey); 10 | 11 | @override 12 | Route createRoute(BuildContext context) { 13 | return PageRouteBuilder( 14 | settings: this, 15 | pageBuilder: (context, animation, secondaryAnimation) { 16 | return screen; 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/home.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/rendering.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:reply/model/router_provider.dart'; 7 | 8 | import 'bottom_drawer.dart'; 9 | import 'colors.dart'; 10 | import 'compose_page.dart'; 11 | import 'mail_view_router.dart'; 12 | import 'model/email_store.dart'; 13 | import 'router.dart'; 14 | import 'settings_bottom_sheet.dart'; 15 | import 'waterfall_notched_rectangle.dart'; 16 | 17 | const _assetsPackage = 'flutter_gallery_assets'; 18 | const _iconAssetLocation = 'reply/icons'; 19 | const _folderIconAssetLocation = '$_iconAssetLocation/twotone_folder.png'; 20 | final mobileMailNavKey = GlobalKey(); 21 | const double _kFlingVelocity = 2.0; 22 | const _kAnimationDuration = Duration(milliseconds: 300); 23 | 24 | class HomePage extends StatefulWidget { 25 | const HomePage({Key? key}) : super(key: key); 26 | 27 | @override 28 | HomePageState createState() => HomePageState(); 29 | } 30 | 31 | class HomePageState extends State with TickerProviderStateMixin { 32 | late final AnimationController _drawerController; 33 | late final AnimationController _dropArrowController; 34 | late final AnimationController _bottomAppBarController; 35 | late final Animation _drawerCurve; 36 | late final Animation _dropArrowCurve; 37 | late final Animation _bottomAppBarCurve; 38 | 39 | final _bottomDrawerKey = GlobalKey(debugLabel: 'Bottom Drawer'); 40 | final _navigationDestinations = const <_Destination>[ 41 | _Destination( 42 | name: 'Inbox', 43 | icon: '$_iconAssetLocation/twotone_inbox.png', 44 | index: 0, 45 | ), 46 | _Destination( 47 | name: 'Starred', 48 | icon: '$_iconAssetLocation/twotone_star.png', 49 | index: 1, 50 | ), 51 | _Destination( 52 | name: 'Sent', 53 | icon: '$_iconAssetLocation/twotone_send.png', 54 | index: 2, 55 | ), 56 | _Destination( 57 | name: 'Trash', 58 | icon: '$_iconAssetLocation/twotone_delete.png', 59 | index: 3, 60 | ), 61 | _Destination( 62 | name: 'Spam', 63 | icon: '$_iconAssetLocation/twotone_error.png', 64 | index: 4, 65 | ), 66 | _Destination( 67 | name: 'Drafts', 68 | icon: '$_iconAssetLocation/twotone_drafts.png', 69 | index: 5, 70 | ), 71 | ]; 72 | 73 | final _folders = { 74 | 'Receipts': _folderIconAssetLocation, 75 | 'Pine Elementary': _folderIconAssetLocation, 76 | 'Taxes': _folderIconAssetLocation, 77 | 'Vacation': _folderIconAssetLocation, 78 | 'Mortgage': _folderIconAssetLocation, 79 | 'Freelance': _folderIconAssetLocation, 80 | }; 81 | 82 | @override 83 | void initState() { 84 | super.initState(); 85 | 86 | _drawerController = AnimationController( 87 | duration: _kAnimationDuration, 88 | value: 0, 89 | vsync: this, 90 | )..addListener(() { 91 | if (_drawerController.status == AnimationStatus.dismissed && 92 | _drawerController.value == 0) { 93 | Provider.of( 94 | context, 95 | listen: false, 96 | ).bottomDrawerVisible = false; 97 | } 98 | 99 | if (_drawerController.value < 0.01) { 100 | setState(() { 101 | //Reload state when drawer is at its smallest to toggle visibility 102 | //If state is reloaded before this drawer closes abruptly instead 103 | //of animating. 104 | }); 105 | } 106 | }); 107 | 108 | _dropArrowController = AnimationController( 109 | duration: _kAnimationDuration, 110 | vsync: this, 111 | ); 112 | 113 | _bottomAppBarController = AnimationController( 114 | vsync: this, 115 | value: 1, 116 | duration: const Duration(milliseconds: 250), 117 | ); 118 | 119 | _drawerCurve = CurvedAnimation( 120 | parent: _drawerController, 121 | curve: standardEasing, 122 | reverseCurve: standardEasing.flipped, 123 | ); 124 | 125 | _dropArrowCurve = CurvedAnimation( 126 | parent: _dropArrowController, 127 | curve: standardEasing, 128 | reverseCurve: standardEasing.flipped, 129 | ); 130 | 131 | _bottomAppBarCurve = CurvedAnimation( 132 | parent: _bottomAppBarController, 133 | curve: standardEasing, 134 | reverseCurve: standardEasing.flipped, 135 | ); 136 | } 137 | 138 | @override 139 | void dispose() { 140 | _drawerController.dispose(); 141 | _dropArrowController.dispose(); 142 | _bottomAppBarController.dispose(); 143 | super.dispose(); 144 | } 145 | 146 | void _onDestinationSelected(String destination) { 147 | var emailStore = Provider.of( 148 | context, 149 | listen: false, 150 | ); 151 | 152 | if (emailStore.onMailView) { 153 | emailStore.currentlySelectedEmailId = -1; 154 | } 155 | 156 | if (emailStore.currentlySelectedInbox != destination) { 157 | emailStore.currentlySelectedInbox = destination; 158 | } 159 | 160 | setState(() {}); 161 | } 162 | 163 | bool get _bottomDrawerVisible { 164 | final status = _drawerController.status; 165 | return status == AnimationStatus.completed || 166 | status == AnimationStatus.forward; 167 | } 168 | 169 | void _toggleBottomDrawerVisibility() { 170 | if (_drawerController.value < 0.4) { 171 | Provider.of( 172 | context, 173 | listen: false, 174 | ).bottomDrawerVisible = true; 175 | _drawerController.animateTo(0.4, curve: standardEasing); 176 | _dropArrowController.animateTo(0.35, curve: standardEasing); 177 | return; 178 | } 179 | 180 | _dropArrowController.forward(); 181 | _drawerController.fling( 182 | velocity: _bottomDrawerVisible ? -_kFlingVelocity : _kFlingVelocity, 183 | ); 184 | } 185 | 186 | double get _bottomDrawerHeight { 187 | final renderBox = 188 | _bottomDrawerKey.currentContext!.findRenderObject() as RenderBox; 189 | return renderBox.size.height; 190 | } 191 | 192 | void _handleDragUpdate(DragUpdateDetails details) { 193 | _drawerController.value -= details.primaryDelta! / _bottomDrawerHeight; 194 | } 195 | 196 | void _handleDragEnd(DragEndDetails details) { 197 | if (_drawerController.isAnimating || 198 | _drawerController.status == AnimationStatus.completed) { 199 | return; 200 | } 201 | 202 | final flingVelocity = 203 | details.velocity.pixelsPerSecond.dy / _bottomDrawerHeight; 204 | 205 | if (flingVelocity < 0.0) { 206 | _drawerController.fling( 207 | velocity: math.max(_kFlingVelocity, -flingVelocity), 208 | ); 209 | } else if (flingVelocity > 0.0) { 210 | _dropArrowController.forward(); 211 | _drawerController.fling( 212 | velocity: math.min(-_kFlingVelocity, -flingVelocity), 213 | ); 214 | } else { 215 | if (_drawerController.value < 0.6) { 216 | _dropArrowController.forward(); 217 | } 218 | _drawerController.fling( 219 | velocity: 220 | _drawerController.value < 0.6 ? -_kFlingVelocity : _kFlingVelocity, 221 | ); 222 | } 223 | } 224 | 225 | bool _handleScrollNotification(ScrollNotification notification) { 226 | if (notification.depth == 0) { 227 | if (notification is UserScrollNotification) { 228 | switch (notification.direction) { 229 | case ScrollDirection.forward: 230 | _bottomAppBarController.forward(); 231 | break; 232 | case ScrollDirection.reverse: 233 | _bottomAppBarController.reverse(); 234 | break; 235 | case ScrollDirection.idle: 236 | break; 237 | } 238 | } 239 | } 240 | return false; 241 | } 242 | 243 | Widget _buildStack(BuildContext context, BoxConstraints constraints) { 244 | final drawerSize = constraints.biggest; 245 | final drawerTop = drawerSize.height; 246 | final ValueChanged updateMailbox = _onDestinationSelected; 247 | 248 | final drawerAnimation = RelativeRectTween( 249 | begin: RelativeRect.fromLTRB(0.0, drawerTop, 0.0, 0.0), 250 | end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0), 251 | ).animate(_drawerCurve); 252 | 253 | return Stack( 254 | clipBehavior: Clip.none, 255 | key: _bottomDrawerKey, 256 | children: [ 257 | NotificationListener( 258 | onNotification: _handleScrollNotification, 259 | child: _MailRouter( 260 | drawerController: _drawerController, 261 | ), 262 | ), 263 | GestureDetector( 264 | onTap: () { 265 | _drawerController.reverse(); 266 | _dropArrowController.reverse(); 267 | }, 268 | child: Visibility( 269 | maintainAnimation: true, 270 | maintainState: true, 271 | visible: _bottomDrawerVisible, 272 | child: FadeTransition( 273 | opacity: _drawerCurve, 274 | child: Container( 275 | height: MediaQuery.of(context).size.height, 276 | width: MediaQuery.of(context).size.width, 277 | color: Theme.of(context).bottomSheetTheme.modalBackgroundColor, 278 | ), 279 | ), 280 | ), 281 | ), 282 | PositionedTransition( 283 | rect: drawerAnimation, 284 | child: Visibility( 285 | visible: _bottomDrawerVisible, 286 | child: BottomDrawer( 287 | onVerticalDragUpdate: _handleDragUpdate, 288 | onVerticalDragEnd: _handleDragEnd, 289 | leading: _BottomDrawerDestinations( 290 | destinations: _navigationDestinations, 291 | drawerController: _drawerController, 292 | dropArrowController: _dropArrowController, 293 | onItemTapped: updateMailbox, 294 | ), 295 | trailing: _BottomDrawerFolderSection(folders: _folders), 296 | ), 297 | ), 298 | ), 299 | ], 300 | ); 301 | } 302 | 303 | @override 304 | Widget build(BuildContext context) { 305 | return Scaffold( 306 | extendBody: true, 307 | body: LayoutBuilder( 308 | builder: _buildStack, 309 | ), 310 | bottomNavigationBar: _AnimatedBottomAppBar( 311 | bottomAppBarController: _bottomAppBarController, 312 | bottomAppBarCurve: _bottomAppBarCurve, 313 | bottomDrawerVisible: _bottomDrawerVisible, 314 | drawerController: _drawerController, 315 | dropArrowCurve: _dropArrowCurve, 316 | toggleBottomDrawerVisibility: _toggleBottomDrawerVisibility, 317 | ), 318 | floatingActionButton: _bottomDrawerVisible 319 | ? null 320 | : const Padding( 321 | padding: EdgeInsetsDirectional.only(bottom: 8), 322 | child: _ReplyFab(), 323 | ), 324 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, 325 | ); 326 | } 327 | } 328 | 329 | class _AnimatedBottomAppBar extends StatelessWidget { 330 | const _AnimatedBottomAppBar({ 331 | required this.bottomAppBarController, 332 | required this.bottomAppBarCurve, 333 | required this.bottomDrawerVisible, 334 | required this.drawerController, 335 | required this.dropArrowCurve, 336 | required this.toggleBottomDrawerVisibility, 337 | }); 338 | 339 | final AnimationController bottomAppBarController; 340 | final Animation bottomAppBarCurve; 341 | final bool bottomDrawerVisible; 342 | final AnimationController drawerController; 343 | final Animation dropArrowCurve; 344 | final VoidCallback toggleBottomDrawerVisibility; 345 | 346 | @override 347 | Widget build(BuildContext context) { 348 | var fadeOut = Tween(begin: 1, end: -1).animate( 349 | drawerController.drive(CurveTween(curve: standardEasing)), 350 | ); 351 | 352 | return Selector( 353 | selector: (context, emailStore) => emailStore.onMailView, 354 | builder: (context, onMailView, child) { 355 | bottomAppBarController.forward(); 356 | 357 | return SizeTransition( 358 | sizeFactor: bottomAppBarCurve, 359 | axisAlignment: -1, 360 | child: Padding( 361 | padding: const EdgeInsetsDirectional.only(top: 2), 362 | child: BottomAppBar( 363 | shape: const WaterfallNotchedRectangle(), 364 | notchMargin: 6, 365 | child: Container( 366 | color: Colors.transparent, 367 | height: kToolbarHeight, 368 | child: Row( 369 | mainAxisSize: MainAxisSize.max, 370 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 371 | children: [ 372 | InkWell( 373 | borderRadius: const BorderRadius.all(Radius.circular(16)), 374 | onTap: toggleBottomDrawerVisibility, 375 | child: Row( 376 | children: [ 377 | const SizedBox(width: 16), 378 | RotationTransition( 379 | turns: Tween( 380 | begin: 0.0, 381 | end: 1.0, 382 | ).animate(dropArrowCurve), 383 | child: const Icon( 384 | Icons.arrow_drop_up, 385 | color: ReplyColors.white50, 386 | ), 387 | ), 388 | const SizedBox(width: 8), 389 | const _ReplyLogo(), 390 | const SizedBox(width: 10), 391 | // TODO: Add Fade through transition between disappearing mailbox title (Motion) 392 | onMailView 393 | ? const SizedBox(width: 48) 394 | : FadeTransition( 395 | opacity: fadeOut, 396 | child: Selector( 397 | selector: (context, emailStore) => 398 | emailStore.currentlySelectedInbox, 399 | builder: ( 400 | context, 401 | currentlySelectedInbox, 402 | child, 403 | ) { 404 | return Text( 405 | currentlySelectedInbox, 406 | style: Theme.of(context) 407 | .textTheme 408 | .bodyLarge! 409 | .copyWith( 410 | color: ReplyColors.white50, 411 | ), 412 | ); 413 | }, 414 | ), 415 | ), 416 | ], 417 | ), 418 | ), 419 | Expanded( 420 | child: Container( 421 | color: Colors.transparent, 422 | child: _BottomAppBarActionItems( 423 | drawerController: drawerController, 424 | drawerVisible: bottomDrawerVisible, 425 | ), 426 | ), 427 | ), 428 | ], 429 | ), 430 | ), 431 | ), 432 | ), 433 | ); 434 | }, 435 | ); 436 | } 437 | } 438 | 439 | class _BottomAppBarActionItems extends StatelessWidget { 440 | const _BottomAppBarActionItems({ 441 | required this.drawerController, 442 | required this.drawerVisible, 443 | }); 444 | 445 | final AnimationController drawerController; 446 | final bool drawerVisible; 447 | 448 | @override 449 | Widget build(BuildContext context) { 450 | return Consumer( 451 | builder: (context, model, child) { 452 | final onMailView = model.onMailView; 453 | var radius = const Radius.circular(12); 454 | final modalBorder = BorderRadius.only( 455 | topRight: radius, 456 | topLeft: radius, 457 | ); 458 | Color? starIconColor; 459 | 460 | if (onMailView) { 461 | var currentEmailStarred = false; 462 | 463 | if (model.emails[model.currentlySelectedInbox]!.isNotEmpty) { 464 | currentEmailStarred = model.isEmailStarred( 465 | model.emails[model.currentlySelectedInbox]! 466 | .elementAt(model.currentlySelectedEmailId), 467 | ); 468 | } 469 | 470 | starIconColor = currentEmailStarred 471 | ? Theme.of(context).colorScheme.secondary 472 | : ReplyColors.white50; 473 | } 474 | 475 | // TODO: Add Fade through transition between bottom app bar actions (Motion) 476 | return drawerVisible 477 | ? Align( 478 | alignment: AlignmentDirectional.bottomEnd, 479 | child: IconButton( 480 | icon: const Icon(Icons.settings), 481 | color: ReplyColors.white50, 482 | onPressed: () async { 483 | drawerController.reverse(); 484 | showModalBottomSheet( 485 | context: context, 486 | shape: RoundedRectangleBorder( 487 | borderRadius: modalBorder, 488 | ), 489 | builder: (context) => const SettingsBottomSheet(), 490 | ); 491 | }, 492 | ), 493 | ) 494 | : onMailView 495 | ? Row( 496 | mainAxisSize: MainAxisSize.max, 497 | mainAxisAlignment: MainAxisAlignment.end, 498 | children: [ 499 | IconButton( 500 | icon: ImageIcon( 501 | const AssetImage( 502 | '$_iconAssetLocation/twotone_star.png', 503 | package: _assetsPackage, 504 | ), 505 | color: starIconColor, 506 | ), 507 | onPressed: () { 508 | model.starEmail( 509 | model.currentlySelectedInbox, 510 | model.currentlySelectedEmailId, 511 | ); 512 | if (model.currentlySelectedInbox == 'Starred') { 513 | mobileMailNavKey.currentState!.pop(); 514 | model.currentlySelectedEmailId = -1; 515 | } 516 | }, 517 | color: ReplyColors.white50, 518 | ), 519 | IconButton( 520 | icon: const ImageIcon( 521 | AssetImage( 522 | '$_iconAssetLocation/twotone_delete.png', 523 | package: _assetsPackage, 524 | ), 525 | ), 526 | onPressed: () { 527 | model.deleteEmail( 528 | model.currentlySelectedInbox, 529 | model.currentlySelectedEmailId, 530 | ); 531 | 532 | mobileMailNavKey.currentState!.pop(); 533 | model.currentlySelectedEmailId = -1; 534 | }, 535 | color: ReplyColors.white50, 536 | ), 537 | IconButton( 538 | icon: const Icon(Icons.more_vert), 539 | onPressed: () {}, 540 | color: ReplyColors.white50, 541 | ), 542 | ], 543 | ) 544 | : Align( 545 | alignment: AlignmentDirectional.bottomEnd, 546 | child: IconButton( 547 | icon: const Icon(Icons.search), 548 | color: ReplyColors.white50, 549 | onPressed: () { 550 | Provider.of( 551 | context, 552 | listen: false, 553 | ).routePath = const ReplySearchPath(); 554 | }, 555 | ), 556 | ); 557 | }, 558 | ); 559 | } 560 | } 561 | 562 | class _BottomDrawerDestinations extends StatelessWidget { 563 | const _BottomDrawerDestinations({ 564 | required this.destinations, 565 | required this.drawerController, 566 | required this.dropArrowController, 567 | required this.onItemTapped, 568 | }); 569 | 570 | final List<_Destination> destinations; 571 | final AnimationController drawerController; 572 | final AnimationController dropArrowController; 573 | final ValueChanged onItemTapped; 574 | 575 | @override 576 | Widget build(BuildContext context) { 577 | final theme = Theme.of(context); 578 | 579 | return Column( 580 | children: [ 581 | for (var destination in destinations) 582 | InkWell( 583 | onTap: () { 584 | onItemTapped(destination.name); 585 | drawerController.reverse(); 586 | dropArrowController.forward(); 587 | }, 588 | child: Selector( 589 | selector: (context, emailStore) => 590 | emailStore.currentlySelectedInbox, 591 | builder: (context, currentlySelectedInbox, child) { 592 | return ListTile( 593 | leading: ImageIcon( 594 | AssetImage( 595 | destination.icon, 596 | package: _assetsPackage, 597 | ), 598 | color: destination.name == currentlySelectedInbox 599 | ? theme.colorScheme.secondary 600 | : ReplyColors.white50.withOpacity(0.64), 601 | ), 602 | title: Text( 603 | destination.name, 604 | style: theme.textTheme.bodyMedium!.copyWith( 605 | color: destination.name == currentlySelectedInbox 606 | ? theme.colorScheme.secondary 607 | : ReplyColors.white50.withOpacity(0.64), 608 | ), 609 | ), 610 | ); 611 | }, 612 | ), 613 | ), 614 | ], 615 | ); 616 | } 617 | } 618 | 619 | class _Destination { 620 | const _Destination({ 621 | required this.name, 622 | required this.icon, 623 | required this.index, 624 | }); 625 | 626 | final String name; 627 | final String icon; 628 | final int index; 629 | } 630 | 631 | class _BottomDrawerFolderSection extends StatelessWidget { 632 | const _BottomDrawerFolderSection({required this.folders}); 633 | 634 | final Map folders; 635 | 636 | @override 637 | Widget build(BuildContext context) { 638 | final theme = Theme.of(context); 639 | 640 | return Column( 641 | children: [ 642 | for (var folder in folders.keys) 643 | InkWell( 644 | onTap: () {}, 645 | child: ListTile( 646 | leading: ImageIcon( 647 | AssetImage( 648 | folders[folder]!, 649 | package: _assetsPackage, 650 | ), 651 | color: ReplyColors.white50.withOpacity(0.64), 652 | ), 653 | title: Text( 654 | folder, 655 | style: theme.textTheme.bodyMedium!.copyWith( 656 | color: ReplyColors.white50.withOpacity(0.64), 657 | ), 658 | ), 659 | ), 660 | ), 661 | ], 662 | ); 663 | } 664 | } 665 | 666 | class _MailRouter extends StatelessWidget { 667 | const _MailRouter({required this.drawerController}); 668 | 669 | final AnimationController drawerController; 670 | 671 | @override 672 | Widget build(BuildContext context) { 673 | final RootBackButtonDispatcher backButtonDispatcher = 674 | Router.of(context).backButtonDispatcher as RootBackButtonDispatcher; 675 | 676 | return Router( 677 | routerDelegate: 678 | MailViewRouterDelegate(drawerController: drawerController), 679 | backButtonDispatcher: ChildBackButtonDispatcher(backButtonDispatcher) 680 | ..takePriority(), 681 | ); 682 | } 683 | } 684 | 685 | class _ReplyLogo extends StatelessWidget { 686 | const _ReplyLogo({Key? key}) : super(key: key); 687 | 688 | @override 689 | Widget build(BuildContext context) { 690 | return const ImageIcon( 691 | AssetImage( 692 | 'reply/reply_logo.png', 693 | package: _assetsPackage, 694 | ), 695 | size: 32, 696 | color: ReplyColors.white50, 697 | ); 698 | } 699 | } 700 | 701 | class _ReplyFab extends StatefulWidget { 702 | const _ReplyFab(); 703 | 704 | @override 705 | _ReplyFabState createState() => _ReplyFabState(); 706 | } 707 | 708 | class _ReplyFabState extends State<_ReplyFab> 709 | with SingleTickerProviderStateMixin { 710 | // TODO: Add Fade through transition between compose and reply FAB (Motion) 711 | static const double _mobileFabDimension = 56; 712 | 713 | @override 714 | Widget build(BuildContext context) { 715 | final theme = Theme.of(context); 716 | const circleFabBorder = CircleBorder(); 717 | 718 | return Selector( 719 | selector: (context, emailStore) => emailStore.onMailView, 720 | builder: (context, onMailView, child) { 721 | // TODO: Add Fade through transition between compose and reply FAB (Motion) 722 | final fabSwitcher = onMailView 723 | ? const Icon( 724 | Icons.reply_all, 725 | color: Colors.black, 726 | ) 727 | : const Icon( 728 | Icons.create, 729 | color: Colors.black, 730 | ); 731 | final tooltip = onMailView ? 'Reply' : 'Compose'; 732 | 733 | // TODO: Add Container Transform from FAB to compose email page (Motion) 734 | return Material( 735 | color: theme.colorScheme.secondary, 736 | shape: circleFabBorder, 737 | child: Tooltip( 738 | message: tooltip, 739 | child: InkWell( 740 | customBorder: circleFabBorder, 741 | onTap: () { 742 | Provider.of( 743 | context, 744 | listen: false, 745 | ).onCompose = true; 746 | 747 | Navigator.of(context).push( 748 | PageRouteBuilder( 749 | pageBuilder: ( 750 | BuildContext context, 751 | Animation animation, 752 | Animation secondaryAnimation, 753 | ) { 754 | return const ComposePage(); 755 | }, 756 | ), 757 | ); 758 | }, 759 | child: SizedBox( 760 | height: _mobileFabDimension, 761 | width: _mobileFabDimension, 762 | child: Center( 763 | child: fabSwitcher, 764 | ), 765 | ), 766 | ), 767 | ), 768 | ); 769 | }, 770 | ); 771 | } 772 | } 773 | 774 | // TODO: Add Fade through transition between compose and reply FAB (Motion) 775 | -------------------------------------------------------------------------------- /lib/inbox.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'mail_card_preview.dart'; 5 | import 'model/email_store.dart'; 6 | 7 | class InboxPage extends StatelessWidget { 8 | const InboxPage({required this.destination, Key? key}) : super(key: key); 9 | 10 | final String destination; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | const horizontalPadding = 4.0; 15 | 16 | return Consumer( 17 | builder: (context, model, child) { 18 | return SafeArea( 19 | bottom: false, 20 | child: model.emails[destination]!.isEmpty 21 | ? Center( 22 | child: Text( 23 | 'Empty in ${destination.toLowerCase()}', 24 | ), 25 | ) 26 | : ListView.separated( 27 | itemCount: model.emails[destination]!.length, 28 | padding: const EdgeInsetsDirectional.only( 29 | start: horizontalPadding, 30 | end: horizontalPadding, 31 | bottom: kToolbarHeight, 32 | ), 33 | primary: false, 34 | separatorBuilder: (context, index) => 35 | const SizedBox(height: 4), 36 | itemBuilder: (context, index) { 37 | return MailPreviewCard( 38 | id: index, 39 | email: model.emails[destination]!.elementAt(index), 40 | onDelete: () => model.deleteEmail(destination, index), 41 | onStar: () => model.starEmail(destination, index), 42 | ); 43 | }, 44 | ), 45 | ); 46 | }, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/mail_card_preview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'colors.dart'; 5 | import 'home.dart'; 6 | import 'mail_view_page.dart'; 7 | import 'model/email_model.dart'; 8 | import 'model/email_store.dart'; 9 | import 'profile_avatar.dart'; 10 | 11 | class MailPreviewCard extends StatelessWidget { 12 | const MailPreviewCard({ 13 | Key? key, 14 | required this.id, 15 | required this.email, 16 | required this.onDelete, 17 | required this.onStar, 18 | }) : super(key: key); 19 | 20 | final int id; 21 | final Email email; 22 | final VoidCallback onDelete; 23 | final VoidCallback onStar; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final theme = Theme.of(context); 28 | 29 | final currentEmailStarred = Provider.of( 30 | context, 31 | listen: false, 32 | ).isEmailStarred(email); 33 | 34 | final colorScheme = theme.colorScheme; 35 | final mailPreview = _MailPreview( 36 | id: id, 37 | email: email, 38 | onStar: onStar, 39 | onDelete: onDelete, 40 | ); 41 | final onStarredInbox = Provider.of( 42 | context, 43 | listen: false, 44 | ).currentlySelectedInbox == 45 | 'Starred'; 46 | 47 | // TODO: Add Container Transform transition from email list to email detail page (Motion) 48 | return Material( 49 | color: theme.cardColor, 50 | child: InkWell( 51 | onTap: () { 52 | Provider.of( 53 | context, 54 | listen: false, 55 | ).currentlySelectedEmailId = id; 56 | 57 | mobileMailNavKey.currentState!.push( 58 | PageRouteBuilder( 59 | pageBuilder: (BuildContext context, Animation animation, 60 | Animation secondaryAnimation) { 61 | return MailViewPage(id: id, email: email); 62 | }, 63 | ), 64 | ); 65 | }, 66 | child: Dismissible( 67 | key: ObjectKey(email), 68 | dismissThresholds: const { 69 | DismissDirection.startToEnd: 0.8, 70 | DismissDirection.endToStart: 0.4, 71 | }, 72 | onDismissed: (direction) { 73 | switch (direction) { 74 | case DismissDirection.endToStart: 75 | if (onStarredInbox) { 76 | onStar(); 77 | } 78 | break; 79 | case DismissDirection.startToEnd: 80 | onDelete(); 81 | break; 82 | default: 83 | } 84 | }, 85 | background: _DismissibleContainer( 86 | icon: 'twotone_delete', 87 | backgroundColor: colorScheme.primary, 88 | iconColor: ReplyColors.blue50, 89 | alignment: Alignment.centerLeft, 90 | padding: const EdgeInsetsDirectional.only(start: 20), 91 | ), 92 | confirmDismiss: (direction) async { 93 | if (direction == DismissDirection.endToStart) { 94 | if (onStarredInbox) { 95 | return true; 96 | } 97 | onStar(); 98 | return false; 99 | } else { 100 | return true; 101 | } 102 | }, 103 | secondaryBackground: _DismissibleContainer( 104 | icon: 'twotone_star', 105 | backgroundColor: currentEmailStarred 106 | ? colorScheme.secondary 107 | : theme.scaffoldBackgroundColor, 108 | iconColor: currentEmailStarred 109 | ? colorScheme.onSecondary 110 | : colorScheme.onBackground, 111 | alignment: Alignment.centerRight, 112 | padding: const EdgeInsetsDirectional.only(end: 20), 113 | ), 114 | child: mailPreview, 115 | ), 116 | ), 117 | ); 118 | } 119 | } 120 | 121 | // TODO: Add Container Transform transition from email list to email detail page (Motion) 122 | 123 | class _DismissibleContainer extends StatelessWidget { 124 | const _DismissibleContainer({ 125 | required this.icon, 126 | required this.backgroundColor, 127 | required this.iconColor, 128 | required this.alignment, 129 | required this.padding, 130 | }); 131 | 132 | final String icon; 133 | final Color backgroundColor; 134 | final Color iconColor; 135 | final Alignment alignment; 136 | final EdgeInsetsDirectional padding; 137 | 138 | @override 139 | Widget build(BuildContext context) { 140 | return AnimatedContainer( 141 | alignment: alignment, 142 | color: backgroundColor, 143 | curve: standardEasing, 144 | duration: kThemeAnimationDuration, 145 | padding: padding, 146 | child: Material( 147 | color: Colors.transparent, 148 | child: ImageIcon( 149 | AssetImage( 150 | 'reply/icons/$icon.png', 151 | package: 'flutter_gallery_assets', 152 | ), 153 | size: 36, 154 | color: iconColor, 155 | ), 156 | ), 157 | ); 158 | } 159 | } 160 | 161 | class _MailPreview extends StatelessWidget { 162 | const _MailPreview({ 163 | required this.id, 164 | required this.email, 165 | this.onStar, 166 | this.onDelete, 167 | }); 168 | 169 | final int id; 170 | final Email email; 171 | final VoidCallback? onStar; 172 | final VoidCallback? onDelete; 173 | 174 | @override 175 | Widget build(BuildContext context) { 176 | final textTheme = Theme.of(context).textTheme; 177 | var emailStore = Provider.of( 178 | context, 179 | listen: false, 180 | ); 181 | 182 | return LayoutBuilder( 183 | builder: (context, constraints) { 184 | return ConstrainedBox( 185 | constraints: BoxConstraints(maxHeight: constraints.maxHeight), 186 | child: Padding( 187 | padding: const EdgeInsets.all(20), 188 | child: Column( 189 | crossAxisAlignment: CrossAxisAlignment.start, 190 | mainAxisSize: MainAxisSize.min, 191 | children: [ 192 | Row( 193 | mainAxisSize: MainAxisSize.max, 194 | crossAxisAlignment: CrossAxisAlignment.start, 195 | children: [ 196 | Expanded( 197 | child: Column( 198 | crossAxisAlignment: CrossAxisAlignment.start, 199 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 200 | children: [ 201 | Text( 202 | '${email.sender} - ${email.time}', 203 | style: textTheme.bodySmall, 204 | ), 205 | const SizedBox(height: 4), 206 | Text(email.subject, style: textTheme.headlineSmall), 207 | const SizedBox(height: 16), 208 | ], 209 | ), 210 | ), 211 | _MailPreviewActionBar( 212 | avatar: email.avatar, 213 | isStarred: emailStore.isEmailStarred(email), 214 | onStar: onStar, 215 | onDelete: onDelete, 216 | ), 217 | ], 218 | ), 219 | Padding( 220 | padding: const EdgeInsetsDirectional.only( 221 | end: 20, 222 | ), 223 | child: Text( 224 | email.message, 225 | overflow: TextOverflow.ellipsis, 226 | maxLines: 1, 227 | style: textTheme.bodyMedium, 228 | ), 229 | ), 230 | if (email.containsPictures) ...[ 231 | Flexible( 232 | fit: FlexFit.loose, 233 | child: Column( 234 | children: const [ 235 | SizedBox(height: 20), 236 | _PicturePreview(), 237 | ], 238 | ), 239 | ), 240 | ], 241 | ], 242 | ), 243 | ), 244 | ); 245 | }, 246 | ); 247 | } 248 | } 249 | 250 | class _PicturePreview extends StatelessWidget { 251 | const _PicturePreview(); 252 | 253 | @override 254 | Widget build(BuildContext context) { 255 | return SizedBox( 256 | height: 96, 257 | child: ListView.builder( 258 | itemCount: 4, 259 | scrollDirection: Axis.horizontal, 260 | itemBuilder: (context, index) { 261 | return Padding( 262 | padding: const EdgeInsetsDirectional.only(end: 4), 263 | child: Image.asset( 264 | 'reply/attachments/paris_${index + 1}.jpg', 265 | gaplessPlayback: true, 266 | package: 'flutter_gallery_assets', 267 | ), 268 | ); 269 | }, 270 | ), 271 | ); 272 | } 273 | } 274 | 275 | class _MailPreviewActionBar extends StatelessWidget { 276 | const _MailPreviewActionBar({ 277 | required this.avatar, 278 | required this.isStarred, 279 | this.onStar, 280 | this.onDelete, 281 | }); 282 | 283 | final String avatar; 284 | final bool isStarred; 285 | final VoidCallback? onStar; 286 | final VoidCallback? onDelete; 287 | 288 | @override 289 | Widget build(BuildContext context) { 290 | return Row( 291 | children: [ 292 | ProfileAvatar(avatar: avatar), 293 | ], 294 | ); 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /lib/mail_view_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'model/email_model.dart'; 5 | import 'model/email_store.dart'; 6 | import 'profile_avatar.dart'; 7 | 8 | class MailViewPage extends StatelessWidget { 9 | const MailViewPage({Key? key, required this.id, required this.email}) 10 | : super(key: key); 11 | 12 | final int id; 13 | final Email email; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Scaffold( 18 | body: SafeArea( 19 | bottom: false, 20 | child: SizedBox( 21 | height: double.infinity, 22 | child: Material( 23 | color: Theme.of(context).cardColor, 24 | child: SingleChildScrollView( 25 | padding: const EdgeInsetsDirectional.only( 26 | top: 42, 27 | start: 20, 28 | end: 20, 29 | ), 30 | child: Column( 31 | crossAxisAlignment: CrossAxisAlignment.start, 32 | children: [ 33 | _MailViewHeader(email: email), 34 | const SizedBox(height: 32), 35 | _MailViewBody(message: email.message), 36 | if (email.containsPictures) ...[ 37 | const SizedBox(height: 28), 38 | const _PictureGrid(), 39 | ], 40 | const SizedBox(height: kToolbarHeight), 41 | ], 42 | ), 43 | ), 44 | ), 45 | ), 46 | ), 47 | ); 48 | } 49 | } 50 | 51 | class _MailViewHeader extends StatelessWidget { 52 | const _MailViewHeader({ 53 | required this.email, 54 | }); 55 | 56 | final Email email; 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | final textTheme = Theme.of(context).textTheme; 61 | 62 | return Column( 63 | children: [ 64 | Row( 65 | crossAxisAlignment: CrossAxisAlignment.start, 66 | children: [ 67 | Expanded( 68 | child: Text( 69 | email.subject, 70 | style: textTheme.headlineMedium!.copyWith(height: 1.1), 71 | ), 72 | ), 73 | IconButton( 74 | icon: const Icon(Icons.keyboard_arrow_down), 75 | onPressed: () { 76 | Provider.of( 77 | context, 78 | listen: false, 79 | ).currentlySelectedEmailId = -1; 80 | Navigator.pop(context); 81 | }, 82 | splashRadius: 20, 83 | ), 84 | ], 85 | ), 86 | const SizedBox(height: 16), 87 | Row( 88 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 89 | children: [ 90 | Column( 91 | crossAxisAlignment: CrossAxisAlignment.start, 92 | mainAxisSize: MainAxisSize.min, 93 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 94 | children: [ 95 | Text('${email.sender} - ${email.time}'), 96 | const SizedBox(height: 4), 97 | Text( 98 | 'To ${email.recipients},', 99 | style: textTheme.bodySmall!.copyWith( 100 | color: Theme.of(context) 101 | .colorScheme 102 | .onSurface 103 | .withOpacity(0.64), 104 | ), 105 | ), 106 | ], 107 | ), 108 | Padding( 109 | padding: const EdgeInsetsDirectional.only(end: 4), 110 | child: ProfileAvatar(avatar: email.avatar), 111 | ), 112 | ], 113 | ), 114 | ], 115 | ); 116 | } 117 | } 118 | 119 | class _MailViewBody extends StatelessWidget { 120 | const _MailViewBody({required this.message}); 121 | 122 | final String message; 123 | 124 | @override 125 | Widget build(BuildContext context) { 126 | return Text( 127 | message, 128 | style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 16), 129 | ); 130 | } 131 | } 132 | 133 | class _PictureGrid extends StatelessWidget { 134 | const _PictureGrid(); 135 | 136 | @override 137 | Widget build(BuildContext context) { 138 | return GridView.builder( 139 | shrinkWrap: true, 140 | physics: const NeverScrollableScrollPhysics(), 141 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 142 | crossAxisCount: 2, 143 | crossAxisSpacing: 4, 144 | mainAxisSpacing: 4, 145 | ), 146 | itemCount: 4, 147 | itemBuilder: (context, index) { 148 | return Image.asset( 149 | 'reply/attachments/paris_${index + 1}.jpg', 150 | gaplessPlayback: true, 151 | package: 'flutter_gallery_assets', 152 | fit: BoxFit.fill, 153 | ); 154 | }, 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/mail_view_router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:reply/custom_transition_page.dart'; 5 | 6 | import 'home.dart'; 7 | import 'inbox.dart'; 8 | import 'model/email_store.dart'; 9 | 10 | class MailViewRouterDelegate extends RouterDelegate 11 | with ChangeNotifier, PopNavigatorRouterDelegateMixin { 12 | MailViewRouterDelegate({required this.drawerController}); 13 | 14 | final AnimationController drawerController; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | bool handlePopPage(Route route, dynamic result) { 19 | return false; 20 | } 21 | 22 | return Selector( 23 | selector: (context, emailStore) => emailStore.currentlySelectedInbox, 24 | builder: (context, currentlySelectedInbox, child) { 25 | return Navigator( 26 | key: navigatorKey, 27 | onPopPage: handlePopPage, 28 | pages: [ 29 | // TODO: Add Fade through transition between mailbox pages (Motion) 30 | CustomTransitionPage( 31 | transitionKey: ValueKey(currentlySelectedInbox), 32 | screen: InboxPage( 33 | destination: currentlySelectedInbox, 34 | ), 35 | ) 36 | ], 37 | ); 38 | }, 39 | ); 40 | } 41 | 42 | @override 43 | GlobalKey get navigatorKey => mobileMailNavKey; 44 | 45 | @override 46 | Future popRoute() { 47 | var emailStore = 48 | Provider.of(navigatorKey.currentContext!, listen: false); 49 | bool onCompose = emailStore.onCompose; 50 | 51 | bool onMailView = emailStore.onMailView; 52 | 53 | // Handles the back button press when we are on the HomePage. When the 54 | // drawer is visible reverse the drawer and do nothing else. If the drawer 55 | // is not visible then we check if we are on the main mailbox. If we are on 56 | // main mailbox then our app will close, if not then it will set the 57 | // mailbox to the main mailbox. 58 | if (!(onMailView || onCompose)) { 59 | if (emailStore.bottomDrawerVisible) { 60 | drawerController.reverse(); 61 | return SynchronousFuture(true); 62 | } 63 | 64 | if (emailStore.currentlySelectedInbox != 'Inbox') { 65 | emailStore.currentlySelectedInbox = 'Inbox'; 66 | return SynchronousFuture(true); 67 | } 68 | return SynchronousFuture(false); 69 | } 70 | 71 | // Handles the back button when on the [ComposePage]. 72 | if (onCompose) { 73 | // TODO: Add Container Transform from FAB to compose email page (Motion) 74 | emailStore.onCompose = false; 75 | return SynchronousFuture(false); 76 | } 77 | 78 | // Handles the back button when the bottom drawer is visible on the 79 | // MailView. Dismisses the drawer on back button press. 80 | if (emailStore.bottomDrawerVisible && onMailView) { 81 | drawerController.reverse(); 82 | return SynchronousFuture(true); 83 | } 84 | 85 | // Handles the back button press when on the MailView. If there is a route 86 | // to pop then pop it, and reset the currentlySelectedEmailId to -1 87 | // to notify listeners that we are no longer on the MailView. 88 | if (navigatorKey.currentState!.canPop()) { 89 | navigatorKey.currentState!.pop(); 90 | Provider.of(navigatorKey.currentContext!, listen: false) 91 | .currentlySelectedEmailId = -1; 92 | return SynchronousFuture(true); 93 | } 94 | 95 | return SynchronousFuture(false); 96 | } 97 | 98 | @override 99 | Future setNewRoutePath(void configuration) { 100 | // This function will never be called. 101 | throw UnimplementedError(); 102 | } 103 | } 104 | 105 | // TODO: Add Fade through transition between mailbox pages (Motion) 106 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'app.dart'; 4 | 5 | void main() => runApp(const ReplyApp()); 6 | -------------------------------------------------------------------------------- /lib/model/email_model.dart: -------------------------------------------------------------------------------- 1 | class Email { 2 | const Email({ 3 | required this.sender, 4 | required this.time, 5 | required this.subject, 6 | required this.message, 7 | required this.avatar, 8 | required this.recipients, 9 | required this.containsPictures, 10 | }); 11 | 12 | final String sender; 13 | final String time; 14 | final String subject; 15 | final String message; 16 | final String avatar; 17 | final String recipients; 18 | final bool containsPictures; 19 | } 20 | -------------------------------------------------------------------------------- /lib/model/email_store.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:reply/settings_bottom_sheet.dart'; 4 | 5 | import 'email_model.dart'; 6 | 7 | const _avatarsLocation = 'reply/avatars'; 8 | 9 | class EmailStore with ChangeNotifier { 10 | final _categories = >{ 11 | 'Inbox': _mainInbox, 12 | 'Starred': _starredInbox, 13 | 'Sent': _outbox, 14 | 'Trash': _trash, 15 | 'Spam': _spam, 16 | 'Drafts': _drafts, 17 | }; 18 | 19 | static final _mainInbox = { 20 | const Email( 21 | sender: 'Google Express', 22 | time: '15 minutes ago', 23 | subject: 'Package shipped!', 24 | message: 'Cucumber Mask Facial has shipped.\n\n' 25 | 'Keep an eye out for a package to arrive between this Thursday and next Tuesday. If for any reason you don\'t receive your package before the end of next week, please reach out to us for details on your shipment.\n\n' 26 | 'As always, thank you for shopping with us and we hope you love our specially formulated Cucumber Mask!', 27 | avatar: '$_avatarsLocation/avatar_express.png', 28 | recipients: 'Jeff', 29 | containsPictures: false, 30 | ), 31 | const Email( 32 | sender: 'Ali Connors', 33 | time: '4 hrs ago', 34 | subject: 'Brunch this weekend?', 35 | message: 36 | 'I\'ll be in your neighborhood doing errands and was hoping to catch you for a coffee this Saturday. If you don\'t have anything scheduled, it would be great to see you! It feels like its been forever.\n\n' 37 | 'If we do get a chance to get together, remind me to tell you about Kim. She stopped over at the house to say hey to the kids and told me all about her trip to Mexico.\n\n' 38 | 'Talk to you soon,\n\n' 39 | 'Ali', 40 | avatar: '$_avatarsLocation/avatar_5.jpg', 41 | recipients: 'Jeff', 42 | containsPictures: false, 43 | ), 44 | const Email( 45 | sender: 'Allison Trabucco', 46 | time: '5 hrs ago', 47 | subject: 'Bonjour from Paris', 48 | message: 'Here are some great shots from my trip...', 49 | avatar: '$_avatarsLocation/avatar_3.jpg', 50 | recipients: 'Jeff', 51 | containsPictures: true, 52 | ), 53 | const Email( 54 | sender: 'Trevor Hansen', 55 | time: '9 hrs ago', 56 | subject: 'Brazil trip', 57 | message: 58 | 'Thought we might be able to go over some details about our upcoming vacation.\n\n' 59 | 'I\'ve been doing a bit of research and have come across a few paces in Northern Brazil that I think we should check out. ' 60 | 'One, the north has some of the most predictable wind on the planet. ' 61 | 'I\'d love to get out on the ocean and kitesurf for a couple of days if we\'re going to be anywhere near or around Taiba. ' 62 | 'I hear it\'s beautiful there and if you\'re up for it, I\'d love to go. Other than that, I haven\'t spent too much time looking into places along our road trip route. ' 63 | 'I\'m assuming we can find places to stay and things to do as we drive and find places we think look interesting. But... I know you\'re more of a planner, so if you have ideas or places in mind, lets jot some ideas down!\n\n' 64 | 'Maybe we can jump on the phone later today if you have a second.', 65 | avatar: '$_avatarsLocation/avatar_8.jpg', 66 | recipients: 'Allison, Kim, Jeff', 67 | containsPictures: false, 68 | ), 69 | const Email( 70 | sender: 'Frank Hawkins', 71 | time: '10 hrs ago', 72 | subject: 'Update to Your Itinerary', 73 | message: '', 74 | avatar: '$_avatarsLocation/avatar_4.jpg', 75 | recipients: 'Jeff', 76 | containsPictures: false, 77 | ), 78 | const Email( 79 | sender: 'Google Express', 80 | time: '12 hrs ago', 81 | subject: 'Delivered', 82 | message: 'Your shoes should be waiting for you at home!', 83 | avatar: '$_avatarsLocation/avatar_express.png', 84 | recipients: 'Jeff', 85 | containsPictures: false, 86 | ), 87 | }; 88 | 89 | static final _starredInbox = {}; 90 | 91 | static final _outbox = { 92 | const Email( 93 | sender: 'Kim Alen', 94 | time: '4 hrs ago', 95 | subject: 'High school reunion?', 96 | message: 97 | 'Hi friends,\n\nI was at the grocery store on Sunday night.. when I ran into Genie Williams! I almost didn\'t recognize her afer 20 years!\n\n' 98 | 'Anyway, it turns out she is on the organizing committee for the high school reunion this fall. I don\'t know if you were planning on going or not, but she could definitely use our help in trying to track down lots of missing alums. ' 99 | 'If you can make it, we\'re doing a little phone-tree party at her place next Saturday, hoping that if we can find one person, thee more will...', 100 | avatar: '$_avatarsLocation/avatar_7.jpg', 101 | recipients: 'Jeff', 102 | containsPictures: false, 103 | ), 104 | const Email( 105 | sender: 'Sandra Adams', 106 | time: '7 hrs ago', 107 | subject: 'Recipe to try', 108 | message: 109 | 'Raspberry Pie: We should make this pie recipe tonight! The filling is ' 110 | 'very quick to put together.', 111 | avatar: '$_avatarsLocation/avatar_2.jpg', 112 | recipients: 'Jeff', 113 | containsPictures: false, 114 | ), 115 | }; 116 | 117 | static final _trash = { 118 | const Email( 119 | sender: 'Frank Hawkins', 120 | time: '4 hrs ago', 121 | subject: 'Your update on the Google Play Store is live!', 122 | message: 123 | 'Your update is now live on the Play Store and available for your alpha users to start testing.\n\n' 124 | 'Your alpha testers will be automatically notified. If you\'d rather send them a link directly, go to your Google Play Console and follow the instructions for obtaining an open alpha testing link.', 125 | avatar: '$_avatarsLocation/avatar_4.jpg', 126 | recipients: 'Jeff', 127 | containsPictures: false, 128 | ), 129 | const Email( 130 | sender: 'Allison Trabucco', 131 | time: '6 hrs ago', 132 | subject: 'Try a free TrailGo account', 133 | message: 134 | 'Looking for the best hiking trails in your area? TrailGo gets you on the path to the outdoors faster than you can pack a sandwich.\n\n' 135 | 'Whether you\'re an experienced hiker or just looking to get outside for the afternoon, there\'s a segment that suits you.', 136 | avatar: '$_avatarsLocation/avatar_3.jpg', 137 | recipients: 'Jeff', 138 | containsPictures: false, 139 | ), 140 | }; 141 | 142 | static final _spam = { 143 | const Email( 144 | sender: 'Allison Trabucco', 145 | time: '4 hrs ago', 146 | subject: 'Free money', 147 | message: 148 | 'You\'ve been selected as a winner in our latest raffle! To claim your prize, click on the link.', 149 | avatar: '$_avatarsLocation/avatar_3.jpg', 150 | recipients: 'Jeff', 151 | containsPictures: false, 152 | ), 153 | }; 154 | 155 | static final _drafts = { 156 | const Email( 157 | sender: 'Sandra Adams', 158 | time: '2 hrs ago', 159 | subject: '(No subject)', 160 | message: 'Hey,\n\n' 161 | 'Wanted to email and see what you thought of', 162 | avatar: '$_avatarsLocation/avatar_2.jpg', 163 | recipients: 'Jeff', 164 | containsPictures: false, 165 | ), 166 | }; 167 | 168 | int _currentlySelectedEmailId = -1; 169 | String _currentlySelectedInbox = 'Inbox'; 170 | bool _onCompose = false; 171 | bool _bottomDrawerVisible = false; 172 | ThemeMode _currentTheme = ThemeMode.system; 173 | SlowMotionSpeedSetting _currentSlowMotionSpeed = 174 | SlowMotionSpeedSetting.normal; 175 | 176 | Map> get emails => 177 | Map>.unmodifiable(_categories); 178 | 179 | void deleteEmail(String category, int id) { 180 | final email = _categories[category]!.elementAt(id); 181 | 182 | _categories.forEach( 183 | (key, value) { 184 | if (value.contains(email)) { 185 | value.remove(email); 186 | } 187 | }, 188 | ); 189 | 190 | notifyListeners(); 191 | } 192 | 193 | void starEmail(String category, int id) { 194 | final email = _categories[category]!.elementAt(id); 195 | var alreadyStarred = isEmailStarred(email); 196 | 197 | if (alreadyStarred) { 198 | _categories['Starred']!.remove(email); 199 | } else { 200 | _categories['Starred']!.add(email); 201 | } 202 | 203 | notifyListeners(); 204 | } 205 | 206 | bool get bottomDrawerVisible => _bottomDrawerVisible; 207 | int get currentlySelectedEmailId => _currentlySelectedEmailId; 208 | String get currentlySelectedInbox => _currentlySelectedInbox; 209 | bool get onMailView => _currentlySelectedEmailId > -1; 210 | bool get onCompose => _onCompose; 211 | ThemeMode get themeMode => _currentTheme; 212 | SlowMotionSpeedSetting get slowMotionSpeed => _currentSlowMotionSpeed; 213 | 214 | bool isEmailStarred(Email email) { 215 | return _categories['Starred']!.contains(email); 216 | } 217 | 218 | set bottomDrawerVisible(bool value) { 219 | _bottomDrawerVisible = value; 220 | notifyListeners(); 221 | } 222 | 223 | set currentlySelectedEmailId(int value) { 224 | _currentlySelectedEmailId = value; 225 | notifyListeners(); 226 | } 227 | 228 | set currentlySelectedInbox(String inbox) { 229 | _currentlySelectedInbox = inbox; 230 | notifyListeners(); 231 | } 232 | 233 | set themeMode(ThemeMode theme) { 234 | _currentTheme = theme; 235 | notifyListeners(); 236 | } 237 | 238 | set slowMotionSpeed(SlowMotionSpeedSetting speed) { 239 | _currentSlowMotionSpeed = speed; 240 | timeDilation = slowMotionSpeed.value; 241 | } 242 | 243 | set onCompose(bool value) { 244 | _onCompose = value; 245 | notifyListeners(); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /lib/model/router_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:reply/router.dart'; 3 | 4 | class RouterProvider with ChangeNotifier { 5 | RouterProvider(ReplyHomePath this._routePath); 6 | 7 | ReplyRoutePath _routePath; 8 | ReplyRoutePath get routePath => _routePath; 9 | 10 | set routePath(ReplyRoutePath? route) { 11 | if (route != _routePath) { 12 | _routePath = route!; 13 | notifyListeners(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/profile_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ProfileAvatar extends StatelessWidget { 4 | const ProfileAvatar({ 5 | required this.avatar, 6 | this.radius = 20, 7 | Key? key, 8 | }) : super(key: key); 9 | 10 | final String avatar; 11 | final double radius; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Material( 16 | color: Colors.transparent, 17 | child: CircleAvatar( 18 | radius: radius, 19 | backgroundColor: Theme.of(context).cardColor, 20 | child: ClipOval( 21 | child: Image.asset( 22 | avatar, 23 | package: 'flutter_gallery_assets', 24 | height: 42, 25 | width: 42, 26 | fit: BoxFit.cover, 27 | ), 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:reply/custom_transition_page.dart'; 5 | import 'package:reply/home.dart'; 6 | import 'package:reply/search_page.dart'; 7 | 8 | import 'model/router_provider.dart'; 9 | 10 | const String _homePageLocation = '/reply/home'; 11 | const String _searchPageLocation = '/reply/search'; 12 | 13 | class ReplyRouterDelegate extends RouterDelegate 14 | with ChangeNotifier, PopNavigatorRouterDelegateMixin { 15 | ReplyRouterDelegate({required this.replyState}) 16 | : navigatorKey = GlobalObjectKey(replyState) { 17 | replyState.addListener(() { 18 | notifyListeners(); 19 | }); 20 | } 21 | 22 | @override 23 | final GlobalKey navigatorKey; 24 | 25 | RouterProvider replyState; 26 | 27 | @override 28 | void dispose() { 29 | replyState.removeListener(notifyListeners); 30 | super.dispose(); 31 | } 32 | 33 | @override 34 | ReplyRoutePath get currentConfiguration => replyState.routePath; 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return MultiProvider( 39 | providers: [ 40 | ChangeNotifierProvider.value(value: replyState), 41 | ], 42 | child: Selector( 43 | selector: (context, routerProvider) => routerProvider.routePath, 44 | builder: (context, routePath, child) { 45 | return Navigator( 46 | key: navigatorKey, 47 | onPopPage: _handlePopPage, 48 | pages: [ 49 | // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion) 50 | const CustomTransitionPage( 51 | transitionKey: ValueKey('Home'), 52 | screen: HomePage(), 53 | ), 54 | if (routePath is ReplySearchPath) 55 | const CustomTransitionPage( 56 | transitionKey: ValueKey('Search'), 57 | screen: SearchPage(), 58 | ), 59 | ], 60 | ); 61 | }, 62 | ), 63 | ); 64 | } 65 | 66 | bool _handlePopPage(Route route, dynamic result) { 67 | // _handlePopPage should not be called on the home page because the 68 | // PopNavigatorRouterDelegateMixin will bubble up the pop to the 69 | // SystemNavigator if there is only one route in the navigator. 70 | assert(route.willHandlePopInternally || 71 | replyState.routePath is ReplySearchPath); 72 | 73 | final bool didPop = route.didPop(result); 74 | if (didPop) replyState.routePath = const ReplyHomePath(); 75 | return didPop; 76 | } 77 | 78 | @override 79 | Future setNewRoutePath(ReplyRoutePath configuration) { 80 | replyState.routePath = configuration; 81 | return SynchronousFuture(null); 82 | } 83 | } 84 | 85 | @immutable 86 | abstract class ReplyRoutePath { 87 | const ReplyRoutePath(); 88 | } 89 | 90 | class ReplyHomePath extends ReplyRoutePath { 91 | const ReplyHomePath(); 92 | } 93 | 94 | class ReplySearchPath extends ReplyRoutePath { 95 | const ReplySearchPath(); 96 | } 97 | 98 | // TODO: Add Shared Z-Axis transition from search icon to search view page (Motion) 99 | 100 | class ReplyRouteInformationParser 101 | extends RouteInformationParser { 102 | @override 103 | Future parseRouteInformation( 104 | RouteInformation routeInformation) async { 105 | final url = Uri.parse(routeInformation.location!); 106 | 107 | if (url.path == _searchPageLocation) { 108 | return SynchronousFuture(const ReplySearchPath()); 109 | } 110 | 111 | return SynchronousFuture(const ReplyHomePath()); 112 | } 113 | 114 | @override 115 | RouteInformation? restoreRouteInformation(ReplyRoutePath configuration) { 116 | if (configuration is ReplyHomePath) { 117 | return const RouteInformation(location: _homePageLocation); 118 | } 119 | if (configuration is ReplySearchPath) { 120 | return const RouteInformation(location: _searchPageLocation); 121 | } 122 | return null; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:reply/model/router_provider.dart'; 4 | import 'package:reply/router.dart'; 5 | 6 | class SearchPage extends StatelessWidget { 7 | const SearchPage({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | body: SafeArea( 13 | child: Material( 14 | color: Theme.of(context).colorScheme.surface, 15 | child: Column( 16 | children: [ 17 | Padding( 18 | padding: const EdgeInsets.all(8), 19 | child: Row( 20 | mainAxisSize: MainAxisSize.max, 21 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 22 | children: [ 23 | BackButton( 24 | onPressed: () { 25 | Provider.of( 26 | context, 27 | listen: false, 28 | ).routePath = const ReplyHomePath(); 29 | }, 30 | ), 31 | const Expanded( 32 | child: TextField( 33 | decoration: InputDecoration.collapsed( 34 | hintText: 'Search email', 35 | ), 36 | ), 37 | ), 38 | IconButton( 39 | icon: const Icon(Icons.mic), 40 | onPressed: () {}, 41 | ) 42 | ], 43 | ), 44 | ), 45 | const Divider(thickness: 1), 46 | Expanded( 47 | child: SingleChildScrollView( 48 | child: Column( 49 | crossAxisAlignment: CrossAxisAlignment.start, 50 | children: const [ 51 | _SectionHeader(title: 'YESTERDAY'), 52 | _SearchHistoryTile( 53 | search: '481 Van Brunt Street', 54 | address: 'Brooklyn, NY', 55 | ), 56 | _SearchHistoryTile( 57 | icon: Icons.home, 58 | search: 'Home', 59 | address: '199 Pacific Street, Brooklyn, NY', 60 | ), 61 | _SectionHeader(title: 'THIS WEEK'), 62 | _SearchHistoryTile( 63 | search: 'BEP GA', 64 | address: 'Forsyth Street, New York, NY', 65 | ), 66 | _SearchHistoryTile( 67 | search: 'Sushi Nakazawa', 68 | address: 'Commerce Street, New York, NY', 69 | ), 70 | _SearchHistoryTile( 71 | search: 'IFC Center', 72 | address: '6th Avenue, New York, NY', 73 | ), 74 | ], 75 | ), 76 | ), 77 | ), 78 | ], 79 | ), 80 | ), 81 | ), 82 | ); 83 | } 84 | } 85 | 86 | class _SectionHeader extends StatelessWidget { 87 | const _SectionHeader({ 88 | required this.title, 89 | }); 90 | final String title; 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | return Padding( 95 | padding: const EdgeInsetsDirectional.only( 96 | start: 16, 97 | top: 16, 98 | bottom: 16, 99 | ), 100 | child: Text( 101 | title, 102 | style: Theme.of(context).textTheme.labelLarge, 103 | ), 104 | ); 105 | } 106 | } 107 | 108 | class _SearchHistoryTile extends StatelessWidget { 109 | const _SearchHistoryTile({ 110 | this.icon = Icons.access_time, 111 | required this.search, 112 | required this.address, 113 | }); 114 | 115 | final IconData icon; 116 | final String search; 117 | final String address; 118 | 119 | @override 120 | Widget build(BuildContext context) { 121 | return ListTile( 122 | leading: Icon(icon), 123 | title: Text(search), 124 | subtitle: Text(address), 125 | onTap: () {}, 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/settings_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'model/email_store.dart'; 5 | 6 | enum SlowMotionSpeedSetting { normal, slow, slower, slowest } 7 | 8 | extension AnimationSpeedSettingExtension on SlowMotionSpeedSetting { 9 | double get value { 10 | switch (this) { 11 | case SlowMotionSpeedSetting.normal: 12 | return 1.0; 13 | case SlowMotionSpeedSetting.slow: 14 | return 5.0; 15 | case SlowMotionSpeedSetting.slower: 16 | return 10.0; 17 | case SlowMotionSpeedSetting.slowest: 18 | return 15.0; 19 | } 20 | } 21 | } 22 | 23 | extension ThemeModeExtension on ThemeMode { 24 | String get name { 25 | switch (this) { 26 | case ThemeMode.system: 27 | return 'System'; 28 | case ThemeMode.light: 29 | return 'Light'; 30 | case ThemeMode.dark: 31 | return 'Dark'; 32 | } 33 | } 34 | } 35 | 36 | class SettingsBottomSheet extends StatefulWidget { 37 | const SettingsBottomSheet({Key? key}) : super(key: key); 38 | 39 | @override 40 | SettingsBottomSheetState createState() => SettingsBottomSheetState(); 41 | } 42 | 43 | class SettingsBottomSheetState extends State { 44 | late SlowMotionSpeedSetting _slowMotionSpeedSetting; 45 | late ThemeMode _themeMode; 46 | 47 | @override 48 | void initState() { 49 | super.initState(); 50 | _themeMode = Provider.of(context, listen: false).themeMode; 51 | _slowMotionSpeedSetting = 52 | Provider.of(context, listen: false).slowMotionSpeed; 53 | } 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | var radius = const Radius.circular(12); 58 | final modalBorder = BorderRadius.only( 59 | topRight: radius, 60 | topLeft: radius, 61 | ); 62 | 63 | return StatefulBuilder(builder: (context, state) { 64 | void setTheme(ThemeMode? theme) { 65 | state(() { 66 | _themeMode = theme!; 67 | }); 68 | Provider.of(context, listen: false).themeMode = theme!; 69 | } 70 | 71 | void setSlowMotionSpeed(SlowMotionSpeedSetting? slowMotionSpeed) { 72 | state(() { 73 | _slowMotionSpeedSetting = slowMotionSpeed!; 74 | }); 75 | Provider.of(context, listen: false).slowMotionSpeed = 76 | slowMotionSpeed!; 77 | } 78 | 79 | return Container( 80 | decoration: BoxDecoration( 81 | borderRadius: modalBorder, 82 | color: Theme.of(context).colorScheme.surface, 83 | ), 84 | child: SingleChildScrollView( 85 | child: Column( 86 | children: [ 87 | ExpansionTile( 88 | title: const Text('Theme'), 89 | children: [ 90 | for (var themeMode in ThemeMode.values) 91 | RadioListTile( 92 | title: Text(themeMode.name), 93 | value: themeMode, 94 | groupValue: _themeMode, 95 | onChanged: setTheme, 96 | ), 97 | ], 98 | ), 99 | ExpansionTile( 100 | title: const Text('Slow Motion'), 101 | children: [ 102 | for (var animationSpeed in SlowMotionSpeedSetting.values) 103 | RadioListTile( 104 | title: Text('${animationSpeed.value.toInt()}x'), 105 | value: animationSpeed, 106 | groupValue: _slowMotionSpeedSetting, 107 | onChanged: setSlowMotionSpeed, 108 | ), 109 | ], 110 | ), 111 | ], 112 | ), 113 | ), 114 | ); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/waterfall_notched_rectangle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | /// A rectangle with a smooth circular notch. 6 | /// 7 | /// See also: 8 | /// 9 | /// * [CircleBorder], a [ShapeBorder] that describes a circle. 10 | class WaterfallNotchedRectangle extends NotchedShape { 11 | /// Creates a [WaterfallNotchedRectangle]. 12 | /// 13 | /// The same object can be used to create multiple shapes. 14 | const WaterfallNotchedRectangle(); 15 | 16 | /// Creates a [Path] that describes a rectangle with a smooth circular notch. 17 | /// 18 | /// `host` is the bounding box for the returned shape. Conceptually this is 19 | /// the rectangle to which the notch will be applied. 20 | /// 21 | /// `guest` is the bounding box of a circle that the notch accommodates. All 22 | /// points in the circle bounded by `guest` will be outside of the returned 23 | /// path. 24 | /// 25 | /// The notch is curve that smoothly connects the host's top edge and 26 | /// the guest circle. 27 | @override 28 | Path getOuterPath(Rect host, Rect? guest) { 29 | if (guest == null || !host.overlaps(guest)) return Path()..addRect(host); 30 | 31 | // The guest's shape is a circle bounded by the guest rectangle. 32 | // So the guest's radius is half the guest width. 33 | final double notchRadius = guest.width / 2.0; 34 | 35 | // We build a path for the notch from 3 segments: 36 | // Segment A - a Bezier curve from the host's top edge to segment B. 37 | // Segment B - an arc with radius notchRadius. 38 | // Segment C - a Bezier curve from segment B back to the host's top edge. 39 | // 40 | // A detailed explanation and the derivation of the formulas below is 41 | // available at: https://goo.gl/Ufzrqn 42 | 43 | // s1, s2 are the two knobs controlling the behavior of the bezzier curve. 44 | const double s1 = 21.0; 45 | const double s2 = 6.0; 46 | 47 | final double r = notchRadius; 48 | final double a = -1.0 * r - s2; 49 | final double b = host.top - guest.center.dy; 50 | 51 | final double n2 = math.sqrt(b * b * r * r * (a * a + b * b - r * r)); 52 | final double p2xA = ((a * r * r) - n2) / (a * a + b * b); 53 | final double p2xB = ((a * r * r) + n2) / (a * a + b * b); 54 | final double p2yA = math.sqrt(r * r - p2xA * p2xA); 55 | final double p2yB = math.sqrt(r * r - p2xB * p2xB); 56 | 57 | final List p = List.filled(6, null, growable: false); 58 | 59 | // p0, p1, and p2 are the control points for segment A. 60 | p[0] = Offset(a - s1, b); 61 | p[1] = Offset(a, b); 62 | final double cmp = b < 0 ? -1.0 : 1.0; 63 | p[2] = cmp * p2yA > cmp * p2yB ? Offset(p2xA, p2yA) : Offset(p2xB, p2yB); 64 | 65 | // p3, p4, and p5 are the control points for segment B, which is a mirror 66 | // of segment A around the y axis. 67 | p[3] = Offset(-1.0 * p[2]!.dx, p[2]!.dy); 68 | p[4] = Offset(-1.0 * p[1]!.dx, p[1]!.dy); 69 | p[5] = Offset(-1.0 * p[0]!.dx, p[0]!.dy); 70 | 71 | // translate all points back to the absolute coordinate system. 72 | for (int i = 0; i < p.length; i += 1) { 73 | p[i] = p[i]! + guest.center; 74 | } 75 | 76 | return Path() 77 | ..moveTo(host.left, host.top) 78 | ..lineTo(p[0]!.dx, p[0]!.dy) 79 | ..quadraticBezierTo(p[1]!.dx, p[1]!.dy, p[2]!.dx, p[2]!.dy) 80 | ..arcToPoint( 81 | p[3]!, 82 | radius: Radius.circular(notchRadius), 83 | clockwise: false, 84 | ) 85 | ..quadraticBezierTo(p[4]!.dx, p[4]!.dy, p[5]!.dx, p[5]!.dy) 86 | ..lineTo(host.right, host.top) 87 | ..lineTo(host.right, host.bottom) 88 | ..lineTo(host.left, host.bottom) 89 | ..close(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | animations: 5 | dependency: "direct main" 6 | description: 7 | name: animations 8 | sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.0.11" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.11.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.1" 28 | characters: 29 | dependency: transitive 30 | description: 31 | name: characters 32 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.3.0" 36 | clock: 37 | dependency: transitive 38 | description: 39 | name: clock 40 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.1.1" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.18.0" 52 | crypto: 53 | dependency: transitive 54 | description: 55 | name: crypto 56 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "3.0.3" 60 | cupertino_icons: 61 | dependency: "direct main" 62 | description: 63 | name: cupertino_icons 64 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.0.8" 68 | fake_async: 69 | dependency: transitive 70 | description: 71 | name: fake_async 72 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.3.1" 76 | ffi: 77 | dependency: transitive 78 | description: 79 | name: ffi 80 | sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "2.1.2" 84 | flutter: 85 | dependency: "direct main" 86 | description: flutter 87 | source: sdk 88 | version: "0.0.0" 89 | flutter_gallery_assets: 90 | dependency: "direct main" 91 | description: 92 | name: flutter_gallery_assets 93 | sha256: f8fecfeebcfbe80a2fabc00c0834046890abe681f59d3e2c5b1028faf887d94b 94 | url: "https://pub.dev" 95 | source: hosted 96 | version: "1.0.2" 97 | flutter_lints: 98 | dependency: "direct dev" 99 | description: 100 | name: flutter_lints 101 | sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" 102 | url: "https://pub.dev" 103 | source: hosted 104 | version: "4.0.0" 105 | flutter_test: 106 | dependency: "direct dev" 107 | description: flutter 108 | source: sdk 109 | version: "0.0.0" 110 | google_fonts: 111 | dependency: "direct main" 112 | description: 113 | name: google_fonts 114 | sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "6.2.1" 118 | http: 119 | dependency: transitive 120 | description: 121 | name: http 122 | sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" 123 | url: "https://pub.dev" 124 | source: hosted 125 | version: "1.2.1" 126 | http_parser: 127 | dependency: transitive 128 | description: 129 | name: http_parser 130 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 131 | url: "https://pub.dev" 132 | source: hosted 133 | version: "4.0.2" 134 | leak_tracker: 135 | dependency: transitive 136 | description: 137 | name: leak_tracker 138 | sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" 139 | url: "https://pub.dev" 140 | source: hosted 141 | version: "10.0.4" 142 | leak_tracker_flutter_testing: 143 | dependency: transitive 144 | description: 145 | name: leak_tracker_flutter_testing 146 | sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" 147 | url: "https://pub.dev" 148 | source: hosted 149 | version: "3.0.3" 150 | leak_tracker_testing: 151 | dependency: transitive 152 | description: 153 | name: leak_tracker_testing 154 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 155 | url: "https://pub.dev" 156 | source: hosted 157 | version: "3.0.1" 158 | lints: 159 | dependency: transitive 160 | description: 161 | name: lints 162 | sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" 163 | url: "https://pub.dev" 164 | source: hosted 165 | version: "4.0.0" 166 | matcher: 167 | dependency: transitive 168 | description: 169 | name: matcher 170 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 171 | url: "https://pub.dev" 172 | source: hosted 173 | version: "0.12.16+1" 174 | material_color_utilities: 175 | dependency: transitive 176 | description: 177 | name: material_color_utilities 178 | sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" 179 | url: "https://pub.dev" 180 | source: hosted 181 | version: "0.8.0" 182 | meta: 183 | dependency: transitive 184 | description: 185 | name: meta 186 | sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" 187 | url: "https://pub.dev" 188 | source: hosted 189 | version: "1.12.0" 190 | nested: 191 | dependency: transitive 192 | description: 193 | name: nested 194 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 195 | url: "https://pub.dev" 196 | source: hosted 197 | version: "1.0.0" 198 | path: 199 | dependency: transitive 200 | description: 201 | name: path 202 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 203 | url: "https://pub.dev" 204 | source: hosted 205 | version: "1.9.0" 206 | path_provider: 207 | dependency: transitive 208 | description: 209 | name: path_provider 210 | sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 211 | url: "https://pub.dev" 212 | source: hosted 213 | version: "2.1.3" 214 | path_provider_android: 215 | dependency: transitive 216 | description: 217 | name: path_provider_android 218 | sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a 219 | url: "https://pub.dev" 220 | source: hosted 221 | version: "2.2.6" 222 | path_provider_foundation: 223 | dependency: transitive 224 | description: 225 | name: path_provider_foundation 226 | sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 227 | url: "https://pub.dev" 228 | source: hosted 229 | version: "2.4.0" 230 | path_provider_linux: 231 | dependency: transitive 232 | description: 233 | name: path_provider_linux 234 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 235 | url: "https://pub.dev" 236 | source: hosted 237 | version: "2.2.1" 238 | path_provider_platform_interface: 239 | dependency: transitive 240 | description: 241 | name: path_provider_platform_interface 242 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 243 | url: "https://pub.dev" 244 | source: hosted 245 | version: "2.1.2" 246 | path_provider_windows: 247 | dependency: transitive 248 | description: 249 | name: path_provider_windows 250 | sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" 251 | url: "https://pub.dev" 252 | source: hosted 253 | version: "2.2.1" 254 | platform: 255 | dependency: transitive 256 | description: 257 | name: platform 258 | sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" 259 | url: "https://pub.dev" 260 | source: hosted 261 | version: "3.1.5" 262 | plugin_platform_interface: 263 | dependency: transitive 264 | description: 265 | name: plugin_platform_interface 266 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 267 | url: "https://pub.dev" 268 | source: hosted 269 | version: "2.1.8" 270 | provider: 271 | dependency: "direct main" 272 | description: 273 | name: provider 274 | sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c 275 | url: "https://pub.dev" 276 | source: hosted 277 | version: "6.1.2" 278 | sky_engine: 279 | dependency: transitive 280 | description: flutter 281 | source: sdk 282 | version: "0.0.99" 283 | source_span: 284 | dependency: transitive 285 | description: 286 | name: source_span 287 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 288 | url: "https://pub.dev" 289 | source: hosted 290 | version: "1.10.0" 291 | stack_trace: 292 | dependency: transitive 293 | description: 294 | name: stack_trace 295 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 296 | url: "https://pub.dev" 297 | source: hosted 298 | version: "1.11.1" 299 | stream_channel: 300 | dependency: transitive 301 | description: 302 | name: stream_channel 303 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 304 | url: "https://pub.dev" 305 | source: hosted 306 | version: "2.1.2" 307 | string_scanner: 308 | dependency: transitive 309 | description: 310 | name: string_scanner 311 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 312 | url: "https://pub.dev" 313 | source: hosted 314 | version: "1.2.0" 315 | term_glyph: 316 | dependency: transitive 317 | description: 318 | name: term_glyph 319 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 320 | url: "https://pub.dev" 321 | source: hosted 322 | version: "1.2.1" 323 | test_api: 324 | dependency: transitive 325 | description: 326 | name: test_api 327 | sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" 328 | url: "https://pub.dev" 329 | source: hosted 330 | version: "0.7.0" 331 | typed_data: 332 | dependency: transitive 333 | description: 334 | name: typed_data 335 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 336 | url: "https://pub.dev" 337 | source: hosted 338 | version: "1.3.2" 339 | vector_math: 340 | dependency: transitive 341 | description: 342 | name: vector_math 343 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 344 | url: "https://pub.dev" 345 | source: hosted 346 | version: "2.1.4" 347 | vm_service: 348 | dependency: transitive 349 | description: 350 | name: vm_service 351 | sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" 352 | url: "https://pub.dev" 353 | source: hosted 354 | version: "14.2.1" 355 | web: 356 | dependency: transitive 357 | description: 358 | name: web 359 | sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" 360 | url: "https://pub.dev" 361 | source: hosted 362 | version: "0.5.1" 363 | win32: 364 | dependency: transitive 365 | description: 366 | name: win32 367 | sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 368 | url: "https://pub.dev" 369 | source: hosted 370 | version: "5.5.1" 371 | xdg_directories: 372 | dependency: transitive 373 | description: 374 | name: xdg_directories 375 | sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d 376 | url: "https://pub.dev" 377 | source: hosted 378 | version: "1.0.4" 379 | sdks: 380 | dart: ">=3.4.0 <4.0.0" 381 | flutter: ">=3.22.0" 382 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: reply 2 | description: The reply material study built in flutter. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `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.0.0+1 19 | 20 | environment: 21 | sdk: '>=3.4.0-0 <4.0.0' 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | # The following adds the Cupertino Icons font to your application. 28 | # Use with the CupertinoIcons class for iOS style icons. 29 | cupertino_icons: ^1.0.3 30 | provider: ^6.0.5 31 | animations: ^2.0.0 32 | google_fonts: ^6.2.1 33 | flutter_gallery_assets: ^1.0.2 34 | 35 | dev_dependencies: 36 | flutter_test: 37 | sdk: flutter 38 | flutter_lints: ^4.0.0 39 | 40 | # For information on the generic Dart part of this file, see the 41 | # following page: https://dart.dev/tools/pub/pubspec 42 | 43 | # The following section is specific to Flutter. 44 | flutter: 45 | 46 | # The following line ensures that the Material Icons font is 47 | # included with your application, so that you can use the icons in 48 | # the material Icons class. 49 | uses-material-design: true 50 | 51 | # To add assets to your application, add an assets section, like this: 52 | assets: 53 | - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-Regular.ttf 54 | - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-Medium.ttf 55 | - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-Bold.ttf 56 | - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-Thin.ttf 57 | - packages/flutter_gallery_assets/fonts/google_fonts/WorkSans-SemiBold.ttf 58 | - packages/flutter_gallery_assets/reply/attachments/paris_1.jpg 59 | - packages/flutter_gallery_assets/reply/attachments/paris_2.jpg 60 | - packages/flutter_gallery_assets/reply/attachments/paris_3.jpg 61 | - packages/flutter_gallery_assets/reply/attachments/paris_4.jpg 62 | - packages/flutter_gallery_assets/reply/avatars/avatar_0.jpg 63 | - packages/flutter_gallery_assets/reply/avatars/avatar_1.jpg 64 | - packages/flutter_gallery_assets/reply/avatars/avatar_2.jpg 65 | - packages/flutter_gallery_assets/reply/avatars/avatar_3.jpg 66 | - packages/flutter_gallery_assets/reply/avatars/avatar_4.jpg 67 | - packages/flutter_gallery_assets/reply/avatars/avatar_5.jpg 68 | - packages/flutter_gallery_assets/reply/avatars/avatar_6.jpg 69 | - packages/flutter_gallery_assets/reply/avatars/avatar_7.jpg 70 | - packages/flutter_gallery_assets/reply/avatars/avatar_8.jpg 71 | - packages/flutter_gallery_assets/reply/avatars/avatar_9.jpg 72 | - packages/flutter_gallery_assets/reply/avatars/avatar_10.jpg 73 | - packages/flutter_gallery_assets/reply/avatars/avatar_express.png 74 | - packages/flutter_gallery_assets/reply/icons/twotone_add_circle_outline.png 75 | - packages/flutter_gallery_assets/reply/icons/twotone_delete.png 76 | - packages/flutter_gallery_assets/reply/icons/twotone_drafts.png 77 | - packages/flutter_gallery_assets/reply/icons/twotone_error.png 78 | - packages/flutter_gallery_assets/reply/icons/twotone_folder.png 79 | - packages/flutter_gallery_assets/reply/icons/twotone_forward.png 80 | - packages/flutter_gallery_assets/reply/icons/twotone_inbox.png 81 | - packages/flutter_gallery_assets/reply/icons/twotone_send.png 82 | - packages/flutter_gallery_assets/reply/icons/twotone_star_on_background.png 83 | - packages/flutter_gallery_assets/reply/icons/twotone_star.png 84 | - packages/flutter_gallery_assets/reply/icons/twotone_stars.png 85 | - packages/flutter_gallery_assets/reply/reply_logo.png 86 | 87 | # An image asset can refer to one or more resolution-specific "variants", see 88 | # https://flutter.dev/assets-and-images/#resolution-aware. 89 | 90 | # For details regarding adding assets from package dependencies, see 91 | # https://flutter.dev/assets-and-images/#from-packages 92 | 93 | # To add custom fonts to your application, add a fonts section here, 94 | # in this "flutter" section. Each entry in this list should have a 95 | # "family" key with the font family name, and a "fonts" key with a 96 | # list giving the asset and other descriptors for the font. For 97 | # example: 98 | # fonts: 99 | # - family: Schyler 100 | # fonts: 101 | # - asset: fonts/Schyler-Regular.ttf 102 | # - asset: fonts/Schyler-Italic.ttf 103 | # style: italic 104 | # - family: Trajan Pro 105 | # fonts: 106 | # - asset: fonts/TrajanPro.ttf 107 | # - asset: fonts/TrajanPro_Bold.ttf 108 | # weight: 700 109 | # 110 | # For details regarding fonts from package dependencies, 111 | # see https://flutter.dev/custom-fonts/#from-packages 112 | -------------------------------------------------------------------------------- /screenshots/reply-transitions-android.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/screenshots/reply-transitions-android.gif -------------------------------------------------------------------------------- /screenshots/reply-transitions-iOS.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/screenshots/reply-transitions-iOS.gif -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:reply/app.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(const ReplyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-components/material-components-flutter-motion-codelab/1508095e80271d53cc1a6716bc6e3d5b1ed7cf8c/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | reply 18 | 19 | 20 | 21 | 24 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reply", 3 | "short_name": "reply", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "The reply material study built in flutter.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | --------------------------------------------------------------------------------