├── .gitignore ├── .metadata ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── google-services.json │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── thecodingpapa │ │ │ │ └── tomato_record │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── fonts │ └── BMDOHYEON_otf.otf └── imgs │ ├── carrot_intro.png │ ├── carrot_intro_pos.png │ ├── happiness.png │ ├── home_1.png │ ├── padlock.png │ ├── placeholder.png │ ├── selected_home_1.png │ ├── selected_placeholder.png │ ├── selected_smartphone_10.png │ ├── selected_user_3.png │ ├── smartphone_10.png │ ├── tomato.png │ ├── user_3.png │ └── won.png ├── 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 │ ├── GoogleService-Info.plist │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ └── Runner.entitlements └── build │ └── Pods.build │ └── Release-iphonesimulator │ ├── BoringSSL-GRPC.build │ └── dgph │ ├── Firebase.build │ └── dgph │ ├── FirebaseAuth.build │ └── dgph │ ├── FirebaseCore.build │ └── dgph │ ├── FirebaseCoreDiagnostics.build │ └── dgph │ ├── FirebaseFirestore.build │ └── dgph │ ├── FirebaseStorage.build │ └── dgph │ ├── Flutter.build │ └── dgph │ ├── GTMSessionFetcher.build │ └── dgph │ ├── GoogleDataTransport.build │ └── dgph │ ├── GoogleUtilities.build │ └── dgph │ ├── Pods-Runner.build │ └── dgph │ ├── PromisesObjC.build │ └── dgph │ ├── abseil.build │ └── dgph │ ├── cloud_firestore.build │ └── dgph │ ├── firebase_auth.build │ └── dgph │ ├── firebase_core.build │ └── dgph │ ├── firebase_storage.build │ └── dgph │ ├── gRPC-C++-gRPCCertificates-Cpp.build │ └── dgph │ ├── gRPC-C++.build │ └── dgph │ ├── gRPC-Core.build │ └── dgph │ ├── image_picker.build │ └── dgph │ ├── leveldb-library.build │ └── dgph │ ├── location.build │ └── dgph │ ├── nanopb.build │ └── dgph │ ├── path_provider.build │ └── dgph │ └── shared_preferences.build │ └── dgph ├── lib ├── constants │ ├── common_size.dart │ ├── data_keys.dart │ ├── keys.dart │ └── shared_pref_keys.dart ├── data │ ├── address_model.dart │ ├── address_model2.dart │ ├── chat_model.dart │ ├── chatroom_model.dart │ ├── item_model.dart │ └── user_model.dart ├── main.dart ├── repo │ ├── algolia_service.dart │ ├── chat_service.dart │ ├── image_storage.dart │ ├── item_service.dart │ └── user_service.dart ├── router │ └── locations.dart ├── screens │ ├── chat │ │ ├── chat.dart │ │ ├── chat_list_page.dart │ │ └── chatroom_screen.dart │ ├── home │ │ ├── items_page.dart │ │ └── map_page.dart │ ├── home_screen.dart │ ├── input │ │ ├── category_input_screen.dart │ │ ├── input_screen.dart │ │ └── multi_image_select.dart │ ├── item │ │ ├── item_detail_screen.dart │ │ └── similar_item.dart │ ├── search │ │ └── search_screen.dart │ ├── splash_screen.dart │ ├── start │ │ ├── address_page.dart │ │ ├── address_service.dart │ │ ├── auth_page.dart │ │ └── intro_page.dart │ └── start_screen.dart ├── states │ ├── category_notifier.dart │ ├── chat_notifier.dart │ ├── select_image_notifier.dart │ └── user_notifier.dart ├── utils │ ├── logger.dart │ └── time_calculation.dart └── widgets │ ├── expandable_fab.dart │ └── item_list_widget.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: f4abaa0735eba4dfd8f33f73363911d63931fe03 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tomato_record 2 | 3 | A new Flutter application. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 30 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.thecodingpapa.tomato_record" 38 | minSdkVersion 16 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | multiDexEnabled true 43 | } 44 | 45 | buildTypes { 46 | release { 47 | // TODO: Add your own signing config for the release build. 48 | // Signing with the debug keys for now, so `flutter run --release` works. 49 | signingConfig signingConfigs.debug 50 | } 51 | } 52 | } 53 | 54 | flutter { 55 | source '../..' 56 | } 57 | 58 | dependencies { 59 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 60 | implementation 'com.android.support:multidex:1.0.3' 61 | } 62 | 63 | 64 | apply plugin: 'com.google.gms.google-services' -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "991789698622", 4 | "project_id": "tomato-record", 5 | "storage_bucket": "tomato-record.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:991789698622:android:c44a5bac175cd2e9d28b6f", 11 | "android_client_info": { 12 | "package_name": "com.thecodingpapa.tomato_record" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "991789698622-casfs1otr3juqr71uh9mqv4rktkbmeoi.apps.googleusercontent.com", 18 | "client_type": 1, 19 | "android_info": { 20 | "package_name": "com.thecodingpapa.tomato_record", 21 | "certificate_hash": "3937b2583d0d05ac1d8079e91f375ad2cc9382b6" 22 | } 23 | }, 24 | { 25 | "client_id": "991789698622-j8mssonoorantvvouuinpgqro3qc917a.apps.googleusercontent.com", 26 | "client_type": 3 27 | } 28 | ], 29 | "api_key": [ 30 | { 31 | "current_key": "AIzaSyC-MSQnsDtWVWtvYQeIyPQbyaZFSfZV9YE" 32 | } 33 | ], 34 | "services": { 35 | "appinvite_service": { 36 | "other_platform_oauth_client": [ 37 | { 38 | "client_id": "991789698622-j8mssonoorantvvouuinpgqro3qc917a.apps.googleusercontent.com", 39 | "client_type": 3 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | ], 46 | "configuration_version": "1" 47 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 14 | 18 | 22 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/thecodingpapa/tomato_record/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.thecodingpapa.tomato_record 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/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.8' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /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-6.7-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 | -------------------------------------------------------------------------------- /assets/fonts/BMDOHYEON_otf.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/fonts/BMDOHYEON_otf.otf -------------------------------------------------------------------------------- /assets/imgs/carrot_intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/carrot_intro.png -------------------------------------------------------------------------------- /assets/imgs/carrot_intro_pos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/carrot_intro_pos.png -------------------------------------------------------------------------------- /assets/imgs/happiness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/happiness.png -------------------------------------------------------------------------------- /assets/imgs/home_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/home_1.png -------------------------------------------------------------------------------- /assets/imgs/padlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/padlock.png -------------------------------------------------------------------------------- /assets/imgs/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/placeholder.png -------------------------------------------------------------------------------- /assets/imgs/selected_home_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/selected_home_1.png -------------------------------------------------------------------------------- /assets/imgs/selected_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/selected_placeholder.png -------------------------------------------------------------------------------- /assets/imgs/selected_smartphone_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/selected_smartphone_10.png -------------------------------------------------------------------------------- /assets/imgs/selected_user_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/selected_user_3.png -------------------------------------------------------------------------------- /assets/imgs/smartphone_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/smartphone_10.png -------------------------------------------------------------------------------- /assets/imgs/tomato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/tomato.png -------------------------------------------------------------------------------- /assets/imgs/user_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/user_3.png -------------------------------------------------------------------------------- /assets/imgs/won.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/assets/imgs/won.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/ephemeral/ 22 | Flutter/app.flx 23 | Flutter/app.zip 24 | Flutter/flutter_assets/ 25 | Flutter/flutter_export_environment.sh 26 | ServiceDefinitions.json 27 | Runner/GeneratedPluginRegistrant.* 28 | 29 | # Exceptions to above rules. 30 | !default.mode1v3 31 | !default.mode2v3 32 | !default.pbxuser 33 | !default.perspectivev3 34 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 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, '10.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/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/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/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingpapa/tomato_record/bfc79c4e42bf1aeee4656f93be46a60990e3b9cc/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 991789698622-uhhv9648g7midrrlmjujqihrf0nvvcti.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.991789698622-uhhv9648g7midrrlmjujqihrf0nvvcti 9 | ANDROID_CLIENT_ID 10 | 991789698622-casfs1otr3juqr71uh9mqv4rktkbmeoi.apps.googleusercontent.com 11 | API_KEY 12 | AIzaSyAINO29E2NmJcDvMUJjPArIBue8x01q_-o 13 | GCM_SENDER_ID 14 | 991789698622 15 | PLIST_VERSION 16 | 1 17 | BUNDLE_ID 18 | com.thecodingpapa.tomatoRecord 19 | PROJECT_ID 20 | tomato-record 21 | STORAGE_BUCKET 22 | tomato-record.appspot.com 23 | IS_ADS_ENABLED 24 | 25 | IS_ANALYTICS_ENABLED 26 | 27 | IS_APPINVITE_ENABLED 28 | 29 | IS_GCM_ENABLED 30 | 31 | IS_SIGNIN_ENABLED 32 | 33 | GOOGLE_APP_ID 34 | 1:991789698622:ios:6a53df66e16dc9c8d28b6f 35 | 36 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSMicrophoneUsageDescription 6 | 영상찍을때 필요해!! 7 | NSCameraUsageDescription 8 | 사진좀 찍고싶어!! 9 | NSPhotoLibraryUsageDescription 10 | 사진좀 사용할게!! 11 | CFBundleDevelopmentRegion 12 | $(DEVELOPMENT_LANGUAGE) 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | tomato_record 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(FLUTTER_BUILD_NAME) 25 | CFBundleSignature 26 | ???? 27 | CFBundleURLTypes 28 | 29 | 30 | CFBundleTypeRole 31 | Editor 32 | CFBundleURLSchemes 33 | 34 | com.googleusercontent.apps.991789698622-uhhv9648g7midrrlmjujqihrf0nvvcti 35 | 36 | 37 | 38 | 39 | CFBundleVersion 40 | $(FLUTTER_BUILD_NUMBER) 41 | LSRequiresIPhoneOS 42 | 43 | NSLocationWhenInUseUsageDescription 44 | 주소때문에 위치 정보좀 사용할게요!! 45 | UILaunchStoryboardName 46 | LaunchScreen 47 | UIMainStoryboardFile 48 | Main 49 | UISupportedInterfaceOrientations 50 | 51 | UIInterfaceOrientationPortrait 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | UISupportedInterfaceOrientations~ipad 56 | 57 | UIInterfaceOrientationPortrait 58 | UIInterfaceOrientationPortraitUpsideDown 59 | UIInterfaceOrientationLandscapeLeft 60 | UIInterfaceOrientationLandscapeRight 61 | 62 | UIViewControllerBasedStatusBarAppearance 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/BoringSSL-GRPC.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/Firebase.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/FirebaseAuth.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/FirebaseCore.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/FirebaseCoreDiagnostics.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/FirebaseFirestore.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/FirebaseStorage.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/Flutter.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/GTMSessionFetcher.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/GoogleDataTransport.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/GoogleUtilities.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/Pods-Runner.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/PromisesObjC.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/abseil.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/cloud_firestore.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/firebase_auth.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/firebase_core.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/firebase_storage.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/gRPC-C++-gRPCCertificates-Cpp.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/gRPC-C++.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/gRPC-Core.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/image_picker.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/leveldb-library.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/location.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/nanopb.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/path_provider.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /ios/build/Pods.build/Release-iphonesimulator/shared_preferences.build/dgph: -------------------------------------------------------------------------------- 1 | DGPH1.04 Aug 25 202119:35:39 /Users 2 | wonjunyang Documents 3 | PlayGroundflutter-project tomato_recordiosPods -------------------------------------------------------------------------------- /lib/constants/common_size.dart: -------------------------------------------------------------------------------- 1 | const double common_padding = 16.0; 2 | const double common_sm_padding = 8.0; 3 | -------------------------------------------------------------------------------- /lib/constants/data_keys.dart: -------------------------------------------------------------------------------- 1 | const COL_USERS = 'users'; 2 | const COL_ITEMS = 'items'; 3 | const COL_USER_ITEMS = 'user_items'; 4 | const COL_CHATROOMS = 'chatrooms'; 5 | const COL_CHATS = 'chats'; 6 | 7 | const DOC_ITEMIMAGE = "itemImage"; 8 | const DOC_ITEMTITLE = "itemTitle"; 9 | const DOC_ITEMKEY = "itemKey"; 10 | const DOC_ITEMADDRESS = "itemAddress"; 11 | const DOC_ITEMPOSITION = "itemPosition"; 12 | const DOC_ITEMPRICE = "itemPrice"; 13 | const DOC_SELLERKEY = "sellerKey"; 14 | const DOC_BUYERKEY = "buyerKey"; 15 | const DOC_SELLERIMAGE = "sellerImage"; 16 | const DOC_BUYERIMAGE = "buyerImage"; 17 | const DOC_GEOFIREPOINT = "geoFirePoint"; 18 | const DOC_GEOPOINT = "geopoint"; 19 | const DOC_LASTMSG = "lastMsg"; 20 | const DOC_LASTMSGTIME = "lastMsgTime"; 21 | const DOC_LASTMSGUSERKEY = "lastMsgUserKey"; 22 | const DOC_CHATROOMKEY = "chatroomKey"; 23 | const DOC_CHATKEY = "chatKey"; 24 | const DOC_USERKEY = "userKey"; 25 | const DOC_MSG = "msg"; 26 | const DOC_IMAGEDOWNLOADURLS = "imageDownloadUrls"; 27 | const DOC_TITLE = "title"; 28 | const DOC_CATEGORY = "category"; 29 | const DOC_PRICE = "price"; 30 | const DOC_NEGOTIABLE = "negotiable"; 31 | const DOC_DETAIL = "detail"; 32 | const DOC_ADDRESS = "address"; 33 | const DOC_CREATEDDATE = "createdDate"; 34 | const DOC_PHONENUMBER = "phoneNumber"; 35 | -------------------------------------------------------------------------------- /lib/constants/keys.dart: -------------------------------------------------------------------------------- 1 | const VWORLD_KEY = "AAECD9BB-1F7E-3F0C-9349-D258A07B51DB"; 2 | -------------------------------------------------------------------------------- /lib/constants/shared_pref_keys.dart: -------------------------------------------------------------------------------- 1 | const SHARED_ADDRESS = 'address'; 2 | const SHARED_LAT = 'latitude'; 3 | const SHARED_LON = 'longitude'; 4 | -------------------------------------------------------------------------------- /lib/data/address_model.dart: -------------------------------------------------------------------------------- 1 | /// page : {"total":"1","current":"1","size":"10"} 2 | /// result : {"crs":"EPSG:900913","type":'address',"items":[{"id":"4113510900106240000",'address':{"zipcode":"13487","category":"road","road":"경기도 성남시 분당구 판교로 242 (삼평동)","parcel":"삼평동 624","bldnm":"","bldnmdc":""},"point":{"x":"14148853.48172358","y":"4495338.919111188"}}]} 3 | 4 | class AddressModel { 5 | Page? _page; 6 | Result? _result; 7 | 8 | Page? get page => _page; 9 | Result? get result => _result; 10 | 11 | AddressModel({Page? page, Result? result}) { 12 | _page = page; 13 | _result = result; 14 | } 15 | 16 | AddressModel.fromJson(dynamic json) { 17 | _page = json['page'] != null ? Page.fromJson(json['page']) : null; 18 | _result = json['result'] != null ? Result.fromJson(json['result']) : null; 19 | } 20 | 21 | Map toJson() { 22 | var map = {}; 23 | if (_page != null) { 24 | map['page'] = _page?.toJson(); 25 | } 26 | if (_result != null) { 27 | map['result'] = _result?.toJson(); 28 | } 29 | return map; 30 | } 31 | } 32 | 33 | /// crs : "EPSG:900913" 34 | /// type : 'address' 35 | /// items : [{"id":"4113510900106240000",'address':{"zipcode":"13487","category":"road","road":"경기도 성남시 분당구 판교로 242 (삼평동)","parcel":"삼평동 624","bldnm":"","bldnmdc":""},"point":{"x":"14148853.48172358","y":"4495338.919111188"}}] 36 | 37 | class Result { 38 | String? _crs; 39 | String? _type; 40 | List? _items; 41 | 42 | String? get crs => _crs; 43 | String? get type => _type; 44 | List? get items => _items; 45 | 46 | Result({String? crs, String? type, List? items}) { 47 | _crs = crs; 48 | _type = type; 49 | _items = items; 50 | } 51 | 52 | Result.fromJson(dynamic json) { 53 | _crs = json['crs']; 54 | _type = json['type']; 55 | if (json['items'] != null) { 56 | _items = []; 57 | json['items'].forEach((v) { 58 | _items?.add(Items.fromJson(v)); 59 | }); 60 | } 61 | } 62 | 63 | Map toJson() { 64 | var map = {}; 65 | map['crs'] = _crs; 66 | map['type'] = _type; 67 | if (_items != null) { 68 | map['items'] = _items?.map((v) => v.toJson()).toList(); 69 | } 70 | return map; 71 | } 72 | } 73 | 74 | /// id : "4113510900106240000" 75 | /// address : {"zipcode":"13487","category":"road","road":"경기도 성남시 분당구 판교로 242 (삼평동)","parcel":"삼평동 624","bldnm":"","bldnmdc":""} 76 | /// point : {"x":"14148853.48172358","y":"4495338.919111188"} 77 | 78 | class Items { 79 | String? _id; 80 | Address? _address; 81 | Point? _point; 82 | 83 | String? get id => _id; 84 | Address? get address => _address; 85 | Point? get point => _point; 86 | 87 | Items({String? id, Address? address, Point? point}) { 88 | _id = id; 89 | _address = address; 90 | _point = point; 91 | } 92 | 93 | Items.fromJson(dynamic json) { 94 | _id = json['id']; 95 | _address = 96 | json['address'] != null ? Address.fromJson(json['address']) : null; 97 | _point = json['point'] != null ? Point.fromJson(json['point']) : null; 98 | } 99 | 100 | Map toJson() { 101 | var map = {}; 102 | map['id'] = _id; 103 | if (_address != null) { 104 | map['address'] = _address?.toJson(); 105 | } 106 | if (_point != null) { 107 | map['point'] = _point?.toJson(); 108 | } 109 | return map; 110 | } 111 | } 112 | 113 | /// x : "14148853.48172358" 114 | /// y : "4495338.919111188" 115 | 116 | class Point { 117 | String? _x; 118 | String? _y; 119 | 120 | String? get x => _x; 121 | String? get y => _y; 122 | 123 | Point({String? x, String? y}) { 124 | _x = x; 125 | _y = y; 126 | } 127 | 128 | Point.fromJson(dynamic json) { 129 | _x = json['x']; 130 | _y = json['y']; 131 | } 132 | 133 | Map toJson() { 134 | var map = {}; 135 | map['x'] = _x; 136 | map['y'] = _y; 137 | return map; 138 | } 139 | } 140 | 141 | /// zipcode : "13487" 142 | /// category : "road" 143 | /// road : "경기도 성남시 분당구 판교로 242 (삼평동)" 144 | /// parcel : "삼평동 624" 145 | /// bldnm : "" 146 | /// bldnmdc : "" 147 | 148 | class Address { 149 | String? _zipcode; 150 | String? _category; 151 | String? _road; 152 | String? _parcel; 153 | String? _bldnm; 154 | String? _bldnmdc; 155 | 156 | String? get zipcode => _zipcode; 157 | String? get category => _category; 158 | String? get road => _road; 159 | String? get parcel => _parcel; 160 | String? get bldnm => _bldnm; 161 | String? get bldnmdc => _bldnmdc; 162 | 163 | Address( 164 | {String? zipcode, 165 | String? category, 166 | String? road, 167 | String? parcel, 168 | String? bldnm, 169 | String? bldnmdc}) { 170 | _zipcode = zipcode; 171 | _category = category; 172 | _road = road; 173 | _parcel = parcel; 174 | _bldnm = bldnm; 175 | _bldnmdc = bldnmdc; 176 | } 177 | 178 | Address.fromJson(dynamic json) { 179 | _zipcode = json['zipcode']; 180 | _category = json['category']; 181 | _road = json['road']; 182 | _parcel = json['parcel']; 183 | _bldnm = json['bldnm']; 184 | _bldnmdc = json['bldnmdc']; 185 | } 186 | 187 | Map toJson() { 188 | var map = {}; 189 | map['zipcode'] = _zipcode; 190 | map['category'] = _category; 191 | map['road'] = _road; 192 | map['parcel'] = _parcel; 193 | map['bldnm'] = _bldnm; 194 | map['bldnmdc'] = _bldnmdc; 195 | return map; 196 | } 197 | } 198 | 199 | /// total : "1" 200 | /// current : "1" 201 | /// size : "10" 202 | 203 | class Page { 204 | String? _total; 205 | String? _current; 206 | String? _size; 207 | 208 | String? get total => _total; 209 | String? get current => _current; 210 | String? get size => _size; 211 | 212 | Page({String? total, String? current, String? size}) { 213 | _total = total; 214 | _current = current; 215 | _size = size; 216 | } 217 | 218 | Page.fromJson(dynamic json) { 219 | _total = json['total']; 220 | _current = json['current']; 221 | _size = json['size']; 222 | } 223 | 224 | Map toJson() { 225 | var map = {}; 226 | map['total'] = _total; 227 | map['current'] = _current; 228 | map['size'] = _size; 229 | return map; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /lib/data/address_model2.dart: -------------------------------------------------------------------------------- 1 | /// input : {"point":{"x":"126.978275264","y":"37.566642192"},"crs":"epsg:4326","type":"both"} 2 | /// result : [{"zipcode":"04524","type":"parcel","text":"서울특별시 중구 태평로1가 31","structure":{"level0":"대한민국","level1":"서울특별시","level2":"중구","level3":"","level4L":"태평로1가","level4LC":"1114010300","level4A":"명동","level4AC":"1114055000","level5":"31","detail":""}},{"zipcode":"04524","type":"road","text":"서울특별시 중구 태평로1가 세종대로 110 서울특별시 청사 신관","structure":{"level0":"대한민국","level1":"서울특별시","level2":"중구","level3":"태평로1가","level4L":"세종대로","level4LC":"2005001","level4A":"명동","level4AC":"1114055000","level5":"110","detail":"서울특별시 청사 신관"}}] 3 | 4 | class AddressModel2 { 5 | Input? _input; 6 | List? _result; 7 | 8 | Input? get input => _input; 9 | List? get result => _result; 10 | 11 | AddressModel2({Input? input, List? result}) { 12 | _input = input; 13 | _result = result; 14 | } 15 | 16 | AddressModel2.fromJson(dynamic json) { 17 | _input = json['input'] != null ? Input.fromJson(json['input']) : null; 18 | if (json['result'] != null) { 19 | _result = []; 20 | json['result'].forEach((v) { 21 | _result?.add(Result.fromJson(v)); 22 | }); 23 | } 24 | } 25 | 26 | Map toJson() { 27 | var map = {}; 28 | if (_input != null) { 29 | map['input'] = _input?.toJson(); 30 | } 31 | if (_result != null) { 32 | map['result'] = _result?.map((v) => v.toJson()).toList(); 33 | } 34 | return map; 35 | } 36 | } 37 | 38 | /// zipcode : "04524" 39 | /// type : "parcel" 40 | /// text : "서울특별시 중구 태평로1가 31" 41 | /// structure : {"level0":"대한민국","level1":"서울특별시","level2":"중구","level3":"","level4L":"태평로1가","level4LC":"1114010300","level4A":"명동","level4AC":"1114055000","level5":"31","detail":""} 42 | 43 | class Result { 44 | String? _zipcode; 45 | String? _type; 46 | String? _text; 47 | Structure? _structure; 48 | 49 | String? get zipcode => _zipcode; 50 | String? get type => _type; 51 | String? get text => _text; 52 | Structure? get structure => _structure; 53 | 54 | Result({String? zipcode, String? type, String? text, Structure? structure}) { 55 | _zipcode = zipcode; 56 | _type = type; 57 | _text = text; 58 | _structure = structure; 59 | } 60 | 61 | Result.fromJson(dynamic json) { 62 | _zipcode = json['zipcode']; 63 | _type = json['type']; 64 | _text = json['text']; 65 | _structure = json['structure'] != null 66 | ? Structure.fromJson(json['structure']) 67 | : null; 68 | } 69 | 70 | Map toJson() { 71 | var map = {}; 72 | map['zipcode'] = _zipcode; 73 | map['type'] = _type; 74 | map['text'] = _text; 75 | if (_structure != null) { 76 | map['structure'] = _structure?.toJson(); 77 | } 78 | return map; 79 | } 80 | } 81 | 82 | /// level0 : "대한민국" 83 | /// level1 : "서울특별시" 84 | /// level2 : "중구" 85 | /// level3 : "" 86 | /// level4L : "태평로1가" 87 | /// level4LC : "1114010300" 88 | /// level4A : "명동" 89 | /// level4AC : "1114055000" 90 | /// level5 : "31" 91 | /// detail : "" 92 | 93 | class Structure { 94 | String? _level0; 95 | String? _level1; 96 | String? _level2; 97 | String? _level3; 98 | String? _level4L; 99 | String? _level4LC; 100 | String? _level4A; 101 | String? _level4AC; 102 | String? _level5; 103 | String? _detail; 104 | 105 | String? get level0 => _level0; 106 | String? get level1 => _level1; 107 | String? get level2 => _level2; 108 | String? get level3 => _level3; 109 | String? get level4L => _level4L; 110 | String? get level4LC => _level4LC; 111 | String? get level4A => _level4A; 112 | String? get level4AC => _level4AC; 113 | String? get level5 => _level5; 114 | String? get detail => _detail; 115 | 116 | Structure( 117 | {String? level0, 118 | String? level1, 119 | String? level2, 120 | String? level3, 121 | String? level4L, 122 | String? level4LC, 123 | String? level4A, 124 | String? level4AC, 125 | String? level5, 126 | String? detail}) { 127 | _level0 = level0; 128 | _level1 = level1; 129 | _level2 = level2; 130 | _level3 = level3; 131 | _level4L = level4L; 132 | _level4LC = level4LC; 133 | _level4A = level4A; 134 | _level4AC = level4AC; 135 | _level5 = level5; 136 | _detail = detail; 137 | } 138 | 139 | Structure.fromJson(dynamic json) { 140 | _level0 = json['level0']; 141 | _level1 = json['level1']; 142 | _level2 = json['level2']; 143 | _level3 = json['level3']; 144 | _level4L = json['level4L']; 145 | _level4LC = json['level4LC']; 146 | _level4A = json['level4A']; 147 | _level4AC = json['level4AC']; 148 | _level5 = json['level5']; 149 | _detail = json['detail']; 150 | } 151 | 152 | Map toJson() { 153 | var map = {}; 154 | map['level0'] = _level0; 155 | map['level1'] = _level1; 156 | map['level2'] = _level2; 157 | map['level3'] = _level3; 158 | map['level4L'] = _level4L; 159 | map['level4LC'] = _level4LC; 160 | map['level4A'] = _level4A; 161 | map['level4AC'] = _level4AC; 162 | map['level5'] = _level5; 163 | map['detail'] = _detail; 164 | return map; 165 | } 166 | } 167 | 168 | /// point : {"x":"126.978275264","y":"37.566642192"} 169 | /// crs : "epsg:4326" 170 | /// type : "both" 171 | 172 | class Input { 173 | Point? _point; 174 | String? _crs; 175 | String? _type; 176 | 177 | Point? get point => _point; 178 | String? get crs => _crs; 179 | String? get type => _type; 180 | 181 | Input({Point? point, String? crs, String? type}) { 182 | _point = point; 183 | _crs = crs; 184 | _type = type; 185 | } 186 | 187 | Input.fromJson(dynamic json) { 188 | _point = json['point'] != null ? Point.fromJson(json['point']) : null; 189 | _crs = json['crs']; 190 | _type = json['type']; 191 | } 192 | 193 | Map toJson() { 194 | var map = {}; 195 | if (_point != null) { 196 | map['point'] = _point?.toJson(); 197 | } 198 | map['crs'] = _crs; 199 | map['type'] = _type; 200 | return map; 201 | } 202 | } 203 | 204 | /// x : "126.978275264" 205 | /// y : "37.566642192" 206 | 207 | class Point { 208 | String? _x; 209 | String? _y; 210 | 211 | String? get x => _x; 212 | String? get y => _y; 213 | 214 | Point({String? x, String? y}) { 215 | _x = x; 216 | _y = y; 217 | } 218 | 219 | Point.fromJson(dynamic json) { 220 | _x = json['x']; 221 | _y = json['y']; 222 | } 223 | 224 | Map toJson() { 225 | var map = {}; 226 | map['x'] = _x; 227 | map['y'] = _y; 228 | return map; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /lib/data/chat_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:tomato_record/constants/data_keys.dart'; 3 | 4 | /// chatKey : "" 5 | /// msg : "" 6 | /// createdDate : "" 7 | /// userKey : "" 8 | /// reference : "" 9 | 10 | class ChatModel { 11 | String? chatKey; 12 | late String msg; 13 | late DateTime createdDate; 14 | late String userKey; 15 | DocumentReference? reference; 16 | 17 | ChatModel( 18 | {required this.msg, 19 | required this.createdDate, 20 | required this.userKey, 21 | this.reference}); 22 | 23 | ChatModel.fromJson(Map json, this.chatKey, this.reference) { 24 | msg = json[DOC_MSG] ?? ""; 25 | createdDate = json[DOC_CREATEDDATE] == null 26 | ? DateTime.now().toUtc() 27 | : (json[DOC_CREATEDDATE] as Timestamp).toDate(); 28 | userKey = json[DOC_USERKEY] ?? ""; 29 | } 30 | 31 | Map toJson() { 32 | var map = {}; 33 | map[DOC_MSG] = msg; 34 | map[DOC_CREATEDDATE] = createdDate; 35 | map[DOC_USERKEY] = userKey; 36 | return map; 37 | } 38 | 39 | ChatModel.fromQuerySnapshot( 40 | QueryDocumentSnapshot> snapshot) 41 | : this.fromJson(snapshot.data(), snapshot.id, snapshot.reference); 42 | 43 | ChatModel.fromSnapshot(DocumentSnapshot> snapshot) 44 | : this.fromJson(snapshot.data()!, snapshot.id, snapshot.reference); 45 | } 46 | -------------------------------------------------------------------------------- /lib/data/chatroom_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:geoflutterfire/geoflutterfire.dart'; 3 | import 'package:tomato_record/constants/data_keys.dart'; 4 | 5 | /// item_image : "" 6 | /// item_title : "" 7 | /// item_key : "" 8 | /// item_address : "" 9 | /// item_price : 0.0 10 | /// seller_key : "" 11 | /// buyer_key : "" 12 | /// seller_image : "" 13 | /// buyer_image : "" 14 | /// geo_fire_point : "" 15 | /// last_msg : "" 16 | /// last_msg_time : "2012-04-21T18:25:43-05:00" 17 | /// last_msg_user_key : "" 18 | /// chatroom_key : "" 19 | 20 | class ChatroomModel { 21 | late String itemImage; 22 | late String itemTitle; 23 | late String itemKey; 24 | late String itemAddress; 25 | late num itemPrice; 26 | late String sellerKey; 27 | late String buyerKey; 28 | late String sellerImage; 29 | late String buyerImage; 30 | late GeoFirePoint geoFirePoint; 31 | late String lastMsg; 32 | late DateTime lastMsgTime; 33 | late String lastMsgUserKey; 34 | late String chatroomKey; 35 | DocumentReference? reference; 36 | 37 | ChatroomModel( 38 | {required this.itemImage, 39 | required this.itemTitle, 40 | required this.itemKey, 41 | required this.itemAddress, 42 | required this.itemPrice, 43 | required this.sellerKey, 44 | required this.buyerKey, 45 | required this.sellerImage, 46 | required this.buyerImage, 47 | required this.geoFirePoint, 48 | this.lastMsg = "", 49 | required this.lastMsgTime, 50 | this.lastMsgUserKey = "", 51 | required this.chatroomKey, 52 | this.reference}); 53 | ChatroomModel.fromJson( 54 | Map json, this.chatroomKey, this.reference) { 55 | itemImage = json[DOC_ITEMIMAGE] ?? ""; 56 | itemTitle = json[DOC_ITEMTITLE] ?? ""; 57 | itemKey = json[DOC_ITEMKEY] ?? ""; 58 | itemAddress = json[DOC_ITEMADDRESS] ?? ""; 59 | itemPrice = json[DOC_ITEMPRICE] ?? 0; 60 | sellerKey = json[DOC_SELLERKEY] ?? ""; 61 | buyerKey = json[DOC_BUYERKEY] ?? ""; 62 | sellerImage = json[DOC_SELLERIMAGE] ?? ""; 63 | buyerImage = json[DOC_BUYERIMAGE] ?? ""; 64 | geoFirePoint = json[DOC_GEOFIREPOINT] == null 65 | ? GeoFirePoint(0, 0) 66 | : GeoFirePoint((json[DOC_GEOFIREPOINT][DOC_GEOPOINT]).latitude, 67 | (json[DOC_GEOFIREPOINT][DOC_GEOPOINT]).longitude); 68 | lastMsg = json[DOC_LASTMSG] ?? ""; 69 | lastMsgTime = json[DOC_LASTMSGTIME] == null 70 | ? DateTime.now().toUtc() 71 | : (json[DOC_LASTMSGTIME] as Timestamp).toDate(); 72 | lastMsgUserKey = json[DOC_LASTMSGUSERKEY] ?? ""; 73 | } 74 | 75 | Map toJson() { 76 | var map = {}; 77 | map[DOC_ITEMIMAGE] = itemImage; 78 | map[DOC_ITEMTITLE] = itemTitle; 79 | map[DOC_ITEMKEY] = itemKey; 80 | map[DOC_ITEMADDRESS] = itemAddress; 81 | map[DOC_ITEMPRICE] = itemPrice; 82 | map[DOC_SELLERKEY] = sellerKey; 83 | map[DOC_BUYERKEY] = buyerKey; 84 | map[DOC_SELLERIMAGE] = sellerImage; 85 | map[DOC_BUYERIMAGE] = buyerImage; 86 | map[DOC_GEOFIREPOINT] = geoFirePoint.data; 87 | map[DOC_LASTMSG] = lastMsg; 88 | map[DOC_LASTMSGTIME] = lastMsgTime; 89 | map[DOC_LASTMSGUSERKEY] = lastMsgUserKey; 90 | return map; 91 | } 92 | 93 | ChatroomModel.fromQuerySnapshot( 94 | QueryDocumentSnapshot> snapshot) 95 | : this.fromJson(snapshot.data(), snapshot.id, snapshot.reference); 96 | 97 | ChatroomModel.fromSnapshot(DocumentSnapshot> snapshot) 98 | : this.fromJson(snapshot.data()!, snapshot.id, snapshot.reference); 99 | 100 | static String generateChatRoomKey(String buyer, String itemKey) { 101 | return '${itemKey}_$buyer'; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/data/item_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:geoflutterfire/geoflutterfire.dart'; 3 | import 'package:tomato_record/constants/data_keys.dart'; 4 | 5 | class ItemModel { 6 | late String itemKey; 7 | late String userKey; 8 | late List imageDownloadUrls; 9 | late String title; 10 | late String category; 11 | late num price; 12 | late bool negotiable; 13 | late String detail; 14 | late String address; 15 | late GeoFirePoint geoFirePoint; 16 | late DateTime createdDate; 17 | DocumentReference? reference; 18 | 19 | ItemModel( 20 | {required this.itemKey, 21 | required this.userKey, 22 | required this.imageDownloadUrls, 23 | required this.title, 24 | required this.category, 25 | required this.price, 26 | required this.negotiable, 27 | required this.detail, 28 | required this.address, 29 | required this.geoFirePoint, 30 | required this.createdDate, 31 | this.reference}); 32 | 33 | ItemModel.fromJson(Map json, this.itemKey, this.reference) { 34 | userKey = json[DOC_USERKEY] ?? ""; 35 | imageDownloadUrls = json[DOC_IMAGEDOWNLOADURLS] != null 36 | ? json[DOC_IMAGEDOWNLOADURLS].cast() 37 | : []; 38 | title = json[DOC_TITLE] ?? ""; 39 | category = json[DOC_CATEGORY] ?? "none"; 40 | price = json[DOC_PRICE] ?? 0; 41 | negotiable = json[DOC_NEGOTIABLE] ?? false; 42 | detail = json[DOC_DETAIL] ?? ""; 43 | address = json[DOC_ADDRESS] ?? ""; 44 | geoFirePoint = json[DOC_GEOFIREPOINT] == null 45 | ? GeoFirePoint(0, 0) 46 | : GeoFirePoint((json[DOC_GEOFIREPOINT][DOC_GEOPOINT]).latitude, 47 | (json[DOC_GEOFIREPOINT][DOC_GEOPOINT]).longitude); 48 | createdDate = json[DOC_CREATEDDATE] == null 49 | ? DateTime.now().toUtc() 50 | : (json[DOC_CREATEDDATE] as Timestamp).toDate(); 51 | } 52 | ItemModel.fromAlgoliaObject(Map json, this.itemKey) { 53 | userKey = json[DOC_USERKEY] ?? ""; 54 | imageDownloadUrls = json[DOC_IMAGEDOWNLOADURLS] != null 55 | ? json[DOC_IMAGEDOWNLOADURLS].cast() 56 | : []; 57 | title = json[DOC_TITLE] ?? ""; 58 | category = json[DOC_CATEGORY] ?? "none"; 59 | price = json[DOC_PRICE] ?? 0; 60 | negotiable = json[DOC_NEGOTIABLE] ?? false; 61 | detail = json[DOC_DETAIL] ?? ""; 62 | address = json[DOC_ADDRESS] ?? ""; 63 | geoFirePoint = GeoFirePoint(0, 0); 64 | createdDate = DateTime.now().toUtc(); 65 | } 66 | 67 | ItemModel.fromQuerySnapshot( 68 | QueryDocumentSnapshot> snapshot) 69 | : this.fromJson(snapshot.data(), snapshot.id, snapshot.reference); 70 | 71 | ItemModel.fromSnapshot(DocumentSnapshot> snapshot) 72 | : this.fromJson(snapshot.data()!, snapshot.id, snapshot.reference); 73 | 74 | Map toJson() { 75 | var map = {}; 76 | map[DOC_USERKEY] = userKey; 77 | map[DOC_IMAGEDOWNLOADURLS] = imageDownloadUrls; 78 | map[DOC_TITLE] = title; 79 | map[DOC_CATEGORY] = category; 80 | map[DOC_PRICE] = price; 81 | map[DOC_NEGOTIABLE] = negotiable; 82 | map[DOC_DETAIL] = detail; 83 | map[DOC_ADDRESS] = address; 84 | map[DOC_GEOFIREPOINT] = geoFirePoint.data; 85 | map[DOC_CREATEDDATE] = createdDate; 86 | return map; 87 | } 88 | 89 | Map toMinJson() { 90 | var map = {}; 91 | map[DOC_IMAGEDOWNLOADURLS] = imageDownloadUrls.sublist(0, 1); 92 | map[DOC_TITLE] = title; 93 | map[DOC_PRICE] = price; 94 | return map; 95 | } 96 | 97 | static String generateItemKey(String uid) { 98 | String timeInMilli = DateTime.now().millisecondsSinceEpoch.toString(); 99 | return '${uid}_$timeInMilli'; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/data/user_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:geoflutterfire/geoflutterfire.dart'; 3 | import 'package:tomato_record/constants/data_keys.dart'; 4 | 5 | class UserModel { 6 | late String userKey; 7 | late String phoneNumber; 8 | late String address; 9 | late GeoFirePoint geoFirePoint; 10 | late DateTime createdDate; 11 | DocumentReference? reference; 12 | 13 | UserModel( 14 | {required this.userKey, 15 | required this.phoneNumber, 16 | required this.address, 17 | required this.geoFirePoint, 18 | required this.createdDate, 19 | this.reference}); 20 | 21 | UserModel.fromJson(Map json, this.userKey, this.reference) 22 | : phoneNumber = json[DOC_PHONENUMBER], 23 | address = json[DOC_ADDRESS], 24 | geoFirePoint = GeoFirePoint( 25 | (json[DOC_GEOFIREPOINT][DOC_GEOPOINT]).latitude, 26 | (json[DOC_GEOFIREPOINT][DOC_GEOPOINT]).longitude), 27 | createdDate = json[DOC_CREATEDDATE] == null 28 | ? DateTime.now().toUtc() 29 | : (json[DOC_CREATEDDATE] as Timestamp).toDate(); 30 | 31 | UserModel.fromSnapshot(DocumentSnapshot> snapshot) 32 | : this.fromJson(snapshot.data()!, snapshot.id, snapshot.reference); 33 | 34 | Map toJson() { 35 | var map = {}; 36 | map[DOC_PHONENUMBER] = phoneNumber; 37 | map[DOC_ADDRESS] = address; 38 | map[DOC_GEOFIREPOINT] = geoFirePoint.data; 39 | map[DOC_CREATEDDATE] = createdDate; 40 | return map; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:beamer/beamer.dart'; 2 | import 'package:firebase_core/firebase_core.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:tomato_record/router/locations.dart'; 6 | import 'package:tomato_record/screens/start_screen.dart'; 7 | import 'package:tomato_record/screens/splash_screen.dart'; 8 | import 'package:tomato_record/states/user_notifier.dart'; 9 | import 'package:tomato_record/utils/logger.dart'; 10 | 11 | final _routerDelegate = BeamerDelegate( 12 | guards: [ 13 | BeamGuard( 14 | pathBlueprints: [ 15 | ...HomeLocation().pathBlueprints, 16 | ...InputLocation().pathBlueprints, 17 | ...ItemLocation().pathBlueprints 18 | ], 19 | check: (context, location) { 20 | return context.watch().user != null; 21 | }, 22 | showPage: BeamPage(child: StartScreen())) 23 | ], 24 | locationBuilder: BeamerLocationBuilder( 25 | beamLocations: [HomeLocation(), InputLocation(), ItemLocation()])); 26 | 27 | void main() { 28 | Provider.debugCheckInvalidValueType = null; 29 | WidgetsFlutterBinding.ensureInitialized(); 30 | runApp(MyApp()); 31 | } 32 | 33 | class MyApp extends StatefulWidget { 34 | @override 35 | _MyAppState createState() => _MyAppState(); 36 | } 37 | 38 | class _MyAppState extends State { 39 | final Future _initialization = Firebase.initializeApp(); 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return FutureBuilder( 44 | future: _initialization, 45 | builder: (context, snapshot) { 46 | return AnimatedSwitcher( 47 | duration: Duration(milliseconds: 300), 48 | child: _splashLoadingWidget(snapshot)); 49 | }); 50 | } 51 | 52 | StatelessWidget _splashLoadingWidget(AsyncSnapshot snapshot) { 53 | if (snapshot.hasError) { 54 | print('error occur while loading.'); 55 | return Text('Error occur'); 56 | } else if (snapshot.connectionState == ConnectionState.done) { 57 | return TomatoApp(); 58 | } else { 59 | return SplashScreen(); 60 | } 61 | } 62 | } 63 | 64 | class TomatoApp extends StatelessWidget { 65 | const TomatoApp({Key? key}) : super(key: key); 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return ChangeNotifierProvider( 70 | create: (BuildContext context) { 71 | return UserNotifier(); 72 | }, 73 | child: MaterialApp.router( 74 | theme: ThemeData( 75 | primarySwatch: Colors.red, 76 | fontFamily: 'DoHyeon', 77 | hintColor: Colors.grey[350], 78 | textTheme: TextTheme( 79 | button: TextStyle(color: Colors.white), 80 | subtitle1: TextStyle(color: Colors.black87, fontSize: 15), 81 | subtitle2: TextStyle(color: Colors.grey, fontSize: 13), 82 | bodyText1: TextStyle( 83 | color: Colors.black87, 84 | fontSize: 12, 85 | fontWeight: FontWeight.normal), 86 | bodyText2: TextStyle( 87 | color: Colors.black54, 88 | fontSize: 12, 89 | fontWeight: FontWeight.w100), 90 | ), 91 | textButtonTheme: TextButtonThemeData( 92 | style: TextButton.styleFrom( 93 | backgroundColor: Colors.red, 94 | primary: Colors.white, 95 | minimumSize: Size(48, 48))), 96 | appBarTheme: AppBarTheme( 97 | backwardsCompatibility: false, 98 | backgroundColor: Colors.white, 99 | foregroundColor: Colors.black87, 100 | elevation: 2, 101 | titleTextStyle: TextStyle( 102 | color: Colors.black87, 103 | ), 104 | actionsIconTheme: IconThemeData(color: Colors.black87)), 105 | bottomNavigationBarTheme: BottomNavigationBarThemeData( 106 | selectedItemColor: Colors.black87, 107 | unselectedItemColor: Colors.black54)), 108 | routeInformationParser: BeamerParser(), 109 | routerDelegate: _routerDelegate, 110 | ), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/repo/algolia_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:algolia/algolia.dart'; 2 | import 'package:tomato_record/data/item_model.dart'; 3 | 4 | const Algolia algolia = Algolia.init( 5 | applicationId: 'O9Q1TLNUFA', 6 | apiKey: '9fa70527a465228fb7da3c5cb2a71e70', 7 | ); 8 | 9 | class AlgoliaService { 10 | static final AlgoliaService _algoliaService = AlgoliaService._internal(); 11 | 12 | factory AlgoliaService() => _algoliaService; 13 | 14 | AlgoliaService._internal(); 15 | 16 | Future> queryItems(String queryStr) async { 17 | AlgoliaQuery query = algolia.instance.index('items').query(queryStr); 18 | 19 | // Perform multiple facetFilters 20 | // query = query.facetFilter('status:published'); 21 | // query = query.facetFilter('isDelete:false'); 22 | AlgoliaQuerySnapshot algoliaSnapshot = await query.getObjects(); 23 | List hits = algoliaSnapshot.hits; 24 | List items = []; 25 | hits.forEach((element) { 26 | ItemModel item = 27 | ItemModel.fromAlgoliaObject(element.data, element.objectID); 28 | items.add(item); 29 | }); 30 | return items; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/repo/chat_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:tomato_record/constants/data_keys.dart'; 5 | import 'package:tomato_record/data/chat_model.dart'; 6 | import 'package:tomato_record/data/chatroom_model.dart'; 7 | 8 | class ChatService { 9 | static final ChatService _chatService = ChatService._internal(); 10 | factory ChatService() => _chatService; 11 | ChatService._internal(); 12 | 13 | Future createNewChatroom(ChatroomModel chatroomModel) async { 14 | DocumentReference> documentReference = 15 | FirebaseFirestore.instance.collection(COL_CHATROOMS).doc( 16 | ChatroomModel.generateChatRoomKey( 17 | chatroomModel.buyerKey, chatroomModel.itemKey)); 18 | final DocumentSnapshot documentSnapshot = await documentReference.get(); 19 | 20 | if (!documentSnapshot.exists) { 21 | await documentReference.set(chatroomModel.toJson()); 22 | } 23 | } 24 | 25 | Future createNewChat(String chatroomKey, ChatModel chatModel) async { 26 | DocumentReference> documentReference = 27 | FirebaseFirestore.instance 28 | .collection(COL_CHATROOMS) 29 | .doc(chatroomKey) 30 | .collection(COL_CHATS) 31 | .doc(); 32 | 33 | DocumentReference> chatroomDocRef = 34 | FirebaseFirestore.instance.collection(COL_CHATROOMS).doc(chatroomKey); 35 | 36 | await documentReference.set(chatModel.toJson()); 37 | 38 | await FirebaseFirestore.instance.runTransaction((transaction) async { 39 | transaction.set(documentReference, chatModel.toJson()); 40 | transaction.update(chatroomDocRef, { 41 | DOC_LASTMSG: chatModel.msg, 42 | DOC_LASTMSGTIME: chatModel.createdDate, 43 | DOC_LASTMSGUSERKEY: chatModel.userKey 44 | }); 45 | }); 46 | } 47 | 48 | Stream connectChatroom(String chatroomKey) { 49 | return FirebaseFirestore.instance 50 | .collection(COL_CHATROOMS) 51 | .doc(chatroomKey) 52 | .snapshots() 53 | .transform(snapshotToChatroom); 54 | } 55 | 56 | var snapshotToChatroom = StreamTransformer< 57 | DocumentSnapshot>, 58 | ChatroomModel>.fromHandlers(handleData: (snapshot, sink) { 59 | ChatroomModel chatroom = ChatroomModel.fromSnapshot(snapshot); 60 | sink.add(chatroom); 61 | }); 62 | 63 | Future> getChatList(String chatroomKey) async { 64 | QuerySnapshot> snapshot = await FirebaseFirestore 65 | .instance 66 | .collection(COL_CHATROOMS) 67 | .doc(chatroomKey) 68 | .collection(COL_CHATS) 69 | .orderBy(DOC_CREATEDDATE, descending: true) 70 | .limit(10) 71 | .get(); 72 | 73 | List chatlist = []; 74 | 75 | snapshot.docs.forEach((docSnapshot) { 76 | ChatModel chatModel = ChatModel.fromQuerySnapshot(docSnapshot); 77 | chatlist.add(chatModel); 78 | }); 79 | return chatlist; 80 | } 81 | 82 | Future> getLatestChats( 83 | String chatroomKey, DocumentReference currentLatestChatRef) async { 84 | QuerySnapshot> snapshot = await FirebaseFirestore 85 | .instance 86 | .collection(COL_CHATROOMS) 87 | .doc(chatroomKey) 88 | .collection(COL_CHATS) 89 | .orderBy(DOC_CREATEDDATE, descending: true) 90 | .endBeforeDocument(await currentLatestChatRef.get()) 91 | .get(); 92 | 93 | List chatlist = []; 94 | 95 | snapshot.docs.forEach((docSnapshot) { 96 | ChatModel chatModel = ChatModel.fromQuerySnapshot(docSnapshot); 97 | chatlist.add(chatModel); 98 | }); 99 | return chatlist; 100 | } 101 | 102 | Future> getOlderChats( 103 | String chatroomKey, DocumentReference oldestChatRef) async { 104 | QuerySnapshot> snapshot = await FirebaseFirestore 105 | .instance 106 | .collection(COL_CHATROOMS) 107 | .doc(chatroomKey) 108 | .collection(COL_CHATS) 109 | .orderBy(DOC_CREATEDDATE, descending: true) 110 | .startAfterDocument(await oldestChatRef.get()) 111 | .limit(10) 112 | .get(); 113 | 114 | List chatlist = []; 115 | 116 | snapshot.docs.forEach((docSnapshot) { 117 | ChatModel chatModel = ChatModel.fromQuerySnapshot(docSnapshot); 118 | chatlist.add(chatModel); 119 | }); 120 | return chatlist; 121 | } 122 | 123 | Future> getMyChatList(String myUserkey) async { 124 | List chatrooms = []; 125 | 126 | QuerySnapshot> buying = await FirebaseFirestore 127 | .instance 128 | .collection(COL_CHATROOMS) 129 | .where(DOC_BUYERKEY, isEqualTo: myUserkey) 130 | .get(); 131 | 132 | QuerySnapshot> selling = await FirebaseFirestore 133 | .instance 134 | .collection(COL_CHATROOMS) 135 | .where(DOC_SELLERKEY, isEqualTo: myUserkey) 136 | .get(); 137 | 138 | buying.docs.forEach((documentSnapshot) { 139 | chatrooms.add(ChatroomModel.fromQuerySnapshot(documentSnapshot)); 140 | }); 141 | selling.docs.forEach((documentSnapshot) { 142 | chatrooms.add(ChatroomModel.fromQuerySnapshot(documentSnapshot)); 143 | }); 144 | 145 | print('chatroom list - ${chatrooms.length}'); 146 | 147 | chatrooms.sort((a, b) => (a.lastMsgTime).compareTo(b.lastMsgTime)); 148 | 149 | return chatrooms; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/repo/image_storage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:firebase_storage/firebase_storage.dart'; 4 | import 'package:tomato_record/utils/logger.dart'; 5 | 6 | class ImageStorage { 7 | static Future> uploadImages( 8 | List images, String itemKey) async { 9 | var metaData = SettableMetadata(contentType: 'image/jpeg'); 10 | 11 | List downloadUrls = []; 12 | 13 | for (int i = 0; i < images.length; i++) { 14 | Reference ref = FirebaseStorage.instance.ref('images/$itemKey/$i.jpg'); 15 | if (images.isNotEmpty) { 16 | await ref.putData(images[i], metaData).catchError((onError) { 17 | logger.e(onError.toString()); 18 | }); 19 | 20 | downloadUrls.add(await ref.getDownloadURL()); 21 | } 22 | } 23 | 24 | return downloadUrls; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/repo/item_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | import 'package:geoflutterfire/geoflutterfire.dart'; 5 | import 'package:latlng/latlng.dart'; 6 | import 'package:tomato_record/constants/data_keys.dart'; 7 | import 'package:tomato_record/data/item_model.dart'; 8 | 9 | class ItemService { 10 | static final ItemService _itemService = ItemService._internal(); 11 | factory ItemService() => _itemService; 12 | ItemService._internal(); 13 | 14 | Future createNewItem( 15 | ItemModel itemModel, String itemKey, String userKey) async { 16 | DocumentReference> itemDocReference = 17 | FirebaseFirestore.instance.collection(COL_ITEMS).doc(itemKey); 18 | DocumentReference> userItemDocReference = 19 | FirebaseFirestore.instance 20 | .collection(COL_USERS) 21 | .doc(userKey) 22 | .collection(COL_USER_ITEMS) 23 | .doc(itemKey); 24 | final DocumentSnapshot documentSnapshot = await itemDocReference.get(); 25 | 26 | if (!documentSnapshot.exists) { 27 | await FirebaseFirestore.instance.runTransaction((transaction) async { 28 | transaction.set(itemDocReference, itemModel.toJson()); 29 | transaction.set(userItemDocReference, itemModel.toMinJson()); 30 | }); 31 | } 32 | } 33 | 34 | Future getItem(String itemKey) async { 35 | DocumentReference> documentReference = 36 | FirebaseFirestore.instance.collection(COL_ITEMS).doc(itemKey); 37 | final DocumentSnapshot> documentSnapshot = 38 | await documentReference.get(); 39 | ItemModel itemModel = ItemModel.fromSnapshot(documentSnapshot); 40 | return itemModel; 41 | } 42 | 43 | Future> getItems(String userKey) async { 44 | CollectionReference> collectionReference = 45 | FirebaseFirestore.instance.collection(COL_ITEMS); 46 | QuerySnapshot> snapshots = await collectionReference 47 | .where(DOC_USERKEY, isNotEqualTo: userKey) 48 | .get(); 49 | 50 | List items = []; 51 | 52 | for (int i = 0; i < snapshots.size; i++) { 53 | ItemModel itemModel = ItemModel.fromQuerySnapshot(snapshots.docs[i]); 54 | items.add(itemModel); 55 | } 56 | 57 | return items; 58 | } 59 | 60 | Future> getUserItems(String userKey, 61 | {String? itemKey}) async { 62 | CollectionReference> collectionReference = 63 | FirebaseFirestore.instance 64 | .collection(COL_USERS) 65 | .doc(userKey) 66 | .collection(COL_USER_ITEMS); 67 | QuerySnapshot> snapshots = 68 | await collectionReference.get(); 69 | 70 | List items = []; 71 | 72 | for (int i = 0; i < snapshots.size; i++) { 73 | ItemModel itemModel = ItemModel.fromQuerySnapshot(snapshots.docs[i]); 74 | if (!(itemKey != null && itemKey == itemModel.itemKey)) 75 | items.add(itemModel); 76 | } 77 | 78 | return items; 79 | } 80 | 81 | Future> getNearByItems(String userKey, LatLng latLng) async { 82 | final geo = Geoflutterfire(); 83 | final itemCol = FirebaseFirestore.instance.collection(COL_ITEMS); 84 | 85 | GeoFirePoint center = GeoFirePoint(latLng.latitude, latLng.longitude); 86 | double radius = 1.5; 87 | var field = DOC_GEOFIREPOINT; 88 | 89 | List items = []; 90 | List>> snapshots = await geo 91 | .collection(collectionRef: itemCol) 92 | .within(center: center, radius: radius, field: field) 93 | .first; 94 | 95 | for (int i = 0; i < snapshots.length; i++) { 96 | ItemModel itemModel = ItemModel.fromSnapshot(snapshots[i]); 97 | //todo: remove my own item 98 | items.add(itemModel); 99 | } 100 | 101 | return items; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/repo/user_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:tomato_record/constants/data_keys.dart'; 3 | import 'package:tomato_record/data/user_model.dart'; 4 | import 'package:tomato_record/utils/logger.dart'; 5 | 6 | class UserService { 7 | static final UserService _userService = UserService._internal(); 8 | factory UserService() => _userService; 9 | UserService._internal(); 10 | 11 | Future createNewUser(Map json, String userKey) async { 12 | DocumentReference> documentReference = 13 | FirebaseFirestore.instance.collection(COL_USERS).doc(userKey); 14 | final DocumentSnapshot documentSnapshot = await documentReference.get(); 15 | 16 | if (!documentSnapshot.exists) { 17 | await documentReference.set(json); 18 | } 19 | } 20 | 21 | Future getUserModel(String userKey) async { 22 | DocumentReference> documentReference = 23 | FirebaseFirestore.instance.collection(COL_USERS).doc(userKey); 24 | final DocumentSnapshot> documentSnapshot = 25 | await documentReference.get(); 26 | UserModel userModel = UserModel.fromSnapshot(documentSnapshot); 27 | return userModel; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/router/locations.dart: -------------------------------------------------------------------------------- 1 | import 'package:beamer/beamer.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/widgets.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:tomato_record/screens/chat/chatroom_screen.dart'; 6 | import 'package:tomato_record/screens/home_screen.dart'; 7 | import 'package:tomato_record/screens/input/category_input_screen.dart'; 8 | import 'package:tomato_record/screens/input/input_screen.dart'; 9 | import 'package:tomato_record/screens/item/item_detail_screen.dart'; 10 | import 'package:tomato_record/screens/search/search_screen.dart'; 11 | import 'package:tomato_record/states/category_notifier.dart'; 12 | import 'package:tomato_record/states/select_image_notifier.dart'; 13 | import 'package:tomato_record/utils/logger.dart'; 14 | 15 | const LOCATION_HOME = 'home'; 16 | const LOCATION_INPUT = 'input'; 17 | const LOCATION_ITEM = 'item'; 18 | const LOCATION_SEARCH = 'search'; 19 | const LOCATION_ITEM_ID = 'item_id'; 20 | const LOCATION_CHATROOM_ID = 'chatroom_id'; 21 | const LOCATION_CATEGORY_INPUT = 'category_input'; 22 | 23 | class HomeLocation extends BeamLocation { 24 | @override 25 | List buildPages(BuildContext context, BeamState state) { 26 | return [ 27 | BeamPage(child: HomeScreen(), key: ValueKey(LOCATION_HOME)), 28 | if (state.pathBlueprintSegments.contains(LOCATION_SEARCH)) 29 | BeamPage(key: ValueKey(LOCATION_SEARCH), child: SearchScreen()), 30 | ]; 31 | } 32 | 33 | @override 34 | List get pathBlueprints => ['/', '/$LOCATION_SEARCH']; 35 | } 36 | 37 | class InputLocation extends BeamLocation { 38 | @override 39 | Widget builder(BuildContext context, Widget navigator) { 40 | return MultiProvider( 41 | providers: [ 42 | ChangeNotifierProvider.value(value: categoryNotifier), 43 | ChangeNotifierProvider(create: (context) => SelectImageNotifier()) 44 | ], 45 | child: super.builder(context, navigator), 46 | ); 47 | } 48 | 49 | @override 50 | List buildPages(BuildContext context, BeamState state) { 51 | return [ 52 | ...HomeLocation().buildPages(context, state), 53 | if (state.pathBlueprintSegments.contains(LOCATION_INPUT)) 54 | BeamPage(key: ValueKey(LOCATION_INPUT), child: InputScreen()), 55 | if (state.pathBlueprintSegments.contains(LOCATION_CATEGORY_INPUT)) 56 | BeamPage( 57 | key: ValueKey(LOCATION_CATEGORY_INPUT), 58 | child: CategoryInputScreen()), 59 | ]; 60 | } 61 | 62 | @override 63 | List get pathBlueprints => 64 | ['/$LOCATION_INPUT', '/$LOCATION_INPUT/$LOCATION_CATEGORY_INPUT']; 65 | } 66 | 67 | class ItemLocation extends BeamLocation { 68 | @override 69 | List buildPages(BuildContext context, BeamState state) { 70 | logger.d('path - ${state.uriBlueprint}\n${state.uri}'); 71 | return [ 72 | ...HomeLocation().buildPages(context, state), 73 | if (state.pathParameters.containsKey(LOCATION_ITEM_ID)) 74 | BeamPage( 75 | key: ValueKey(LOCATION_ITEM_ID), 76 | child: 77 | ItemDetailScreen(state.pathParameters[LOCATION_ITEM_ID] ?? "")), 78 | if (state.pathParameters.containsKey(LOCATION_CHATROOM_ID)) 79 | BeamPage( 80 | key: ValueKey(LOCATION_CHATROOM_ID), 81 | child: ChatroomScreen( 82 | chatroomKey: state.pathParameters[LOCATION_CHATROOM_ID] ?? "")), 83 | ]; 84 | } 85 | 86 | @override 87 | List get pathBlueprints => [ 88 | '/$LOCATION_SEARCH/:$LOCATION_ITEM_ID/:$LOCATION_CHATROOM_ID', 89 | '/$LOCATION_ITEM/:$LOCATION_ITEM_ID/:$LOCATION_CHATROOM_ID', 90 | '/:$LOCATION_CHATROOM_ID' 91 | ]; 92 | } 93 | -------------------------------------------------------------------------------- /lib/screens/chat/chat.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:tomato_record/data/chat_model.dart'; 4 | 5 | const roundedCorner = Radius.circular(20); 6 | 7 | class Chat extends StatelessWidget { 8 | final Size size; 9 | final bool isMine; 10 | final ChatModel chatModel; 11 | const Chat( 12 | {Key? key, 13 | required this.size, 14 | required this.isMine, 15 | required this.chatModel}) 16 | : super(key: key); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return isMine ? _buildMyMsg(context) : _buildOthersMsg(context); 21 | } 22 | 23 | Row _buildOthersMsg(BuildContext context) { 24 | return Row( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | mainAxisAlignment: MainAxisAlignment.start, 27 | children: [ 28 | ExtendedImage.network( 29 | 'https://randomuser.me/api/portraits/women/26.jpg', 30 | width: 40, 31 | height: 40, 32 | fit: BoxFit.cover, 33 | borderRadius: BorderRadius.circular(6), 34 | shape: BoxShape.rectangle, 35 | ), 36 | SizedBox( 37 | width: 6, 38 | ), 39 | Row( 40 | crossAxisAlignment: CrossAxisAlignment.end, 41 | mainAxisAlignment: MainAxisAlignment.start, 42 | children: [ 43 | Container( 44 | child: Text(chatModel.msg, 45 | style: Theme.of(context).textTheme.bodyText1!), 46 | padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), 47 | constraints: 48 | BoxConstraints(minHeight: 40, maxWidth: size.width * 0.5), 49 | decoration: BoxDecoration( 50 | color: Colors.grey[300], 51 | borderRadius: BorderRadius.only( 52 | topRight: roundedCorner, 53 | topLeft: Radius.circular(2), 54 | bottomRight: roundedCorner, 55 | bottomLeft: roundedCorner)), 56 | ), 57 | SizedBox( 58 | width: 6, 59 | ), 60 | Text('오전 10:25'), 61 | ], 62 | ), 63 | ], 64 | ); 65 | } 66 | 67 | Row _buildMyMsg(BuildContext context) { 68 | return Row( 69 | crossAxisAlignment: CrossAxisAlignment.end, 70 | mainAxisAlignment: MainAxisAlignment.end, 71 | children: [ 72 | Text('오전 10:25'), 73 | SizedBox( 74 | width: 6, 75 | ), 76 | Container( 77 | child: Text( 78 | chatModel.msg, 79 | style: Theme.of(context) 80 | .textTheme 81 | .bodyText1! 82 | .copyWith(color: Colors.white), 83 | ), 84 | padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), 85 | constraints: 86 | BoxConstraints(minHeight: 40, maxWidth: size.width * 0.6), 87 | decoration: BoxDecoration( 88 | color: Colors.red, 89 | borderRadius: BorderRadius.only( 90 | topLeft: roundedCorner, 91 | topRight: Radius.circular(2), 92 | bottomRight: roundedCorner, 93 | bottomLeft: roundedCorner)), 94 | ), 95 | ], 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/screens/chat/chat_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:beamer/src/beamer.dart'; 2 | import 'package:extended_image/extended_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:tomato_record/data/chatroom_model.dart'; 5 | import 'package:tomato_record/repo/chat_service.dart'; 6 | 7 | class ChatListPage extends StatelessWidget { 8 | final String userKey; 9 | const ChatListPage({Key? key, required this.userKey}) : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return FutureBuilder>( 14 | future: ChatService().getMyChatList(userKey), 15 | builder: (context, snapshot) { 16 | Size size = MediaQuery.of(context).size; 17 | return Scaffold( 18 | body: ListView.separated( 19 | itemBuilder: (context, index) { 20 | ChatroomModel chatroomModel = snapshot.data![index]; 21 | bool iamBuyer = chatroomModel.buyerKey == userKey; 22 | 23 | return ListTile( 24 | onTap: () { 25 | context.beamToNamed('/${chatroomModel.chatroomKey}'); 26 | }, 27 | leading: ExtendedImage.network( 28 | 'https://randomuser.me/api/portraits/women/11.jpg', 29 | shape: BoxShape.circle, 30 | fit: BoxFit.cover, 31 | height: size.width / 8, 32 | width: size.width / 8, 33 | ), 34 | trailing: ExtendedImage.network( 35 | chatroomModel.itemImage, 36 | shape: BoxShape.rectangle, 37 | fit: BoxFit.cover, 38 | height: size.width / 8, 39 | width: size.width / 8, 40 | borderRadius: BorderRadius.circular(4), 41 | ), 42 | title: RichText( 43 | maxLines: 2, 44 | overflow: TextOverflow.ellipsis, 45 | text: TextSpan( 46 | text: iamBuyer 47 | ? chatroomModel.sellerKey 48 | : chatroomModel.buyerKey, 49 | style: Theme.of(context).textTheme.subtitle1, 50 | children: [ 51 | TextSpan(text: " "), 52 | TextSpan( 53 | text: "${chatroomModel.itemAddress}", 54 | style: Theme.of(context).textTheme.subtitle2, 55 | ) 56 | ]), 57 | ), 58 | subtitle: Text( 59 | chatroomModel.lastMsg, 60 | maxLines: 1, 61 | overflow: TextOverflow.ellipsis, 62 | style: Theme.of(context).textTheme.bodyText1, 63 | ), 64 | ); 65 | }, 66 | separatorBuilder: (context, index) { 67 | return Divider( 68 | thickness: 1, 69 | height: 1, 70 | color: Colors.grey[300], 71 | ); 72 | }, 73 | itemCount: snapshot.hasData ? snapshot.data!.length : 0), 74 | ); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/screens/chat/chatroom_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_multi_formatter/flutter_multi_formatter.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:shimmer/shimmer.dart'; 6 | import 'package:tomato_record/data/chat_model.dart'; 7 | import 'package:tomato_record/data/chatroom_model.dart'; 8 | import 'package:tomato_record/data/user_model.dart'; 9 | import 'package:tomato_record/screens/chat/chat.dart'; 10 | import 'package:tomato_record/states/chat_notifier.dart'; 11 | import 'package:tomato_record/states/user_notifier.dart'; 12 | 13 | class ChatroomScreen extends StatefulWidget { 14 | final String chatroomKey; 15 | 16 | const ChatroomScreen({Key? key, required this.chatroomKey}) : super(key: key); 17 | 18 | @override 19 | _ChatroomScreenState createState() => _ChatroomScreenState(); 20 | } 21 | 22 | class _ChatroomScreenState extends State { 23 | TextEditingController _textEditingController = TextEditingController(); 24 | late ChatNotifier _chatNotifier; 25 | 26 | @override 27 | void initState() { 28 | _chatNotifier = ChatNotifier(widget.chatroomKey); 29 | super.initState(); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return ChangeNotifierProvider.value( 35 | value: _chatNotifier, 36 | child: Consumer( 37 | builder: (context, chatNotifier, child) { 38 | Size _size = MediaQuery.of(context).size; 39 | UserModel userModel = context.read().userModel!; 40 | return Scaffold( 41 | appBar: AppBar(), 42 | backgroundColor: Colors.grey[200], 43 | body: SafeArea( 44 | child: Column( 45 | children: [ 46 | _buildItemInfo(context), 47 | Expanded( 48 | child: Container( 49 | color: Colors.white, 50 | child: ListView.separated( 51 | reverse: true, 52 | padding: EdgeInsets.all(16), 53 | itemBuilder: (context, index) { 54 | bool isMine = chatNotifier.chatList[index].userKey == 55 | userModel.userKey; 56 | return Chat( 57 | size: _size, 58 | isMine: isMine, 59 | chatModel: chatNotifier.chatList[index], 60 | ); 61 | }, 62 | separatorBuilder: (context, index) { 63 | return SizedBox( 64 | height: 12, 65 | ); 66 | }, 67 | itemCount: chatNotifier.chatList.length), 68 | )), 69 | _buildInputBar(userModel) 70 | ], 71 | ), 72 | ), 73 | ); 74 | }, 75 | ), 76 | ); 77 | } 78 | 79 | MaterialBanner _buildItemInfo(BuildContext context) { 80 | ChatroomModel? chatroomModel = context.read().chatroomModel; 81 | return MaterialBanner( 82 | padding: EdgeInsets.zero, 83 | leadingPadding: EdgeInsets.zero, 84 | actions: [Container()], 85 | content: Column( 86 | crossAxisAlignment: CrossAxisAlignment.start, 87 | children: [ 88 | Row( 89 | crossAxisAlignment: CrossAxisAlignment.center, 90 | children: [ 91 | Padding( 92 | padding: const EdgeInsets.only( 93 | left: 16, right: 12, top: 12, bottom: 12), 94 | child: chatroomModel == null 95 | ? Shimmer.fromColors( 96 | highlightColor: Colors.grey[300]!, 97 | baseColor: Colors.grey, 98 | child: Container( 99 | width: 32, height: 32, color: Colors.white), 100 | ) 101 | : ExtendedImage.network( 102 | chatroomModel.itemImage, 103 | fit: BoxFit.cover, 104 | width: 32, 105 | height: 32, 106 | ), 107 | ), 108 | Column( 109 | crossAxisAlignment: CrossAxisAlignment.start, 110 | children: [ 111 | RichText( 112 | text: TextSpan( 113 | text: '거래완료', 114 | style: Theme.of(context).textTheme.bodyText1, 115 | children: [ 116 | TextSpan(text: ' '), 117 | TextSpan( 118 | text: chatroomModel == null 119 | ? "" 120 | : chatroomModel.itemTitle, 121 | style: Theme.of(context).textTheme.bodyText2) 122 | ]), 123 | ), 124 | RichText( 125 | text: TextSpan( 126 | text: chatroomModel == null 127 | ? "" 128 | : chatroomModel.itemPrice.toCurrencyString( 129 | mantissaLength: 0, trailingSymbol: '원'), 130 | style: Theme.of(context).textTheme.bodyText1, 131 | children: [ 132 | TextSpan( 133 | text: '(가격제안불가)', 134 | style: Theme.of(context) 135 | .textTheme 136 | .bodyText2! 137 | .copyWith(color: Colors.black12)) 138 | ]), 139 | ) 140 | ], 141 | ) 142 | ], 143 | ), 144 | Padding( 145 | padding: const EdgeInsets.only(left: 16, bottom: 12), 146 | child: SizedBox( 147 | height: 32, 148 | child: TextButton.icon( 149 | onPressed: () {}, 150 | icon: Icon( 151 | Icons.edit, 152 | size: 16, 153 | color: Colors.black87, 154 | ), 155 | label: Text('후기 남기기', 156 | style: Theme.of(context) 157 | .textTheme 158 | .bodyText1! 159 | .copyWith(color: Colors.black87)), 160 | style: TextButton.styleFrom( 161 | backgroundColor: Colors.white, 162 | shape: RoundedRectangleBorder( 163 | borderRadius: BorderRadius.circular(4), 164 | side: 165 | BorderSide(color: Colors.grey[300]!, width: 1)))), 166 | ), 167 | ), 168 | ], 169 | ), 170 | ); 171 | } 172 | 173 | Widget _buildInputBar(UserModel userModel) { 174 | return SizedBox( 175 | height: 48, 176 | child: Row( 177 | children: [ 178 | IconButton( 179 | onPressed: () {}, 180 | icon: Icon( 181 | Icons.add, 182 | color: Colors.grey, 183 | )), 184 | Expanded( 185 | child: TextFormField( 186 | controller: _textEditingController, 187 | decoration: InputDecoration( 188 | hintText: '메세지를 입력하세요.', 189 | isDense: true, 190 | fillColor: Colors.white, 191 | filled: true, 192 | suffixIcon: GestureDetector( 193 | onTap: () { 194 | print('icon clicked'); 195 | }, 196 | child: 197 | Icon(Icons.emoji_emotions_outlined, color: Colors.grey), 198 | ), 199 | suffixIconConstraints: BoxConstraints.tight(Size(40, 40)), 200 | contentPadding: EdgeInsets.all(10), 201 | border: OutlineInputBorder( 202 | borderRadius: BorderRadius.circular(20), 203 | borderSide: BorderSide(color: Colors.grey))), 204 | )), 205 | IconButton( 206 | onPressed: () async { 207 | ChatModel chatModel = ChatModel( 208 | userKey: userModel.userKey, 209 | msg: _textEditingController.text, 210 | createdDate: DateTime.now()); 211 | 212 | _chatNotifier.addNewChat(chatModel); 213 | print('${_textEditingController.text}'); 214 | _textEditingController.clear(); 215 | }, 216 | icon: Icon( 217 | Icons.send, 218 | color: Colors.grey, 219 | )), 220 | ], 221 | ), 222 | ); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /lib/screens/home/items_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tomato_record/constants/common_size.dart'; 3 | import 'package:shimmer/shimmer.dart'; 4 | import 'package:tomato_record/data/item_model.dart'; 5 | import 'package:tomato_record/repo/item_service.dart'; 6 | import 'package:tomato_record/widgets/item_list_widget.dart'; 7 | 8 | class ItemsPage extends StatefulWidget { 9 | final String userKey; 10 | const ItemsPage({Key? key, required this.userKey}) : super(key: key); 11 | 12 | @override 13 | State createState() => _ItemsPageState(); 14 | } 15 | 16 | class _ItemsPageState extends State { 17 | bool init = false; 18 | List items = []; 19 | 20 | @override 21 | void initState() { 22 | if (!init) { 23 | _onRefresh(); 24 | init = true; 25 | } 26 | super.initState(); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return LayoutBuilder( 32 | builder: (context, constraints) { 33 | Size size = MediaQuery.of(context).size; 34 | final imgSize = size.width / 4; 35 | 36 | return AnimatedSwitcher( 37 | duration: Duration(milliseconds: 300), 38 | child: (items.isNotEmpty) 39 | ? _listView(imgSize) 40 | : _shimmerListView(imgSize)); 41 | }, 42 | ); 43 | } 44 | 45 | Future _onRefresh() async { 46 | items.clear(); 47 | items.addAll(await ItemService().getItems(widget.userKey)); 48 | setState(() {}); 49 | } 50 | 51 | Widget _listView(double imgSize) { 52 | return RefreshIndicator( 53 | onRefresh: _onRefresh, 54 | child: ListView.separated( 55 | padding: EdgeInsets.all(common_padding), 56 | separatorBuilder: (context, index) { 57 | return Divider( 58 | height: common_padding * 2 + 1, 59 | thickness: 1, 60 | color: Colors.grey[200], 61 | indent: common_sm_padding, 62 | endIndent: common_sm_padding, 63 | ); 64 | }, 65 | itemBuilder: (context, index) { 66 | ItemModel item = items[index]; 67 | return ItemListWidget(item, imgSize: imgSize); 68 | }, 69 | itemCount: items.length, 70 | ), 71 | ); 72 | } 73 | 74 | Widget _shimmerListView(double imgSize) { 75 | return Shimmer.fromColors( 76 | baseColor: Colors.grey[300]!, 77 | highlightColor: Colors.grey[100]!, 78 | enabled: true, 79 | child: ListView.separated( 80 | padding: EdgeInsets.all(common_padding), 81 | separatorBuilder: (context, index) { 82 | return Divider( 83 | height: common_padding * 2 + 1, 84 | thickness: 1, 85 | color: Colors.grey[200], 86 | indent: common_sm_padding, 87 | endIndent: common_sm_padding, 88 | ); 89 | }, 90 | itemBuilder: (context, index) { 91 | return SizedBox( 92 | height: imgSize, 93 | child: Row( 94 | children: [ 95 | Container( 96 | height: imgSize, 97 | width: imgSize, 98 | decoration: BoxDecoration( 99 | shape: BoxShape.rectangle, 100 | color: Colors.white, 101 | borderRadius: BorderRadius.circular(12), 102 | )), 103 | SizedBox( 104 | width: common_sm_padding, 105 | ), 106 | Expanded( 107 | child: Column( 108 | crossAxisAlignment: CrossAxisAlignment.start, 109 | children: [ 110 | Container( 111 | height: 14, 112 | width: 150, 113 | decoration: BoxDecoration( 114 | shape: BoxShape.rectangle, 115 | color: Colors.white, 116 | borderRadius: BorderRadius.circular(3), 117 | )), 118 | SizedBox( 119 | height: 4, 120 | ), 121 | Container( 122 | height: 12, 123 | width: 180, 124 | decoration: BoxDecoration( 125 | shape: BoxShape.rectangle, 126 | color: Colors.white, 127 | borderRadius: BorderRadius.circular(3), 128 | )), 129 | SizedBox( 130 | height: 4, 131 | ), 132 | Container( 133 | height: 14, 134 | width: 100, 135 | decoration: BoxDecoration( 136 | shape: BoxShape.rectangle, 137 | color: Colors.white, 138 | borderRadius: BorderRadius.circular(3), 139 | )), 140 | Expanded( 141 | child: Container(), 142 | ), 143 | Row( 144 | mainAxisAlignment: MainAxisAlignment.end, 145 | children: [ 146 | Container( 147 | height: 14, 148 | width: 80, 149 | decoration: BoxDecoration( 150 | shape: BoxShape.rectangle, 151 | color: Colors.white, 152 | borderRadius: BorderRadius.circular(3), 153 | )), 154 | ], 155 | ) 156 | ], 157 | )) 158 | ], 159 | ), 160 | ); 161 | }, 162 | itemCount: 10, 163 | ), 164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/screens/home/map_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:latlng/latlng.dart'; 4 | import 'package:map/map.dart'; 5 | import 'package:tomato_record/data/item_model.dart'; 6 | import 'package:tomato_record/data/user_model.dart'; 7 | import 'package:tomato_record/repo/item_service.dart'; 8 | import 'package:beamer/beamer.dart'; 9 | import 'package:tomato_record/router/locations.dart'; 10 | 11 | class MapPage extends StatefulWidget { 12 | final UserModel _userModel; 13 | 14 | const MapPage(this._userModel, {Key? key}) : super(key: key); 15 | 16 | @override 17 | _MapPageState createState() => _MapPageState(); 18 | } 19 | 20 | class _MapPageState extends State { 21 | late final controller; 22 | 23 | Offset? _dragStart; 24 | double _scaleData = 1.0; 25 | _scaleStart(ScaleStartDetails details) { 26 | _dragStart = details.focalPoint; 27 | _scaleData = 1.0; 28 | } 29 | 30 | _scaleUpdate(ScaleUpdateDetails details) { 31 | var scaleDiff = details.scale - _scaleData; 32 | _scaleData = details.scale; 33 | controller.zoom += scaleDiff; 34 | 35 | final now = details.focalPoint; 36 | final diff = now - _dragStart!; 37 | _dragStart = now; 38 | controller.drag(diff.dx, diff.dy); 39 | setState(() {}); 40 | } 41 | 42 | Widget _buildMarkerWidget(Offset offset, {Color color = Colors.red}) { 43 | return Positioned( 44 | left: offset.dx, 45 | top: offset.dy, 46 | width: 24, 47 | height: 24, 48 | child: Icon( 49 | Icons.location_on, 50 | color: color, 51 | )); 52 | } 53 | 54 | Widget _buildItemWidget(Offset offset, ItemModel itemModel) { 55 | return Positioned( 56 | left: offset.dx, 57 | top: offset.dy, 58 | width: 24, 59 | height: 24, 60 | child: InkWell( 61 | onTap: () { 62 | context.beamToNamed('/$LOCATION_ITEM/${itemModel.itemKey}'); 63 | }, 64 | child: ExtendedImage.network( 65 | itemModel.imageDownloadUrls[0], 66 | fit: BoxFit.cover, 67 | shape: BoxShape.circle, 68 | ), 69 | )); 70 | } 71 | 72 | @override 73 | void initState() { 74 | controller = MapController( 75 | location: LatLng(widget._userModel.geoFirePoint.latitude, 76 | widget._userModel.geoFirePoint.longitude), 77 | ); 78 | super.initState(); 79 | } 80 | 81 | @override 82 | Widget build(BuildContext context) { 83 | return MapLayoutBuilder( 84 | builder: (BuildContext context, MapTransformer transformer) { 85 | final myLocationOnMap = transformer.fromLatLngToXYCoords(LatLng( 86 | widget._userModel.geoFirePoint.latitude, 87 | widget._userModel.geoFirePoint.longitude)); 88 | 89 | final myLocationWidget = 90 | _buildMarkerWidget(myLocationOnMap, color: Colors.black); 91 | 92 | Size size = MediaQuery.of(context).size; 93 | final middleOnScreen = Offset(size.width / 2, size.height / 2); 94 | 95 | final latLngOnMap = transformer.fromXYCoordsToLatLng(middleOnScreen); 96 | print("${latLngOnMap.latitude}, ${latLngOnMap.longitude}"); 97 | 98 | return FutureBuilder>( 99 | future: ItemService() 100 | .getNearByItems(widget._userModel.userKey, latLngOnMap), 101 | builder: (context, snapshot) { 102 | List nearByItems = []; 103 | 104 | if (snapshot.hasData) { 105 | snapshot.data!.forEach((item) { 106 | final offset = transformer.fromLatLngToXYCoords(LatLng( 107 | item.geoFirePoint.latitude, item.geoFirePoint.longitude)); 108 | nearByItems.add(_buildItemWidget(offset, item)); 109 | }); 110 | } 111 | 112 | return Stack( 113 | children: [ 114 | GestureDetector( 115 | onScaleStart: _scaleStart, 116 | onScaleUpdate: _scaleUpdate, 117 | child: Map( 118 | controller: controller, 119 | builder: (context, x, y, z) { 120 | //Legal notice: This url is only used for demo and educational purposes. You need a license key for production use. 121 | 122 | //Google Maps 123 | final url = 124 | 'https://www.google.com/maps/vt/pb=!1m4!1m3!1i$z!2i$x!3i$y!2m3!1e0!2sm!3i420120488!3m7!2sen!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0!5m1!1e0!23i4111425'; 125 | 126 | final darkUrl = 127 | 'https://maps.googleapis.com/maps/vt?pb=!1m5!1m4!1i$z!2i$x!3i$y!4i256!2m3!1e0!2sm!3i556279080!3m17!2sen-US!3sUS!5e18!12m4!1e68!2m2!1sset!2sRoadmap!12m3!1e37!2m1!1ssmartmaps!12m4!1e26!2m2!1sstyles!2zcC52Om9uLHMuZTpsfHAudjpvZmZ8cC5zOi0xMDAscy5lOmwudC5mfHAuczozNnxwLmM6I2ZmMDAwMDAwfHAubDo0MHxwLnY6b2ZmLHMuZTpsLnQuc3xwLnY6b2ZmfHAuYzojZmYwMDAwMDB8cC5sOjE2LHMuZTpsLml8cC52Om9mZixzLnQ6MXxzLmU6Zy5mfHAuYzojZmYwMDAwMDB8cC5sOjIwLHMudDoxfHMuZTpnLnN8cC5jOiNmZjAwMDAwMHxwLmw6MTd8cC53OjEuMixzLnQ6NXxzLmU6Z3xwLmM6I2ZmMDAwMDAwfHAubDoyMCxzLnQ6NXxzLmU6Zy5mfHAuYzojZmY0ZDYwNTkscy50OjV8cy5lOmcuc3xwLmM6I2ZmNGQ2MDU5LHMudDo4MnxzLmU6Zy5mfHAuYzojZmY0ZDYwNTkscy50OjJ8cy5lOmd8cC5sOjIxLHMudDoyfHMuZTpnLmZ8cC5jOiNmZjRkNjA1OSxzLnQ6MnxzLmU6Zy5zfHAuYzojZmY0ZDYwNTkscy50OjN8cy5lOmd8cC52Om9ufHAuYzojZmY3ZjhkODkscy50OjN8cy5lOmcuZnxwLmM6I2ZmN2Y4ZDg5LHMudDo0OXxzLmU6Zy5mfHAuYzojZmY3ZjhkODl8cC5sOjE3LHMudDo0OXxzLmU6Zy5zfHAuYzojZmY3ZjhkODl8cC5sOjI5fHAudzowLjIscy50OjUwfHMuZTpnfHAuYzojZmYwMDAwMDB8cC5sOjE4LHMudDo1MHxzLmU6Zy5mfHAuYzojZmY3ZjhkODkscy50OjUwfHMuZTpnLnN8cC5jOiNmZjdmOGQ4OSxzLnQ6NTF8cy5lOmd8cC5jOiNmZjAwMDAwMHxwLmw6MTYscy50OjUxfHMuZTpnLmZ8cC5jOiNmZjdmOGQ4OSxzLnQ6NTF8cy5lOmcuc3xwLmM6I2ZmN2Y4ZDg5LHMudDo0fHMuZTpnfHAuYzojZmYwMDAwMDB8cC5sOjE5LHMudDo2fHAuYzojZmYyYjM2Mzh8cC52Om9uLHMudDo2fHMuZTpnfHAuYzojZmYyYjM2Mzh8cC5sOjE3LHMudDo2fHMuZTpnLmZ8cC5jOiNmZjI0MjgyYixzLnQ6NnxzLmU6Zy5zfHAuYzojZmYyNDI4MmIscy50OjZ8cy5lOmx8cC52Om9mZixzLnQ6NnxzLmU6bC50fHAudjpvZmYscy50OjZ8cy5lOmwudC5mfHAudjpvZmYscy50OjZ8cy5lOmwudC5zfHAudjpvZmYscy50OjZ8cy5lOmwuaXxwLnY6b2Zm!4e0&key=AIzaSyAOqYYyBbtXQEtcHG7hwAwyCPQSYidG8yU&token=31440'; 128 | //Mapbox Streets 129 | // final url = 130 | // 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/$z/$x/$y?access_token=YOUR_MAPBOX_ACCESS_TOKEN'; 131 | 132 | return ExtendedImage.network( 133 | url, 134 | fit: BoxFit.cover, 135 | ); 136 | }, 137 | ), 138 | ), 139 | myLocationWidget, 140 | ...nearByItems 141 | ], 142 | ); 143 | }); 144 | }, 145 | controller: controller, 146 | ); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/screens/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:beamer/src/beamer.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:tomato_record/data/user_model.dart'; 7 | import 'package:tomato_record/router/locations.dart'; 8 | import 'package:tomato_record/screens/chat/chat_list_page.dart'; 9 | import 'package:tomato_record/screens/home/items_page.dart'; 10 | import 'package:tomato_record/screens/home/map_page.dart'; 11 | import 'package:tomato_record/states/user_notifier.dart'; 12 | import 'package:tomato_record/widgets/expandable_fab.dart'; 13 | 14 | class HomeScreen extends StatefulWidget { 15 | const HomeScreen({Key? key}) : super(key: key); 16 | 17 | @override 18 | _HomeScreenState createState() => _HomeScreenState(); 19 | } 20 | 21 | class _HomeScreenState extends State { 22 | int _bottomSelectedIndex = 0; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | UserModel? userModel = context.read().userModel; 27 | return Scaffold( 28 | body: (userModel == null) 29 | ? Container() 30 | : IndexedStack( 31 | index: _bottomSelectedIndex, 32 | children: [ 33 | ItemsPage(userKey: userModel.userKey), 34 | MapPage(userModel), 35 | ChatListPage(userKey: userModel.userKey), 36 | Container( 37 | color: Colors.accents[9], 38 | ) 39 | ], 40 | ), 41 | floatingActionButton: ExpandableFab( 42 | distance: 90, 43 | children: [ 44 | MaterialButton( 45 | onPressed: () { 46 | context.beamToNamed('/$LOCATION_INPUT'); 47 | }, 48 | shape: CircleBorder(), 49 | height: 40, 50 | color: Theme.of(context).colorScheme.primary, 51 | child: Icon(Icons.add), 52 | ), 53 | MaterialButton( 54 | onPressed: () {}, 55 | shape: CircleBorder(), 56 | height: 40, 57 | color: Theme.of(context).colorScheme.primary, 58 | child: Icon(Icons.add), 59 | ), 60 | ], 61 | ), 62 | appBar: AppBar( 63 | centerTitle: false, 64 | title: Text( 65 | '정왕동', 66 | style: Theme.of(context).appBarTheme.titleTextStyle, 67 | ), 68 | actions: [ 69 | IconButton( 70 | onPressed: () { 71 | FirebaseAuth.instance.signOut(); 72 | context.beamToNamed("/"); 73 | }, 74 | icon: Icon(CupertinoIcons.nosign)), 75 | IconButton( 76 | onPressed: () { 77 | context.beamToNamed('/$LOCATION_SEARCH'); 78 | }, 79 | icon: Icon(CupertinoIcons.search)), 80 | IconButton(onPressed: () {}, icon: Icon(CupertinoIcons.text_justify)), 81 | ], 82 | ), 83 | bottomNavigationBar: BottomNavigationBar( 84 | currentIndex: _bottomSelectedIndex, 85 | type: BottomNavigationBarType.fixed, 86 | items: [ 87 | BottomNavigationBarItem( 88 | icon: ImageIcon(AssetImage(_bottomSelectedIndex == 0 89 | ? 'assets/imgs/selected_home_1.png' 90 | : 'assets/imgs/home_1.png')), 91 | label: '홈'), 92 | BottomNavigationBarItem( 93 | icon: ImageIcon(AssetImage(_bottomSelectedIndex == 1 94 | ? 'assets/imgs/selected_placeholder.png' 95 | : 'assets/imgs/placeholder.png')), 96 | label: '내 근처'), 97 | BottomNavigationBarItem( 98 | icon: ImageIcon(AssetImage(_bottomSelectedIndex == 2 99 | ? 'assets/imgs/selected_smartphone_10.png' 100 | : 'assets/imgs/smartphone_10.png')), 101 | label: '채팅'), 102 | BottomNavigationBarItem( 103 | icon: ImageIcon(AssetImage(_bottomSelectedIndex == 3 104 | ? 'assets/imgs/selected_user_3.png' 105 | : 'assets/imgs/user_3.png')), 106 | label: '내정보'), 107 | ], 108 | onTap: (index) { 109 | setState(() { 110 | _bottomSelectedIndex = index; 111 | }); 112 | }, 113 | ), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/screens/input/category_input_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tomato_record/states/category_notifier.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:beamer/beamer.dart'; 5 | 6 | class CategoryInputScreen extends StatelessWidget { 7 | const CategoryInputScreen({Key? key}) : super(key: key); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | appBar: AppBar( 13 | title: Text( 14 | '카테고리 선택', 15 | style: Theme.of(context).textTheme.headline6, 16 | ), 17 | ), 18 | body: ListView.separated( 19 | itemBuilder: (context, index) { 20 | return ListTile( 21 | onTap: () { 22 | context.read().setNewCategoryWithKor( 23 | categoriesMapEngToKor.values.elementAt(index)); 24 | Beamer.of(context).beamBack(); 25 | }, 26 | title: Text( 27 | categoriesMapEngToKor.values.elementAt(index), 28 | style: TextStyle( 29 | color: context 30 | .read() 31 | .currentCategoryInKor == 32 | categoriesMapEngToKor.values.elementAt(index) 33 | ? Theme.of(context).primaryColor 34 | : Colors.black87), 35 | ), 36 | ); 37 | }, 38 | separatorBuilder: (context, index) { 39 | return Divider( 40 | height: 1, 41 | thickness: 1, 42 | color: Colors.grey[300], 43 | ); 44 | }, 45 | itemCount: categoriesMapEngToKor.length)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/screens/input/input_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:extended_image/extended_image.dart'; 4 | import 'package:firebase_auth/firebase_auth.dart'; 5 | import 'package:firebase_storage/firebase_storage.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:beamer/beamer.dart'; 8 | import 'package:flutter_multi_formatter/flutter_multi_formatter.dart'; 9 | import 'package:tomato_record/constants/common_size.dart'; 10 | import 'package:tomato_record/data/item_model.dart'; 11 | import 'package:tomato_record/repo/image_storage.dart'; 12 | import 'package:tomato_record/repo/item_service.dart'; 13 | import 'package:tomato_record/router/locations.dart'; 14 | import 'package:tomato_record/screens/input/multi_image_select.dart'; 15 | import 'package:provider/provider.dart'; 16 | import 'package:tomato_record/states/category_notifier.dart'; 17 | import 'package:tomato_record/states/select_image_notifier.dart'; 18 | import 'package:tomato_record/states/user_notifier.dart'; 19 | import 'package:tomato_record/utils/logger.dart'; 20 | 21 | class InputScreen extends StatefulWidget { 22 | const InputScreen({Key? key}) : super(key: key); 23 | 24 | @override 25 | _InputScreenState createState() => _InputScreenState(); 26 | } 27 | 28 | class _InputScreenState extends State { 29 | bool _suggestPriceSelected = false; 30 | 31 | TextEditingController _priceController = TextEditingController(); 32 | var _border = 33 | UnderlineInputBorder(borderSide: BorderSide(color: Colors.transparent)); 34 | 35 | var _divider = Divider( 36 | height: 1, 37 | thickness: 1, 38 | color: Colors.grey[350], 39 | indent: common_padding, 40 | endIndent: common_padding, 41 | ); 42 | 43 | bool isCreatingItem = false; 44 | 45 | TextEditingController _titleController = TextEditingController(); 46 | TextEditingController _detailController = TextEditingController(); 47 | 48 | void attemptCreateItem() async { 49 | if (FirebaseAuth.instance.currentUser == null) return; 50 | isCreatingItem = true; 51 | setState(() {}); 52 | 53 | final String userKey = FirebaseAuth.instance.currentUser!.uid; 54 | final String itemKey = ItemModel.generateItemKey(userKey); 55 | 56 | List images = context.read().images; 57 | 58 | UserNotifier userNotifier = context.read(); 59 | 60 | if (userNotifier.userModel == null) return; 61 | 62 | List downloadUrls = 63 | await ImageStorage.uploadImages(images, itemKey); 64 | 65 | final num? price = 66 | num.tryParse(_priceController.text.replaceAll(new RegExp(r"\D"), '')); 67 | 68 | ItemModel itemModel = ItemModel( 69 | itemKey: itemKey, 70 | userKey: userKey, 71 | imageDownloadUrls: downloadUrls, 72 | title: _titleController.text, 73 | category: context.read().currentCategoryInEng, 74 | price: price ?? 0, 75 | negotiable: _suggestPriceSelected, 76 | detail: _detailController.text, 77 | address: userNotifier.userModel!.address, 78 | geoFirePoint: userNotifier.userModel!.geoFirePoint, 79 | createdDate: DateTime.now().toUtc(), 80 | ); 81 | 82 | logger.d('upload finished - ${downloadUrls.toString()}'); 83 | 84 | await ItemService() 85 | .createNewItem(itemModel, itemKey, userNotifier.user!.uid); 86 | context.beamBack(); 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return LayoutBuilder( 92 | builder: (BuildContext context, BoxConstraints constraints) { 93 | Size _size = MediaQuery.of(context).size; 94 | return IgnorePointer( 95 | ignoring: isCreatingItem, 96 | child: Scaffold( 97 | appBar: AppBar( 98 | leading: TextButton( 99 | onPressed: () { 100 | context.beamBack(); 101 | }, 102 | style: TextButton.styleFrom( 103 | primary: Colors.black87, 104 | backgroundColor: 105 | Theme.of(context).appBarTheme.backgroundColor), 106 | child: Text( 107 | '뒤로', 108 | style: Theme.of(context).textTheme.bodyText1, 109 | )), 110 | bottom: PreferredSize( 111 | preferredSize: Size(_size.width, 2), 112 | child: isCreatingItem 113 | ? LinearProgressIndicator( 114 | minHeight: 2, 115 | ) 116 | : Container(), 117 | ), 118 | title: Text( 119 | '중고거래 글쓰기', 120 | style: Theme.of(context).textTheme.headline6, 121 | ), 122 | actions: [ 123 | TextButton( 124 | onPressed: attemptCreateItem, 125 | style: TextButton.styleFrom( 126 | primary: Colors.black87, 127 | backgroundColor: 128 | Theme.of(context).appBarTheme.backgroundColor), 129 | child: Text( 130 | '완료', 131 | style: Theme.of(context).textTheme.bodyText1, 132 | )), 133 | ], 134 | ), 135 | body: ListView( 136 | children: [ 137 | MultiImageSelect(), 138 | _divider, 139 | TextFormField( 140 | controller: _titleController, 141 | decoration: InputDecoration( 142 | hintText: '글 제목', 143 | contentPadding: 144 | EdgeInsets.symmetric(horizontal: common_padding), 145 | border: _border, 146 | enabledBorder: _border, 147 | focusedBorder: _border), 148 | ), 149 | _divider, 150 | ListTile( 151 | onTap: () { 152 | context.beamToNamed( 153 | '/$LOCATION_INPUT/$LOCATION_CATEGORY_INPUT'); 154 | }, 155 | dense: true, 156 | title: Text( 157 | context.watch().currentCategoryInKor), 158 | trailing: Icon(Icons.navigate_next), 159 | ), 160 | _divider, 161 | Row( 162 | children: [ 163 | Expanded( 164 | child: Padding( 165 | padding: const EdgeInsets.only(left: common_padding), 166 | child: TextFormField( 167 | keyboardType: TextInputType.number, 168 | controller: _priceController, 169 | onChanged: (value) { 170 | if (value == '0원') { 171 | _priceController.clear(); 172 | } 173 | 174 | setState(() {}); 175 | }, 176 | inputFormatters: [ 177 | MoneyInputFormatter( 178 | mantissaLength: 0, trailingSymbol: '원') 179 | ], 180 | decoration: InputDecoration( 181 | hintText: '얼마에 파시겠어요?', 182 | prefixIcon: ImageIcon( 183 | ExtendedAssetImageProvider('assets/imgs/won.png'), 184 | color: (_priceController.text.isEmpty) 185 | ? Colors.grey[350] 186 | : Colors.black87, 187 | ), 188 | prefixIconConstraints: BoxConstraints(maxWidth: 20), 189 | contentPadding: EdgeInsets.symmetric( 190 | vertical: common_sm_padding), 191 | border: UnderlineInputBorder( 192 | borderSide: 193 | BorderSide(color: Colors.transparent)), 194 | focusedBorder: UnderlineInputBorder( 195 | borderSide: 196 | BorderSide(color: Colors.transparent)), 197 | enabledBorder: UnderlineInputBorder( 198 | borderSide: 199 | BorderSide(color: Colors.transparent))), 200 | ), 201 | )), 202 | TextButton.icon( 203 | onPressed: () { 204 | setState(() { 205 | _suggestPriceSelected = !_suggestPriceSelected; 206 | }); 207 | }, 208 | icon: Icon( 209 | _suggestPriceSelected 210 | ? Icons.check_circle 211 | : Icons.check_circle_outline, 212 | color: _suggestPriceSelected 213 | ? Theme.of(context).primaryColor 214 | : Colors.black54, 215 | ), 216 | label: Text('가격제안 받기', 217 | style: TextStyle( 218 | color: _suggestPriceSelected 219 | ? Theme.of(context).primaryColor 220 | : Colors.black54)), 221 | style: TextButton.styleFrom( 222 | backgroundColor: Colors.transparent, 223 | primary: Colors.black45), 224 | ) 225 | ], 226 | ), 227 | _divider, 228 | TextFormField( 229 | controller: _detailController, 230 | maxLines: null, 231 | keyboardType: TextInputType.multiline, 232 | decoration: InputDecoration( 233 | hintText: '올릴 게시글 내용을 작성해주세요.', 234 | contentPadding: 235 | EdgeInsets.symmetric(horizontal: common_padding), 236 | border: _border, 237 | enabledBorder: _border, 238 | focusedBorder: _border), 239 | ), 240 | ], 241 | ), 242 | ), 243 | ); 244 | }, 245 | ); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /lib/screens/input/multi_image_select.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:extended_image/extended_image.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:image_picker/image_picker.dart'; 6 | import 'package:tomato_record/constants/common_size.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:tomato_record/states/select_image_notifier.dart'; 9 | 10 | class MultiImageSelect extends StatefulWidget { 11 | MultiImageSelect({ 12 | Key? key, 13 | }) : super(key: key); 14 | 15 | @override 16 | State createState() => _MultiImageSelectState(); 17 | } 18 | 19 | class _MultiImageSelectState extends State { 20 | bool _isPickingImages = false; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return LayoutBuilder( 25 | builder: (context, constraints) { 26 | SelectImageNotifier selectImageNotifier = 27 | context.watch(); 28 | Size _size = MediaQuery.of(context).size; 29 | var imageSize = (_size.width / 3) - common_padding * 2; 30 | var imageCorner = 16.0; 31 | return SizedBox( 32 | height: _size.width / 3, 33 | width: _size.width, 34 | child: ListView( 35 | scrollDirection: Axis.horizontal, 36 | children: [ 37 | Padding( 38 | padding: const EdgeInsets.all(common_padding), 39 | child: InkWell( 40 | onTap: () async { 41 | _isPickingImages = true; 42 | setState(() {}); 43 | final ImagePicker _picker = ImagePicker(); 44 | final List? images = 45 | await _picker.pickMultiImage(imageQuality: 10); 46 | if (images != null && images.isNotEmpty) { 47 | await context 48 | .read() 49 | .setNewImages(images); 50 | } 51 | _isPickingImages = false; 52 | setState(() {}); 53 | }, 54 | child: Container( 55 | child: _isPickingImages 56 | ? Padding( 57 | padding: const EdgeInsets.all(8.0), 58 | child: CircularProgressIndicator(), 59 | ) 60 | : Column( 61 | mainAxisAlignment: MainAxisAlignment.center, 62 | children: [ 63 | Icon( 64 | Icons.camera_alt_rounded, 65 | color: Colors.grey, 66 | ), 67 | Text( 68 | '0/10', 69 | style: Theme.of(context).textTheme.subtitle2, 70 | ) 71 | ], 72 | ), 73 | width: imageSize, 74 | decoration: BoxDecoration( 75 | borderRadius: BorderRadius.circular(imageCorner), 76 | border: Border.all(color: Colors.grey, width: 1)), 77 | ), 78 | ), 79 | ), 80 | ...List.generate( 81 | selectImageNotifier.images.length, 82 | (index) => Stack( 83 | children: [ 84 | Padding( 85 | padding: const EdgeInsets.only( 86 | right: common_padding, 87 | top: common_padding, 88 | bottom: common_padding), 89 | child: ExtendedImage.memory( 90 | selectImageNotifier.images[index], 91 | width: imageSize, 92 | height: imageSize, 93 | fit: BoxFit.cover, 94 | loadStateChanged: (state) { 95 | switch (state.extendedImageLoadState) { 96 | case LoadState.loading: 97 | return Container( 98 | width: imageSize, 99 | height: imageSize, 100 | padding: EdgeInsets.all(imageSize / 3), 101 | child: CircularProgressIndicator()); 102 | case LoadState.completed: 103 | return null; 104 | case LoadState.failed: 105 | return Icon(Icons.cancel); 106 | } 107 | }, 108 | borderRadius: BorderRadius.circular(imageCorner), 109 | shape: BoxShape.rectangle, 110 | ), 111 | ), 112 | Positioned( 113 | right: 0, 114 | top: 0, 115 | width: 40, 116 | height: 40, 117 | child: IconButton( 118 | padding: EdgeInsets.all(8), 119 | onPressed: () { 120 | selectImageNotifier.removeImage(index); 121 | }, 122 | icon: Icon(Icons.remove_circle), 123 | color: Colors.black54, 124 | ), 125 | ) 126 | ], 127 | )) 128 | ], 129 | ), 130 | ); 131 | }, 132 | ); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/screens/item/similar_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:beamer/src/beamer.dart'; 2 | import 'package:extended_image/extended_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:tomato_record/constants/common_size.dart'; 5 | import 'package:tomato_record/data/item_model.dart'; 6 | import 'package:tomato_record/screens/item/item_detail_screen.dart'; 7 | 8 | class SimilarItem extends StatelessWidget { 9 | final ItemModel _itemModel; 10 | const SimilarItem(this._itemModel, {Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return InkWell( 15 | onTap: () { 16 | Navigator.of(context).push(MaterialPageRoute(builder: (context) { 17 | return ItemDetailScreen(_itemModel.itemKey); 18 | })); 19 | }, 20 | child: Column( 21 | crossAxisAlignment: CrossAxisAlignment.stretch, 22 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 23 | children: [ 24 | AspectRatio( 25 | aspectRatio: 5 / 4, 26 | child: ExtendedImage.network( 27 | _itemModel.imageDownloadUrls[0], 28 | fit: BoxFit.cover, 29 | borderRadius: BorderRadius.circular(8), 30 | shape: BoxShape.rectangle, 31 | ), 32 | ), 33 | Text( 34 | _itemModel.title, 35 | overflow: TextOverflow.ellipsis, 36 | maxLines: 1, 37 | style: Theme.of(context).textTheme.subtitle1, 38 | ), 39 | Padding( 40 | padding: const EdgeInsets.only(bottom: common_sm_padding), 41 | child: Text( 42 | '${_itemModel.price.toString()}원', 43 | style: Theme.of(context).textTheme.subtitle2, 44 | ), 45 | ) 46 | ], 47 | ), 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/screens/search/search_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tomato_record/constants/common_size.dart'; 3 | import 'package:tomato_record/data/item_model.dart'; 4 | import 'package:tomato_record/repo/algolia_service.dart'; 5 | import 'package:beamer/beamer.dart'; 6 | import 'package:tomato_record/widgets/item_list_widget.dart'; 7 | 8 | class SearchScreen extends StatefulWidget { 9 | const SearchScreen({Key? key}) : super(key: key); 10 | 11 | @override 12 | _SearchScreenState createState() => _SearchScreenState(); 13 | } 14 | 15 | class _SearchScreenState extends State { 16 | static final borderStyle = OutlineInputBorder( 17 | borderRadius: BorderRadius.circular(8), 18 | borderSide: BorderSide(color: Colors.grey[200]!)); 19 | 20 | final TextEditingController _textEditingController = TextEditingController(); 21 | 22 | final List items = []; 23 | 24 | bool isProcessing = false; 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Scaffold( 29 | appBar: AppBar( 30 | titleSpacing: 0, 31 | title: Padding( 32 | padding: const EdgeInsets.only( 33 | right: 8.0, 34 | ), 35 | child: Container( 36 | child: Center( 37 | child: TextFormField( 38 | controller: _textEditingController, 39 | autofocus: true, 40 | onFieldSubmitted: (value) async { 41 | isProcessing = true; 42 | setState(() {}); 43 | List newItems = 44 | await AlgoliaService().queryItems(value); 45 | if (newItems.isNotEmpty) { 46 | items.clear(); 47 | items.addAll(newItems); 48 | } 49 | print('${items.toString()}'); 50 | isProcessing = false; 51 | setState(() {}); 52 | }, 53 | decoration: InputDecoration( 54 | isDense: true, 55 | fillColor: Colors.grey[200], 56 | contentPadding: 57 | EdgeInsets.symmetric(horizontal: 8, vertical: 8), 58 | filled: true, 59 | hintText: '아이템 검색', 60 | enabledBorder: borderStyle, 61 | focusedBorder: borderStyle), 62 | ), 63 | ), 64 | ), 65 | ), 66 | ), 67 | body: Stack( 68 | children: [ 69 | if (isProcessing) 70 | LinearProgressIndicator( 71 | minHeight: 2, 72 | ), 73 | ListView.separated( 74 | padding: EdgeInsets.all(common_padding), 75 | itemBuilder: (context, index) { 76 | ItemModel item = items[index]; 77 | Size size = MediaQuery.of(context).size; 78 | return ItemListWidget(item, imgSize: size.width / 4); 79 | }, 80 | separatorBuilder: (context, index) { 81 | return Divider( 82 | height: common_padding * 2 + 1, 83 | thickness: 1, 84 | color: Colors.grey[200], 85 | indent: common_sm_padding, 86 | endIndent: common_sm_padding, 87 | ); 88 | }, 89 | itemCount: items.length), 90 | ], 91 | ), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/screens/splash_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class SplashScreen extends StatelessWidget { 5 | const SplashScreen({Key? key}) : super(key: key); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return Container( 10 | color: Colors.white, 11 | child: Center( 12 | child: Column( 13 | mainAxisAlignment: MainAxisAlignment.center, 14 | children: [ 15 | ExtendedImage.asset('assets/imgs/tomato.png'), 16 | CircularProgressIndicator( 17 | color: Colors.red, 18 | ) 19 | ], 20 | )), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/screens/start/address_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:location/location.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | import 'package:tomato_record/constants/common_size.dart'; 7 | import 'package:tomato_record/constants/shared_pref_keys.dart'; 8 | import 'package:tomato_record/data/address_model.dart'; 9 | import 'package:tomato_record/data/address_model2.dart'; 10 | import 'package:tomato_record/screens/start/address_service.dart'; 11 | import 'package:tomato_record/utils/logger.dart'; 12 | import 'package:provider/provider.dart'; 13 | 14 | class AddressPage extends StatefulWidget { 15 | AddressPage({Key? key}) : super(key: key); 16 | 17 | @override 18 | _AddressPageState createState() => _AddressPageState(); 19 | } 20 | 21 | class _AddressPageState extends State { 22 | TextEditingController _addressController = TextEditingController(); 23 | 24 | AddressModel? _addressModel; 25 | List _addressModel2List = []; 26 | bool _isGettingLocation = false; 27 | 28 | @override 29 | void dispose() { 30 | _addressController.dispose(); 31 | super.dispose(); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return SafeArea( 37 | minimum: EdgeInsets.only(left: common_padding, right: common_padding), 38 | child: Column( 39 | crossAxisAlignment: CrossAxisAlignment.stretch, 40 | children: [ 41 | TextFormField( 42 | controller: _addressController, 43 | onFieldSubmitted: (text) async { 44 | _addressModel2List.clear(); 45 | _addressModel = await AddressService().searchAddressByStr(text); 46 | setState(() {}); 47 | }, 48 | decoration: InputDecoration( 49 | prefixIcon: Icon( 50 | Icons.search, 51 | color: Colors.grey, 52 | ), 53 | hintText: '도로명으로 검색', 54 | hintStyle: TextStyle(color: Theme.of(context).hintColor), 55 | border: UnderlineInputBorder( 56 | borderSide: BorderSide(color: Colors.grey)), 57 | prefixIconConstraints: 58 | BoxConstraints(minWidth: 24, minHeight: 24)), 59 | ), 60 | TextButton.icon( 61 | onPressed: () async { 62 | _addressModel = null; 63 | _addressModel2List.clear(); 64 | setState(() { 65 | _isGettingLocation = true; 66 | }); 67 | Location location = new Location(); 68 | 69 | bool _serviceEnabled; 70 | PermissionStatus _permissionGranted; 71 | LocationData _locationData; 72 | 73 | _serviceEnabled = await location.serviceEnabled(); 74 | if (!_serviceEnabled) { 75 | _serviceEnabled = await location.requestService(); 76 | if (!_serviceEnabled) { 77 | return; 78 | } 79 | } 80 | 81 | _permissionGranted = await location.hasPermission(); 82 | if (_permissionGranted == PermissionStatus.denied) { 83 | _permissionGranted = await location.requestPermission(); 84 | if (_permissionGranted != PermissionStatus.granted) { 85 | return; 86 | } 87 | } 88 | 89 | _locationData = await location.getLocation(); 90 | logger.d(_locationData); 91 | List addresses = await AddressService() 92 | .findAddressByCoordinate( 93 | log: _locationData.longitude!, 94 | lat: _locationData.latitude!); 95 | 96 | _addressModel2List.addAll(addresses); 97 | 98 | setState(() { 99 | _isGettingLocation = false; 100 | }); 101 | }, 102 | icon: _isGettingLocation 103 | ? SizedBox( 104 | width: 24, 105 | height: 24, 106 | child: CircularProgressIndicator( 107 | color: Colors.white, 108 | ), 109 | ) 110 | : Icon( 111 | CupertinoIcons.compass, 112 | color: Colors.white, 113 | size: 20, 114 | ), 115 | label: Text( 116 | _isGettingLocation ? '위치 찾는 중...' : '현재 위치 찾기', 117 | style: Theme.of(context).textTheme.button, 118 | ), 119 | ), 120 | if (_addressModel != null) 121 | Expanded( 122 | child: ListView.builder( 123 | padding: EdgeInsets.symmetric(vertical: common_padding), 124 | itemBuilder: (context, index) { 125 | if (_addressModel == null || 126 | _addressModel!.result == null || 127 | _addressModel!.result!.items == null || 128 | _addressModel!.result!.items![index].address == null) 129 | return Container(); 130 | return ListTile( 131 | onTap: () { 132 | _saveAddressAndGoToNextPage( 133 | _addressModel!.result!.items![index].address!.road ?? 134 | "", 135 | num.parse( 136 | _addressModel!.result!.items![index].point!.y ?? 137 | "0"), 138 | num.parse( 139 | _addressModel!.result!.items![index].point!.x ?? 140 | "0")); 141 | }, 142 | title: Text( 143 | _addressModel!.result!.items![index].address!.road ?? 144 | ""), 145 | subtitle: Text( 146 | _addressModel!.result!.items![index].address!.parcel ?? 147 | ""), 148 | ); 149 | }, 150 | itemCount: (_addressModel == null || 151 | _addressModel!.result == null || 152 | _addressModel!.result!.items == null) 153 | ? 0 154 | : _addressModel!.result!.items!.length, 155 | ), 156 | ), 157 | if (_addressModel2List.isNotEmpty) 158 | Expanded( 159 | child: ListView.builder( 160 | padding: EdgeInsets.symmetric(vertical: common_padding), 161 | itemBuilder: (context, index) { 162 | if (_addressModel2List[index].result == null || 163 | _addressModel2List[index].result!.isEmpty) 164 | return Container(); 165 | return ListTile( 166 | onTap: () { 167 | _saveAddressAndGoToNextPage( 168 | _addressModel2List[index].result![0].text ?? "", 169 | num.parse( 170 | _addressModel2List[index].input!.point!.y ?? "0"), 171 | num.parse(_addressModel2List[index].input!.point!.x ?? 172 | "0")); 173 | }, 174 | title: 175 | Text(_addressModel2List[index].result![0].text ?? ""), 176 | subtitle: Text( 177 | _addressModel2List[index].result![0].zipcode ?? ""), 178 | ); 179 | }, 180 | itemCount: _addressModel2List.length, 181 | ), 182 | ), 183 | ], 184 | ), 185 | ); 186 | } 187 | 188 | _saveAddressAndGoToNextPage(String address, num lat, num lon) async { 189 | await _saveAddressOnSharedPreference(address, lat, lon); 190 | 191 | context.read().animateToPage(2, 192 | duration: Duration(milliseconds: 500), curve: Curves.ease); 193 | } 194 | 195 | _saveAddressOnSharedPreference(String address, num lat, num lon) async { 196 | SharedPreferences prefs = await SharedPreferences.getInstance(); 197 | await prefs.setString(SHARED_ADDRESS, address); 198 | await prefs.setDouble(SHARED_LAT, lat.toDouble()); 199 | await prefs.setDouble(SHARED_LON, lon.toDouble()); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lib/screens/start/address_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:tomato_record/constants/keys.dart'; 3 | import 'package:tomato_record/data/address_model.dart'; 4 | import 'package:tomato_record/data/address_model2.dart'; 5 | import 'package:tomato_record/utils/logger.dart'; 6 | 7 | class AddressService { 8 | Future searchAddressByStr(String text) async { 9 | final formData = { 10 | 'key': VWORLD_KEY, 11 | 'request': 'search', 12 | 'type': 'ADDRESS', 13 | 'category': 'ROAD', 14 | 'query': text, 15 | 'size': 30, 16 | }; 17 | 18 | final response = await Dio() 19 | .get('http://api.vworld.kr/req/search', queryParameters: formData) 20 | .catchError((e) { 21 | logger.e(e.message); 22 | }); 23 | 24 | AddressModel addressModel = 25 | AddressModel.fromJson(response.data["response"]); 26 | logger.d(addressModel); 27 | return addressModel; 28 | } 29 | 30 | Future> findAddressByCoordinate( 31 | {required double log, required double lat}) async { 32 | final List> formDatas = >[]; 33 | 34 | formDatas.add({ 35 | 'key': VWORLD_KEY, 36 | 'service': 'address', 37 | 'request': 'getAddress', 38 | 'type': 'PARCEL', 39 | 'point': '$log,$lat', 40 | }); 41 | formDatas.add({ 42 | 'key': VWORLD_KEY, 43 | 'service': 'address', 44 | 'request': 'getAddress', 45 | 'type': 'PARCEL', 46 | 'point': '${log - 0.01},$lat', 47 | }); 48 | formDatas.add({ 49 | 'key': VWORLD_KEY, 50 | 'service': 'address', 51 | 'request': 'getAddress', 52 | 'type': 'PARCEL', 53 | 'point': '${log + 0.01},$lat', 54 | }); 55 | formDatas.add({ 56 | 'key': VWORLD_KEY, 57 | 'service': 'address', 58 | 'request': 'getAddress', 59 | 'type': 'PARCEL', 60 | 'point': '$log,${lat - 0.01}', 61 | }); 62 | formDatas.add({ 63 | 'key': VWORLD_KEY, 64 | 'service': 'address', 65 | 'request': 'getAddress', 66 | 'type': 'PARCEL', 67 | 'point': '$log,${lat + 0.01}', 68 | }); 69 | 70 | List addresses = []; 71 | 72 | for (Map formData in formDatas) { 73 | final response = await Dio() 74 | .get('http://api.vworld.kr/req/address', queryParameters: formData) 75 | .catchError((e) { 76 | logger.e(e.message); 77 | }); 78 | 79 | AddressModel2 addressModel = 80 | AddressModel2.fromJson(response.data["response"]); 81 | 82 | if (response.data['response']['status'] == 'OK') 83 | addresses.add(addressModel); 84 | } 85 | 86 | logger.d(addresses); 87 | return addresses; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/screens/start/auth_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_multi_formatter/flutter_multi_formatter.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | import 'package:tomato_record/constants/common_size.dart'; 7 | import 'package:tomato_record/constants/shared_pref_keys.dart'; 8 | import 'package:tomato_record/states/user_notifier.dart'; 9 | import 'package:provider/provider.dart'; 10 | import 'package:tomato_record/utils/logger.dart'; 11 | 12 | class AuthPage extends StatefulWidget { 13 | AuthPage({Key? key}) : super(key: key); 14 | 15 | @override 16 | _AuthPageState createState() => _AuthPageState(); 17 | } 18 | 19 | const duration = Duration(milliseconds: 300); 20 | 21 | class _AuthPageState extends State { 22 | final inputBorder = 23 | OutlineInputBorder(borderSide: BorderSide(color: Colors.grey)); 24 | 25 | TextEditingController _phoneNumberController = 26 | TextEditingController(text: "010"); 27 | 28 | TextEditingController _codeController = TextEditingController(); 29 | 30 | GlobalKey _formKey = GlobalKey(); 31 | 32 | VerificationStatus _verificationStatus = VerificationStatus.none; 33 | 34 | String? _verificationId; 35 | int? _forceResendingToken; 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return LayoutBuilder( 40 | builder: (context, constraints) { 41 | Size size = MediaQuery.of(context).size; 42 | return IgnorePointer( 43 | ignoring: _verificationStatus == VerificationStatus.verifying, 44 | child: Form( 45 | key: _formKey, 46 | child: Scaffold( 47 | appBar: AppBar( 48 | title: Text( 49 | '전화번호 로그인', 50 | style: Theme.of(context).appBarTheme.titleTextStyle, 51 | ), 52 | ), 53 | body: Padding( 54 | padding: const EdgeInsets.all(common_padding), 55 | child: Column( 56 | crossAxisAlignment: CrossAxisAlignment.stretch, 57 | children: [ 58 | Row( 59 | children: [ 60 | ExtendedImage.asset( 61 | 'assets/imgs/padlock.png', 62 | width: size.width * 0.15, 63 | height: size.width * 0.15, 64 | ), 65 | SizedBox( 66 | width: common_sm_padding, 67 | ), 68 | Text('''토마토마켓은 휴대폰 번호로 가입해요. 69 | 번호는 안전하게 보관 되며 70 | 어디에도 공개되지 않아요.''') 71 | ], 72 | ), 73 | SizedBox( 74 | height: common_padding, 75 | ), 76 | TextFormField( 77 | controller: _phoneNumberController, 78 | keyboardType: TextInputType.phone, 79 | inputFormatters: [ 80 | MaskedInputFormatter("000 0000 0000") 81 | ], 82 | decoration: InputDecoration( 83 | focusedBorder: inputBorder, border: inputBorder), 84 | validator: (phoneNumber) { 85 | if (phoneNumber != null && phoneNumber.length == 13) { 86 | return null; 87 | } else { 88 | //error 89 | return '전화번호 똑바로 입력해줄래?'; 90 | } 91 | }), 92 | SizedBox( 93 | height: common_sm_padding, 94 | ), 95 | TextButton( 96 | onPressed: () async { 97 | if (_verificationStatus == 98 | VerificationStatus.codeSending) return; 99 | 100 | if (_formKey.currentState != null) { 101 | bool passed = _formKey.currentState!.validate(); 102 | print(passed); 103 | if (passed) { 104 | String phoneNum = _phoneNumberController.text; 105 | phoneNum = phoneNum.replaceAll(' ', ''); 106 | phoneNum = 107 | phoneNum.replaceFirst('0', ''); //1055555555 108 | 109 | FirebaseAuth auth = FirebaseAuth.instance; 110 | 111 | setState(() { 112 | _verificationStatus = 113 | VerificationStatus.codeSending; 114 | }); 115 | 116 | await auth.verifyPhoneNumber( 117 | phoneNumber: '+82$phoneNum', 118 | forceResendingToken: _forceResendingToken, 119 | verificationCompleted: 120 | (PhoneAuthCredential credential) async { 121 | logger 122 | .d('verificationCompleted - $credential'); 123 | await auth.signInWithCredential(credential); 124 | }, 125 | codeAutoRetrievalTimeout: 126 | (String verificationId) {}, 127 | codeSent: (String verificationId, 128 | int? forceResendingToken) async { 129 | setState(() { 130 | _verificationStatus = 131 | VerificationStatus.codeSent; 132 | }); 133 | _verificationId = verificationId; 134 | _forceResendingToken = forceResendingToken; 135 | }, 136 | verificationFailed: 137 | (FirebaseAuthException error) { 138 | logger.e(error.message); 139 | 140 | setState(() { 141 | _verificationStatus = 142 | VerificationStatus.none; 143 | }); 144 | }, 145 | ); 146 | } 147 | } 148 | }, 149 | child: (_verificationStatus == 150 | VerificationStatus.codeSending) 151 | ? SizedBox( 152 | height: 26, 153 | width: 26, 154 | child: CircularProgressIndicator( 155 | color: Colors.white, 156 | ), 157 | ) 158 | : Text('인증문자 발송')), 159 | SizedBox( 160 | height: common_padding, 161 | ), 162 | AnimatedOpacity( 163 | duration: Duration(milliseconds: 300), 164 | curve: Curves.easeInOut, 165 | opacity: (_verificationStatus == VerificationStatus.none) 166 | ? 0 167 | : 1, 168 | child: AnimatedContainer( 169 | duration: duration, 170 | curve: Curves.easeInOut, 171 | height: getVerificationHeight(_verificationStatus), 172 | child: TextFormField( 173 | controller: _codeController, 174 | keyboardType: TextInputType.number, 175 | inputFormatters: [MaskedInputFormatter("000000")], 176 | decoration: InputDecoration( 177 | focusedBorder: inputBorder, border: inputBorder), 178 | ), 179 | ), 180 | ), 181 | AnimatedContainer( 182 | duration: duration, 183 | curve: Curves.easeInOut, 184 | height: getVerificationBtnHeight(_verificationStatus), 185 | child: TextButton( 186 | onPressed: () { 187 | attemptVerify(context); 188 | }, 189 | child: (_verificationStatus == 190 | VerificationStatus.verifying) 191 | ? CircularProgressIndicator( 192 | color: Colors.white, 193 | ) 194 | : Text('인증'))), 195 | ], 196 | ), 197 | ), 198 | ), 199 | ), 200 | ); 201 | }, 202 | ); 203 | } 204 | 205 | double getVerificationHeight(VerificationStatus status) { 206 | switch (status) { 207 | case VerificationStatus.none: 208 | return 0; 209 | case VerificationStatus.codeSending: 210 | case VerificationStatus.codeSent: 211 | case VerificationStatus.verifying: 212 | case VerificationStatus.verificationDone: 213 | return 60 + common_sm_padding; 214 | } 215 | } 216 | 217 | double getVerificationBtnHeight(VerificationStatus status) { 218 | switch (status) { 219 | case VerificationStatus.none: 220 | return 0; 221 | case VerificationStatus.codeSending: 222 | case VerificationStatus.codeSent: 223 | case VerificationStatus.verifying: 224 | case VerificationStatus.verificationDone: 225 | return 48; 226 | } 227 | } 228 | 229 | void attemptVerify(BuildContext context) async { 230 | setState(() { 231 | _verificationStatus = VerificationStatus.verifying; 232 | }); 233 | try { 234 | // Create a PhoneAuthCredential with the code 235 | PhoneAuthCredential credential = PhoneAuthProvider.credential( 236 | verificationId: _verificationId!, smsCode: _codeController.text); 237 | 238 | // Sign the user in (or link) with the credential 239 | await FirebaseAuth.instance.signInWithCredential(credential); 240 | } catch (e) { 241 | logger.e('verification failed!!'); 242 | SnackBar snackbar = SnackBar( 243 | content: Text('입력하신 코드가 틀려요!'), 244 | ); 245 | ScaffoldMessenger.of(context).showSnackBar(snackbar); 246 | } 247 | 248 | setState(() { 249 | _verificationStatus = VerificationStatus.verificationDone; 250 | }); 251 | } 252 | 253 | _getAddress() async { 254 | SharedPreferences prefs = await SharedPreferences.getInstance(); 255 | String address = prefs.getString(SHARED_ADDRESS) ?? ""; 256 | double lat = prefs.getDouble(SHARED_LAT) ?? 0; 257 | double lon = prefs.getDouble(SHARED_LON) ?? 0; 258 | } 259 | } 260 | 261 | enum VerificationStatus { 262 | none, 263 | codeSending, 264 | codeSent, 265 | verifying, 266 | verificationDone 267 | } 268 | -------------------------------------------------------------------------------- /lib/screens/start/intro_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:extended_image/extended_image.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:tomato_record/constants/common_size.dart'; 5 | import 'package:tomato_record/states/user_notifier.dart'; 6 | import 'package:tomato_record/utils/logger.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class IntroPage extends StatelessWidget { 10 | IntroPage({ 11 | Key? key, 12 | }) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return LayoutBuilder( 17 | builder: (context, constraints) { 18 | Size size = MediaQuery.of(context).size; 19 | 20 | final imgSize = size.width - 32; 21 | final sizeOfPosImg = imgSize * 0.1; 22 | 23 | return SafeArea( 24 | child: Padding( 25 | padding: const EdgeInsets.symmetric(horizontal: common_padding), 26 | child: Column( 27 | mainAxisAlignment: MainAxisAlignment.spaceAround, 28 | children: [ 29 | Text( 30 | '토마토마켓', 31 | style: Theme.of(context) 32 | .textTheme 33 | .headline3! 34 | .copyWith(color: Theme.of(context).colorScheme.primary), 35 | ), 36 | SizedBox( 37 | width: imgSize, 38 | height: imgSize, 39 | child: Stack( 40 | children: [ 41 | ExtendedImage.asset('assets/imgs/carrot_intro.png'), 42 | Positioned( 43 | width: sizeOfPosImg, 44 | left: imgSize * 0.45, 45 | top: imgSize * 0.45, 46 | height: sizeOfPosImg, 47 | child: ExtendedImage.asset( 48 | 'assets/imgs/carrot_intro_pos.png')), 49 | ], 50 | ), 51 | ), 52 | Text( 53 | '우리 동네 중고 직거래 토마토마켓', 54 | style: Theme.of(context).textTheme.headline6, 55 | ), 56 | Text( 57 | '''토마토마켓은 동네 직거래 마켓이에요. 58 | 내 동네를 설정하고 시작해보세요.''', 59 | style: Theme.of(context).textTheme.subtitle1, 60 | ), 61 | Column( 62 | crossAxisAlignment: CrossAxisAlignment.stretch, 63 | children: [ 64 | TextButton( 65 | onPressed: () async { 66 | context.read().animateToPage(1, 67 | duration: Duration(milliseconds: 500), 68 | curve: Curves.ease); 69 | logger.d('on text button clicked!!!'); 70 | }, 71 | child: Text( 72 | '내 동네 설정하고 시작하기', 73 | style: Theme.of(context).textTheme.button, 74 | ), 75 | style: TextButton.styleFrom( 76 | backgroundColor: Theme.of(context).primaryColor), 77 | ), 78 | ], 79 | ) 80 | ], 81 | ), 82 | ), 83 | ); 84 | }, 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/screens/start_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:tomato_record/screens/start/address_page.dart'; 4 | import 'package:tomato_record/screens/start/auth_page.dart'; 5 | import 'package:tomato_record/screens/start/intro_page.dart'; 6 | 7 | class StartScreen extends StatelessWidget { 8 | StartScreen({Key? key}) : super(key: key); 9 | 10 | PageController _pageController = PageController(); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Provider.value( 15 | value: _pageController, 16 | child: Scaffold( 17 | body: PageView(controller: _pageController, 18 | // physics: NeverScrollableScrollPhysics(), 19 | children: [IntroPage(), AddressPage(), AuthPage()]), 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/states/category_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:tomato_record/utils/logger.dart'; 3 | 4 | CategoryNotifier categoryNotifier = CategoryNotifier(); 5 | 6 | class CategoryNotifier extends ChangeNotifier { 7 | String _selectedCategoryInEng = 'none'; 8 | 9 | String get currentCategoryInEng => _selectedCategoryInEng; 10 | String get currentCategoryInKor { 11 | logger.d("currentCategoryInKor called!!!!"); 12 | return categoriesMapEngToKor[_selectedCategoryInEng]!; 13 | } 14 | 15 | void setNewCategoryWithEng(String newCategory) { 16 | if (categoriesMapEngToKor.keys.contains(newCategory)) { 17 | _selectedCategoryInEng = newCategory; 18 | notifyListeners(); 19 | } 20 | } 21 | 22 | void setNewCategoryWithKor(String newCategory) { 23 | if (categoriesMapEngToKor.values.contains(newCategory)) { 24 | _selectedCategoryInEng = categoriesMapKorToEng[newCategory]!; 25 | notifyListeners(); 26 | } 27 | } 28 | } 29 | 30 | const Map categoriesMapEngToKor = { 31 | 'none': '선택', 32 | 'furniture': '가구', 33 | 'electronics': '전자기기', 34 | 'kids': '유아동', 35 | 'sports': '스포츠', 36 | 'woman': '여성', 37 | 'man': '남성', 38 | 'makeup': '메이크업', 39 | }; 40 | 41 | const Map categoriesMapKorToEng = { 42 | '선택': 'none', 43 | '가구': 'furniture', 44 | '전자기기': 'electronics', 45 | '유아동': 'kids', 46 | '스포츠': 'sports', 47 | '여성': 'woman', 48 | '남성': 'man', 49 | '메이크업': 'makeup', 50 | }; 51 | -------------------------------------------------------------------------------- /lib/states/chat_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:tomato_record/data/chat_model.dart'; 3 | import 'package:tomato_record/data/chatroom_model.dart'; 4 | import 'package:tomato_record/repo/chat_service.dart'; 5 | 6 | class ChatNotifier extends ChangeNotifier { 7 | ChatroomModel? _chatroomModel; 8 | List _chatList = []; 9 | final String _chatroomKey; 10 | 11 | ChatNotifier(this._chatroomKey) { 12 | ChatService().connectChatroom(_chatroomKey).listen((chatroomModel) { 13 | this._chatroomModel = chatroomModel; 14 | 15 | if (this._chatList.isEmpty) { 16 | ChatService().getChatList(_chatroomKey).then((chatList) { 17 | _chatList.addAll(chatList); 18 | notifyListeners(); 19 | }); 20 | } else { 21 | if (this._chatList[0].reference == null) this._chatList.removeAt(0); 22 | ChatService() 23 | .getLatestChats(_chatroomKey, this._chatList[0].reference!) 24 | .then((latestChats) { 25 | this._chatList.insertAll(0, latestChats); 26 | notifyListeners(); 27 | }); 28 | } 29 | }); 30 | } 31 | 32 | void addNewChat(ChatModel chatModel) { 33 | _chatList.insert(0, chatModel); 34 | notifyListeners(); 35 | 36 | ChatService().createNewChat(_chatroomKey, chatModel); 37 | } 38 | 39 | List get chatList => _chatList; 40 | 41 | ChatroomModel? get chatroomModel => _chatroomModel; 42 | 43 | String get chatroomKey => _chatroomKey; 44 | } 45 | -------------------------------------------------------------------------------- /lib/states/select_image_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:image_picker/image_picker.dart'; 5 | 6 | class SelectImageNotifier extends ChangeNotifier { 7 | List _images = []; 8 | 9 | Future setNewImages(List? newImages) async { 10 | if (newImages != null && newImages.isNotEmpty) { 11 | _images.clear(); 12 | for (int index = 0; index < newImages.length; index++) { 13 | _images.add(await newImages[index].readAsBytes()); 14 | print('_images.add(await xfile.readAsBytes());'); 15 | } 16 | print('setNewImages notifyListeners!'); 17 | notifyListeners(); 18 | } 19 | } 20 | 21 | void removeImage(int index) { 22 | if (_images.length >= index) { 23 | _images.removeAt(index); 24 | notifyListeners(); 25 | } 26 | } 27 | 28 | List get images => _images; 29 | } 30 | -------------------------------------------------------------------------------- /lib/states/user_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:geoflutterfire/geoflutterfire.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | import 'package:tomato_record/constants/shared_pref_keys.dart'; 6 | import 'package:tomato_record/data/user_model.dart'; 7 | import 'package:tomato_record/repo/user_service.dart'; 8 | import 'package:tomato_record/utils/logger.dart'; 9 | 10 | class UserNotifier extends ChangeNotifier { 11 | UserNotifier() { 12 | initUser(); 13 | } 14 | 15 | User? _user; 16 | UserModel? _userModel; 17 | 18 | void initUser() { 19 | FirebaseAuth.instance.authStateChanges().listen((user) async { 20 | await _setNewUser(user); 21 | notifyListeners(); 22 | }); 23 | } 24 | 25 | Future _setNewUser(User? user) async { 26 | _user = user; 27 | if (user != null && user.phoneNumber != null) { 28 | SharedPreferences prefs = await SharedPreferences.getInstance(); 29 | String address = prefs.getString(SHARED_ADDRESS) ?? ""; 30 | double lat = prefs.getDouble(SHARED_LAT) ?? 0; 31 | double lon = prefs.getDouble(SHARED_LON) ?? 0; 32 | String phoneNumber = user.phoneNumber!; 33 | String userKey = user.uid; 34 | 35 | UserModel userModel = UserModel( 36 | userKey: "", 37 | phoneNumber: phoneNumber, 38 | address: address, 39 | geoFirePoint: GeoFirePoint(lat, lon), 40 | createdDate: DateTime.now().toUtc()); 41 | 42 | await UserService().createNewUser(userModel.toJson(), userKey); 43 | _userModel = await UserService().getUserModel(userKey); 44 | logger.d(_userModel!.toJson().toString()); 45 | } 46 | } 47 | 48 | User? get user => _user; 49 | UserModel? get userModel => _userModel; 50 | } 51 | -------------------------------------------------------------------------------- /lib/utils/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | final logger = Logger(); 4 | -------------------------------------------------------------------------------- /lib/utils/time_calculation.dart: -------------------------------------------------------------------------------- 1 | class TimeCalculation { 2 | static String getTimeDiff(DateTime createdDate) { 3 | DateTime now = DateTime.now(); 4 | Duration timeDiff = now.difference(createdDate); 5 | if (timeDiff.inHours <= 1) { 6 | return '방금 전'; 7 | } else if (timeDiff.inHours <= 24) { 8 | return '${timeDiff.inHours}시간 전'; 9 | } else { 10 | return '${timeDiff.inDays}일 전'; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/widgets/expandable_fab.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:math'; 3 | 4 | @immutable 5 | class ExpandableFab extends StatefulWidget { 6 | const ExpandableFab({ 7 | Key? key, 8 | this.initialOpen, 9 | required this.distance, 10 | required this.children, 11 | }) : super(key: key); 12 | 13 | final bool? initialOpen; 14 | final double distance; 15 | final List children; 16 | static const Duration duration = Duration(milliseconds: 300); 17 | 18 | @override 19 | _ExpandableFabState createState() => _ExpandableFabState(); 20 | } 21 | 22 | class _ExpandableFabState extends State 23 | with SingleTickerProviderStateMixin { 24 | bool _open = false; 25 | late AnimationController _controller; 26 | Animation? _expandAnimation; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | _open = widget.initialOpen ?? false; 32 | _controller = AnimationController( 33 | value: _open ? 1 : 0.0, 34 | duration: ExpandableFab.duration, 35 | vsync: this, 36 | ); 37 | _expandAnimation = CurvedAnimation( 38 | curve: Curves.fastOutSlowIn, 39 | reverseCurve: Curves.easeOutQuad, 40 | parent: _controller, 41 | ); 42 | } 43 | 44 | @override 45 | void dispose() { 46 | _controller.dispose(); 47 | super.dispose(); 48 | } 49 | 50 | void _toggle() { 51 | setState(() { 52 | _open = !_open; 53 | if (_open) { 54 | _controller.forward(); 55 | } else { 56 | _controller.reverse(); 57 | } 58 | }); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return SizedBox.expand( 64 | child: Stack( 65 | alignment: Alignment.bottomRight, 66 | clipBehavior: Clip.none, 67 | children: [ 68 | Container( 69 | height: 56, 70 | width: 56, 71 | child: Center(child: _buildTapToCloseFab())), 72 | _buildTapToOpenFab(), 73 | ]..insertAll(1, _buildExpandingActionButtons()), 74 | ), 75 | ); 76 | } 77 | 78 | List _buildExpandingActionButtons() { 79 | final children = []; 80 | final count = widget.children.length; 81 | final step = 90.0 / (count - 1); 82 | for (var i = 0, angleInDegrees = 0; 83 | i < count; 84 | i++, angleInDegrees += step.toInt()) { 85 | children.add( 86 | _ExpandingActionButton( 87 | directionInDegrees: angleInDegrees.toDouble(), 88 | maxDistance: widget.distance, 89 | progress: _expandAnimation, 90 | child: widget.children[i], 91 | ), 92 | ); 93 | } 94 | return children; 95 | } 96 | 97 | Widget _buildTapToCloseFab() { 98 | return AnimatedContainer( 99 | transformAlignment: Alignment.center, 100 | transform: Matrix4.rotationZ(_open ? 0 : pi / 4), 101 | duration: ExpandableFab.duration, 102 | curve: Curves.easeOut, 103 | child: FloatingActionButton( 104 | heroTag: 'btn1', 105 | onPressed: _toggle, 106 | mini: true, 107 | backgroundColor: Colors.white, 108 | child: Icon( 109 | Icons.close, 110 | color: Theme.of(context).primaryColor, 111 | ), 112 | ), 113 | ); 114 | } 115 | 116 | Widget _buildTapToOpenFab() { 117 | return IgnorePointer( 118 | ignoring: _open, 119 | child: AnimatedContainer( 120 | transformAlignment: Alignment.center, 121 | transform: Matrix4.rotationZ(_open ? 0 : pi / 4), 122 | duration: ExpandableFab.duration, 123 | curve: Curves.easeOut, 124 | child: AnimatedOpacity( 125 | opacity: _open ? 0.0 : 1.0, 126 | duration: ExpandableFab.duration, 127 | child: FloatingActionButton( 128 | heroTag: 'btn2', 129 | onPressed: _toggle, 130 | child: const Icon(Icons.close), 131 | ), 132 | ), 133 | ), 134 | ); 135 | } 136 | } 137 | 138 | @immutable 139 | class _ExpandingActionButton extends StatelessWidget { 140 | _ExpandingActionButton({ 141 | Key? key, 142 | required this.directionInDegrees, 143 | required this.maxDistance, 144 | required this.progress, 145 | required this.child, 146 | }) : super(key: key); 147 | 148 | final double directionInDegrees; 149 | final double maxDistance; 150 | final Animation? progress; 151 | final Widget child; 152 | 153 | @override 154 | Widget build(BuildContext context) { 155 | return AnimatedBuilder( 156 | animation: progress!, 157 | builder: (BuildContext context, Widget? child) { 158 | final offset = Offset.fromDirection( 159 | directionInDegrees * (pi / 180.0), 160 | progress!.value * maxDistance, 161 | ); 162 | return Positioned( 163 | right: offset.dx - 16, 164 | bottom: offset.dy, 165 | child: Transform.rotate( 166 | angle: (1.0 - progress!.value) * pi / 2, 167 | child: Container(height: 56, child: Center(child: child)), 168 | ), 169 | ); 170 | }, 171 | child: FadeTransition( 172 | opacity: progress!, 173 | child: child, 174 | ), 175 | ); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /lib/widgets/item_list_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:beamer/beamer.dart'; 5 | import 'package:tomato_record/constants/common_size.dart'; 6 | import 'package:tomato_record/data/item_model.dart'; 7 | import 'package:tomato_record/router/locations.dart'; 8 | import 'package:tomato_record/utils/logger.dart'; 9 | 10 | class ItemListWidget extends StatelessWidget { 11 | final ItemModel item; 12 | double? imgSize; 13 | ItemListWidget(this.item, {Key? key, this.imgSize}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | if (imgSize == null) { 18 | Size size = MediaQuery.of(context).size; 19 | imgSize = size.width / 4; 20 | } 21 | 22 | return InkWell( 23 | onTap: () { 24 | BeamState beamState = Beamer.of(context).currentConfiguration!; 25 | String currentPath = beamState.uri.toString(); 26 | String newPath = (currentPath == '/') 27 | ? '/$LOCATION_ITEM/${item.itemKey}' 28 | : '$currentPath/${item.itemKey}'; 29 | 30 | logger.d('newPath - $newPath'); 31 | context.beamToNamed(newPath); 32 | }, 33 | child: SizedBox( 34 | height: imgSize, 35 | child: Row( 36 | children: [ 37 | SizedBox( 38 | height: imgSize, 39 | width: imgSize, 40 | child: ExtendedImage.network( 41 | item.imageDownloadUrls[0], 42 | fit: BoxFit.cover, 43 | shape: BoxShape.rectangle, 44 | borderRadius: BorderRadius.circular(12), 45 | )), 46 | SizedBox( 47 | width: common_sm_padding, 48 | ), 49 | Expanded( 50 | child: Column( 51 | crossAxisAlignment: CrossAxisAlignment.start, 52 | children: [ 53 | Text( 54 | item.title, 55 | style: Theme.of(context).textTheme.subtitle1, 56 | ), 57 | Text( 58 | '53일전', 59 | style: Theme.of(context).textTheme.subtitle2, 60 | ), 61 | Text('${item.price.toString()}원'), 62 | Expanded( 63 | child: Container(), 64 | ), 65 | Row( 66 | mainAxisAlignment: MainAxisAlignment.end, 67 | children: [ 68 | SizedBox( 69 | height: 14, 70 | child: FittedBox( 71 | fit: BoxFit.fitHeight, 72 | child: Row( 73 | children: [ 74 | Icon( 75 | CupertinoIcons.chat_bubble_2, 76 | color: Colors.grey, 77 | ), 78 | Text( 79 | '23', 80 | style: TextStyle(color: Colors.grey), 81 | ), 82 | Icon( 83 | CupertinoIcons.heart, 84 | color: Colors.grey, 85 | ), 86 | Text( 87 | '30', 88 | style: TextStyle(color: Colors.grey), 89 | ), 90 | ], 91 | ), 92 | ), 93 | ), 94 | ], 95 | ) 96 | ], 97 | )) 98 | ], 99 | ), 100 | ), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: tomato_record 2 | description: A new Flutter application. 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: ">=2.12.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | 28 | # The following adds the Cupertino Icons font to your application. 29 | # Use with the CupertinoIcons class for iOS style icons. 30 | cupertino_icons: ^1.0.2 31 | extended_image: ^4.1.0 32 | logger: ^1.0.0 33 | beamer: ^0.14.1 34 | flutter_multi_formatter: ^2.3.8 35 | provider: ^6.0.0 36 | dio: ^4.0.0 37 | location: ^4.3.0 38 | shared_preferences: ^2.0.7 39 | firebase_core: ^1.7.0 40 | firebase_auth: ^3.0.2 41 | cloud_firestore: ^2.5.3 42 | firebase_storage: ^10.0.4 43 | shimmer: ^2.0.0 44 | geoflutterfire: ^3.0.1 45 | image_picker: ^0.8.4+2 46 | smooth_page_indicator: ^1.0.0+2 47 | map: ^1.0.0 48 | latlng: ^0.1.0 49 | algolia: ^1.0.4 50 | 51 | dev_dependencies: 52 | flutter_test: 53 | sdk: flutter 54 | 55 | # For information on the generic Dart part of this file, see the 56 | # following page: https://dart.dev/tools/pub/pubspec 57 | 58 | # The following section is specific to Flutter. 59 | flutter: 60 | 61 | # The following line ensures that the Material Icons font is 62 | # included with your application, so that you can use the icons in 63 | # the material Icons class. 64 | uses-material-design: true 65 | 66 | # To add assets to your application, add an assets section, like this: 67 | assets: 68 | - assets/imgs/ 69 | # An image asset can refer to one or more resolution-specific "variants", see 70 | # https://flutter.dev/assets-and-images/#resolution-aware. 71 | 72 | # For details regarding adding assets from package dependencies, see 73 | # https://flutter.dev/assets-and-images/#from-packages 74 | 75 | # To add custom fonts to your application, add a fonts section here, 76 | # in this "flutter" section. Each entry in this list should have a 77 | # "family" key with the font family name, and a "fonts" key with a 78 | # list giving the asset and other descriptors for the font. For 79 | # example: 80 | fonts: 81 | - family: DoHyeon 82 | fonts: 83 | - asset: assets/fonts/BMDOHYEON_otf.otf 84 | # - family: Trajan Pro 85 | # fonts: 86 | # - asset: fonts/TrajanPro.ttf 87 | # - asset: fonts/TrajanPro_Bold.ttf 88 | # weight: 700 89 | # 90 | # For details regarding fonts from package dependencies, 91 | # see https://flutter.dev/custom-fonts/#from-packages 92 | -------------------------------------------------------------------------------- /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 | 11 | import 'package:tomato_record/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------