diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..48d5f81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/README.md b/README.md index 26875245..0f761958 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # guam_community_client +![Dart](https://img.shields.io/badge/Dart-2.16.0-brightgreen.svg) +![Flutter](https://img.shields.io/badge/flutter-2.10.0-blue.svg) +![Provider](https://img.shields.io/badge/provider-5.0.0-yellowgreen.svg) + + - **We Connect Developers** - We develop Guam using Flutter framework based on Dart language. diff --git a/android/app/build.gradle b/android/app/build.gradle index fa89f965..6240f001 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,20 +13,27 @@ if (flutterRoot == null) { def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = '5' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = '5' +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'com.google.gms.google-services' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 31 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -34,18 +41,35 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.wafflestudio.guam.guam_community_client" - minSdkVersion 16 + applicationId "com.wafflestudio.guam_community" + minSdkVersion 19 targetSdkVersion 30 + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName } + signingConfigs { + release { + // https://stackoverflow.com/questions/54457245/a-problem-occurred-evaluating-project-app-path-may-not-be-null-or-empty-st + // https://itwise.tistory.com/47 + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig signingConfigs.release + + /// https://flutter-ko.dev/docs/deployment/android + minifyEnabled true + useProguard true + + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } } } @@ -55,5 +79,9 @@ flutter { } dependencies { + // kotlin version dependency issue: + // https://stackoverflow.com/questions/64822045/could-not-find-com-google-firebasefirebase-analytics-ktx-required-by-project implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.google.firebase:firebase-analytics-ktx:21.0.0' + implementation 'com.google.firebase:firebase-auth-ktx:21.0.4' } diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 00000000..1ec475de --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,90 @@ +{ + "project_info": { + "project_number": "648780047414", + "project_id": "waffle-guam", + "storage_bucket": "waffle-guam.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:648780047414:android:f3e6e071c18e0e8721a497", + "android_client_info": { + "package_name": "com.wafflestudio.guam_community" + } + }, + "oauth_client": [ + { + "client_id": "648780047414-d2fbe0qcdiugckgq8stcqvgef89ie6a9.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.wafflestudio.guam_community", + "certificate_hash": "e53cba4c1facd94625b7c877a80a183694acaf05" + } + }, + { + "client_id": "648780047414-9sscdi6fbqrfnme42cvub06b9sgl9ojm.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAYoZLtqIgtE8eLeyNgCoLYIa3f3UYmXDs" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "648780047414-9sscdi6fbqrfnme42cvub06b9sgl9ojm.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "648780047414-erb4hojdlnk09dk8nvcjjgdps2q3cc8n.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.wafflestudio.guam_community" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:648780047414:android:3fc58c8364ffee5c21a497", + "android_client_info": { + "package_name": "com.wafflestudio.guam_community" + } + }, + "oauth_client": [ + { + "client_id": "648780047414-9sscdi6fbqrfnme42cvub06b9sgl9ojm.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAYoZLtqIgtE8eLeyNgCoLYIa3f3UYmXDs" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "648780047414-9sscdi6fbqrfnme42cvub06b9sgl9ojm.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "648780047414-erb4hojdlnk09dk8nvcjjgdps2q3cc8n.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.wafflestudio.guam_community" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..d24d7f10 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,8 @@ +## Flutter wrapper +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } +-dontwarn io.flutter.embedding.** \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index c512e48a..ab650831 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.wafflestudio.guam_community"> diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e269dc1d..548a7bb3 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,7 @@ + package="com.wafflestudio.guam_community"> + to determine the Window backgrounds behind the Flutter UI. --> - + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/wafflestudio/guam/guam_community_client/MainActivity.kt b/android/app/src/main/kotlin/com/wafflestudio/guam/guam_community_client/MainActivity.kt index af9a6f44..1376155c 100644 --- a/android/app/src/main/kotlin/com/wafflestudio/guam/guam_community_client/MainActivity.kt +++ b/android/app/src/main/kotlin/com/wafflestudio/guam/guam_community_client/MainActivity.kt @@ -1,4 +1,4 @@ -package com.wafflestudio.guam.guam_community_client +package com.wafflestudio.guam_community import io.flutter.embedding.android.FlutterActivity diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b..79e08d03 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79..269b6a6d 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d43914..4d34a35f 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d3..09fee5f6 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372ee..21d31aca 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index c512e48a..ab650831 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="com.wafflestudio.guam_community"> diff --git a/android/build.gradle b/android/build.gradle index 9b6ed06e..d268d4cd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() jcenter() @@ -8,6 +8,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.google.gms:google-services:4.3.10' } } diff --git a/assets/backgrounds/back_0.75x.png b/assets/backgrounds/back_0.75x.png new file mode 100644 index 00000000..7d08c169 Binary files /dev/null and b/assets/backgrounds/back_0.75x.png differ diff --git a/assets/backgrounds/back_1x.png b/assets/backgrounds/back_1x.png new file mode 100644 index 00000000..1555e944 Binary files /dev/null and b/assets/backgrounds/back_1x.png differ diff --git a/assets/backgrounds/back_2x.png b/assets/backgrounds/back_2x.png new file mode 100644 index 00000000..2dfca89e Binary files /dev/null and b/assets/backgrounds/back_2x.png differ diff --git a/assets/backgrounds/front_0.75x.png b/assets/backgrounds/front_0.75x.png new file mode 100644 index 00000000..35ceedd0 Binary files /dev/null and b/assets/backgrounds/front_0.75x.png differ diff --git a/assets/backgrounds/front_1x.png b/assets/backgrounds/front_1x.png new file mode 100644 index 00000000..0b2ed5bc Binary files /dev/null and b/assets/backgrounds/front_1x.png differ diff --git a/assets/backgrounds/front_2x.png b/assets/backgrounds/front_2x.png new file mode 100644 index 00000000..02a1e39c Binary files /dev/null and b/assets/backgrounds/front_2x.png differ diff --git a/assets/backgrounds/splash/star_splash.svg b/assets/backgrounds/splash/star_splash.svg new file mode 100644 index 00000000..cca09bf1 --- /dev/null +++ b/assets/backgrounds/splash/star_splash.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/fonts/SpoqaHanSansNeo-Medium.ttf b/assets/fonts/SpoqaHanSansNeo-Medium.ttf new file mode 100644 index 00000000..2208350f Binary files /dev/null and b/assets/fonts/SpoqaHanSansNeo-Medium.ttf differ diff --git a/assets/fonts/SpoqaHanSansNeo-Regular.ttf b/assets/fonts/SpoqaHanSansNeo-Regular.ttf new file mode 100644 index 00000000..64c4af72 Binary files /dev/null and b/assets/fonts/SpoqaHanSansNeo-Regular.ttf differ diff --git a/assets/gifs/guam_progress_indicator.gif b/assets/gifs/guam_progress_indicator.gif new file mode 100644 index 00000000..2f0f6673 Binary files /dev/null and b/assets/gifs/guam_progress_indicator.gif differ diff --git a/assets/icons/back.svg b/assets/icons/back.svg new file mode 100644 index 00000000..0fadd898 --- /dev/null +++ b/assets/icons/back.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/baloon.svg b/assets/icons/baloon.svg new file mode 100644 index 00000000..a065269c --- /dev/null +++ b/assets/icons/baloon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/blog.svg b/assets/icons/blog.svg new file mode 100644 index 00000000..e8002f8c --- /dev/null +++ b/assets/icons/blog.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/camera.svg b/assets/icons/camera.svg new file mode 100644 index 00000000..61e1bef1 --- /dev/null +++ b/assets/icons/camera.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/cancel_filled.svg b/assets/icons/cancel_filled.svg new file mode 100644 index 00000000..17fe9c9b --- /dev/null +++ b/assets/icons/cancel_filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/cancel_filled_x_transparent.svg b/assets/icons/cancel_filled_x_transparent.svg new file mode 100644 index 00000000..cca24136 --- /dev/null +++ b/assets/icons/cancel_filled_x_transparent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/cancel_outlined.svg b/assets/icons/cancel_outlined.svg new file mode 100644 index 00000000..399572b3 --- /dev/null +++ b/assets/icons/cancel_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/check.svg b/assets/icons/check.svg new file mode 100644 index 00000000..82a1e3e0 --- /dev/null +++ b/assets/icons/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/comment.svg b/assets/icons/comment.svg new file mode 100644 index 00000000..b0901346 --- /dev/null +++ b/assets/icons/comment.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/delete_outlined.svg b/assets/icons/delete_outlined.svg new file mode 100644 index 00000000..d6d96af7 --- /dev/null +++ b/assets/icons/delete_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/down.svg b/assets/icons/down.svg new file mode 100644 index 00000000..8eab77d5 --- /dev/null +++ b/assets/icons/down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/github.svg b/assets/icons/github.svg new file mode 100644 index 00000000..f89f3392 --- /dev/null +++ b/assets/icons/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/home_filled.svg b/assets/icons/home_filled.svg new file mode 100644 index 00000000..6fc11d0f --- /dev/null +++ b/assets/icons/home_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/home_outlined.svg b/assets/icons/home_outlined.svg new file mode 100644 index 00000000..2e290d61 --- /dev/null +++ b/assets/icons/home_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/like_filled.svg b/assets/icons/like_filled.svg new file mode 100644 index 00000000..2819643b --- /dev/null +++ b/assets/icons/like_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/like_outlined.svg b/assets/icons/like_outlined.svg new file mode 100644 index 00000000..d85a30e2 --- /dev/null +++ b/assets/icons/like_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/message.svg b/assets/icons/message.svg new file mode 100644 index 00000000..8021fbb6 --- /dev/null +++ b/assets/icons/message.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/message_default.svg b/assets/icons/message_default.svg new file mode 100644 index 00000000..3a5e3dc0 --- /dev/null +++ b/assets/icons/message_default.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/message_new.svg b/assets/icons/message_new.svg new file mode 100644 index 00000000..5b84695c --- /dev/null +++ b/assets/icons/message_new.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/more.svg b/assets/icons/more.svg new file mode 100644 index 00000000..aab4d84c --- /dev/null +++ b/assets/icons/more.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/notification_filled_default.svg b/assets/icons/notification_filled_default.svg new file mode 100644 index 00000000..1c9401fb --- /dev/null +++ b/assets/icons/notification_filled_default.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/notification_filled_new.svg b/assets/icons/notification_filled_new.svg new file mode 100644 index 00000000..f0f13d3d --- /dev/null +++ b/assets/icons/notification_filled_new.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/notification_outlined_default.svg b/assets/icons/notification_outlined_default.svg new file mode 100644 index 00000000..e5294605 --- /dev/null +++ b/assets/icons/notification_outlined_default.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/notification_outlined_new.svg b/assets/icons/notification_outlined_new.svg new file mode 100644 index 00000000..d35ca1f4 --- /dev/null +++ b/assets/icons/notification_outlined_new.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/picture.svg b/assets/icons/picture.svg new file mode 100644 index 00000000..925442d3 --- /dev/null +++ b/assets/icons/picture.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg new file mode 100644 index 00000000..d39c3fc1 --- /dev/null +++ b/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/profile_filled.svg b/assets/icons/profile_filled.svg new file mode 100644 index 00000000..185cef09 --- /dev/null +++ b/assets/icons/profile_filled.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/profile_image.svg b/assets/icons/profile_image.svg new file mode 100644 index 00000000..2a5b79f9 --- /dev/null +++ b/assets/icons/profile_image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/profile_outlined.svg b/assets/icons/profile_outlined.svg new file mode 100644 index 00000000..2858ee05 --- /dev/null +++ b/assets/icons/profile_outlined.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/right.svg b/assets/icons/right.svg new file mode 100644 index 00000000..dad56da5 --- /dev/null +++ b/assets/icons/right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/scrap_filled.svg b/assets/icons/scrap_filled.svg new file mode 100644 index 00000000..57e1afad --- /dev/null +++ b/assets/icons/scrap_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/scrap_outlined.svg b/assets/icons/scrap_outlined.svg new file mode 100644 index 00000000..b2bd4736 --- /dev/null +++ b/assets/icons/scrap_outlined.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/search.svg b/assets/icons/search.svg new file mode 100644 index 00000000..ce5af060 --- /dev/null +++ b/assets/icons/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/setting.svg b/assets/icons/setting.svg new file mode 100644 index 00000000..a25def0d --- /dev/null +++ b/assets/icons/setting.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/write.svg b/assets/icons/write.svg new file mode 100644 index 00000000..7085e676 --- /dev/null +++ b/assets/icons/write.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/profile_image.png b/assets/images/profile_image.png new file mode 100644 index 00000000..9a3c2125 Binary files /dev/null and b/assets/images/profile_image.png differ diff --git a/assets/logos/google_logo.svg b/assets/logos/google_logo.svg new file mode 100644 index 00000000..c54974f6 --- /dev/null +++ b/assets/logos/google_logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/logos/kakao_logo.svg b/assets/logos/kakao_logo.svg new file mode 100644 index 00000000..ec683b08 --- /dev/null +++ b/assets/logos/kakao_logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/ios/.gitignore b/ios/.gitignore index 151026b9..7a7f9873 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -1,3 +1,4 @@ +**/dgph *.mode1v3 *.mode2v3 *.moved-aside diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 9367d483..8d4492f9 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee8..f4df79c3 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 00000000..0e5527f5 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,49 @@ +# Uncomment this line to define a global platform for your project +# Exact version of ios platform should be used according to the highest version from 'Pods/Local Podspecs/.' +# https://stackoverflow.com/questions/52398435/cocoapods-could-not-find-compatible-versions-for-pod-firebase-core-cloud-fir +platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + # https://velog.io/@tykan/Flutter-v1-프로젝트에서-v2로-갈아타기-Flutter.h-Not-Found-error + flutter_additional_ios_build_settings(target) + # https://kth496.tistory.com/9 + target.build_configurations.each do |config| + config.build_settings['ENABLE_BITCODE'] = 'NO' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 00000000..ce1e9775 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,938 @@ +PODS: + - abseil/algorithm (1.20211102.0): + - abseil/algorithm/algorithm (= 1.20211102.0) + - abseil/algorithm/container (= 1.20211102.0) + - abseil/algorithm/algorithm (1.20211102.0): + - abseil/base/config + - abseil/algorithm/container (1.20211102.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/base (1.20211102.0): + - abseil/base/atomic_hook (= 1.20211102.0) + - abseil/base/base (= 1.20211102.0) + - abseil/base/base_internal (= 1.20211102.0) + - abseil/base/config (= 1.20211102.0) + - abseil/base/core_headers (= 1.20211102.0) + - abseil/base/dynamic_annotations (= 1.20211102.0) + - abseil/base/endian (= 1.20211102.0) + - abseil/base/errno_saver (= 1.20211102.0) + - abseil/base/fast_type_id (= 1.20211102.0) + - abseil/base/log_severity (= 1.20211102.0) + - abseil/base/malloc_internal (= 1.20211102.0) + - abseil/base/pretty_function (= 1.20211102.0) + - abseil/base/raw_logging_internal (= 1.20211102.0) + - abseil/base/spinlock_wait (= 1.20211102.0) + - abseil/base/strerror (= 1.20211102.0) + - abseil/base/throw_delegate (= 1.20211102.0) + - abseil/base/atomic_hook (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/base (1.20211102.0): + - abseil/base/atomic_hook + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/base/spinlock_wait + - abseil/meta/type_traits + - abseil/base/base_internal (1.20211102.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/base/config (1.20211102.0) + - abseil/base/core_headers (1.20211102.0): + - abseil/base/config + - abseil/base/dynamic_annotations (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian (1.20211102.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver (1.20211102.0): + - abseil/base/config + - abseil/base/fast_type_id (1.20211102.0): + - abseil/base/config + - abseil/base/log_severity (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal (1.20211102.0): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/base/pretty_function (1.20211102.0) + - abseil/base/raw_logging_internal (1.20211102.0): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/base/spinlock_wait (1.20211102.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/strerror (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/throw_delegate (1.20211102.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/container/common (1.20211102.0): + - abseil/meta/type_traits + - abseil/types/optional + - abseil/container/compressed_tuple (1.20211102.0): + - abseil/utility/utility + - abseil/container/container_memory (1.20211102.0): + - abseil/base/config + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/container/fixed_array (1.20211102.0): + - abseil/algorithm/algorithm + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/container/flat_hash_map (1.20211102.0): + - abseil/algorithm/container + - abseil/container/container_memory + - abseil/container/hash_function_defaults + - abseil/container/raw_hash_map + - abseil/memory/memory + - abseil/container/hash_function_defaults (1.20211102.0): + - abseil/base/config + - abseil/hash/hash + - abseil/strings/cord + - abseil/strings/strings + - abseil/container/hash_policy_traits (1.20211102.0): + - abseil/meta/type_traits + - abseil/container/hashtable_debug_hooks (1.20211102.0): + - abseil/base/config + - abseil/container/hashtablez_sampler (1.20211102.0): + - abseil/base/base + - abseil/base/core_headers + - abseil/container/have_sse + - abseil/debugging/stacktrace + - abseil/memory/memory + - abseil/profiling/exponential_biased + - abseil/profiling/sample_recorder + - abseil/synchronization/synchronization + - abseil/utility/utility + - abseil/container/have_sse (1.20211102.0) + - abseil/container/inlined_vector (1.20211102.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/inlined_vector_internal + - abseil/memory/memory + - abseil/container/inlined_vector_internal (1.20211102.0): + - abseil/base/core_headers + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/span + - abseil/container/layout (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/utility/utility + - abseil/container/raw_hash_map (1.20211102.0): + - abseil/base/throw_delegate + - abseil/container/container_memory + - abseil/container/raw_hash_set + - abseil/container/raw_hash_set (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/container/common + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/hash_policy_traits + - abseil/container/hashtable_debug_hooks + - abseil/container/hashtablez_sampler + - abseil/container/have_sse + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/utility/utility + - abseil/debugging/debugging_internal (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/errno_saver + - abseil/base/raw_logging_internal + - abseil/debugging/demangle_internal (1.20211102.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/stacktrace (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/debugging_internal + - abseil/debugging/symbolize (1.20211102.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/debugging/demangle_internal + - abseil/strings/strings + - abseil/functional/bind_front (1.20211102.0): + - abseil/base/base_internal + - abseil/container/compressed_tuple + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/functional/function_ref (1.20211102.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/hash/city (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/hash/hash (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/container/fixed_array + - abseil/hash/city + - abseil/hash/low_level_hash + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/types/optional + - abseil/types/variant + - abseil/utility/utility + - abseil/hash/low_level_hash (1.20211102.0): + - abseil/base/config + - abseil/base/endian + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/memory (1.20211102.0): + - abseil/memory/memory (= 1.20211102.0) + - abseil/memory/memory (1.20211102.0): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/meta (1.20211102.0): + - abseil/meta/type_traits (= 1.20211102.0) + - abseil/meta/type_traits (1.20211102.0): + - abseil/base/config + - abseil/numeric/bits (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/int128 (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/bits + - abseil/numeric/representation (1.20211102.0): + - abseil/base/config + - abseil/profiling/exponential_biased (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/profiling/sample_recorder (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/random/distributions (1.20211102.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/internal/fastmath + - abseil/random/internal/generate_real + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/traits + - abseil/random/internal/uniform_helper + - abseil/random/internal/wide_multiply + - abseil/strings/strings + - abseil/random/internal/distribution_caller (1.20211102.0): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/utility/utility + - abseil/random/internal/fast_uniform_bits (1.20211102.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/fastmath (1.20211102.0): + - abseil/numeric/bits + - abseil/random/internal/generate_real (1.20211102.0): + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/fastmath + - abseil/random/internal/traits + - abseil/random/internal/iostream_state_saver (1.20211102.0): + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/random/internal/nonsecure_base (1.20211102.0): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/types/optional + - abseil/types/span + - abseil/random/internal/pcg_engine (1.20211102.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/fastmath + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/platform (1.20211102.0): + - abseil/base/config + - abseil/random/internal/pool_urbg (1.20211102.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/random/internal/randen + - abseil/random/internal/seed_material + - abseil/random/internal/traits + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/random/internal/randen (1.20211102.0): + - abseil/base/raw_logging_internal + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes + - abseil/random/internal/randen_slow + - abseil/random/internal/randen_engine (1.20211102.0): + - abseil/base/endian + - abseil/meta/type_traits + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/randen + - abseil/random/internal/randen_hwaes (1.20211102.0): + - abseil/base/config + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes_impl + - abseil/random/internal/randen_hwaes_impl (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/random/internal/randen_slow (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/random/internal/salted_seed_seq (1.20211102.0): + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/seed_material + - abseil/types/optional + - abseil/types/span + - abseil/random/internal/seed_material (1.20211102.0): + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/random/internal/fast_uniform_bits + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/random/internal/traits (1.20211102.0): + - abseil/base/config + - abseil/random/internal/uniform_helper (1.20211102.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/traits + - abseil/random/internal/wide_multiply (1.20211102.0): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/random/random (1.20211102.0): + - abseil/random/distributions + - abseil/random/internal/nonsecure_base + - abseil/random/internal/pcg_engine + - abseil/random/internal/pool_urbg + - abseil/random/internal/randen_engine + - abseil/random/seed_sequences + - abseil/random/seed_gen_exception (1.20211102.0): + - abseil/base/config + - abseil/random/seed_sequences (1.20211102.0): + - abseil/container/inlined_vector + - abseil/random/internal/nonsecure_base + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/status/status (1.20211102.0): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/functional/function_ref + - abseil/strings/cord + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/status/statusor (1.20211102.0): + - abseil/base/base + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/status/status + - abseil/strings/strings + - abseil/types/variant + - abseil/utility/utility + - abseil/strings/cord (1.20211102.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/container/fixed_array + - abseil/container/inlined_vector + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_info + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_scope + - abseil/strings/cordz_update_tracker + - abseil/strings/internal + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/strings/cord_internal (1.20211102.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/container/inlined_vector + - abseil/container/layout + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/strings/cordz_functions (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/profiling/exponential_biased + - abseil/strings/cordz_handle (1.20211102.0): + - abseil/base/base + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/synchronization/synchronization + - abseil/strings/cordz_info (1.20211102.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_handle + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_tracker + - abseil/synchronization/synchronization + - abseil/types/span + - abseil/strings/cordz_statistics (1.20211102.0): + - abseil/base/config + - abseil/strings/cordz_update_tracker + - abseil/strings/cordz_update_scope (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/cord_internal + - abseil/strings/cordz_info + - abseil/strings/cordz_update_tracker + - abseil/strings/cordz_update_tracker (1.20211102.0): + - abseil/base/config + - abseil/strings/internal (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/strings/str_format (1.20211102.0): + - abseil/strings/str_format_internal + - abseil/strings/str_format_internal (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/numeric/representation + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/strings/strings (1.20211102.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/internal + - abseil/synchronization/graphcycles_internal (1.20211102.0): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/synchronization/kernel_timeout_internal (1.20211102.0): + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/time/time + - abseil/synchronization/synchronization (1.20211102.0): + - abseil/base/atomic_hook + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/synchronization/graphcycles_internal + - abseil/synchronization/kernel_timeout_internal + - abseil/time/time + - abseil/time (1.20211102.0): + - abseil/time/internal (= 1.20211102.0) + - abseil/time/time (= 1.20211102.0) + - abseil/time/internal (1.20211102.0): + - abseil/time/internal/cctz (= 1.20211102.0) + - abseil/time/internal/cctz (1.20211102.0): + - abseil/time/internal/cctz/civil_time (= 1.20211102.0) + - abseil/time/internal/cctz/time_zone (= 1.20211102.0) + - abseil/time/internal/cctz/civil_time (1.20211102.0): + - abseil/base/config + - abseil/time/internal/cctz/time_zone (1.20211102.0): + - abseil/base/config + - abseil/time/internal/cctz/civil_time + - abseil/time/time (1.20211102.0): + - abseil/base/base + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/time/internal/cctz/civil_time + - abseil/time/internal/cctz/time_zone + - abseil/types (1.20211102.0): + - abseil/types/any (= 1.20211102.0) + - abseil/types/bad_any_cast (= 1.20211102.0) + - abseil/types/bad_any_cast_impl (= 1.20211102.0) + - abseil/types/bad_optional_access (= 1.20211102.0) + - abseil/types/bad_variant_access (= 1.20211102.0) + - abseil/types/compare (= 1.20211102.0) + - abseil/types/optional (= 1.20211102.0) + - abseil/types/span (= 1.20211102.0) + - abseil/types/variant (= 1.20211102.0) + - abseil/types/any (1.20211102.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/types/bad_any_cast + - abseil/utility/utility + - abseil/types/bad_any_cast (1.20211102.0): + - abseil/base/config + - abseil/types/bad_any_cast_impl + - abseil/types/bad_any_cast_impl (1.20211102.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/types/bad_optional_access (1.20211102.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/types/bad_variant_access (1.20211102.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/types/compare (1.20211102.0): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/types/optional (1.20211102.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/bad_optional_access + - abseil/utility/utility + - abseil/types/span (1.20211102.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/meta/type_traits + - abseil/types/variant (1.20211102.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/types/bad_variant_access + - abseil/utility/utility + - abseil/utility/utility (1.20211102.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/meta/type_traits + - BoringSSL-GRPC (0.0.24): + - BoringSSL-GRPC/Implementation (= 0.0.24) + - BoringSSL-GRPC/Interface (= 0.0.24) + - BoringSSL-GRPC/Implementation (0.0.24): + - BoringSSL-GRPC/Interface (= 0.0.24) + - BoringSSL-GRPC/Interface (0.0.24) + - cloud_firestore (3.1.17): + - Firebase/Firestore (= 8.15.0) + - firebase_core + - Flutter + - Firebase/Analytics (8.15.0): + - Firebase/Core + - Firebase/Auth (8.15.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 8.15.0) + - Firebase/Core (8.15.0): + - Firebase/CoreOnly + - FirebaseAnalytics (~> 8.15.0) + - Firebase/CoreOnly (8.15.0): + - FirebaseCore (= 8.15.0) + - Firebase/Firestore (8.15.0): + - Firebase/CoreOnly + - FirebaseFirestore (~> 8.15.0) + - Firebase/Messaging (8.15.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 8.15.0) + - firebase_analytics (9.1.9): + - Firebase/Analytics (= 8.15.0) + - firebase_core + - Flutter + - firebase_auth (3.3.19): + - Firebase/Auth (= 8.15.0) + - firebase_core + - Flutter + - firebase_core (1.17.1): + - Firebase/CoreOnly (= 8.15.0) + - Flutter + - firebase_messaging (11.4.1): + - Firebase/Messaging (= 8.15.0) + - firebase_core + - Flutter + - FirebaseAnalytics (8.15.0): + - FirebaseAnalytics/AdIdSupport (= 8.15.0) + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - FirebaseAnalytics/AdIdSupport (8.15.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleAppMeasurement (= 8.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - FirebaseAuth (8.15.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/Environment (~> 7.7) + - GTMSessionFetcher/Core (~> 1.5) + - FirebaseCore (8.15.0): + - FirebaseCoreDiagnostics (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - FirebaseCoreDiagnostics (8.15.0): + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - nanopb (~> 2.30908.0) + - FirebaseFirestore (8.15.0): + - abseil/algorithm (~> 1.20211102.0) + - abseil/base (~> 1.20211102.0) + - abseil/container/flat_hash_map (~> 1.20211102.0) + - abseil/memory (~> 1.20211102.0) + - abseil/meta (~> 1.20211102.0) + - abseil/strings/strings (~> 1.20211102.0) + - abseil/time (~> 1.20211102.0) + - abseil/types (~> 1.20211102.0) + - FirebaseCore (~> 8.0) + - "gRPC-C++ (~> 1.44.0)" + - leveldb-library (~> 1.22) + - nanopb (~> 2.30908.0) + - FirebaseInstallations (8.15.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - PromisesObjC (< 3.0, >= 1.2) + - FirebaseMessaging (8.15.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Reachability (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - nanopb (~> 2.30908.0) + - Flutter (1.0.0) + - flutter_secure_storage (3.3.1): + - Flutter + - GoogleAppMeasurement (8.15.0): + - GoogleAppMeasurement/AdIdSupport (= 8.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (8.15.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 8.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (8.15.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleDataTransport (9.1.4): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.7.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/Network (7.7.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.7.0)" + - GoogleUtilities/Reachability (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/Logger + - "gRPC-C++ (1.44.0)": + - "gRPC-C++/Implementation (= 1.44.0)" + - "gRPC-C++/Interface (= 1.44.0)" + - "gRPC-C++/Implementation (1.44.0)": + - abseil/base/base (= 1.20211102.0) + - abseil/base/core_headers (= 1.20211102.0) + - abseil/container/flat_hash_map (= 1.20211102.0) + - abseil/container/inlined_vector (= 1.20211102.0) + - abseil/functional/bind_front (= 1.20211102.0) + - abseil/hash/hash (= 1.20211102.0) + - abseil/memory/memory (= 1.20211102.0) + - abseil/random/random (= 1.20211102.0) + - abseil/status/status (= 1.20211102.0) + - abseil/status/statusor (= 1.20211102.0) + - abseil/strings/cord (= 1.20211102.0) + - abseil/strings/str_format (= 1.20211102.0) + - abseil/strings/strings (= 1.20211102.0) + - abseil/synchronization/synchronization (= 1.20211102.0) + - abseil/time/time (= 1.20211102.0) + - abseil/types/optional (= 1.20211102.0) + - abseil/types/variant (= 1.20211102.0) + - abseil/utility/utility (= 1.20211102.0) + - "gRPC-C++/Interface (= 1.44.0)" + - gRPC-Core (= 1.44.0) + - "gRPC-C++/Interface (1.44.0)" + - gRPC-Core (1.44.0): + - gRPC-Core/Implementation (= 1.44.0) + - gRPC-Core/Interface (= 1.44.0) + - gRPC-Core/Implementation (1.44.0): + - abseil/base/base (= 1.20211102.0) + - abseil/base/core_headers (= 1.20211102.0) + - abseil/container/flat_hash_map (= 1.20211102.0) + - abseil/container/inlined_vector (= 1.20211102.0) + - abseil/functional/bind_front (= 1.20211102.0) + - abseil/hash/hash (= 1.20211102.0) + - abseil/memory/memory (= 1.20211102.0) + - abseil/random/random (= 1.20211102.0) + - abseil/status/status (= 1.20211102.0) + - abseil/status/statusor (= 1.20211102.0) + - abseil/strings/cord (= 1.20211102.0) + - abseil/strings/str_format (= 1.20211102.0) + - abseil/strings/strings (= 1.20211102.0) + - abseil/synchronization/synchronization (= 1.20211102.0) + - abseil/time/time (= 1.20211102.0) + - abseil/types/optional (= 1.20211102.0) + - abseil/types/variant (= 1.20211102.0) + - abseil/utility/utility (= 1.20211102.0) + - BoringSSL-GRPC (= 0.0.24) + - gRPC-Core/Interface (= 1.44.0) + - Libuv-gRPC (= 0.0.10) + - gRPC-Core/Interface (1.44.0) + - GTMSessionFetcher/Core (1.7.2) + - hexcolor (0.0.1): + - Flutter + - image_picker (0.0.1): + - Flutter + - kakao_flutter_sdk (0.9.0): + - Flutter + - leveldb-library (1.22.1) + - Libuv-gRPC (0.0.10): + - Libuv-gRPC/Implementation (= 0.0.10) + - Libuv-gRPC/Interface (= 0.0.10) + - Libuv-gRPC/Implementation (0.0.10): + - Libuv-gRPC/Interface (= 0.0.10) + - Libuv-gRPC/Interface (0.0.10) + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) + - package_info (0.0.1): + - Flutter + - package_info_plus (0.4.5): + - Flutter + - path_provider_ios (0.0.1): + - Flutter + - PromisesObjC (2.1.0) + - shared_preferences_ios (0.0.1): + - Flutter + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) + - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) + - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) + - Flutter (from `Flutter`) + - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - hexcolor (from `.symlinks/plugins/hexcolor/ios`) + - image_picker (from `.symlinks/plugins/image_picker/ios`) + - kakao_flutter_sdk (from `.symlinks/plugins/kakao_flutter_sdk/ios`) + - package_info (from `.symlinks/plugins/package_info/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - abseil + - BoringSSL-GRPC + - Firebase + - FirebaseAnalytics + - FirebaseAuth + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseFirestore + - FirebaseInstallations + - FirebaseMessaging + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleUtilities + - "gRPC-C++" + - gRPC-Core + - GTMSessionFetcher + - leveldb-library + - Libuv-gRPC + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + cloud_firestore: + :path: ".symlinks/plugins/cloud_firestore/ios" + firebase_analytics: + :path: ".symlinks/plugins/firebase_analytics/ios" + firebase_auth: + :path: ".symlinks/plugins/firebase_auth/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_messaging: + :path: ".symlinks/plugins/firebase_messaging/ios" + Flutter: + :path: Flutter + flutter_secure_storage: + :path: ".symlinks/plugins/flutter_secure_storage/ios" + hexcolor: + :path: ".symlinks/plugins/hexcolor/ios" + image_picker: + :path: ".symlinks/plugins/image_picker/ios" + kakao_flutter_sdk: + :path: ".symlinks/plugins/kakao_flutter_sdk/ios" + package_info: + :path: ".symlinks/plugins/package_info/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_ios: + :path: ".symlinks/plugins/path_provider_ios/ios" + shared_preferences_ios: + :path: ".symlinks/plugins/shared_preferences_ios/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + abseil: ebe5b5529fb05d93a8bdb7951607be08b7fa71bc + BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33 + cloud_firestore: 936969669b510f2affd43583582d7b117c5f08d1 + Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d + firebase_analytics: 99eefffcacf3ab694db7926dd1291833e1300853 + firebase_auth: a6470a6974d42f83117932c913aecb926182f907 + firebase_core: 318de541b0e61d3f24262982a3f0b54afe72439b + firebase_messaging: 943cfe65e0b3f457240489ce67655e40da1d270c + FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa + FirebaseAuth: 3e73bf8abf4fbb40f8b421f361f4cc48ee57388c + FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1 + FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb + FirebaseFirestore: d7023faff8e1b4fd69d0adbcf18e65129bc03842 + FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd + FirebaseMessaging: 5e5118a2383b3531e730d974680954c679ca0a13 + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec + GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e + GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b + GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + "gRPC-C++": 9675f953ace2b3de7c506039d77be1f2e77a8db2 + gRPC-Core: 943e491cb0d45598b0b0eb9e910c88080369290b + GTMSessionFetcher: 5595ec75acf5be50814f81e9189490412bad82ba + hexcolor: fdfb9c4258ad96e949c2dbcdf790a62194b8aa89 + image_picker: 50e7c7ff960e5f58faa4d1f4af84a771c671bc4a + kakao_flutter_sdk: bb7397452b15fb550037aea8e6d347143488af71 + leveldb-library: 50c7b45cbd7bf543c81a468fe557a16ae3db8729 + Libuv-gRPC: 55e51798e14ef436ad9bc45d12d43b77b49df378 + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 + package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e + path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72 + shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + +PODFILE CHECKSUM: 609cb5a2bf46ec2075f2ca694021109a66497419 + +COCOAPODS: 1.11.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6b34e871..8babac84 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -13,6 +13,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A9564A82D9B725FB560C7225 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F974F64635BDDE42ED5AD49 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -31,9 +32,12 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2F974F64635BDDE42ED5AD49 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3D4A3F80E9F85E4D3A9283F8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 79A8568BC35A9C9F3C0563C4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -42,6 +46,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E06153A184AAE1222449B181 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,6 +54,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A9564A82D9B725FB560C7225 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -72,6 +78,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + AB2B218DD76E850EA9A72872 /* Pods */, + A04CEE233E526F9434E1E075 /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +106,24 @@ path = Runner; sourceTree = ""; }; + A04CEE233E526F9434E1E075 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2F974F64635BDDE42ED5AD49 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + AB2B218DD76E850EA9A72872 /* Pods */ = { + isa = PBXGroup; + children = ( + 3D4A3F80E9F85E4D3A9283F8 /* Pods-Runner.debug.xcconfig */, + E06153A184AAE1222449B181 /* Pods-Runner.release.xcconfig */, + 79A8568BC35A9C9F3C0563C4 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +131,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 91D6F0AA168A36CBA4D8FFE7 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 736DE4A2813BDCF7D1E2DC30 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -127,7 +155,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -183,6 +211,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 736DE4A2813BDCF7D1E2DC30 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 91D6F0AA168A36CBA4D8FFE7 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -283,15 +350,20 @@ }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = 79A8568BC35A9C9F3C0563C4 /* Pods-Runner.profile.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZP42A55SVA; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.wafflestudio.guam.guamCommunityClient; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0.4; + PRODUCT_BUNDLE_IDENTIFIER = "com.wafflestudio.guam-community"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -399,7 +471,8 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -407,15 +480,20 @@ }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + baseConfigurationReference = 3D4A3F80E9F85E4D3A9283F8 /* Pods-Runner.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZP42A55SVA; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.wafflestudio.guam.guamCommunityClient; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0.4; + PRODUCT_BUNDLE_IDENTIFIER = "com.wafflestudio.guam-community"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -426,15 +504,20 @@ }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + baseConfigurationReference = E06153A184AAE1222449B181 /* Pods-Runner.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ZP42A55SVA; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.wafflestudio.guam.guamCommunityClient; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0.4; + PRODUCT_BUNDLE_IDENTIFIER = "com.wafflestudio.guam-community"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cf..c87d15a3 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + - - + + diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 00000000..2987e12a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 00000000..bf496252 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 00000000..928d6640 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 00000000..02f18693 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 00000000..989feffb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 00000000..edd7c0cc Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 00000000..6bcd6554 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 00000000..15644b76 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 00000000..55f99660 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 00000000..d426ecee Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 00000000..f1eec470 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 00000000..eb276391 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 00000000..03883d22 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 00000000..d1c54640 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 00000000..73a25993 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 00000000..9a8bce41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 00000000..1a37925c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 00000000..6bb7e369 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 00000000..ffa79786 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 00000000..7549a9c3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 00000000..3c3b53c8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 00000000..0f68246e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 00000000..49f758b6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 00000000..095434b4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 00000000..37751e6c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 00000000..75fe4a5a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 00000000..94ec2409 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 00000000..f94120bf Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 00000000..8908ee5f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 00000000..27d78204 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 00000000..5e0bd6ce Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fab..04de9d48 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,330 @@ { "images" : [ { - "size" : "20x20", + "filename" : "40.png", "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { - "size" : "20x20", + "filename" : "60.png", "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" + "scale" : "3x", + "size" : "20x20" }, { - "size" : "29x29", + "filename" : "29.png", "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", + "filename" : "58.png", "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { - "size" : "29x29", + "filename" : "87.png", "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" + "scale" : "3x", + "size" : "29x29" }, { - "size" : "40x40", + "filename" : "80.png", "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { - "size" : "40x40", + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "scale" : "1x", + "size" : "57x57" }, { - "size" : "60x60", + "filename" : "114.png", "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" + "scale" : "2x", + "size" : "57x57" }, { - "size" : "60x60", + "filename" : "120.png", "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "scale" : "2x", + "size" : "60x60" }, { - "size" : "20x20", + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.png", "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "scale" : "1x", + "size" : "20x20" }, { - "size" : "20x20", + "filename" : "40.png", "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { - "size" : "29x29", + "filename" : "29.png", "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", + "filename" : "58.png", "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { - "size" : "40x40", + "filename" : "40.png", "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "scale" : "1x", + "size" : "40x40" }, { - "size" : "40x40", + "filename" : "80.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "50.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "50x50" + }, + { + "filename" : "100.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "50x50" + }, + { + "filename" : "72.png", "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "scale" : "1x", + "size" : "72x72" }, { - "size" : "76x76", + "filename" : "144.png", "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "scale" : "2x", + "size" : "72x72" }, { - "size" : "76x76", + "filename" : "76.png", "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "scale" : "1x", + "size" : "76x76" }, { - "size" : "83.5x83.5", + "filename" : "152.png", "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "scale" : "2x", + "size" : "76x76" }, { - "size" : "1024x1024", + "filename" : "167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024.png", "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "48.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "24x24", + "subtype" : "38mm" + }, + { + "filename" : "55.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "27.5x27.5", + "subtype" : "42mm" + }, + { + "filename" : "58.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "33x33", + "subtype" : "45mm" + }, + { + "filename" : "80.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "40x40", + "subtype" : "38mm" + }, + { + "filename" : "88.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "44x44", + "subtype" : "40mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "46x46", + "subtype" : "41mm" + }, + { + "filename" : "100.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "50x50", + "subtype" : "44mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "51x51", + "subtype" : "45mm" + }, + { + "filename" : "172.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "86x86", + "subtype" : "38mm" + }, + { + "filename" : "196.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "98x98", + "subtype" : "42mm" + }, + { + "filename" : "216.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "108x108", + "subtype" : "44mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "117x117", + "subtype" : "45mm" + }, + { + "filename" : "1024.png", + "idiom" : "watch-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } } diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada47..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd96..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde1211..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd96..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b860..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b860..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d3..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f585..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index f3c28516..61851a1f 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,9 @@ - - + + + - - + + @@ -14,13 +15,14 @@ - + - + + diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 00000000..b2d127f4 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 648780047414-erb4hojdlnk09dk8nvcjjgdps2q3cc8n.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.648780047414-erb4hojdlnk09dk8nvcjjgdps2q3cc8n + ANDROID_CLIENT_ID + 648780047414-d2fbe0qcdiugckgq8stcqvgef89ie6a9.apps.googleusercontent.com + API_KEY + AIzaSyCkeKrJihj2WMWtlqWxfzL2aQTtNr-ytTg + GCM_SENDER_ID + 648780047414 + PLIST_VERSION + 1 + BUNDLE_ID + com.wafflestudio.guam-community + PROJECT_ID + waffle-guam + STORAGE_BUCKET + waffle-guam.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:648780047414:ios:054210930e5e474921a497 + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 12d96fc9..10004a8a 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + GUAM CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,13 +17,30 @@ CFBundlePackageType APPL CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) + $(MARKETING_VERSION) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + kakao367d8cf339e2ba59376ba647c7135dd2 + + + CFBundleVersion - $(FLUTTER_BUILD_NUMBER) + $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + NSCameraUsageDescription + To take an avatar photo + NSMicrophoneUsageDescription + To upload an image + NSPhotoLibraryUsageDescription + To choose an avatar UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/lib/commons/back.dart b/lib/commons/back.dart new file mode 100644 index 00000000..29c6b5fb --- /dev/null +++ b/lib/commons/back.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class Back extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + color: GuamColorFamily.grayscaleWhite, + margin: EdgeInsets.zero, + child: IconButton( + icon: SvgPicture.asset('assets/icons/back.svg'), + onPressed: () => Navigator.maybePop(context) + ), + ); + } +} diff --git a/lib/commons/bottom_modal/bottom_modal_default.dart b/lib/commons/bottom_modal/bottom_modal_default.dart new file mode 100644 index 00000000..49d83db9 --- /dev/null +++ b/lib/commons/bottom_modal/bottom_modal_default.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class BottomModalDefault extends StatelessWidget { + final Function onPressed; + final String text; + + const BottomModalDefault({this.onPressed, this.text}); + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: onPressed, + child: Container( + child: Text( + text, + style: TextStyle( + fontSize: 16, + color: GuamColorFamily.grayscaleGray1, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ) + ); + } +} diff --git a/lib/commons/bottom_modal/bottom_modal_with_alert.dart b/lib/commons/bottom_modal/bottom_modal_with_alert.dart new file mode 100644 index 00000000..d648641b --- /dev/null +++ b/lib/commons/bottom_modal/bottom_modal_with_alert.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +import '../custom_divider.dart'; +import 'bottom_modal_default.dart'; + +class BottomModalWithAlert extends StatelessWidget { + final String funcName; + final String title; + final String body; + final Function func; + + BottomModalWithAlert({this.funcName, this.title, this.body, this.func}); + + @override + Widget build(BuildContext context) { + return BottomModalDefault( + text: funcName, + onPressed: () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: GuamColorFamily.grayscaleWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + builder: (context) => SingleChildScrollView( + child: Container( + padding: EdgeInsets.only(left: 24, top: 24, right: 18, bottom: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: TextStyle(fontSize: 18, color: GuamColorFamily.grayscaleGray2), + ), + TextButton( + child: Text( + '취소', + style: TextStyle(fontSize: 16, color: GuamColorFamily.purpleCore, + ), + ), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size(30, 26), + alignment: Alignment.centerRight, + ), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + CustomDivider(color: GuamColorFamily.grayscaleGray7), + Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Text( + body, + style: TextStyle(fontSize: 14, height: 1.6, fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular), + ), + ), + Center( + child: TextButton( + onPressed: func, + child: Text( + funcName, + style: TextStyle(fontSize: 16, color: GuamColorFamily.redCore), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/commons/bottom_modal/bottom_modal_with_choice.dart b/lib/commons/bottom_modal/bottom_modal_with_choice.dart new file mode 100644 index 00000000..fca1c6ac --- /dev/null +++ b/lib/commons/bottom_modal/bottom_modal_with_choice.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import '../custom_divider.dart'; + +class BottomModalWithChoice extends StatelessWidget { + final String title; + final String back; + final String body; + final String alert; + final String confirm; + final List children; + + BottomModalWithChoice({this.title, this.back, this.body, this.alert, this.confirm, this.children}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.only(left: 24, top: 24, right: 18, bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: TextStyle(fontSize: 18, color: GuamColorFamily.grayscaleGray2), + ), + TextButton( + child: Text( + back, + style: TextStyle(fontSize: 16, color: GuamColorFamily.purpleCore), + ), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size(30, 26), + alignment: Alignment.centerRight, + ), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + CustomDivider(color: GuamColorFamily.grayscaleGray7), + if (body != null) + Padding( + padding: EdgeInsets.only(top: 20, bottom: 10), + child: Text( + body, + style: TextStyle(fontSize: 14, height: 1.6, fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular), + ), + ), + Container( + padding: EdgeInsets.only(top: 10, bottom: 20), + child: Column( + children: children, + ), + ), + if (alert != null) + Padding( + padding: EdgeInsets.only(bottom: 10), + child: Container( + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20), + decoration: BoxDecoration( + border: Border.all(color: GuamColorFamily.grayscaleGray6), + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + child: Text( + alert, + style: TextStyle( + fontSize: 14, + height: 1.6, + color: GuamColorFamily.grayscaleGray2, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + ), + if (confirm != null) + Center( + child: TextButton( + onPressed: () {}, + child: Text( + confirm, + style: TextStyle(fontSize: 16, color: GuamColorFamily.redCore), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/commons/bottom_modal/bottom_modal_with_message.dart b/lib/commons/bottom_modal/bottom_modal_with_message.dart new file mode 100644 index 00000000..4a40ed43 --- /dev/null +++ b/lib/commons/bottom_modal/bottom_modal_with_message.dart @@ -0,0 +1,120 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; +import 'package:guam_community_client/screens/messages/message_bottom_modal.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; + +import '../../providers/messages/messages.dart'; + +class BottomModalWithMessage extends StatefulWidget { + final String funcName; + final String title; + final Profile profile; + final Function func; + + BottomModalWithMessage({this.funcName, this.title, this.profile, this.func}); + + @override + State createState() => _BottomModalWithMessageState(); +} + +class _BottomModalWithMessageState extends State with Toast { + @override + Widget build(BuildContext context) { + final msgProvider = context.read(); + Map input = {'text': '', 'image': []}; // image + bool sending = false; + + void toggleSending() { + setState(() => sending = !sending); + } + + Future sendMessage({List files}) async { + toggleSending(); + try { + await msgProvider.sendMessage( + fields: { + 'to': widget.profile.id.toString(), + 'text': input['text'] == '' && files.isNotEmpty ? '사진' : input['text'], + // 서버 수정 전까지 사진만 달랑 보내는 경우 텍스트를 '사진'으로 지정하여 전송. + }, + files: files, + ).then((successful) { + toggleSending(); + if (successful) { + Navigator.maybePop(context); + } else { + print("Error!"); + // showToast(success: false, msg: '쪽지를 발송할 수 없습니다.'); + } + }); + } catch (e) { + print(e); + } + } + + return Container( + padding: EdgeInsets.only(left: 24, top: 24, right: 18, bottom: 14), + decoration: BoxDecoration( + color: GuamColorFamily.grayscaleWhite, + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: TextStyle(fontSize: 18, color: GuamColorFamily.grayscaleGray2), + ), + TextButton( + child: Text( + '취소', + style: TextStyle(fontSize: 16, color: GuamColorFamily.purpleCore, + ), + ), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size(30, 26), + alignment: Alignment.centerRight, + ), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + Padding(padding: EdgeInsets.only(bottom: 14)), + MessageBottomModal(input), + Padding(padding: EdgeInsets.only(bottom: 14)), + !sending ? Center( + child: TextButton( + onPressed: !sending + ? () async => await sendMessage( + files: input['image'] != [] + ? [...input['image'].map((e) => File(e.path))] + : []) + : null, + child: Text( + widget.funcName, + style: TextStyle(fontSize: 16, color: GuamColorFamily.purpleCore), + ), + ), + ) : Center( + child: Container( + width: 20, + height: 20, + margin: EdgeInsets.only(top: 13, bottom: 15), + child: CircularProgressIndicator( + color: GuamColorFamily.purpleLight1, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/commons/button_size_circular_progress_indicator.dart b/lib/commons/button_size_circular_progress_indicator.dart new file mode 100644 index 00000000..52abe552 --- /dev/null +++ b/lib/commons/button_size_circular_progress_indicator.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class ButtonSizeCircularProgressIndicator extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 18), + child: SizedBox ( + width: 20, + height: 20, + child: CircularProgressIndicator(color: GuamColorFamily.purpleCore), + ), + ); + } +} diff --git a/lib/commons/color_of_category.dart b/lib/commons/color_of_category.dart new file mode 100644 index 00000000..c6442cc0 --- /dev/null +++ b/lib/commons/color_of_category.dart @@ -0,0 +1,16 @@ +import 'package:guam_community_client/styles/colors.dart'; +import 'package:hexcolor/hexcolor.dart'; + +colorOfCategory(String category) { + HexColor textColor; + + switch (category) { + case '개발': textColor = GuamColorFamily.purpleCore; break; + case '데이터분석': textColor = GuamColorFamily.greenCore; break; + case '디자인': textColor = GuamColorFamily.pinkCore; break; + case '기획/마케팅': textColor = GuamColorFamily.blueCore; break; + case '기타': textColor = GuamColorFamily.orangeCore; break; + default: textColor = GuamColorFamily.grayscaleGray1; break; + } + return textColor; +} diff --git a/lib/commons/common_confirm_dialog.dart b/lib/commons/common_confirm_dialog.dart new file mode 100644 index 00000000..8766340a --- /dev/null +++ b/lib/commons/common_confirm_dialog.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +class CommonConfirmDialog extends StatelessWidget { + final String dialogText; + final String confirmText; + final String declineText; + final Function onPressConfirm; + final Function onPressDecline; + + CommonConfirmDialog({@required this.dialogText, confirmText = "확인", + declineText = "취소", this.onPressConfirm, this.onPressDecline}) + : this.confirmText = confirmText, this.declineText = declineText; + + @override + Widget build(BuildContext context) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)) + ), + titlePadding: EdgeInsets.all(0), + title: Container( + padding: EdgeInsets.all(10), + child: Row( + children: [ + Icon(Icons.notifications_none, size: 16), + Text(" 알림", style: TextStyle(fontSize: 12)) + ] + ) + ), + contentPadding: EdgeInsets.all(10), + content: Text( + dialogText, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.black) + ), + actions: [ + TextButton( + child: Text( + confirmText, + style: TextStyle( + fontSize: 14, + color: Color.fromRGBO(85, 88, 255, 1), + ) + ), + onPressed: () { + // 일단은 dialog 2개 연달아 띄우는 tutorial을 위해 순서 유지 + Navigator.of(context).pop(); + if (onPressConfirm != null) onPressConfirm(); + }, + ), + TextButton( + child: Text( + declineText, + style: TextStyle(fontSize: 14, color: Colors.red) + ), + onPressed: () { + // 일단은 dialog 2개 연달아 띄우는 tutorial을 위해 순서 유지 + Navigator.of(context).pop(); + if (onPressDecline != null) onPressDecline(); + }, + ), + ], + ); + } +} diff --git a/lib/commons/common_icon_button.dart b/lib/commons/common_icon_button.dart new file mode 100644 index 00000000..0810a667 --- /dev/null +++ b/lib/commons/common_icon_button.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class CommonIconButton extends StatelessWidget { + final IconData icon; + final Function onPressed; + final Color iconColor; + + CommonIconButton({@required this.icon, @required this.onPressed, this.iconColor}); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: Icon(icon, color: iconColor), + padding: EdgeInsets.zero, + constraints: BoxConstraints( + maxWidth: 24, maxHeight: 24 + ), + onPressed: onPressed + ); + } +} diff --git a/lib/commons/common_img_nickname.dart b/lib/commons/common_img_nickname.dart new file mode 100644 index 00000000..7477abe9 --- /dev/null +++ b/lib/commons/common_img_nickname.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/helpers/http_request.dart'; +import 'package:guam_community_client/helpers/svg_provider.dart'; +import 'package:guam_community_client/screens/profiles/profiles_app.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:hexcolor/hexcolor.dart'; + +class CommonImgNickname extends StatelessWidget { + final int userId; + final double fontSize; + final double imageSize; + final String imgUrl; + final String nickname; + final HexColor nicknameColor; + final bool profileClickable; + + CommonImgNickname({this.userId, this.fontSize=12, this.imageSize=24, this.imgUrl, this.nickname, this.nicknameColor, this.profileClickable=true}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: profileClickable + ? () => Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute(builder: (_) => ProfilesApp(userId: userId)), + ) + : null, + child: Row( + children: [ + Container( + height: imageSize, + width: imageSize, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + fit: BoxFit.cover, + image: imgUrl != null + ? NetworkImage(HttpRequest().s3BaseAuthority + imgUrl) + : SvgProvider('assets/icons/profile_image.svg') + ), + ), + ), + Padding( + padding: EdgeInsets.only(left: 8), + child: Text( + nickname ?? "", + style: TextStyle( + fontSize: fontSize, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: nicknameColor, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/commons/common_text_button.dart b/lib/commons/common_text_button.dart new file mode 100644 index 00000000..24fd0400 --- /dev/null +++ b/lib/commons/common_text_button.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:hexcolor/hexcolor.dart'; + +class CommonTextButton extends StatelessWidget { + final double fontSize; + final String text; + final Function onPressed; + final HexColor textColor; + final String fontFamily; + + CommonTextButton({this.text, this.fontSize, this.fontFamily, this.textColor, this.onPressed}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: Text( + text, + style: TextStyle( + fontSize: fontSize, + fontFamily: fontFamily, + color: textColor + ), + ), + style: TextButton.styleFrom( + minimumSize: Size.zero, + alignment: Alignment.centerLeft, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: onPressed, + ); + } +} diff --git a/lib/commons/common_text_field.dart b/lib/commons/common_text_field.dart new file mode 100644 index 00000000..d06d8d96 --- /dev/null +++ b/lib/commons/common_text_field.dart @@ -0,0 +1,291 @@ +import 'dart:io'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mentions/flutter_mentions.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/commons/custom_divider.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:provider/provider.dart'; +import '../helpers/pick_image.dart'; +import '../providers/messages/messages.dart'; +import 'common_img_nickname.dart'; +import 'image/image_thumbnail.dart'; +import 'button_size_circular_progress_indicator.dart'; + +class CommonTextField extends StatefulWidget { + final int messageTo; + final String sendButton; + final Function onTap; + final Function addImage; + final Function removeImage; + final dynamic editTarget; + final List> mentionList; + + CommonTextField({this.sendButton='등록', @required this.onTap, this.messageTo, this.addImage, this.removeImage, this.editTarget, this.mentionList}); + + @override + State createState() => _CommonTextFieldState(); +} + +class _CommonTextFieldState extends State { + final double maxImgSize = 80; + final double imgSheetHeight = 96; + double mentionTargetHeight = 160; + double bottomSheetHeight = 56; + String content = ''; + bool sending = false; + bool activeMention = false; + List imageFileList = []; + List mentionTargetIds = []; + RegExp mentionRegexp = new RegExp(r"@\[__(.*?)__\]"); /// mention할 Id를 마크다운에서 추출하는 정규식 + + GlobalKey key = GlobalKey(); + + @override + void dispose() { + imageFileList.clear(); + super.dispose(); + } + + void setContent(){ + setState(() => content = key.currentState.controller.text); + } + + void setImageFile(PickedFile val) { + setState(() { + if (val != null) imageFileList.add(val); + }); + } + + void deleteImageFile(int idx) { + setState(() => imageFileList.removeAt(idx)); + } + + void toggleSending() { + setState(() => sending = !sending); + } + + @override + Widget build(BuildContext context) { + bool isEdit = widget.editTarget != null; + + Future send() async { + toggleSending(); + try { + /// 멘션 리스트 중복 제거 + Iterable matches = mentionRegexp + .allMatches(key.currentState.controller.markupText); + if (matches.length > 0) + matches.forEach((match) { + int mentionId = int.parse(match.group(1)); + if (!mentionTargetIds.contains(mentionId)) + mentionTargetIds.add(mentionId); + }); + if (widget.messageTo != null) { + /// 쪽지함에서 쪽지 작성 + await widget.onTap( + files: [...imageFileList.map((e) => File(e.path))], + fields: { + 'to': widget.messageTo.toString(), + 'text': content == '' && imageFileList.isNotEmpty ? '사진' : content, + // 서버 수정 전까지 사진만 달랑 보내는 경우 텍스트를 '사진'으로 지정하여 전송. + }, + ).then((successful) { + /// successful == msgSended bool from MessageDetail Widget + if (successful) { + imageFileList.clear(); + mentionTargetIds.clear(); + key.currentState.controller.text = ''; + FocusScope.of(context).unfocus(); + } + }); + } else { + /// 게시글 or 댓글 작성 + if (isEdit && content != '') { + await widget.onTap( + id: widget.editTarget.id, + fields: { + "mentionIds": mentionTargetIds.join(','), + "content": content, + }, + ).then((successful) { + if (successful) { + key.currentState.controller.text = ''; + FocusScope.of(context).unfocus(); + } + }); + } else { + if (content != '' || imageFileList.isNotEmpty) + await widget.onTap( + files: [...imageFileList.map((e) => File(e.path))], + fields: { + "mentionIds": mentionTargetIds.join(','), + "content": content, + }, + ).then((successful) { + if (successful) { + imageFileList.clear(); + mentionTargetIds.clear(); + key.currentState.controller.text = ''; + FocusScope.of(context).unfocus(); + } + }); + } + } + } catch (e) { + print(e); + } finally { + toggleSending(); + } + } + + return SizedBox( + height: imageFileList.isNotEmpty + ? imgSheetHeight + bottomSheetHeight + : bottomSheetHeight, // 56 .. 73 .. 90 .. 107 + child: DecoratedBox( + decoration: BoxDecoration( + color: GuamColorFamily.grayscaleWhite, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: Offset(0, -2), // changes position of shadow + ), + ], + ), + child: Column( + children: [ + Stack( + alignment: Alignment.bottomCenter, + children: [ + imageFileList.isNotEmpty + ? Container( + color: GuamColorFamily.grayscaleGray1.withOpacity(0.4), + padding: EdgeInsets.only(left: 23, top: 8, bottom: 8), + constraints: BoxConstraints(maxHeight: maxImgSize + 15), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: imageFileList.length, + itemBuilder: (_, idx) => Stack( + children: [ + Container( + padding: EdgeInsets.only(right: 14.87), + child: ImageThumbnail( + width: maxImgSize, + height: maxImgSize, + image: Image( + image: FileImage(File(imageFileList[idx].path)), + fit: BoxFit.fill, + ), + ), + ), + Positioned( + top: 2, + right: 14, + child: IconButton( + iconSize: 23, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/cancel_filled.svg'), + onPressed: () { + deleteImageFile(idx); + if (imageFileList.isEmpty) widget.removeImage(); + }, + ), + ) + ], + ), + ), + ) + : Container(), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: IconButton( + iconSize: 24, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/camera.svg'), + onPressed: !sending + ? () => pickImage(type: 'gallery').then((img) { + setImageFile(img); + widget.addImage(); + }) + : null, + ), + ), + Expanded( + child: FlutterMentions( + key: key, + suggestionPosition: SuggestionPosition.Top, + style: TextStyle(fontSize: 14), + onChanged: (e) => setContent(), + cursorColor: GuamColorFamily.purpleCore, + decoration: InputDecoration( + hintText: "댓글을 남겨주세요.", + hintStyle: TextStyle(fontSize: 14, color: GuamColorFamily.grayscaleGray5), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: EdgeInsets.only(top: 18, bottom: 18), + ), + mentions: [ + Mention( + trigger: "@", + style: TextStyle(color: GuamColorFamily.purpleLight1), + data: widget.mentionList, + suggestionBuilder: (data) => SingleChildScrollView( + dragStartBehavior: DragStartBehavior.start, + child: Column( + children: [ + Container( + height: 53, + padding: EdgeInsets.only(left: 10), + color: GuamColorFamily.purpleLight3, + child: CommonImgNickname( + fontSize: 13, + imageSize: 41, + profileClickable: false, + nickname: data['display'], + imgUrl: data['photo'] ?? null, + ), + ), + CustomDivider(thickness: 0.5), + ], + ), + ), + ), + ], + ), + ), + !sending ? TextButton( + onPressed: !sending ? send : null, + style: TextButton.styleFrom( + padding: EdgeInsets.only(right: 6), + minimumSize: Size(30, 26), + alignment: Alignment.center, + ), + child: Text( + widget.sendButton, + style: TextStyle( + color: GuamColorFamily.purpleCore, + fontSize: 16, + ), + ), + ) : ButtonSizeCircularProgressIndicator(), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/commons/custom_app_bar.dart b/lib/commons/custom_app_bar.dart new file mode 100644 index 00000000..1fc8849f --- /dev/null +++ b/lib/commons/custom_app_bar.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class CustomAppBar extends StatelessWidget with PreferredSizeWidget { + final String title; + final dynamic leading; + final dynamic trailing; + final dynamic bottom; + final Color backgroundColor; + + CustomAppBar({this.title, this.leading, this.trailing, this.bottom, this.backgroundColor}); + + @override + Size get preferredSize => Size.fromHeight(AppBar().preferredSize.height); + + @override + Widget build(BuildContext context) { + var textColor = GuamColorFamily.grayscaleGray1; + var iconColor = GuamColorFamily.grayscaleGray1; + return AppBar( + centerTitle: true, + elevation: 0.7, + title: Text( + title ?? "", + style: TextStyle(color: textColor), + ), + leading: leading != null + ? Material(color: Colors.transparent, child: leading) + : null, + automaticallyImplyLeading: false, + bottom: bottom, + actions: trailing == null + ? [] + : [Material(color: Colors.transparent, child: trailing)], + backgroundColor: backgroundColor ?? GuamColorFamily.grayscaleWhite, + iconTheme: IconThemeData( + color: iconColor, + ), + ); + } +} diff --git a/lib/commons/custom_divider.dart b/lib/commons/custom_divider.dart new file mode 100644 index 00000000..323b6386 --- /dev/null +++ b/lib/commons/custom_divider.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:hexcolor/hexcolor.dart'; + +class CustomDivider extends StatelessWidget { + final double height; + final double thickness; + final HexColor color; + + CustomDivider({ + this.height = 1, + this.thickness = 1, + this.color, + }); + + @override + Widget build(BuildContext context) { + return Divider( + height: height, + thickness: thickness, + color: color, + ); + } +} diff --git a/lib/commons/functions_category_boardType.dart b/lib/commons/functions_category_boardType.dart new file mode 100644 index 00000000..94e141e1 --- /dev/null +++ b/lib/commons/functions_category_boardType.dart @@ -0,0 +1,66 @@ +import 'package:guam_community_client/styles/colors.dart'; +import 'package:hexcolor/hexcolor.dart'; + +// dummy data의 boardType이 enum으로 잘 담겨져 오면 지울 함수 +transferBoardId(int boardId) { + String boardType; + switch (boardId) { + case 1: boardType = '익명'; break; + case 2: boardType = '자유'; break; + case 3: boardType = '구인'; break; + case 4: boardType = '정보공유'; break; + case 5: boardType = '홍보'; break; + default: boardType = '자유'; break; + } + return boardType; +} + +transferBoardType(String boardType) { + int boardId; + switch (boardType) { + case '익명': boardId = 1; break; + case '자유': boardId = 2; break; + case '구인': boardId = 3; break; + case '정보공유': boardId = 4; break; + case '홍보': boardId = 5; break; + } + return boardId; +} + +transferCategoryId(int categoryId) { + String category; + switch (categoryId) { + case 1: category = '개발'; break; + case 2: category = '데이터분석'; break; + case 3: category = '디자인'; break; + case 4: category = '기획/마케팅'; break; + case 5: category = '기타'; break; + } + return category; +} + +transferCategory(String category) { + int categoryId; + switch (category) { + case '개발': categoryId = 1; break; + case '데이터분석': categoryId = 2; break; + case '디자인': categoryId = 3; break; + case '기획/마케팅': categoryId = 4; break; + case '기타': categoryId = 5; break; + } + return categoryId; +} + +colorOfCategory(String category) { + HexColor textColor; + + switch (category) { + case '개발': textColor = GuamColorFamily.purpleCore; break; + case '데이터분석': textColor = GuamColorFamily.greenCore; break; + case '디자인': textColor = GuamColorFamily.pinkCore; break; + case '기획/마케팅': textColor = GuamColorFamily.blueCore; break; + case '기타': textColor = GuamColorFamily.orangeCore; break; + default: textColor = GuamColorFamily.grayscaleGray2; break; + } + return textColor; +} diff --git a/lib/commons/guam_progress_indicator.dart b/lib/commons/guam_progress_indicator.dart new file mode 100644 index 00000000..1585fa3a --- /dev/null +++ b/lib/commons/guam_progress_indicator.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +Widget guamProgressIndicator({double size=80}){ + return Image.asset( + "assets/gifs/guam_progress_indicator.gif", + height: size, + width: size, + ); +} \ No newline at end of file diff --git a/lib/commons/icon_text.dart b/lib/commons/icon_text.dart new file mode 100644 index 00000000..11b0c4ae --- /dev/null +++ b/lib/commons/icon_text.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:hexcolor/hexcolor.dart'; + +class IconText extends StatelessWidget { + final double iconSize; + final double fontSize; + final double paddingBtw; + final String text; + final String iconPath; + final Function onPressed; + final HexColor iconColor; + final HexColor textColor; + + IconText({this.iconSize=20, this.fontSize=12, this.paddingBtw=10, this.text="", this.iconPath, this.onPressed, this.iconColor, this.textColor}); + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: onPressed, + style: TextButton.styleFrom( + padding: EdgeInsets.only(right: paddingBtw), + minimumSize: Size.zero, + alignment: Alignment.centerLeft, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + label: Text( + text, + style: TextStyle( + color: textColor, + fontSize: fontSize, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + icon: iconPath != null ? SvgPicture.asset( + iconPath, + color: iconColor, + width: iconSize, + height: iconSize, + ) : null, + ); + } +} diff --git a/lib/commons/image/closable_image_expanded.dart b/lib/commons/image/closable_image_expanded.dart new file mode 100644 index 00000000..b1915edd --- /dev/null +++ b/lib/commons/image/closable_image_expanded.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import '../custom_app_bar.dart'; +import 'image_expanded.dart'; + +class ClosableImageExpanded extends StatelessWidget { + final Widget image; + final String imagePath; + + ClosableImageExpanded({this.image, this.imagePath}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + backgroundColor: Colors.black, + trailing: IconButton( + padding: EdgeInsets.only(right: 12), + constraints: BoxConstraints(), + onPressed: () => Navigator.of(context).pop(), + icon: SvgPicture.asset( + 'assets/icons/cancel_outlined.svg', + color: GuamColorFamily.grayscaleWhite, + width: 32, + height: 32, + ), + ), + ), + body: ImageExpanded( + image: image ?? null, + imagePath: imagePath ?? null, + ), + ); + } +} diff --git a/lib/commons/image/image_carousel.dart b/lib/commons/image/image_carousel.dart new file mode 100644 index 00000000..3ce09a6e --- /dev/null +++ b/lib/commons/image/image_carousel.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import '../custom_app_bar.dart'; +import 'image_expanded.dart'; +import 'dart:math'; + +class ImageCarousel extends StatefulWidget { + final List pictures; /// 서버 수정 후 List 로... + final int initialPage; + final bool showImageCount; + + // Actions for trailing + final bool showImageActions; + final Function deleteFunc; + + ImageCarousel({@required this.pictures, this.initialPage, this.showImageCount=true, + @required this.showImageActions, this.deleteFunc,}); + + @override + State createState() => ImageCarouselState(); +} + +class ImageCarouselState extends State { + List picturesState; /// 서버 수정 후 List 로... + int currPage; + + @override + void initState() { + super.initState(); + picturesState = widget.pictures; + currPage = widget.initialPage ?? 0; + } + + void afterDelete() { + setState(() { + picturesState.removeAt(currPage); + currPage = max(currPage - 1, 0); + if (picturesState.isEmpty) Navigator.of(context).pop(); // pop when delete last image + }); + } + + void switchPage(int idx) => setState(() {currPage = idx;}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar( + backgroundColor: Colors.black, + trailing: IconButton( + padding: EdgeInsets.only(right: 12), + constraints: BoxConstraints(), + onPressed: () => Navigator.of(context).pop(), + icon: SvgPicture.asset( + 'assets/icons/cancel_outlined.svg', + color: GuamColorFamily.grayscaleWhite, + width: 32, + height: 32, + ), + ), + // trailing: widget.showImageActions ? IconButton( + // icon: Icon(Icons.more_vert), + // color: Colors.grey, + // onPressed: () { + // if (Platform.isAndroid) { + // showMaterialModalBottomSheet( + // context: context, + // builder: (_) => BottomModalContent( + // deleteText: "이미지 삭제", + // deleteFunc: () async { + // await widget.deleteFunc(imageId: picturesState[currPage].id) + // .then((successful) { + // if (successful) { + // Navigator.of(context).pop(); // pop Modal Bottom Content + // afterDelete(); + // } + // }); + // } + // ) + // ); + // } else { + // showCupertinoModalBottomSheet( + // context: context, + // builder: (_) => BottomModalContent( + // deleteText: "이미지 삭제", + // deleteFunc: () async { + // await widget.deleteFunc(imageId: picturesState[currPage].id) + // .then((successful) { + // if (successful) { + // Navigator.of(context).pop(); // pop Modal Bottom Content + // afterDelete(); + // } + // }); + // } + // ) + // ); + // } + // }, + // ) : null, + ), + body: Stack( + children: [ + CarouselSlider( + options: CarouselOptions( + height: double.infinity, + viewportFraction: 1, + enableInfiniteScroll: false, + scrollPhysics: ClampingScrollPhysics(), + initialPage: currPage, + onPageChanged: (idx, _) => switchPage(idx) + ), + /// 서버 수정 후 e가 아니라 e.urlPath 로... + items: [...picturesState.map((e) => ImageExpanded(imagePath: e))] + ), + if (widget.showImageCount) + Center( + child: Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).size.height*0.65), + child: Container( + width: 49, + height: 25, + decoration: BoxDecoration( + color: GuamColorFamily.grayscaleGray3, + borderRadius: BorderRadius.circular(10), + ), + padding: EdgeInsets.only(left: 12, top: 5, right: 12, bottom: 2), + child: Text( + '${currPage+1} / ${picturesState.length}', + style: TextStyle(color: Colors.white, fontSize: 12), + ), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/commons/image/image_container.dart b/lib/commons/image/image_container.dart new file mode 100644 index 00000000..ee3a631c --- /dev/null +++ b/lib/commons/image/image_container.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/helpers/http_request.dart'; +import 'package:transparent_image/transparent_image.dart'; +import 'closable_image_expanded.dart'; + +class ImageThumbnail extends StatelessWidget { + /* + * image: for uploaded native image file via ImagePicker, etc., + * imagePath: for network image path (S3) + * IMPORTANT: only 1 of above should be passed to parameter + * */ + final Widget image; + final String imagePath; + final double height; + final double width; + + ImageThumbnail({this.image, this.imagePath, this.height, this.width}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: Container( + height: height, + width: width, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: image ?? FadeInImage( + placeholder: MemoryImage(kTransparentImage), + image: NetworkImage(HttpRequest().s3BaseAuthority + imagePath), + fit: BoxFit.cover, + ), + ), + ), + onTap: () => Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute( + builder: (_) => ClosableImageExpanded( + image: image ?? null, + imagePath: imagePath ?? null + ) + ) + ) + ); + } +} diff --git a/lib/commons/image/image_expanded.dart b/lib/commons/image/image_expanded.dart new file mode 100644 index 00000000..1eebe8cf --- /dev/null +++ b/lib/commons/image/image_expanded.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:transparent_image/transparent_image.dart'; +import '../../helpers/http_request.dart'; + +class ImageExpanded extends StatelessWidget{ + final Widget image; + final String imagePath; + + ImageExpanded({this.image, this.imagePath}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(bottom: 72), + height: MediaQuery.of(context).size.height, + color: Colors.black, + child: Center( + child: Container( + width: double.infinity, + child: InteractiveViewer( + child: image ?? FadeInImage( + placeholder: MemoryImage(kTransparentImage), + image: NetworkImage(HttpRequest().s3BaseAuthority + imagePath), + fit: BoxFit.fitWidth, + ) + ), + ), + ), + ); + } +} diff --git a/lib/commons/image/image_thumbnail.dart b/lib/commons/image/image_thumbnail.dart new file mode 100644 index 00000000..578f307d --- /dev/null +++ b/lib/commons/image/image_thumbnail.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:transparent_image/transparent_image.dart'; +import '../../helpers/http_request.dart'; +import 'closable_image_expanded.dart'; + +class ImageThumbnail extends StatelessWidget { + /* + * image: for uploaded native image file via ImagePicker, etc., + * imagePath: for network image path (S3) + * IMPORTANT: only 1 of above should be passed to parameter + * */ + final Widget image; + final String imagePath; + final double height; + final double width; + + ImageThumbnail({this.image, this.imagePath, this.height, this.width}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + child: Container( + height: height, + width: width, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: image ?? FadeInImage( + placeholder: MemoryImage(kTransparentImage), + image: NetworkImage(HttpRequest().s3BaseAuthority + imagePath), + fit: BoxFit.cover, + ), + ), + ), + onTap: () => Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute( + builder: (_) => ClosableImageExpanded( + image: image ?? null, + imagePath: imagePath ?? null + ) + ) + ) + ); + } +} diff --git a/lib/commons/next.dart b/lib/commons/next.dart new file mode 100644 index 00000000..e3d31dd1 --- /dev/null +++ b/lib/commons/next.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class Next extends StatelessWidget { + final Function onPressed; + + Next({@required this.onPressed}); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: SvgPicture.asset('assets/icons/right.svg'), + onPressed: onPressed, + color: GuamColorFamily.grayscaleGray5, + ); + } +} diff --git a/lib/commons/next_button.dart b/lib/commons/next_button.dart new file mode 100644 index 00000000..70c7ef41 --- /dev/null +++ b/lib/commons/next_button.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class NextButton extends StatelessWidget { + final String label; + final Function onTap; + + NextButton({ + @required this.label, + @required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.fromLTRB(5, 60, 5, 20), + child: InkWell( + onTap: onTap, + child: Container( + alignment: Alignment.center, + height: 56, + width: MediaQuery.of(context).size.width * 0.9, + decoration: BoxDecoration( + color: GuamColorFamily.purpleCore, + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + child: Text( + label, + style: TextStyle( + color: GuamColorFamily.grayscaleWhite, + fontSize: 16, + ), + ), + ) + ) + ); + } +} + diff --git a/lib/commons/sub_headings.dart b/lib/commons/sub_headings.dart new file mode 100644 index 00000000..39ee7783 --- /dev/null +++ b/lib/commons/sub_headings.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class SubHeadings extends StatelessWidget { + final String subheading; + final double fontSize; + final Color fontColor; + final String fontFamily; + + SubHeadings(this.subheading, {this.fontSize = 16, this.fontColor = Colors.black, this.fontFamily = GuamFontFamily.SpoqaHanSansNeoRegular}); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.centerLeft, + child: Text( + subheading, + style: TextStyle( + fontSize: fontSize, + color: fontColor, + fontFamily: fontFamily, + ), + ), + ); + } +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 00000000..70240890 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,61 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + // ignore: missing_enum_constant_in_switch + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAYoZLtqIgtE8eLeyNgCoLYIa3f3UYmXDs', + appId: '1:648780047414:android:3fc58c8364ffee5c21a497', + messagingSenderId: '648780047414', + projectId: 'waffle-guam', + storageBucket: 'waffle-guam.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyCkeKrJihj2WMWtlqWxfzL2aQTtNr-ytTg', + appId: '1:648780047414:ios:054210930e5e474921a497', + messagingSenderId: '648780047414', + projectId: 'waffle-guam', + storageBucket: 'waffle-guam.appspot.com', + androidClientId: '648780047414-d2fbe0qcdiugckgq8stcqvgef89ie6a9.apps.googleusercontent.com', + iosClientId: '648780047414-erb4hojdlnk09dk8nvcjjgdps2q3cc8n.apps.googleusercontent.com', + iosBundleId: 'com.wafflestudio.guam-community', + ); +} diff --git a/lib/helpers/decode_ko.dart b/lib/helpers/decode_ko.dart new file mode 100644 index 00000000..986c3e04 --- /dev/null +++ b/lib/helpers/decode_ko.dart @@ -0,0 +1,4 @@ +import 'dart:convert'; + +// Translate json response to utf-8 json +String decodeKo(dynamic json) => utf8.decode(json.bodyBytes); diff --git a/lib/helpers/http_request.dart b/lib/helpers/http_request.dart new file mode 100644 index 00000000..5ad8814e --- /dev/null +++ b/lib/helpers/http_request.dart @@ -0,0 +1,147 @@ +import 'dart:convert'; +import 'dart:io' show File; +import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; +import 'package:path/path.dart' as p; +import '../mixins/toast.dart'; + +class HttpRequest with Toast { + // TODO: Remove immigrationAuthority & @isHttps param after immigrationAuthority is merged to gateway + + final String gatewayAuthority = "guam.jon-snow-korea.com"; + final String immigrationAuthority = "guam-immigration.jon-snow-korea.com"; + final String s3BaseAuthority = "https://guam.s3.ap-northeast-2.amazonaws.com/"; + + Future get({bool isHttps = true, String authority, String path, dynamic queryParams, String authToken}) async { + try { + final uri = isHttps + ? Uri.https(authority ?? gatewayAuthority, path, queryParams) + : Uri.http(authority ?? gatewayAuthority, path, queryParams); + + final response = await http.get( + uri, + headers: {'Content-Type': "application/json", 'Authorization': 'Bearer $authToken'}, + ); + + return response; + } catch (e) { + print("Error on GET request: $e"); + showToast(success: false, msg: e); + } + } + + Future post({bool isHttps = true, String authority, String path, String authToken, dynamic queryParams, dynamic body}) async { + try { + final uri = isHttps + ? Uri.https(authority ?? gatewayAuthority, path, queryParams) + : Uri.http(authority ?? gatewayAuthority, path, queryParams); + + final response = await http.post( + uri, + headers: {'Content-Type': "application/json", 'Authorization': 'Bearer $authToken'}, + body: jsonEncode(body), + ); + + return response; + } catch (e) { + print("Error on POST request: $e"); + showToast(success: false, msg: e); + } + } + + // pluralImage boolean 으로 "images" or "image" 구분. + Future postMultipart({bool isHttps = true, String authority, String path, String authToken, Map fields, List files, bool pluralImages=true}) async { + try { + final uri = isHttps + ? Uri.https(authority ?? gatewayAuthority, path) + : Uri.http(authority ?? gatewayAuthority, path); + + http.MultipartRequest request = http.MultipartRequest("POST", uri); + request.headers['Authorization'] = 'Bearer $authToken'; + fields.entries.forEach((e) => request.fields[e.key] = e.value); + + if (files != null) + files.forEach((e) async { + final multipartFile = http.MultipartFile( + pluralImages ? "images" : "image", + e.readAsBytes().asStream(), + e.lengthSync(), + filename: e.path.split("/").last, + contentType: MediaType("image", "${p.extension(e.path)}") + ); + request.files.add(multipartFile); + }); + http.Response response = await http.Response.fromStream(await request.send()); + return response; + } catch (e) { + print("Error on POST Multipart request: $e"); + showToast(success: false, msg: e); + } + } + + Future patch({bool isHttps = true, String authority, String path, String authToken, dynamic body}) async { + try { + final uri = isHttps + ? Uri.https(authority ?? gatewayAuthority, path) + : Uri.http(authority ?? gatewayAuthority, path); + + final response = await http.patch( + uri, + headers: {'Content-Type': "application/json", 'Authorization': 'Bearer $authToken'}, + body: jsonEncode(body), + ); + + return response; + } catch (e) { + print("Error on PUT request: $e"); + showToast(success: false, msg: e); + } + } + + Future patchMultipart({bool isHttps = true, String authority, String path, String authToken, Map fields, List files}) async { + try { + final uri = isHttps + ? Uri.https(authority ?? gatewayAuthority, path) + : Uri.http(authority ?? gatewayAuthority, path); + + http.MultipartRequest request = http.MultipartRequest("PATCH", uri); + request.headers['Authorization'] = 'Bearer $authToken'; + fields.entries.forEach((e) => request.fields[e.key] = e.value); + + if (files != null) + files.forEach((e) async { + final multipartFile = http.MultipartFile( + "profileImage", + e.readAsBytes().asStream(), + e.lengthSync(), + filename: e.path.split("/").last, + contentType: MediaType("image", "${p.extension(e.path)}"), + ); + request.files.add(multipartFile); + }); + http.Response response = await http.Response.fromStream(await request.send()); + return response; + } catch (e) { + print("Error on PATCH Multipart request: ${e.toString()}"); + showToast(success: false, msg: e.toString()); + } + } + + Future delete({bool isHttps = true, String authority, String path, dynamic queryParams, String authToken}) async { + try { + final uri = isHttps + ? Uri.https(authority ?? gatewayAuthority, path, queryParams) + : Uri.http(authority ?? gatewayAuthority, path, queryParams); + + final response = await http.delete( + uri, + headers: {'Content-Type': "application/json", 'Authorization': 'Bearer $authToken'}, + ); + + return response; + } catch (e) { + print("Error on DELETE request: $e"); + showToast(success: false, msg: e); + } + } +} diff --git a/lib/helpers/pick_image.dart b/lib/helpers/pick_image.dart new file mode 100644 index 00000000..f156e0b7 --- /dev/null +++ b/lib/helpers/pick_image.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; + +Future pickImage({@required type}) async { + final ImagePicker picker = ImagePicker(); + switch (type) { + case 'gallery': + return await picker.getImage(source: ImageSource.gallery, imageQuality: 30); + break; + case 'camera': + return await picker.getImage(source: ImageSource.camera, imageQuality: 30); + break; + default: + print("We can pick images from either gallery or camera."); + return null; + } +} diff --git a/lib/helpers/svg_provider.dart b/lib/helpers/svg_provider.dart new file mode 100644 index 00000000..6f614520 --- /dev/null +++ b/lib/helpers/svg_provider.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui show Image, Picture; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_svg/flutter_svg.dart'; + +/// Rasterizes given svg picture for displaying in [Image] widget: +/// +/// ```dart +/// Image( +/// width: 32, +/// height: 32, +/// image: Svg('assets/my_icon.svg'), +/// ) +/// ``` +class SvgProvider extends ImageProvider { + /// Path to svg file or asset + final String path; + + /// Size in logical pixels to render. + /// Useful for [DecorationImage]. + /// If not specified, will use size from [Image]. + /// If [Image] not specifies size too, will use default size 100x100. + final Size size; // nullable + + /// Color to tint the SVG + final Color color; + + /// Defines if the SVG is loaded from assets or read from a file. + final bool isAsset; + + /// Width and height can also be specified from [Image] constrictor. + /// Default size is 100x100 logical pixels. + /// Different size can be specified in [Image] parameters + const SvgProvider(this.path, {this.size, this.color, this.isAsset: true}); + + @override + Future obtainKey(ImageConfiguration configuration) { + final double logicWidth = size?.width ?? configuration.size?.width ?? 100; + final double logicHeight = size?.height ?? configuration.size?.width ?? 100; + final double scale = configuration.devicePixelRatio ?? 1.0; + final Color color = this.color ?? Colors.transparent; + + return SynchronousFuture( + SvgImageKey( + assetName: path, + pixelWidth: (logicWidth * scale).round(), + pixelHeight: (logicHeight * scale).round(), + scale: scale, + color: color), + ); + } + + @override + ImageStreamCompleter load(SvgImageKey key, nil) { + return OneFrameImageStreamCompleter( + _loadAsync(key, isAsset), + ); + } + + static Future _loadAsync(SvgImageKey key, bool isAsset) async { + String rawSvg = (isAsset) + ? await rootBundle.loadString(key.assetName) + : await File(key.assetName).readAsString(); + + final DrawableRoot svgRoot = await svg.fromSvgString(rawSvg, key.assetName); + final ui.Picture picture = svgRoot.toPicture( + size: Size( + key.pixelWidth.toDouble(), + key.pixelHeight.toDouble(), + ), + clipToViewBox: false, + colorFilter: + ColorFilter.mode(key.color ?? Colors.transparent, BlendMode.srcATop), + ); + final ui.Image image = await picture.toImage( + key.pixelWidth, + key.pixelHeight, + ); + return ImageInfo( + image: image, + scale: key.scale, + ); + } + + // Note: == and hashCode not overrided as changes in properties + // (width, height and scale) are not observable from the here. + // [SvgImageKey] instances will be compared instead. + + @override + String toString() => '$runtimeType(${describeIdentity(path)})'; +} + +@immutable +class SvgImageKey { + const SvgImageKey({ + @required this.assetName, + @required this.pixelWidth, + @required this.pixelHeight, + @required this.scale, + this.color, + }); + + /// Path to svg asset. + final String assetName; + + /// Width in physical pixels. + /// Used when raterizing. + final int pixelWidth; + + /// Height in physical pixels. + /// Used when raterizing. + final int pixelHeight; + + /// Color to tint the SVG + final Color color; + + /// Used to calculate logical size from physical, i.e. + /// logicalWidth = [pixelWidth] / [scale], + /// logicalHeight = [pixelHeight] / [scale]. + /// Should be equal to [MediaQueryData.devicePixelRatio]. + final double scale; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is SvgImageKey && + other.assetName == assetName && + other.pixelWidth == pixelWidth && + other.pixelHeight == pixelHeight && + other.scale == scale; + } + + @override + int get hashCode => hashValues(assetName, pixelWidth, pixelHeight, scale); + + @override + String toString() => '${objectRuntimeType(this, 'SvgImageKey')}' + '(assetName: "$assetName", pixelWidth: $pixelWidth, pixelHeight: $pixelHeight, scale: $scale)'; +} diff --git a/lib/main.dart b/lib/main.dart index b9bfc389..ba7e04c6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,113 +1,72 @@ +import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; +import 'package:guam_community_client/screens/app/splash/splash_screen.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; +import 'providers/user_auth/authenticate.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'firebase_options.dart'; +import './screens/user_auth/auth.dart'; + +void main() async { + // Returns an instance of the WidgetsBinding, creating and initializing it if necessary. + // WidgetsBinding provides interaction w/ Flutter Engine. + WidgetsFlutterBinding.ensureInitialized(); + + // Use platform channels to call native code to initialize Firebase. + // Thus, 'async' main() and placed next to ensureInitialized() + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); -void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { - // This widget is the root of your application. @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), + return FutureBuilder( + // future: Future.delayed(Duration(milliseconds: 1500)), + // 개발시에 주석처리하면 hot reload 시 초기화 안시킬 수 있음. + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return MaterialApp( + initialRoute: '/', + debugShowCheckedModeBanner: false, + routes: { + '/': (context) => SplashScreen(), + }, + ); // 초기 로딩 시 Splash Screen + } else if (snapshot.hasError) { + return MaterialApp(home: ErrorScreen()); // 초기 로딩 에러 시 Error Screen + } else { + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Authenticate()), + ], + child: MaterialApp( + initialRoute: '/', + routes: { + '/': (context) => Auth(), + }, + builder: BotToastInit(), + debugShowCheckedModeBanner: false, + theme: ThemeData( + primaryColor: GuamColorFamily.purpleCore, + fontFamily: GuamFontFamily.SpoqaHanSansNeoMedium, + ), + ), + ); + } + }, ); } } -class MyHomePage extends StatefulWidget { - MyHomePage({Key? key, required this.title}) : super(key: key); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); - } - +class ErrorScreen extends StatelessWidget { @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, - ), - ], - ), - ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. - ); + return Text("Error Page"); } } diff --git a/lib/mixins/toast.dart b/lib/mixins/toast.dart new file mode 100644 index 00000000..5fd791b2 --- /dev/null +++ b/lib/mixins/toast.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:bot_toast/bot_toast.dart'; + +mixin Toast { + void showToast ({@required bool success, @required String msg}) { + BotToast.showText( + text: msg ?? (success ? "완료되었습니다" : "오류가 발생했습니다"), + contentColor: success + ? Color.fromRGBO(85, 88, 255, 1) + : Color.fromRGBO(235, 87, 87, 1), + borderRadius: BorderRadius.circular(30), + textStyle: TextStyle( + fontSize: 14, + color: Colors.white + ) + ); + } +} diff --git a/lib/models/boards/category.dart b/lib/models/boards/category.dart new file mode 100644 index 00000000..7d4a3488 --- /dev/null +++ b/lib/models/boards/category.dart @@ -0,0 +1,16 @@ +/// category.title 은 게시글 수정 시 non-final 로 정의 +class Category { + final int postId; + final int categoryId; + String title; + + Category({this.postId, this.categoryId, this.title}); + + factory Category.fromJson(dynamic json) { + return Category( + postId: json['postId'], + categoryId: json['categoryId'], + title: json['title'], + ); + } +} diff --git a/lib/models/boards/comment.dart b/lib/models/boards/comment.dart new file mode 100644 index 00000000..479b7c54 --- /dev/null +++ b/lib/models/boards/comment.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; + +class Comment extends ChangeNotifier { + final int id; + final int postId; + final Profile profile; + final String content; + final bool isMine; + final bool isLiked; + final List imagePaths; /// 임시 방편 (추후 pictures 활용 예정) + final int likeCount; + final String createdAt; + + Comment({ + this.id, + this.postId, + this.profile, + this.content, + this.isMine, + this.isLiked, + this.imagePaths, + this.likeCount, + this.createdAt, + }); + + factory Comment.fromJson(Map json) { + Profile profile; + + if (json['user'] != null) { + profile = Profile.fromJson(json['user']); + } + + return Comment( + id: json['id'], + postId: json['postId'], + profile: profile, + content: json['content'], + isMine: json['isMine'], + isLiked: json['isLiked'], + imagePaths: json['imagePaths'], + likeCount: json['likeCount'], + createdAt: json['createdAt'], + ); + } +} diff --git a/lib/models/boards/post.dart b/lib/models/boards/post.dart new file mode 100644 index 00000000..c0d4f733 --- /dev/null +++ b/lib/models/boards/post.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guam_community_client/commons/functions_category_boardType.dart'; +import 'package:guam_community_client/models/boards/category.dart' as Category; +import 'package:guam_community_client/models/boards/comment.dart'; +import '../profiles/profile.dart'; + +/// title, content, boardType, category는 게시글 수정 시 non-final로 정의 +class Post extends ChangeNotifier { + final int id; + final int boardId; + String boardType; // ex) 익명, 홍보, 정보공유 게시판 + final Profile profile; + String title; + String content; + Category.Category category; // ex) 데이터분석, 개발, 디자인 + final List imagePaths; // 서버 수정 전까지 쓰이는 임시방편 속성 + final List comments; + final int likeCount; + final int commentCount; + final int scrapCount; + final bool isMine; + final bool isLiked; + final bool isScrapped; + final String createdAt; + + Post({ + this.id, + this.profile, + this.boardId, + this.boardType, + this.title, + this.content, + this.category, + this.imagePaths, + this.comments, + this.likeCount, + this.commentCount, + this.scrapCount, + this.isMine, + this.isLiked, + this.isScrapped, + this.createdAt, + }); + + /// Json Encoding for filter value comparison in search tab + Map toJson() { + return { + 'id': this.id, + 'like': this.likeCount, + 'createdAt': this.createdAt, + }; + } + + factory Post.fromJson(Map json) { + Profile profile; + Category.Category category; + List comments; + + /** + * Server에서 profile 대신 user라는 이름으로 주고 있는데, + * 클라를 다 user로 고치든 서버에서 profile로 받아오든 할 것. + **/ + if (json['user'] != null) { + profile = Profile.fromJson(json['user']); + } + + if (json['category'] != null) { + category = Category.Category.fromJson(json['category']); + } + + if (json['comments'] != null) { + comments = [...json['comments'].map((comment) => Comment.fromJson({ + 'id': comment['id'], + 'user': comment['user'], + 'content': comment['content'], + 'imagePaths': json['imagePaths'], + 'isLiked': comment['isLiked'], + 'likeCount': comment['likeCount'], + 'createdAt': json['createdAt'], + }))]; + } + + return Post( + id: json['id'], + profile: profile, + boardId: json['boardId'], + boardType: transferBoardId(json['boardId']), + title: json['title'], + content: json['content'], + category: category, + imagePaths: json['imagePaths'], + comments: comments, + likeCount: json['likeCount'], + commentCount: json['commentCount'], + scrapCount: json['scrapCount'], + isMine: json['isMine'], + isLiked: json['isLiked'], + isScrapped: json['isScrapped'], + createdAt: json['createdAt'], + ); + } +} diff --git a/lib/models/filter.dart b/lib/models/filter.dart new file mode 100644 index 00000000..1b90a1b6 --- /dev/null +++ b/lib/models/filter.dart @@ -0,0 +1,6 @@ +class Filter { + final String key; + final String label; + + Filter({this.key, this.label}); +} diff --git a/lib/models/messages/message.dart b/lib/models/messages/message.dart new file mode 100644 index 00000000..f70a2019 --- /dev/null +++ b/lib/models/messages/message.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; + +class Message extends ChangeNotifier { + final int id; + final int sentBy; + final int sentTo; + final bool isRead; + final String text; + final String imagePath; + final String createdAt; + + Message({ + this.id, + this.sentBy, + this.sentTo, + this.isRead, + this.text, + this.imagePath, + this.createdAt, + }); + + factory Message.fromJson(Map json) { + return Message( + id: json['id'], + sentBy: json['sentBy'], + sentTo: json['sentTo'], + isRead: json['isRead'], + text: json['text'], + imagePath: json['imagePath'], + createdAt: json['createdAt'], + ); + } +} diff --git a/lib/models/messages/message_box.dart b/lib/models/messages/message_box.dart new file mode 100644 index 00000000..2dfcfe88 --- /dev/null +++ b/lib/models/messages/message_box.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guam_community_client/models/messages/message.dart' as Message; +import '../profiles/profile.dart'; + +class MessageBox extends ChangeNotifier { + final Profile otherProfile; // 상대방 프로필 + final Message.Message lastLetter; // 가장 마지막으로 보낸 메시지 (사진만 있는 경우 '사진'으로 보냄) + + MessageBox({ + this.otherProfile, + this.lastLetter, + }); + + factory MessageBox.fromJson(Map json) { + Profile otherProfile; + Message.Message lastLetter; + + if (json['pair'] != null) { + otherProfile = Profile.fromJson(json['pair']); + } + + if (json['lastLetter'] != null) { + lastLetter = Message.Message.fromJson(json['lastLetter']); + } + + return MessageBox( + otherProfile: otherProfile, + lastLetter: lastLetter, + ); + } +} diff --git a/lib/models/notification.dart b/lib/models/notification.dart new file mode 100644 index 00000000..4b19d4d4 --- /dev/null +++ b/lib/models/notification.dart @@ -0,0 +1,42 @@ +import 'package:guam_community_client/models/profiles/profile.dart'; + +class Notification { + final int id; + final int userId; + final String kind; + final String body; + final String linkUrl; + final Profile writer; + final bool isRead; + final String createdAt; + + Notification({ + this.id, + this.userId, + this.kind, + this.body, + this.linkUrl, + this.writer, + this.isRead, + this.createdAt, + }); + + factory Notification.fromJson(dynamic json) { + Profile writer; + + if (json['writer'] != null) { + writer = Profile.fromJson(json['writer']); + } + + return Notification( + id: json['id'], + userId: json['userId'], + kind: json['kind'], + body: json['body'], + linkUrl: json['linkUrl'], + writer: writer, + isRead: json['isRead'], + createdAt: json['createdAt'], + ); + } +} diff --git a/lib/models/picture.dart b/lib/models/picture.dart new file mode 100644 index 00000000..9607901a --- /dev/null +++ b/lib/models/picture.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; + +class Picture { + final int id; + final String urlPath; + + Picture({this.id, @required this.urlPath}); + + factory Picture.fromJson(dynamic json) { + return Picture( + id: json['id'], + urlPath: json['urlPath'], + ); + } +} diff --git a/lib/models/profiles/interest.dart b/lib/models/profiles/interest.dart new file mode 100644 index 00000000..2654e0a8 --- /dev/null +++ b/lib/models/profiles/interest.dart @@ -0,0 +1,5 @@ +class Interest { + final String name; + + Interest({this.name}); +} diff --git a/lib/models/profiles/profile.dart b/lib/models/profiles/profile.dart new file mode 100644 index 00000000..da11edef --- /dev/null +++ b/lib/models/profiles/profile.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guam_community_client/models/boards/post.dart'; +import 'package:guam_community_client/models/boards/comment.dart'; +import './interest.dart'; + +class Profile extends ChangeNotifier { + final int id; + final String nickname; + final String intro; + final String email; + final String profileImg; + final String githubId; + final String blogUrl; + final bool profileSet; + final List interests; + + // TODO: Remove from Profile attributes. Directly request at the view. + final List myPosts; + final List scrappedPosts; + final List myComments; + + Profile({ + this.id, + this.nickname, + this.intro, + this.email, + this.profileImg, + this.githubId, + this.blogUrl, + this.profileSet, + this.interests, + this.myPosts, + this.scrappedPosts, + this.myComments, + }); + + factory Profile.fromJson(Map json) { + List interests; + List myPosts; + List scrappedPosts; + List myComments; + + if (json["interests"] != null) { + interests = [...json['interests'].map((i) => Interest(name: i['name']))]; + } + + if (json['myPosts'] != null) { + myPosts = [...json['myPosts'].map((post) => Post.fromJson({ + 'id': post['id'], + 'title': post['title'], + 'content': post['content'], + 'like': post['like'], + 'scrap': post['scrap'], + 'createdAt': post['createdAt'], + }))]; + } + + if (json['scrappedPosts'] != null) { + scrappedPosts = [...json['scrappedPosts'].map((post) => Post.fromJson({ + 'id': post['id'], + 'title': post['title'], + 'content': post['content'], + 'like': post['like'], + 'scrap': post['scrap'], + 'createdAt': post['createdAt'], + }))]; + } + + if (json['myComments'] != null) { + myComments = [...json['myComments'].map((comment) => Comment.fromJson({ + 'id': comment['id'], + 'comment': comment['comment'], + 'like': comment['like'], + 'createdAt': comment['createdAt'], + }))]; + } + + return Profile( + id: json['id'], + nickname: json['nickname'], + intro: json['introduction'], + email: json['email'], + profileImg: json['profileImage'], + githubId: json['githubId'], + blogUrl: json['blogUrl'], + profileSet: json['profileSet'], + interests: interests, + myPosts: myPosts, + scrappedPosts: scrappedPosts, + myComments: myComments, + ); + } + + /// flutter_mentions needs to have data as follows: + Map toJson() => { + 'id': id.toString(), + 'display': nickname, + 'photo': profileImg ?? null + }; +} diff --git a/lib/providers/home/home_provider.dart b/lib/providers/home/home_provider.dart new file mode 100644 index 00000000..5eca888d --- /dev/null +++ b/lib/providers/home/home_provider.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class HomeProvider with ChangeNotifier { + int _idx = 0; + + final List> bottomNavItems = [ + { + 'label': '홈', + 'selected_icon': SvgPicture.asset( + 'assets/icons/home_filled.svg', + color: GuamColorFamily.purpleCore, + ), + 'unselected_icon': SvgPicture.asset( + 'assets/icons/home_outlined.svg', + color: GuamColorFamily.grayscaleGray4, + ), + }, + { + 'label': '검색', + 'selected_icon': SvgPicture.asset( + 'assets/icons/search.svg', + color: GuamColorFamily.purpleCore, + ), + 'unselected_icon': SvgPicture.asset( + 'assets/icons/search.svg', + color: GuamColorFamily.grayscaleGray4, + ), + }, + { + 'label': '알림', + 'selected_icon': SvgPicture.asset( + 'assets/icons/notification_filled_default.svg', + color: GuamColorFamily.purpleCore, + ), + 'unselected_icon': SvgPicture.asset( + 'assets/icons/notification_outlined_default.svg', + color: GuamColorFamily.grayscaleGray4, + ), + }, + { + 'label': '프로필', + 'selected_icon': SvgPicture.asset( + 'assets/icons/profile_filled.svg', + color: GuamColorFamily.purpleCore, + ), + 'unselected_icon': SvgPicture.asset( + 'assets/icons/profile_outlined.svg', + color: GuamColorFamily.grayscaleGray4, + ), + }, + ]; + + get idx => _idx; + + set idx(val) { + _idx = val; + notifyListeners(); + } + + Map get bottomNavItem => bottomNavItems[_idx]; +} diff --git a/lib/providers/messages/messages.dart b/lib/providers/messages/messages.dart new file mode 100644 index 00000000..3bf4a777 --- /dev/null +++ b/lib/providers/messages/messages.dart @@ -0,0 +1,170 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guam_community_client/helpers/http_request.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/models/messages/message.dart'; +import 'package:guam_community_client/models/messages/message_box.dart'; +import '../../helpers/decode_ko.dart'; +import '../user_auth/authenticate.dart'; + +class Messages extends ChangeNotifier with Toast { + Authenticate _authProvider; + List _messageBoxes; + List _messages; + bool loading = false; + + Messages(Authenticate authProvider) { + _authProvider = authProvider; + fetchMessageBoxes(); + } + + List get messageBoxes => _messageBoxes; + List get messages => _messages; + + Future> fetchMessageBoxes() async { + loading = true; + try { + String authToken = await _authProvider.getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest() + .get( + path: "community/api/v1/letters", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final List jsonList = json.decode(jsonUtf8)["letterBoxes"]; + _messageBoxes = jsonList.map((e) => MessageBox.fromJson(e)).toList(); + loading = false; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 400: msg = '메시지를 불러올 수 없습니다.'; break; + case 401: msg = '열람 권한이 없습니다.'; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return _messageBoxes; + } + + Future deleteMessageBox(int otherProfileId) async { + bool successful = false; + loading = true; + + try { + String authToken = await _authProvider.getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest() + .delete( + path: "community/api/v1/letters/$otherProfileId", + authToken: authToken, + ).then((response) { + if (response.statusCode == 200) { + showToast(success: true, msg: '쪽지함을 삭제했습니다.'); + successful = true; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = '삭제 권한이 없습니다.'; break; + case 404: msg = '비활성화된 유저입니다.'; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future> getMessages(int otherProfileId) async { + loading = true; + try { + String authToken = await _authProvider.getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest() + .get( + authToken: authToken, + path: "community/api/v1/letters/$otherProfileId", + ).then((response) { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final List jsonList = json.decode(jsonUtf8)["letters"]; + _messages = jsonList.map((e) => Message.fromJson(e)).toList(); + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = '접근 권한이 없습니다.'; break; + case 403: msg = '상대방으로부터 차단되었습니다.'; break; + case 404: msg = '존재하지 않는 쪽지함입니다.'; break; + } + showToast(success: false, msg: msg); + } + }); + } + } catch (e) { + print(e); + } finally { + loading = false; + notifyListeners(); + } + return _messages; + } + + Future sendMessage({Map fields, dynamic files}) async { + bool successful = false; + loading = true; + + try { + String authToken = await _authProvider.getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest() + .postMultipart( + pluralImages: false, // pluralImage boolean 으로 "images" or "image" 구분 + path: "community/api/v1/letters", + authToken: authToken, + fields: fields, + files: files, + ).then((response) async { + if (response.statusCode == 200) { + successful = true; + loading = false; + showToast(success: true, msg: '쪽지를 발송했습니다.'); + } else { + String msg = "알 수 없는 오류가 발생했습니다."; + switch (response.statusCode) { + case 400: msg = "메시지를 입력해주세요."; break; + case 401: msg = "권한이 없습니다."; break; + case 403: msg = "쪽지를 보낼 수 없는 상대입니다."; break; + case 404: msg = "존재하지 않는 사용자입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } +} diff --git a/lib/providers/notifications/notifications.dart b/lib/providers/notifications/notifications.dart new file mode 100644 index 00000000..f07f6c85 --- /dev/null +++ b/lib/providers/notifications/notifications.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guam_community_client/helpers/http_request.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/models/notification.dart' as Notification; + +import '../../helpers/decode_ko.dart'; +import '../user_auth/authenticate.dart'; + +class Notifications extends ChangeNotifier with Toast { + bool loading = false; + bool _hasNext; + Authenticate _authProvider; + List _notifications; + List _newNotifications; + + Notifications(Authenticate authProvider) { + _authProvider = authProvider; + fetchNotifications(); + } + + bool get hasNext => _hasNext; + List get notifications => _notifications; + List get newNotifications => _newNotifications; + + Future fetchNotifications({int page=0, int size=20}) async { + loading = true; + + try { + String authToken = await _authProvider.getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest() + .get( + path: "community/api/v1/push", + queryParams: { + "page": page.toString(), + "size": size.toString(), + }, + authToken: await _authProvider.getFirebaseIdToken(), + ).then((response) async { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final List jsonList = json.decode(jsonUtf8)["content"]; + _hasNext = json.decode(jsonUtf8)["hasNext"]; + _notifications = jsonList.map((e) => Notification.Notification.fromJson(e)).toList(); + + loading = false; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 400: msg = '정보를 모두 입력해주세요.'; break; + case 401: msg = '열람 권한이 없습니다.'; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + } + + /// For Pagination in BoardsFeed Widget using _loadMore() + Future addNotifications({int page=1, int size=20}) async { + loading = true; + try { + await HttpRequest() + .get( + path: "community/api/v1/push", + queryParams: { + "page": page.toString(), + "size": size.toString(), + }, + authToken: await _authProvider.getFirebaseIdToken(), + ).then((response) async { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final List jsonList = json.decode(jsonUtf8)["content"]; + _hasNext = json.decode(jsonUtf8)["hasNext"]; + _newNotifications = jsonList.map((e) => Notification.Notification.fromJson(e)).toList(); + + loading = false; + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: '더 이상 알림을 불러올 수 없습니다.'); + } + }); + loading = false; + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return _newNotifications; + } + + Future readNotifications({int userId, List pushEventIds}) async { + loading = true; + + try { + String authToken = await _authProvider.getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest().post( + path: "community/api/v1/push/read", + queryParams: { + "userId": userId.toString(), + "pushEventIds": pushEventIds, + }, + authToken: await _authProvider.getFirebaseIdToken(), + ).then((response) async { + if (response.statusCode == 200) { + loading = false; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 400: msg = '정보를 모두 입력해주세요.'; break; + case 401: msg = '열람 권한이 없습니다.'; break; + case 404: msg = '존재하지 않는 알림입니다.'; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + } +} diff --git a/lib/providers/posts/posts.dart b/lib/providers/posts/posts.dart new file mode 100644 index 00000000..bc84ebe5 --- /dev/null +++ b/lib/providers/posts/posts.dart @@ -0,0 +1,580 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/providers/search/search.dart'; +import 'package:flutter/foundation.dart'; +import 'package:guam_community_client/models/boards/comment.dart'; +import '../../helpers/decode_ko.dart'; +import '../../helpers/http_request.dart'; +import '../../models/boards/post.dart'; +import '../../models/filter.dart'; +import '../user_auth/authenticate.dart'; + +class Posts extends ChangeNotifier with Toast { + Authenticate _authProvider; + Post _post; + bool _hasNext; + List _posts; + List _newPosts; + List _comments; + int _boardId = 0; // default : 피드게시판 + int _createdPostId; + bool loading = false; + + Posts(Authenticate authProvider) { + _authProvider = authProvider; + fetchPosts(boardId); + } + + Post get post => _post; + bool get hasNext => _hasNext; + int get boardId => _boardId; + int get createdPostId => _createdPostId; + List get posts => _posts; + List get newPosts => _newPosts; + List get comments => _comments; + + /// ==== Posts ==== + Future fetchPosts(int boardId) async { + print(await _authProvider.getFirebaseIdToken(),); + loading = true; + try { + await HttpRequest() + .get( + path: "community/api/v1/posts", + queryParams: {"boardId": boardId.toString()}, + authToken: await _authProvider.getFirebaseIdToken(), + ).then((response) async { + /// 현재 게시판 위치 저장해두기 (게시판 reload 시 사용) + _boardId = boardId; + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final List jsonList = json.decode(jsonUtf8)["content"]; + _hasNext = json.decode(jsonUtf8)["hasNext"]; + _posts = jsonList.map((e) => Post.fromJson(e)).toList(); + + // Default search with first filter + sortSearchedPosts(Search.filters.first); + loading = false; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 400: msg = '정보를 모두 입력해주세요.'; break; + case 401: msg = '열람 권한이 없습니다.'; break; + case 404: msg = '존재하지 않는 게시판입니다.'; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return _posts; + } + + /// For Pagination in BoardsFeed Widget using _loadMore() + Future addPosts({int boardId, int beforePostId}) async { + loading = true; + try { + await HttpRequest() + .get( + path: "community/api/v1/posts", + queryParams: { + "boardId": boardId.toString(), + "beforePostId": beforePostId.toString(), + }, + authToken: await _authProvider.getFirebaseIdToken(), + ).then((response) async { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final List jsonList = json.decode(jsonUtf8)["content"]; + _hasNext = json.decode(jsonUtf8)["hasNext"]; + _newPosts = jsonList.map((e) => Post.fromJson(e)).toList(); + loading = false; + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: '더 이상 게시글을 불러올 수 없습니다.'); + } + }); + loading = false; + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return _newPosts; + } + + void sortSearchedPosts(Filter f) { + _posts.sort((a, b) => b.toJson()[f.key].compareTo(a.toJson()[f.key])); + notifyListeners(); + } + + Future createPost({Map fields, dynamic files}) async { + bool successful = false; + loading = true; + + try { + String authToken = await _authProvider.getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest() + .postMultipart( + path: "community/api/v1/posts", + authToken: authToken, + fields: fields, + files: files, + ).then((response) async { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final Map jsonData = json.decode(jsonUtf8); + _createdPostId = jsonData['postId']; + successful = true; + loading = false; + showToast(success: true, msg: '게시글을 작성했습니다.'); + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 400: msg = '정보를 모두 입력해주세요.'; break; + case 401: msg = '글쓰기 권한이 없습니다.'; break; + case 404: msg = '정보를 모두 입력해주세요.'; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future getPost(int postId) async { + loading = true; + try { + String authToken = await _authProvider.getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest() + .get( + authToken: authToken, + path: "community/api/v1/posts/$postId", + ).then((response) { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final Map jsonData = json.decode(jsonUtf8); + _post = Post.fromJson(jsonData); + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = '접근 권한이 없습니다.'; break; + case 404: msg = '존재하지 않는 게시글입니다.'; break; + } + showToast(success: false, msg: msg); + } + }); + } + } catch (e) { + print(e); + } finally { + loading = false; + notifyListeners(); + } + return _post; + } + + Future editPost({int postId, Map body}) async { + bool successful = false; + loading = true; + + try { + String authToken = await _authProvider.getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest() + .patch( + path: "community/api/v1/posts/$postId", + authToken: authToken, + body: body, + ).then((response) async { + if (response.statusCode == 200) { + successful = true; + loading = false; + final jsonUtf8 = decodeKo(response); + final Map jsonData = json.decode(jsonUtf8); + await getPost(jsonData['postId']); + showToast(success: true, msg: '게시글을 수정했습니다.'); + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 400: msg = '정보를 모두 입력해주세요.'; break; + case 401: msg = '수정 권한이 없습니다.'; break; + case 404: msg = '정보를 모두 입력해주세요.'; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future deletePost(int postId) async { + bool successful = false; + loading = true; + + try { + String authToken = await _authProvider.getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest() + .delete( + path: "community/api/v1/posts/$postId", + authToken: authToken, + ).then((response) { + print(response.statusCode); + if (response.statusCode == 200) { + showToast(success: true, msg: '게시글을 삭제했습니다.'); + successful = true; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = '삭제 권한이 없습니다.'; break; + case 404: msg = '존재하지 않는 게시글입니다.'; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future likePost({int postId}) async { + loading = true; + bool successful = false; + try { + String authToken = await _authProvider.getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest() + .post( + path: "community/api/v1/posts/$postId/likes", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + loading = false; + successful = true; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = "권한이 없습니다."; break; + case 404: msg = "존재하지 않는 게시글입니다."; break; + case 409: msg = "이미 '좋아요'한 글입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future unlikePost({int postId}) async { + loading = true; + bool successful = false; + try { + String authToken = await _authProvider.getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest() + .delete( + path: "community/api/v1/posts/$postId/likes", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + loading = false; + successful = true; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = "권한이 없습니다."; break; + case 404: msg = "존재하지 않는 게시글입니다."; break; + case 409: msg = "이미 '좋아요' 취소한 글입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future scrapPost({int postId}) async { + loading = true; + bool successful = false; + try { + String authToken = await _authProvider.getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest() + .post( + path: "community/api/v1/posts/$postId/scraps", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + loading = false; + successful = true; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = "권한이 없습니다."; break; + case 404: msg = "존재하지 않는 게시글입니다."; break; + case 409: msg = "이미 스크랩한 글입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future unscrapPost({int postId}) async { + loading = true; + bool successful = false; + try { + String authToken = await _authProvider.getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest() + .delete( + path: "community/api/v1/posts/$postId/scraps", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + loading = false; + successful = true; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = "권한이 없습니다."; break; + case 404: msg = "존재하지 않는 게시글입니다."; break; + case 409: msg = "이미 스크랩 취소한 글입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + /// ==== Comments ==== + Future fetchComments(int postId) async { + List comments; + try { + loading = true; + await HttpRequest() + .get( + path: "community/api/v1/posts/$postId/comments", + authToken: await _authProvider.getFirebaseIdToken(), + ).then((response) async { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final List jsonList = json.decode(jsonUtf8)["content"]; + comments = jsonList.map((e) => Comment.fromJson(e)).toList(); + loading = false; + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: err); + } + }); + loading = false; + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return comments; + } + + Future createComment({int postId, Map fields, dynamic files}) async { + bool successful = false; + loading = true; + + try { + String authToken = await _authProvider.getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest() + .postMultipart( + path: "community/api/v1/posts/$postId/comments", + authToken: authToken, + fields: fields, + files: files, + ).then((response) async { + if (response.statusCode == 200) { + successful = true; + loading = false; + showToast(success: true, msg: '댓글을 작성했습니다.'); + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 400: msg = "빈 댓글은 입력할 수 없습니다."; break; + case 401: msg = "권한이 없습니다."; break; + case 404: msg = "존재하지 않는 글입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future deleteComment({int postId, int commentId}) async { + bool successful = false; + loading = true; + + try { + String authToken = await _authProvider.getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest() + .delete( + path: "community/api/v1/posts/$postId/comments/$commentId", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + fetchComments(postId); + showToast(success: true, msg: '댓글을 삭제했습니다.'); + successful = true; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = "권한이 없습니다."; break; + case 404: msg = "존재하지 않는 댓글입니다."; break; + case 409: msg = "이미 삭제된 댓글입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future likeComment({int postId, int commentId}) async { + loading = true; + bool successful = false; + try { + String authToken = await _authProvider.getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest() + .post( + path: "community/api/v1/posts/$postId/comments/$commentId/likes", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + loading = false; + successful = true; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = "권한이 없습니다."; break; + case 404: msg = "존재하지 않는 댓글입니다."; break; + case 409: msg = "이미 '좋아요'한 댓글입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } + + Future unlikeComment({int postId, int commentId}) async { + loading = true; + bool successful = false; + try { + String authToken = await _authProvider.getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest() + .delete( + path: "community/api/v1/posts/$postId/comments/$commentId/likes", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + loading = false; + successful = true; + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = "권한이 없습니다."; break; + case 404: msg = "존재하지 않는 댓글입니다."; break; + case 409: msg = "이미 '좋아요' 취소한 댓글입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + loading = false; + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return successful; + } +} diff --git a/lib/providers/search/search.dart b/lib/providers/search/search.dart new file mode 100644 index 00000000..5760ae46 --- /dev/null +++ b/lib/providers/search/search.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/models/filter.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../helpers/decode_ko.dart'; +import '../../helpers/http_request.dart'; +import '../user_auth/authenticate.dart'; +import '../../models/boards/post.dart'; + +class Search extends ChangeNotifier with Toast { + Authenticate _authProvider; + List searchedPosts = []; + + List history = []; // Recently searched word is at the back + static const String searchHistoryKey = 'search-history'; + static const int maxNHistory = 5; + + static List filters = [ + Filter( + key: 'createdAt', + label: '시간순', + ), + Filter( + key: 'like', + label: '추천순', + ), + ]; + + Search(Authenticate authProvider) { + _authProvider = authProvider; + getHistory(); + } + + bool loading = false; + + bool historyFull() => history.length >= maxNHistory; + + Future getHistory() async { + await SharedPreferences.getInstance() + .then((storage) => history = storage.getStringList(searchHistoryKey) ?? []); + } + + Future saveHistory(String word) async { + try { + if (word.trim() == '') return; + if (historyFull()) history.removeAt(0); + if (!history.contains(word)) history.add(word); + await SharedPreferences.getInstance() + .then((storage) => storage.setStringList(searchHistoryKey, history)); + } catch(e) { + print(e); + } finally { + notifyListeners(); + } + } + + Future removeHistory(String word) async { + try { + history.remove(word); + await SharedPreferences.getInstance() + .then((storage) => storage.setStringList(searchHistoryKey, history)); + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + } + + Future searchPosts({@required String query, BuildContext context}) async { + loading = true; + try { + if (query == null || query.trim() == '') { + searchedPosts.clear(); + return; + } + await HttpRequest() + .get( + path: "community/api/v1/posts", + queryParams: {"keyword": query}, + authToken: await _authProvider.getFirebaseIdToken(), + ).then((response) async { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final List jsonList = json.decode(jsonUtf8)["content"]; + searchedPosts = jsonList.map((e) => Post.fromJson(e)).toList(); + loading = false; + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: err); + } + }); + + // Default search with first filter + sortSearchedPosts(filters[0]); + loading = false; + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + } + + void sortSearchedPosts(Filter f) { + searchedPosts.sort((a, b) => b.toJson()[f.key].compareTo(a.toJson()[f.key])); + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/providers/user_auth/authenticate.dart b/lib/providers/user_auth/authenticate.dart new file mode 100644 index 00000000..ab5baa8f --- /dev/null +++ b/lib/providers/user_auth/authenticate.dart @@ -0,0 +1,263 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../../helpers/http_request.dart'; +import '../../helpers/decode_ko.dart'; +import 'dart:convert'; + +class Authenticate extends ChangeNotifier with Toast { + final _kakaoClientId = "367d8cf339e2ba59376ba647c7135dd2"; + final _kakaoJavascriptClientId = "2edf60d1ebf23061d200cfe4a68a235a"; + + FirebaseAuth auth = FirebaseAuth.instance; + get kakaoClientId => _kakaoClientId; + get kakaoJavascriptClientId => _kakaoJavascriptClientId; + + Profile me; + Profile user; + bool loading = false; + + Authenticate() { + getMyProfile(); + } + + bool userSignedIn() => auth.currentUser != null && me != null; // 로그인 된 유저 존재 여부 + bool profileExists() => me != null && me.profileSet; // 프로필까지 만든 정상 유저인지 여부 + bool isMe(int userId) => me.id == userId; + + void toggleLoading() { + loading = !loading; + notifyListeners(); + } + + Future kakaoSignIn(String kakaoAccessToken) async { + try { + await HttpRequest().get( + isHttps: false, // TODO: remove after immigration heads to gateway + authority: HttpRequest().immigrationAuthority, + path: "/api/v1/user/token", + queryParams: {"kakaoToken": kakaoAccessToken}, + ).then((response) async { + if (response.statusCode == 200) { + final customToken = jsonDecode(response.body)['customToken']; + await auth.signInWithCustomToken(customToken); + await getMyProfile(); + showToast(success: true, msg: "카카오 로그인 성공!"); + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: err); + } + }); + } on FirebaseAuthException { + showToast(success: false, msg: "Firebase 인증에 문제가 발생했습니다."); + } catch (e) { + showToast(success: false, msg: e.message); + } + } + + Future getFirebaseIdToken() async { + String idToken; + try { + User user = auth.currentUser; + idToken = await user.getIdToken(); + } on NoSuchMethodError { + throw new Exception("로그인이 필요합니다."); + } catch (e) { + throw new Exception(e); + } + return idToken; + } + + Future signOut() async { + await auth.signOut(); + showToast(success: true, msg: "로그아웃 되었습니다."); + notifyListeners(); + } + + Future getMyProfile() async { + try { + String authToken = await getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest().get( + path: "community/api/v1/users/me", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final Map jsonData = json.decode(jsonUtf8); + me = Profile.fromJson(jsonData); + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: err); + } + }); + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + } + + Future setProfile({Map fields, dynamic files}) async { + bool successful = false; + + try { + toggleLoading(); + String authToken = await getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest().patchMultipart( + path: "community/api/v1/users/${me.id}", + fields: fields, + files: files, + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + await getMyProfile(); + successful = true; + showToast(success: true, msg: "프로필을 설정했습니다."); + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: err); + } + }); + } + } catch (e) { + print(e); + } finally { + toggleLoading(); + notifyListeners(); + } + return successful; + } + + Future getUserProfile(int userId) async { + try { + String authToken = await getFirebaseIdToken(); + if (authToken.isNotEmpty) { + await HttpRequest().get( + path: "community/api/v1/users/$userId", + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + final jsonUtf8 = decodeKo(response); + final Map jsonData = json.decode(jsonUtf8); + user = Profile.fromJson(jsonData); + // TODO: set fcm token when impl. push notification + // setMyFcmToken(); + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: err); + } + }); + } + } catch (e) { + print(e); + } finally { + notifyListeners(); + } + return user; + } + + Future setInterest({Map body}) async { + bool successful = false; + try { + toggleLoading(); + String authToken = await getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest().post( + path: "community/api/v1/users/${me.id}/interest", + body: body, + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + await getMyProfile(); + successful = true; + showToast(success: true, msg: "관심사를 등록했습니다."); + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: err); + } + }); + } + } catch (e) { + print(e); + } finally { + toggleLoading(); + } + return successful; + } + + Future deleteInterest({dynamic queryParams}) async { + bool successful = false; + try { + toggleLoading(); + String authToken = await getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest().delete( + path: "community/api/v1/users/${me.id}/interest", + queryParams: queryParams, + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + await getMyProfile(); + successful = true; + showToast(success: true, msg: "해당 관심사를 삭제했습니다."); + } else { + final jsonUtf8 = decodeKo(response); + final String err = json.decode(jsonUtf8)["message"]; + showToast(success: false, msg: err); + } + }); + } + } catch (e) { + print(e); + } finally { + toggleLoading(); + } + return successful; + } + + Future blockUser({userId}) async { + bool successful = false; + try { + toggleLoading(); + String authToken = await getFirebaseIdToken(); + + if (authToken.isNotEmpty) { + await HttpRequest().post( + path: "community/api/v1/block", + queryParams: {"targetId": userId.toString()}, + authToken: authToken, + ).then((response) async { + if (response.statusCode == 200) { + await getMyProfile(); + successful = true; + showToast(success: true, msg: "해당 사용자를 차단했습니다."); + } else { + String msg = '알 수 없는 오류가 발생했습니다.: ${response.statusCode}'; + switch (response.statusCode) { + case 401: msg = "권한이 없습니다."; break; + case 404: msg = "존재하지 않는 유저입니다."; break; + case 409: msg = "이미 차단한 유저입니다."; break; + } + showToast(success: false, msg: msg); + } + }); + } + } catch (e) { + print(e); + } finally { + toggleLoading(); + } + return successful; + } +} diff --git a/lib/screens/app/app.dart b/lib/screens/app/app.dart new file mode 100644 index 00000000..3a653c6f --- /dev/null +++ b/lib/screens/app/app.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/providers/home/home_provider.dart'; +import 'package:provider/provider.dart'; +import 'bottom_navigation.dart'; +import 'tab_item.dart'; +import 'tab_navigator.dart'; + +class App extends StatefulWidget { + @override + State createState() => AppState(); +} + +class AppState extends State { + var _currentTab = TabItem.home; + final _navigatorKeys = { + TabItem.home: GlobalKey(), + TabItem.search: GlobalKey(), + TabItem.notification: GlobalKey(), + TabItem.profile: GlobalKey(), + }; + + void _selectTab(TabItem tabItem) { + if (tabItem == _currentTab) { + // pop to first route + _navigatorKeys[tabItem].currentState.popUntil((route) => route.isFirst); + } else { + setState(() => _currentTab = tabItem); + } + } + + @override + Widget build(BuildContext context) { + DateTime currentBackPressTime; + + return ChangeNotifierProvider( + create: (_) => HomeProvider(), + child: WillPopScope( + onWillPop: () async { + final isFirstRouteInCurrentTab = await _navigatorKeys[_currentTab].currentState.maybePop(); + if (isFirstRouteInCurrentTab) { + // if not on the 'main' tab + if (_currentTab == TabItem.home) { + // select 'main' tab + _selectTab(TabItem.home); + // back button handled by app + return false; + } + } + DateTime now = DateTime.now(); + if (currentBackPressTime == null || + now.difference(currentBackPressTime) > Duration(seconds: 2)) { + currentBackPressTime = now; + // 토스트 넣을 때 '뒤로가기를 한번 더 누르면 앱이 종료 됩니다.' 처리하기 + // Fluttertoast.showToast(msg: exit_warning); + return Future.value(false); + } + return Future.value(true); + // let system handle back button if we're on the first route + return isFirstRouteInCurrentTab; + }, + child: Scaffold( + body: Stack(children: [ + _buildOffstageNavigator(TabItem.home), + _buildOffstageNavigator(TabItem.search), + _buildOffstageNavigator(TabItem.notification), + _buildOffstageNavigator(TabItem.profile), + ]), + bottomNavigationBar: BottomNavigation( + currentTab: _currentTab, + onSelectTab: _selectTab, + ), + ), + ), + ); + } + + Widget _buildOffstageNavigator(TabItem tabItem) { + return Offstage( + offstage: _currentTab != tabItem, + child: TabNavigator( + navigatorKey: _navigatorKeys[tabItem], + tabItem: tabItem, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/app/bottom_navigation.dart b/lib/screens/app/bottom_navigation.dart new file mode 100644 index 00000000..dac0a8c0 --- /dev/null +++ b/lib/screens/app/bottom_navigation.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/providers/home/home_provider.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; +import 'tab_item.dart'; + +class BottomNavigation extends StatelessWidget { + BottomNavigation({@required this.currentTab, @required this.onSelectTab}); + + final TabItem currentTab; + final ValueChanged onSelectTab; + + @override + Widget build(BuildContext context) { + final homeProvider = context.read(); + + return BottomNavigationBar( + items: homeProvider.bottomNavItems.map((e) => + BottomNavigationBarItem( + label: e['label'], + icon: currentTab.index == homeProvider.bottomNavItems.indexOf(e) + ? e['selected_icon'] + : e['unselected_icon'], + ) + ).toList(), + selectedFontSize: 10, + unselectedFontSize: 10, + currentIndex: currentTab.index, + type: BottomNavigationBarType.fixed, + selectedItemColor: GuamColorFamily.purpleCore, + backgroundColor: GuamColorFamily.grayscaleWhite, + unselectedItemColor: GuamColorFamily.grayscaleGray4, + onTap: (index) { + onSelectTab(TabItem.values[index]); + homeProvider.idx = index; + }, + ); + } +} diff --git a/lib/screens/app/splash/splash_screen.dart b/lib/screens/app/splash/splash_screen.dart new file mode 100644 index 00000000..d41b578d --- /dev/null +++ b/lib/screens/app/splash/splash_screen.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/helpers/svg_provider.dart'; +import 'package:guam_community_client/screens/app/splash/splash_text.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:hexcolor/hexcolor.dart'; + +class SplashScreen extends StatefulWidget { + @override _SplashScreenState createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State with TickerProviderStateMixin{ + AnimationController _animationController; + + _setAnimation(double begin, double end){ + return CurvedAnimation( + parent: _animationController, + curve: Interval(begin, end, curve: Curves.fastOutSlowIn) + ); + } + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.fitWidth, + alignment: Alignment.bottomCenter, + image: AssetImage('assets/backgrounds/back_0.75x.png'), + ) + ), + ), + Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.fitWidth, + alignment: Alignment.bottomCenter, + image: AssetImage('assets/backgrounds/front_0.75x.png'), + ) + ), + child: Stack( + children: [ + // 위에서부터 아래로 star 배치 + _starSplash( + begin: 1/4, end: 1, // animation + width: 26, height: 26, // size of star + top: size.height*0.12, left: size.width*0.24, // position of star (iPhone 13 : 375x812) + color: GuamColorFamily.purpleDark1, + ), + _starSplash( + begin: 1/20, end: 1, + width: 32, height: 32, + top: size.height*0.19, left: size.width*0.88, + color: GuamColorFamily.purpleCore, + ), + _starSplash( + begin: 1/10, end: 1, + width: 25, height: 25, + top: size.height*0.32, left: size.width*0.42, + color: GuamColorFamily.purpleLight1, + ), + _starSplash( + begin: 1/2, end: 1, + width: 25, height: 25, + top: size.height*0.50, left: size.width*0.76, + color: GuamColorFamily.purpleLight2, + ), + _starSplash( + begin: 1/2, end: 1, + width: 32, height: 32, + top: size.height*0.57, left: size.width*0.16, + color: GuamColorFamily.purpleLight2, + ), + _starSplash( + begin: 1/30, end: 1, + width: 25, height: 25, + top: size.height*0.68, left: size.width*0.57, + color: GuamColorFamily.purpleLight3, + ), + SplashText(), + ], + ), + ), + ], + ), + ); + } + + Widget _starSplash({double top, double left, double width, double height, double begin, double end, HexColor color}){ + return Positioned( + top: top, + left: left, + child: ScaleTransition( + scale: _setAnimation(begin, end), + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + image: DecorationImage( + image: SvgProvider( + 'assets/backgrounds/splash/star_splash.svg', + color: color, + ), + ), + ) + ) + ), + ); + } +} diff --git a/lib/screens/app/splash/splash_text.dart b/lib/screens/app/splash/splash_text.dart new file mode 100644 index 00000000..9d0226aa --- /dev/null +++ b/lib/screens/app/splash/splash_text.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + + +class SplashText extends StatefulWidget { + final bool animation; + + SplashText({this.animation=true}); + + @override + State createState() => _SplashTextState(); +} + +class _SplashTextState extends State with SingleTickerProviderStateMixin { + AnimationController _animationController; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.animation) { + return ScaleTransition( + scale: CurvedAnimation( + parent: _animationController, + curve: Interval(1 / 100, 1, curve: Curves.fastOutSlowIn), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'IT인들의 커뮤니티', + style: TextStyle( + fontSize: 18, + color: GuamColorFamily.purpleLight3, + ), + ), + Text( + 'Guam', + style: TextStyle( + height: 1.3, + fontSize: 56, + fontWeight: FontWeight.w700, + fontFamily: GuamFontFamily.Poppins, + color: GuamColorFamily.purpleDark1, + ), + ) + ], + ), + ), + ); + } else { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'IT인들의 커뮤니티', + style: TextStyle( + fontSize: 18, + color: GuamColorFamily.purpleLight3, + ), + ), + Text( + 'Guam', + style: TextStyle( + height: 1.3, + fontSize: 56, + fontWeight: FontWeight.w700, + fontFamily: GuamFontFamily.Poppins, + color: GuamColorFamily.purpleDark1, + ), + ) + ], + ), + ); + } + } +} diff --git a/lib/screens/app/tab_item.dart b/lib/screens/app/tab_item.dart new file mode 100644 index 00000000..b324d3fc --- /dev/null +++ b/lib/screens/app/tab_item.dart @@ -0,0 +1,8 @@ +enum TabItem { home, search, notification, profile } + +const Map tabName = { + TabItem.home: '홈', + TabItem.search: '검색', + TabItem.notification: '알림', + TabItem.profile: '프로필', +}; diff --git a/lib/screens/app/tab_navigator.dart b/lib/screens/app/tab_navigator.dart new file mode 100644 index 00000000..2ea22d19 --- /dev/null +++ b/lib/screens/app/tab_navigator.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/screens/boards/boards_app.dart'; +import 'package:guam_community_client/screens/app/tab_item.dart'; +import 'package:guam_community_client/screens/notifications/notifications_app.dart'; +import 'package:guam_community_client/screens/profiles/profiles_app.dart'; +import 'package:guam_community_client/screens/search/search_app.dart'; +import 'package:jiffy/jiffy.dart'; + +class TabNavigatorRoutes { + static const String root = '/'; +} + +class TabNavigator extends StatelessWidget { + + TabNavigator({@required this.navigatorKey, @required this.tabItem}); + final GlobalKey navigatorKey; + final TabItem tabItem; + + Map _routeBuilders(BuildContext context) { + switch(tabItem){ + case TabItem.search: return {TabNavigatorRoutes.root: (context) => SearchApp()}; + case TabItem.notification: return {TabNavigatorRoutes.root: (context) => NotificationsApp()}; + case TabItem.profile: return {TabNavigatorRoutes.root: (context) => ProfilesApp()}; + default: return {TabNavigatorRoutes.root: (context) => BoardsApp()}; + } + } + + @override + Widget build(BuildContext context) { + /// The locale affects dateTime of every children to be translated into Korean. + Jiffy.locale('ko'); + final routeBuilders = _routeBuilders(context); + return Navigator( + key: navigatorKey, + initialRoute: TabNavigatorRoutes.root, + onGenerateRoute: (routeSettings) { + return MaterialPageRoute( + builder: (context) => routeBuilders[routeSettings.name](context), + ); + }, + ); + } +} diff --git a/lib/screens/boards/boards_app.dart b/lib/screens/boards/boards_app.dart new file mode 100644 index 00000000..d3954193 --- /dev/null +++ b/lib/screens/boards/boards_app.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/providers/messages/messages.dart'; +import 'package:guam_community_client/providers/posts/posts.dart'; +import 'package:guam_community_client/providers/search/search.dart'; +import 'package:guam_community_client/providers/user_auth/authenticate.dart'; +import 'package:guam_community_client/screens/boards/boards_type.dart'; +import 'package:guam_community_client/screens/boards/posts/post_button.dart'; +import 'package:guam_community_client/screens/messages/message_box.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; +import '../../commons/custom_app_bar.dart'; +import 'boards_feed.dart'; +import 'boards_type.dart'; + +class BoardsApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Posts(authProvider)), + ChangeNotifierProvider(create: (_) => Search(authProvider)), + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: BoardsAppScaffold(), + ); + } +} + +class BoardsAppScaffold extends StatefulWidget { + @override + State createState() => _BoardsAppScaffoldState(); +} + +class _BoardsAppScaffoldState extends State { + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 6, + child: Scaffold( + backgroundColor: GuamColorFamily.purpleLight3, + appBar: PreferredSize( + preferredSize: Size.fromHeight(100), + child: CustomAppBar( + title: '홈', + trailing: MessageBox(), + bottom: TabBar( + isScrollable: true, + physics: BouncingScrollPhysics(), + labelColor: GuamColorFamily.grayscaleGray1, + unselectedLabelColor: GuamColorFamily.grayscaleGray4, + indicatorColor: GuamColorFamily.grayscaleGray1, + indicatorWeight: 2, + tabs: [ + ...boardsList.map((board) => Tab( + child: Text(boardsType[board['name']]) + )) + ], + ), + ), + ), + body: TabBarView( + physics: BouncingScrollPhysics(), + children: [ + ...boardsList.map((board) => BoardsFeed(boardId: board['id'])) + ], + ), + floatingActionButton: PostButton() + ) + ); + } +} diff --git a/lib/screens/boards/boards_feed.dart b/lib/screens/boards/boards_feed.dart new file mode 100644 index 00000000..87ab040b --- /dev/null +++ b/lib/screens/boards/boards_feed.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/guam_progress_indicator.dart'; +import 'package:guam_community_client/screens/boards/posts/post_list.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; +import 'package:guam_community_client/providers/posts/posts.dart'; + +class BoardsFeed extends StatefulWidget { + final int boardId; + + BoardsFeed({this.boardId}); + + @override + State createState() => _BoardsFeedState(); +} + +class _BoardsFeedState extends State { + List _posts = []; + int _beforePostId; + bool _hasNextPage = true; + bool _isFirstLoadRunning = false; + bool _isLoadMoreRunning = false; + ScrollController _scrollController = ScrollController(); + + void _firstLoad() async { + setState(() => _isFirstLoadRunning = true); + try { + await context.read().fetchPosts(widget.boardId); + _posts = context.read().posts; + } catch (err) { + print('알 수 없는 오류가 발생했습니다.'); + } + setState(() => _isFirstLoadRunning = false); + } + + void _loadMore() async { + if (_hasNextPage == true && + _isFirstLoadRunning == false && + _isLoadMoreRunning == false && + _scrollController.position.extentAfter < 300) { + setState(() => _isLoadMoreRunning = true); + _beforePostId = _posts.last.id; + try { + final fetchedPosts = await context.read().addPosts( + boardId: widget.boardId, + beforePostId: _beforePostId, + ); + if (fetchedPosts != null && fetchedPosts.length > 0) { + setState(() => _posts.addAll(fetchedPosts)); + } else { + // This means there is no more data + // and therefore, we will not send another GET request + setState(() => _hasNextPage = false); + } + } catch (err) { + print('알 수 없는 오류가 발생했습니다.'); + } + setState(() => _isLoadMoreRunning = false); + } + } + + @override + void initState() { + _firstLoad(); + _scrollController = ScrollController()..addListener(_loadMore); + super.initState(); + } + + @override + void dispose() { + _scrollController.removeListener(_loadMore); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _isFirstLoadRunning + ? Center(child: guamProgressIndicator()) + : RefreshIndicator( + color: Color(0xF9F8FFF), // GuamColorFamily.purpleLight1 + onRefresh: () async => _firstLoad(), + child: Container( + height: double.infinity, + child: SingleChildScrollView( + controller: _scrollController, + physics: AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + PostList(_posts), + if (_isLoadMoreRunning == true) + Padding( + padding: EdgeInsets.only(top: 10, bottom: 40), + child: guamProgressIndicator(size: 40), + ), + if (_hasNextPage == false) + Container( + color: GuamColorFamily.purpleLight2, + padding: EdgeInsets.only(top: 10, bottom: 10), + child: Center(child: Text( + '모든 게시글을 불러왔습니다!', + style: TextStyle( + fontSize: 13, + color: GuamColorFamily.grayscaleGray2, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + )), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/boards/boards_type.dart b/lib/screens/boards/boards_type.dart new file mode 100644 index 00000000..7b7e1f64 --- /dev/null +++ b/lib/screens/boards/boards_type.dart @@ -0,0 +1,20 @@ +enum BoardsType { feed, anonymous, free, recruit, info, advertisement } + +const Map boardsType = { + BoardsType.feed: '피드', + BoardsType.anonymous: '익명', + BoardsType.free: '자유', + BoardsType.recruit: '구인', + BoardsType.info: '정보공유', + BoardsType.advertisement: '홍보', +}; + +// TODO: Server로부터 fetchBoardId 받아오면 바뀔 예정 +List> boardsList = [ + {'id': 0, 'name': BoardsType.feed}, + {'id': 1, 'name': BoardsType.anonymous}, + {'id': 2, 'name': BoardsType.free}, + {'id': 3, 'name': BoardsType.recruit}, + {'id': 4, 'name': BoardsType.info}, + {'id': 5, 'name': BoardsType.advertisement}, +]; diff --git a/lib/screens/boards/category_type.dart b/lib/screens/boards/category_type.dart new file mode 100644 index 00000000..0790fcfe --- /dev/null +++ b/lib/screens/boards/category_type.dart @@ -0,0 +1,17 @@ +enum CategoryType { dev, data, design, marketing, etc } + +const Map categoryType = { + CategoryType.dev: '개발', + CategoryType.data: '데이터분석', + CategoryType.design: '디자인', + CategoryType.marketing: '기획/마케팅', + CategoryType.etc: '기타', +}; + +List> categoryList = [ + {'id': 1, 'name': CategoryType.dev}, + {'id': 2, 'name': CategoryType.data}, + {'id': 3, 'name': CategoryType.design}, + {'id': 4, 'name': CategoryType.marketing}, + {'id': 5, 'name': CategoryType.etc}, +]; diff --git a/lib/screens/boards/comments/comment_banner.dart b/lib/screens/boards/comments/comment_banner.dart new file mode 100644 index 00000000..52da69c5 --- /dev/null +++ b/lib/screens/boards/comments/comment_banner.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/commons/common_img_nickname.dart'; +import 'package:guam_community_client/models/boards/comment.dart'; +import 'package:guam_community_client/screens/boards/comments/comment_more.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import '../../../styles/fonts.dart'; + +class CommentBanner extends StatelessWidget { + final Comment comment; + final bool isAuthor; + final Function deleteFunc; + + CommentBanner(this.comment, this.isAuthor, this.deleteFunc); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + children: [ + CommonImgNickname( + userId: comment.profile.id, + nickname: comment.profile.nickname, + profileClickable: comment.profile.id != 0, + imgUrl: comment.profile.profileImg ?? null, + nicknameColor: GuamColorFamily.grayscaleGray3, + ), + if (isAuthor && comment.profile.id != 0) + /// 익명게시판의 경우 id가 모두 0인 것을 이용해 익명게시글 내 댓글에서 '작성자' 표시는 항상 안보이게 만듦. + Container( + margin: EdgeInsets.only(left: 4), + padding: EdgeInsets.symmetric(vertical: 2, horizontal: 8), + decoration: BoxDecoration( + color: GuamColorFamily.purpleLight3, + borderRadius: BorderRadius.all(Radius.circular(15)), + ), + child: Text( + '작성자', + style: TextStyle( + fontSize: 10, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.purpleLight1, + ), + ), + ), + Spacer(), + IconButton( + iconSize: 20, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset( + 'assets/icons/more.svg', + color: GuamColorFamily.grayscaleGray5, + ), + onPressed: () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: GuamColorFamily.grayscaleWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ) + ), + builder: (context) => CommentMore(comment, deleteFunc), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/boards/comments/comment_body.dart b/lib/screens/boards/comments/comment_body.dart new file mode 100644 index 00000000..1b88c45d --- /dev/null +++ b/lib/screens/boards/comments/comment_body.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:guam_community_client/commons/icon_text.dart'; +import 'package:guam_community_client/models/boards/comment.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../../commons/image/image_container.dart'; +import '../../../helpers/http_request.dart'; +import '../../../providers/posts/posts.dart'; + +class CommentBody extends StatefulWidget { + final Comment comment; + + CommentBody(this.comment); + + @override + State createState() => _CommentBodyState(); +} + +class _CommentBodyState extends State { + bool isLiked; + int likeCount; + + @override + void initState() { + isLiked = widget.comment.isLiked; + likeCount = widget.comment.likeCount; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final double maxImgSize = 96; + final postsProvider = context.watch(); + + Future likeOrUnlikeComment() async { + try { + if (!isLiked) { + return await postsProvider.likeComment( + postId: widget.comment.postId, + commentId: widget.comment.id, + ).then((successful) { + if (successful) { + setState(() { + isLiked = true; + likeCount ++; + }); + } else { + return postsProvider.fetchPosts(postsProvider.boardId); + } + }); + } else { + return await postsProvider.unlikeComment( + postId: widget.comment.postId, + commentId: widget.comment.id, + ).then((successful) { + if (successful) { + setState(() { + isLiked = !isLiked; + likeCount --; + }); + } else { + return postsProvider.fetchPosts(postsProvider.boardId); + } + }); + } + } catch (e) { + print(e); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 32, right: 12), + child: Linkify( + onOpen: (link) async { + if (await canLaunch(link.url)) { + await launch(link.url); + } else { + throw 'Could not launch $link'; + } + }, + text: widget.comment.content, + style: TextStyle( + height: 1.6, + fontSize: 13, + color: GuamColorFamily.grayscaleGray2, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + linkStyle: TextStyle( + height: 1.6, + fontSize: 14, + color: GuamColorFamily.blueCore, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + if (widget.comment.imagePaths.isNotEmpty) + Container( + padding: EdgeInsets.only(left: 23, top: 8, bottom: 8), + constraints: BoxConstraints(maxHeight: maxImgSize + 15), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: widget.comment.imagePaths.length, + itemBuilder: (_, idx) => Container( + padding: EdgeInsets.only(right: 10), + child: ImageThumbnail( + width: maxImgSize, + height: maxImgSize, + image: Image( + image: NetworkImage(HttpRequest().s3BaseAuthority + widget.comment.imagePaths[idx]), + fit: BoxFit.fill, + ), + ), + ), + ), + ), + Padding( + padding: EdgeInsets.only(left: 32, top: 4, bottom: 8), + child: Row( + children: [ + IconText( + iconSize: 18, + fontSize: 10, + text: likeCount.toString(), + iconPath: isLiked + ? 'assets/icons/like_filled.svg' + : 'assets/icons/like_outlined.svg', + onPressed: likeOrUnlikeComment, + iconColor: isLiked + ? GuamColorFamily.redCore + : GuamColorFamily.grayscaleGray5, + textColor: GuamColorFamily.grayscaleGray5, + ), + Padding( + padding: EdgeInsets.only(left: 10), + child: Text( + Jiffy(widget.comment.createdAt).fromNow(), + style: TextStyle( + fontSize: 9, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray4, + ), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/screens/boards/comments/comment_more.dart b/lib/screens/boards/comments/comment_more.dart new file mode 100644 index 00000000..6e7fb803 --- /dev/null +++ b/lib/screens/boards/comments/comment_more.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/models/boards/comment.dart'; +import 'package:guam_community_client/providers/messages/messages.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +import '../../../commons/bottom_modal/bottom_modal_default.dart'; +import '../../../commons/bottom_modal/bottom_modal_with_alert.dart'; +import '../../../commons/bottom_modal/bottom_modal_with_message.dart'; +import '../../../providers/posts/posts.dart'; +import '../../../providers/user_auth/authenticate.dart'; +import '../posts/post_comment_report.dart'; + +class CommentMore extends StatelessWidget { + final Comment comment; + final Function deleteFunc; + + const CommentMore(this.comment, this.deleteFunc); + + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Posts(authProvider)), + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: Builder( + builder: (context) { + Posts postsProvider = context.read(); + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.only(left: 24, top: 24, bottom: 21), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: comment.isMine ? [ + // BottomModalDefault( + // text: '수정하기', + // onPressed: () { + // Navigator.pop(context); + // }, + // ), + BottomModalWithAlert( + funcName: '삭제하기', + title: '댓글을 삭제하시겠어요?', + body: '삭제된 댓글은 복원할 수 없습니다.', + func: () => postsProvider.deleteComment( + postId: comment.postId, commentId: comment.id + ).then((successful) { + if (successful) { + deleteFunc(comment.id); + postsProvider.fetchPosts(postsProvider.boardId); + Navigator.pop(context); + Navigator.pop(context); + } + }), + ), + ] : [ + BottomModalDefault( + text: '쪽지 보내기', + onPressed: () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + builder: (context) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: SingleChildScrollView( + child: BottomModalWithMessage( + funcName: '보내기', + title: '${comment.profile.nickname} 님에게 쪽지 보내기', + profile: comment.profile, + func: null, + ), + ), + ), + ), + ), + // PostCommentReport(comment.profile), + // BottomModalWithAlert( + // funcName: '차단하기', + // title: '${comment.profile.nickname} 님을 차단하시겠어요?', + // body: '사용자를 차단하면, 해당 사용자의 게시글 및 댓글을 확인 할 수 없으며, 서로 쪽지를 주고 받을 수 없습니다.\n\n차단계정 관리는 프로필>계정 설정> 차단 목록 관리 탭에서 확인 가능합니다', + // func: () {}, + // ), + ], + ), + ), + ); + } + ), + ); + } +} diff --git a/lib/screens/boards/comments/comments.dart b/lib/screens/boards/comments/comments.dart new file mode 100644 index 00000000..ca69bc71 --- /dev/null +++ b/lib/screens/boards/comments/comments.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/models/boards/comment.dart'; +import 'comment_banner.dart'; +import 'comment_body.dart'; + +class Comments extends StatelessWidget { + final Comment comment; + final bool isAuthor; + final Function deleteFunc; + + Comments({this.comment, this.isAuthor, this.deleteFunc}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommentBanner(comment, isAuthor, deleteFunc), + CommentBody(comment), + ], + ); + } +} diff --git a/lib/screens/boards/posts/creation/post_creation.dart b/lib/screens/boards/posts/creation/post_creation.dart new file mode 100644 index 00000000..a8495aa6 --- /dev/null +++ b/lib/screens/boards/posts/creation/post_creation.dart @@ -0,0 +1,291 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/commons/custom_app_bar.dart'; +import 'package:guam_community_client/commons/custom_divider.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/screens/boards/posts/creation/post_creation_board.dart'; +import 'package:guam_community_client/screens/boards/posts/creation/post_creation_content.dart'; +import 'package:guam_community_client/screens/boards/posts/creation/post_creation_image.dart'; +import 'package:guam_community_client/screens/boards/posts/creation/post_creation_category.dart'; +import 'package:guam_community_client/screens/boards/posts/creation/post_creation_title.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +import '../../../../commons/functions_category_boardType.dart'; +import '../../../../commons/guam_progress_indicator.dart'; +import '../../../../models/boards/post.dart'; +import '../../../../providers/posts/posts.dart'; +import '../../../../providers/user_auth/authenticate.dart'; +import '../detail/post_detail.dart'; + +class PostCreation extends StatefulWidget { + final bool isEdit; + final dynamic editTarget; + + PostCreation({this.isEdit = false, this.editTarget}); + + @override + State createState() => _PostCreationState(); +} + +class _PostCreationState extends State with Toast { + Map input = {}; + bool isBoardAnonymous = false; + + @override + void initState() { + Post editPost = widget.editTarget; + if (widget.editTarget != null) { + input = { + 'title': editPost.title, + 'content': editPost.content, + 'boardType': editPost.boardType, + 'boardId': transferBoardType(editPost.boardType), + 'category': editPost.category != null ? editPost.category.title : "", + 'categoryId': editPost.category != null ? editPost.category.categoryId : 0, + 'images': editPost.imagePaths, /// 이미지는 S3 주소 받아와서 그대로 전송 (수정 불가능) + }; + } else { + input = { + 'title': '', + 'content': '', + 'boardId': '', + 'boardType': '', + 'categoryId': '', + 'category': '', + 'images': [], + }; + } + super.initState(); + } + + Future createOrUpdatePost({List files}) async { + Posts postProvider = context.read(); + Authenticate authProvider = context.read(); + + bool successful = false; + Map fields = { + 'title': input['title'], + 'content': input['content'], + 'boardId': input['boardId'].toString(), + 'categoryId': input['categoryId'].toString(), + }; + + try { + if (widget.isEdit && widget.editTarget != null) { + return await postProvider.editPost( + postId: widget.editTarget.id, + body: fields, + ).then((successful) { + if (successful) { + Navigator.pop(context, fields); + successful = true; + } + }); + } else { + return await postProvider.createPost( + fields: fields, + files: files, + ).then((successful) { + if (successful) { + Navigator.pop(context); + postProvider.fetchPosts(0); + /// 게시글 생성 후 getPost(createdPostId) 하여 새로운 게시글로 이동 + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Posts(authProvider)), + ], + child: FutureBuilder( + future: postProvider.getPost(postProvider.createdPostId), + builder: (_, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return PostDetail(snapshot.data); + } else if (snapshot.hasError) { + Navigator.pop(context); + postProvider.fetchPosts(0); + showToast(success: false, msg: '게시글을 찾을 수 없습니다.'); + return null; + } else { + return Center(child: guamProgressIndicator()); + } + } + ), + ), + ), + ); + successful = true; + } + }); + } + } catch (e) { + print(e); + } + return successful; + } + + void setBoardAnonymous(String boardType) { + setState(() => boardType == '익명' + ? isBoardAnonymous = true + : isBoardAnonymous = false + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + leading: Card( + elevation: 0, + color: GuamColorFamily.grayscaleWhite, + margin: EdgeInsets.zero, + child: IconButton( + icon: SvgPicture.asset('assets/icons/back.svg'), + onPressed: () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: GuamColorFamily.grayscaleWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + builder: (context) => SingleChildScrollView( + child: Container( + padding: EdgeInsets.only(left: 24, top: 24, right: 18, bottom: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '게시글 ${widget.isEdit ? '수정': '작성'}을 취소하시겠어요?', + style: TextStyle(fontSize: 18, color: GuamColorFamily.grayscaleGray2), + ), + TextButton( + child: Text( + '돌아가기', + style: TextStyle(fontSize: 16, color: GuamColorFamily.purpleCore, + ), + ), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size(30, 26), + alignment: Alignment.centerRight, + ), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + CustomDivider(color: GuamColorFamily.grayscaleGray7), + Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Text( + widget.isEdit + ? '수정된 내용이 사라집니다.' + : '게시글은 임시저장되지 않습니다.', + style: TextStyle( + fontSize: 14, + height: 1.6, + color: GuamColorFamily.grayscaleGray2, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + Center( + child: TextButton( + onPressed: () { + if (widget.isEdit) { + Navigator.pop(context); + Navigator.pop(context); + } else { + Navigator.of(context).pushNamedAndRemoveUntil( + '/', (route) => false + ); + } + }, + child: Text( + '취소하기', + style: TextStyle(fontSize: 16, color: GuamColorFamily.redCore), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + trailing: Padding( + padding: EdgeInsets.only(right: 11), + child: TextButton( + onPressed: () async { + await createOrUpdatePost( + files: (input['images'] != [] && !widget.isEdit) + ? [...input['images'].map((e) => File(e.path))] + : [] + ); + }, + style: TextButton.styleFrom( + minimumSize: Size(30, 26), + alignment: Alignment.center, + ), + child: Text( + widget.isEdit ? '수정' : '등록', + style: TextStyle( + color: GuamColorFamily.purpleCore, + fontSize: 16, + ), + ), + ), + ), + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + color: GuamColorFamily.grayscaleWhite, + padding: EdgeInsets.only(left: 24, top: 10, right: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PostCreationBoard(input, widget.isEdit, setBoardAnonymous), + PostCreationTitle(input), + CustomDivider(color: GuamColorFamily.grayscaleGray7), + PostCreationContent(input), + ], + ), + ), + CustomDivider( + height: 12, + thickness: 12, + color: GuamColorFamily.purpleLight3, + ), + Container( + color: GuamColorFamily.grayscaleWhite, + width: double.infinity, + padding: EdgeInsets.only(left: 24, top: 20, right: 0, bottom: 42), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // if (!isBoardAnonymous) + PostCreationCategory(input), + if (!widget.isEdit) PostCreationImage(input) + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/creation/post_creation_board.dart b/lib/screens/boards/posts/creation/post_creation_board.dart new file mode 100644 index 00000000..a39e76b2 --- /dev/null +++ b/lib/screens/boards/posts/creation/post_creation_board.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/commons/bottom_modal/bottom_modal_with_choice.dart'; +import 'package:guam_community_client/commons/functions_category_boardType.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +import '../../boards_type.dart'; + +class PostCreationBoard extends StatefulWidget { + final Map input; + final bool isEdit; + final Function setBoardAnonymous; + + PostCreationBoard(this.input, this.isEdit, this.setBoardAnonymous); + + @override + _PostCreationBoardState createState() => _PostCreationBoardState(); +} + +class _PostCreationBoardState extends State { + @override + void initState() { + super.initState(); + } + + void setBoardType(String boardType){ + setState(() { + widget.input['boardType'] = boardType; + widget.input['boardId'] = transferBoardType(boardType).toString(); + widget.setBoardAnonymous(boardType); + }); + } + + @override + Widget build(BuildContext context) { + bool isBoardAnonymous = widget.input['boardType'] == '익명'; + + return Container( + child: TextButton( + child: Row( + children: [ + Text( + widget.input['boardType'] == '' + ? '게시판을 선택해주세요.' + : widget.input['boardType'] + '게시판', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: widget.input['boardType'] == '' || (isBoardAnonymous && widget.isEdit) + ? GuamColorFamily.grayscaleGray3 + : GuamColorFamily.purpleCore, + ), + ), + if ( !(isBoardAnonymous && widget.isEdit) ) + IconButton( + onPressed: null, + padding: EdgeInsets.only(left: 4), + constraints: BoxConstraints(), + icon: SvgPicture.asset( + 'assets/icons/down.svg', + color: widget.input['boardType'] == '' + ? GuamColorFamily.grayscaleGray3 + : GuamColorFamily.purpleCore, + width: 20, + height: 20, + ), + ), + ], + ), + style: TextButton.styleFrom(padding: EdgeInsets.zero, minimumSize: Size(136, 23)), + onPressed: (isBoardAnonymous && widget.isEdit) + ? null + : () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: GuamColorFamily.grayscaleWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ) + ), + builder: (context) => SingleChildScrollView( + child: Column( + children: [ + BottomModalWithChoice( + title: '게시판을 선택해주세요.', + back: '완료', + children: [ + /// 게시글 생성 시 피드 게시판 제외 위해 1번째 항목부터, + /// 게시글 수정 시 피드&익명 게시판 제외하기 위해 2번째 항목부터 포함 + ...boardsList.sublist(widget.isEdit? 2 : 1) + .map((board) => _boardType(boardsType[board['name']]) + ) + ], + ), + ], + ), + ), + ) + ), + ); + } + + Widget _boardType(String boardType) { + return Builder( + builder: (context) => InkWell( + onTap: () { + setBoardType(boardType); + Navigator.pop(context); + }, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 20, + child: Text( + boardType + '게시판', + style: TextStyle( + fontSize: 16, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: boardType == widget.input['boardType'] + ? GuamColorFamily.purpleCore + : GuamColorFamily.grayscaleGray3, + ), + ), + ), + if (boardType == widget.input['boardType']) + IconButton( + padding: EdgeInsets.only(right: 8), + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/check.svg'), + onPressed: null, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/creation/post_creation_category.dart b/lib/screens/boards/posts/creation/post_creation_category.dart new file mode 100644 index 00000000..0082dce6 --- /dev/null +++ b/lib/screens/boards/posts/creation/post_creation_category.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/screens/boards/category_type.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +import '../../../../commons/functions_category_boardType.dart'; + +class PostCreationCategory extends StatefulWidget { + final Map input; + + PostCreationCategory(this.input); + + @override + _PostCreationCategoryState createState() => _PostCreationCategoryState(); +} + +class _PostCreationCategoryState extends State { + @override + void initState() { + super.initState(); + } + + void setCategory(String category){ + setState(() { + widget.input['category'] = category; + widget.input['categoryId'] = transferCategory(category).toString(); + }); + } + + void initCategory(){ + setState(() { + widget.input['category'] = ''; + widget.input['categoryId'] = 0; + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '카테고리를 선택해주세요.', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray3, + ), + ), + if (widget.input['category'] == '') + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ...categoryList.map((c) => _categoryChip(categoryType[c['name']])), + ], + ), + ), + if (widget.input['category'] != '') + Padding( + padding: EdgeInsets.only(top: 8), + child: Row( + children: [ + Text( + "#" + widget.input['category'], + style: TextStyle( + fontSize: 16, color: GuamColorFamily.blueCore), + ), + IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + onPressed: () => initCategory(), + icon: SvgPicture.asset( + 'assets/icons/cancel_outlined.svg', + color: GuamColorFamily.blueCore, + width: 20, + height: 20, + ), + ) + ], + ), + ), + ], + ), + ); + } + + Widget _categoryChip(String category) { + return Padding( + padding: EdgeInsets.only(right: 8), + child: ChoiceChip( + selected: false, + pressElevation: 2, + labelPadding: EdgeInsets.symmetric(horizontal: 4), + backgroundColor: GuamColorFamily.purpleLight3, + onSelected: (e) => setCategory(category), + label: Text( + "#" + category, + style: TextStyle( + fontSize: 14, + color: GuamColorFamily.purpleLight1, + ), + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/creation/post_creation_content.dart b/lib/screens/boards/posts/creation/post_creation_content.dart new file mode 100644 index 00000000..08acd989 --- /dev/null +++ b/lib/screens/boards/posts/creation/post_creation_content.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class PostCreationContent extends StatefulWidget { + final Map input; + + PostCreationContent(this.input); + + @override + _PostCreationContentState createState() => _PostCreationContentState(); +} + +class _PostCreationContentState extends State { + final _contentTextFieldController = TextEditingController(); + + @override + void initState() { + _contentTextFieldController.text = widget.input['content']; + super.initState(); + } + + void setContent(){ + setState(() => widget.input['content'] = _contentTextFieldController.text); + } + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height * 0.45, + child: TextField( + keyboardType: TextInputType.multiline, + controller: _contentTextFieldController, + onChanged: (e) => setContent(), + maxLines: null, + style: TextStyle(fontSize: 14, height: 1.6, color: GuamColorFamily.grayscaleGray2), + decoration: InputDecoration( + hintText: "내용을 입력해주세요.", + hintStyle: TextStyle(fontSize: 14, color: GuamColorFamily.grayscaleGray5), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: EdgeInsets.only(top: 20), + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/creation/post_creation_image.dart b/lib/screens/boards/posts/creation/post_creation_image.dart new file mode 100644 index 00000000..740c1b60 --- /dev/null +++ b/lib/screens/boards/posts/creation/post_creation_image.dart @@ -0,0 +1,146 @@ +import 'dart:io'; + +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/commons/image/image_thumbnail.dart'; +import 'package:guam_community_client/helpers/pick_image.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:image_picker/image_picker.dart'; + +class PostCreationImage extends StatefulWidget { + final Map input; + + PostCreationImage(this.input); + + @override + _PostCreationImageState createState() => _PostCreationImageState(); +} + +class _PostCreationImageState extends State { + final double maxImgSize = 80; + final double imgSheetHeight = 96; + bool sending = false; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + widget.input['images'].clear(); + super.dispose(); + } + + void setImageFile(PickedFile val) { + setState(() { + if (val != null) widget.input['images'].add(val); + }); + } + + void deleteImageFile(int idx) { + setState(() => widget.input['images'].removeAt(idx)); + } + + void toggleSending() { + setState(() => sending = !sending); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.only(bottom: 10), + child: Text( + '사진을 첨부해보세요.', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray3, + ) + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + InkWell( + onTap: !sending + ? () => pickImage(type: 'gallery').then((img) => setImageFile(img)) + : null, + child: SizedBox( + height: 80, + width: 80, + child: DottedBorder( + strokeWidth: 1, + dashPattern: [4, 5], + borderType: BorderType.RRect, + radius: Radius.circular(12), + color: GuamColorFamily.purpleLight2, + child: Center( + child: IconButton( + iconSize: 24, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset( + 'assets/icons/plus.svg', + color: GuamColorFamily.purpleLight1, + ), + onPressed: !sending + ? () => pickImage(type: 'gallery').then((img) => setImageFile(img)) + : null, + ), + ), + ), + ), + ), + widget.input['images'].isNotEmpty + ? SizedBox( + height: maxImgSize, + width: (maxImgSize + 14.87) * widget.input['images'].length, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: widget.input['images'].length, + itemBuilder: (_, idx) => + Stack( + children: [ + Container( + padding: EdgeInsets.only(left: 14.87), + child: ImageThumbnail( + width: maxImgSize, + height: maxImgSize, + image: Image( + image: FileImage(File(widget.input['images'][idx].path)), + fit: BoxFit.fill, + ), + ), + ), + Positioned( + top: 2, + right: 2, + child: IconButton( + iconSize: 23, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/cancel_filled.svg'), + onPressed: () => deleteImageFile(idx), + ), + ) + ], + ), + ), + ) + : Container(), + Padding(padding: EdgeInsets.only(right: 14.87)), + ], + ), + ), + ], + ); + } +} diff --git a/lib/screens/boards/posts/creation/post_creation_title.dart b/lib/screens/boards/posts/creation/post_creation_title.dart new file mode 100644 index 00000000..d7424e06 --- /dev/null +++ b/lib/screens/boards/posts/creation/post_creation_title.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class PostCreationTitle extends StatefulWidget { + final Map input; + + PostCreationTitle(this.input); + + @override + _PostCreationTitleState createState() => _PostCreationTitleState(); +} + +class _PostCreationTitleState extends State { + final _titleTextFieldController = TextEditingController(); + + @override + void initState() { + _titleTextFieldController.text = widget.input['title']; + super.initState(); + } + + void setTitle(){ + setState(() { + widget.input['title'] = _titleTextFieldController.text; + }); + } + + @override + Widget build(BuildContext context) { + return TextField( + keyboardType: TextInputType.multiline, + controller: _titleTextFieldController, + onChanged: (e) => setTitle(), + maxLines: 1, + style: TextStyle(fontSize: 18), + decoration: InputDecoration( + hintText: "제목을 입력해주세요.", + hintStyle: TextStyle(fontSize: 18, color: GuamColorFamily.grayscaleGray5), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + ), + ); + } +} diff --git a/lib/screens/boards/posts/detail/post_detail.dart b/lib/screens/boards/posts/detail/post_detail.dart new file mode 100644 index 00000000..229d0b4f --- /dev/null +++ b/lib/screens/boards/posts/detail/post_detail.dart @@ -0,0 +1,296 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mentions/flutter_mentions.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/commons/back.dart'; +import 'package:guam_community_client/commons/common_text_field.dart'; +import 'package:guam_community_client/commons/custom_app_bar.dart'; +import 'package:guam_community_client/commons/custom_divider.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/models/boards/category.dart'; +import 'package:guam_community_client/models/boards/post.dart'; +import 'package:guam_community_client/providers/posts/posts.dart'; +import 'package:guam_community_client/screens/boards/comments/comments.dart'; +import 'package:guam_community_client/screens/boards/posts/detail/post_detail_banner.dart'; +import 'package:guam_community_client/screens/boards/posts/detail/post_detail_body.dart'; +import 'package:guam_community_client/screens/boards/posts/detail/post_detail_more.dart'; +import 'package:guam_community_client/screens/boards/posts/post_info.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; + +import '../../../../commons/functions_category_boardType.dart'; +import '../../../../providers/messages/messages.dart'; +import '../../../../providers/user_auth/authenticate.dart'; + +class PostDetail extends StatefulWidget { + final Post post; + + PostDetail(this.post); + + @override + State createState() => _PostDetailState(); +} + +class _PostDetailState extends State with Toast { + Post _post; + Future comments; + bool isScrapped; + int scrapCount; + bool commentImageExist = false; + final int maxRenderImgCnt = 4; + Set mentionListId = {}; + List> mentionList = []; + List> result = []; + + @override + void initState() { + _post = widget.post; + mentionList = [_post.profile.toJson()]; + mentionListId = {_post.profile.id}; + comments = context.read().fetchComments(_post.id); + isScrapped = widget.post.isScrapped; + scrapCount = widget.post.scrapCount; + super.initState(); + } + + /// 게시글 수정 시, API의 request는 Client가 들고있다는 원칙 및 서버 통신 성공 가정 하에 + /// 수정 버튼 클릭하면 사용자에게 수정 내용 바로 반영되도록 만듦. + getEditedPost(Map editedPost) { + if (editedPost == null) return; + int editedBoardId = int.parse(editedPost['boardId']); + int editedCategoryId = int.parse(editedPost['categoryId']); + + Category editedCategory = Category( + postId: widget.post.id, + categoryId: editedCategoryId, + title: transferCategoryId(editedCategoryId), + ); + + setState(() { + _post.title = editedPost['title']; + _post.content = editedPost['content']; + _post.boardType = transferBoardId(editedBoardId); + _post.category = editedCategory; + }); + } + + void addCommentImage() { + setState(() => commentImageExist = true); + } + + void removeCommentImage() { + setState(() => commentImageExist = false); + } + + @override + void dispose() { + mentionList.clear(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final postsProvider = context.watch(); + Authenticate authProvider = context.read(); + + void fetchComments() { + comments = postsProvider.fetchComments(_post.id); + } + + Future createComment({Map fields, dynamic files}) async { + return await postsProvider.createComment( + postId: _post.id, + fields: fields, + files: files, + ).then((successful) { + if (successful) fetchComments(); + return successful; + }); + } + + Future scrapOrUnscrapPost() async { + try { + if (!isScrapped) { + return await postsProvider.scrapPost( + postId: widget.post.id, + ).then((successful) { + if (successful) { + setState(() { + isScrapped = true; + scrapCount ++; + }); + } else { + return postsProvider.fetchPosts(postsProvider.boardId); + } + }); + } else { + return await postsProvider.unscrapPost( + postId: widget.post.id, + ).then((successful) { + if (successful) { + setState(() { + isScrapped = !isScrapped; + scrapCount --; + }); + } else { + return postsProvider.fetchPosts(postsProvider.boardId); + } + }); + } + } catch (e) { + print(e); + } + } + + return Portal( + child: Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + leading: Back(), + trailing: Padding( + padding: EdgeInsets.only(right: 11), + child: Row( + children: [ + // IconButton( + // padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10), + // constraints: BoxConstraints(), + // icon: SvgPicture.asset(isScrapped + // ? 'assets/icons/scrap_filled.svg' + // : 'assets/icons/scrap_outlined.svg' + // ), + // onPressed: scrapOrUnscrapPost, + // ), + IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/more.svg'), + onPressed: () => showModalBottomSheet( + /// DetailMore에서 Detail로 수정된 게시글 정보 넘기기 + context: context, + useRootNavigator: true, + builder: (_) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Posts(authProvider)), + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: PostDetailMore(_post, getEditedPost) + ), + backgroundColor: GuamColorFamily.grayscaleWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + ), + ), + ], + ), + ), + ), + body: RefreshIndicator( + color: Color(0xF9F8FFF), // GuamColorFamily.purpleLight1 + onRefresh: () => context.read().getPost(widget.post.id), + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Padding( + padding: EdgeInsets.only( + left: 24, + right: 24, + bottom: commentImageExist ? 156 : 56, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PostDetailBanner(_post), + Padding( + padding: EdgeInsets.only(top: 8, bottom: 20), + child: CustomDivider(color: GuamColorFamily.grayscaleGray7), + ), + PostDetailBody(_post), + Padding( + padding: EdgeInsets.only(top: 14, bottom: 8), + child: PostInfo( + post: _post, + iconSize: 24, + showProfile: false, + iconColor: GuamColorFamily.grayscaleGray4, + ), + ), + CustomDivider(color: GuamColorFamily.grayscaleGray7), + Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: FutureBuilder( + /// FutureBuilder의 future에 명시하는 비동기 함수가 반복해서 실행되는 + /// 문제를 해결하고자 initState에서 정의시킨다. + future: comments, + builder: (_, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + if (snapshot.data.isNotEmpty) { + /// FutureBuilder의 snapshot에서 들고 있어 map으로 뿌려주는 + /// Comment 인스턴스는 Provider에 속하지 않으므로 + /// snapshot에 직접 접근해서 지우는 방식을 택하자. + void deleteComment(int commentId) { + setState(() { + snapshot.data.removeWhere((c) => c.id == commentId); + }); + } + Future.delayed( + Duration.zero, () { + if (this.mounted) + setState(() => snapshot.data.forEach((e) { + if (!mentionListId.contains(e.profile.id)) + mentionList.add(e.profile.toJson()); + mentionListId.add(e.profile.id); + })); + } + ); + return Column( + children: [ + ...snapshot.data.map((comment) => Comments( + comment: comment, + deleteFunc: deleteComment, + isAuthor: _post.profile.id == comment.profile.id, + )) + ], + ); + } else { + return Padding( + padding: EdgeInsets.only(top: 24), + child: Center( + child: Text( + "작성된 댓글이 없습니다.", + style: TextStyle(fontSize: 13, color: GuamColorFamily.grayscaleGray5), + ), + ), + ); + } + } else if (snapshot.hasError) { + showToast(success: true, msg: '알 수 없는 오류가 발생했습니다.'); + return null; + } else { + return Center(child: CircularProgressIndicator( + color: GuamColorFamily.purpleLight3, + )); + } + } + ), + ), + ], + ), + ), + ), + ), + bottomSheet: Container( + color: Colors.black.withOpacity(0.3), + child: CommonTextField( + editTarget: null, + onTap: createComment, + mentionList: mentionList, + addImage: addCommentImage, + removeImage: removeCommentImage, + ), + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/detail/post_detail_banner.dart b/lib/screens/boards/posts/detail/post_detail_banner.dart new file mode 100644 index 00000000..d63b15d5 --- /dev/null +++ b/lib/screens/boards/posts/detail/post_detail_banner.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/color_of_category.dart'; +import 'package:guam_community_client/commons/common_img_nickname.dart'; +import 'package:guam_community_client/models/boards/post.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:intl/intl.dart'; + +class PostDetailBanner extends StatelessWidget { + final Post post; + + PostDetailBanner(this.post); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (post.category != null) + TextButton( + onPressed: null, + child: Text( + "#" + post.category.title, + style: TextStyle( + fontSize: 16, + color: colorOfCategory(post.category.title), + ), + ), + style: TextButton.styleFrom( + minimumSize: Size.zero, + padding: EdgeInsets.only(top: 24), + alignment: Alignment.centerLeft, + ), + ), + Padding( + padding: EdgeInsets.only(left: post.category == null ? 0 : 8), + child: TextButton( + onPressed: null, + child: Text( + post.boardType + '게시판', + style: TextStyle( + fontSize: 12, + color: colorOfCategory( + post.category != null ? post.category.title : '') + .withOpacity(0.5), + ), + ), + style: TextButton.styleFrom( + minimumSize: Size.zero, + padding: EdgeInsets.only(top: 24), + alignment: Alignment.centerLeft, + ), + ), + ), + ], + ), + Container( + padding: EdgeInsets.only(top: 8), + child: Text(post.title, style: TextStyle(fontSize: 18)), + ), + Padding( + padding: EdgeInsets.only(top: 8), + child: Row( + children: [ + CommonImgNickname( + userId: post.profile.id, + nickname: post.profile.nickname, + profileClickable: post.profile.id != 0, + // 익명 프로필은 프로필 열람 불가 + imgUrl: post.profile.profileImg ?? null, + nicknameColor: GuamColorFamily.grayscaleGray3, + ), + Spacer(), + Text( + DateFormat('yyyy.MM.dd HH:mm').format(DateTime.parse(post.createdAt)), + style: TextStyle( + fontSize: 12, + color: GuamColorFamily.grayscaleGray5, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/screens/boards/posts/detail/post_detail_body.dart b/lib/screens/boards/posts/detail/post_detail_body.dart new file mode 100644 index 00000000..f8585305 --- /dev/null +++ b/lib/screens/boards/posts/detail/post_detail_body.dart @@ -0,0 +1,85 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:guam_community_client/commons/image/image_carousel.dart'; +import 'package:guam_community_client/models/boards/post.dart'; +import 'package:guam_community_client/providers/posts/posts.dart'; +import 'package:guam_community_client/screens/boards/posts/post_image.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PostDetailBody extends StatelessWidget { + final int maxRenderImgCnt = 4; + final Post post; + + PostDetailBody(this.post); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.only(bottom: 64), + child: Linkify( + onOpen: (link) async { + if (await canLaunch(link.url)) { + await launch(link.url); + } else { + throw 'Could not launch $link'; + } + }, + text: post.content, + style: TextStyle( + height: 1.6, + fontSize: 14, + color: GuamColorFamily.grayscaleGray2, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + linkStyle: TextStyle( + height: 1.6, + fontSize: 14, + color: GuamColorFamily.blueCore, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + if (post.imagePaths.isNotEmpty) + GridView.builder( + physics: NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: min(post.imagePaths.length, maxRenderImgCnt), + crossAxisSpacing: 10, + childAspectRatio: 1, + ), + shrinkWrap: true, + itemCount: min(post.imagePaths.length, maxRenderImgCnt), + itemBuilder: (_, idx) => InkWell( + child: PostImage( + picture: post.imagePaths[idx], + blur: post.imagePaths.length > maxRenderImgCnt && idx == maxRenderImgCnt - 1, + hiddenImgCnt: post.imagePaths.length - maxRenderImgCnt, + ), + onTap: () { + Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider.value( + value: context.read(), // necessary? + child: ImageCarousel( + pictures: [...this.post.imagePaths], + initialPage: idx, + showImageActions: true, + ), + ) + ) + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/screens/boards/posts/detail/post_detail_more.dart b/lib/screens/boards/posts/detail/post_detail_more.dart new file mode 100644 index 00000000..01df2c3e --- /dev/null +++ b/lib/screens/boards/posts/detail/post_detail_more.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +import '../../../../commons/bottom_modal/bottom_modal_default.dart'; +import '../../../../commons/bottom_modal/bottom_modal_with_alert.dart'; +import '../../../../commons/bottom_modal/bottom_modal_with_message.dart'; +import '../../../../models/boards/post.dart'; +import '../../../../providers/messages/messages.dart'; +import '../../../../providers/posts/posts.dart'; +import '../../../../providers/user_auth/authenticate.dart'; +import '../creation/post_creation.dart'; +import '../post_comment_report.dart'; + +class PostDetailMore extends StatelessWidget { + final Post post; + final Function getEditedPost; + + PostDetailMore(this.post, this.getEditedPost); + + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + + void _navigatePage(BuildContext context) async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Posts(authProvider)), + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: PostCreation(isEdit: true, editTarget: post), + ), + ), + ); + getEditedPost(result); + } + + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.only(left: 24, top: 24, bottom: 21), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: post.isMine ? [ + BottomModalDefault( + text: '수정하기', + onPressed: () => _navigatePage(context), + ), + BottomModalWithAlert( + funcName: '삭제하기', + title: '게시글을 삭제하시겠어요?', + body: '삭제된 게시글은 복원할 수 없습니다.', + func: () async { + await context.read().deletePost(post.id) + .then((successful) { + if (successful) { + Navigator.of(context).pushNamedAndRemoveUntil( + '/', (route) => true + ); + } + }); + }, + ), + ] : [ + BottomModalDefault( + text: '쪽지 보내기', + onPressed: () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + builder: (context) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: SingleChildScrollView( + child: BottomModalWithMessage( + funcName: '보내기', + title: '${post.profile.nickname} 님에게 쪽지 보내기', + profile: post.profile, + func: null, + ), + ), + ), + ), + ), + // PostCommentReport(post.profile), + // BottomModalWithAlert( + // funcName: '차단하기', + // title: '${post.profile.nickname} 님을 차단하시겠어요?', + // body: '사용자를 차단하면, 해당 사용자의 게시글 및 댓글을 확인 할 수 없으며, 서로 쪽지를 주고 받을 수 없습니다.\n\n차단계정 관리는 프로필>계정 설정> 차단 목록 관리 탭에서 확인 가능합니다', + // func: () {}, + // ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/post_button.dart b/lib/screens/boards/posts/post_button.dart new file mode 100644 index 00000000..c02dcff7 --- /dev/null +++ b/lib/screens/boards/posts/post_button.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; + +import '../../../providers/posts/posts.dart'; +import 'creation/post_creation.dart'; + +class PostButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + height: 56, + width: 56, + child: FittedBox( + child: FloatingActionButton( + onPressed: () {}, + backgroundColor: GuamColorFamily.purpleCore, + child: IconButton( + icon: SvgPicture.asset( + 'assets/icons/write.svg', + color: GuamColorFamily.grayscaleWhite, + width: 30, + height: 30, + ), + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ChangeNotifierProvider.value( + value: context.read(), + child: PostCreation(), + ) + ) + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/post_comment_report.dart b/lib/screens/boards/posts/post_comment_report.dart new file mode 100644 index 00000000..dec51108 --- /dev/null +++ b/lib/screens/boards/posts/post_comment_report.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/commons/bottom_modal/bottom_modal_default.dart'; +import 'package:guam_community_client/commons/bottom_modal/bottom_modal_with_choice.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; + +class PostCommentReport extends StatefulWidget { + final Profile profile; + + PostCommentReport(this.profile); + + @override + _PostCommentReportState createState() => _PostCommentReportState(); +} + +class _PostCommentReportState extends State { + String reportReason = ''; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BottomModalDefault( + text: '신고하기', + onPressed: () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: GuamColorFamily.grayscaleWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + builder: (context) => StatefulBuilder( + builder: (BuildContext context, StateSetter myState) { + return BottomModalWithChoice( + title: '사용자 신고하기', + back: '취소', + body: '${widget.profile.nickname} 님을 신고하는 이유를 알려주세요', + alert: '허위 신고자는 서비스 이용에 불이익을 받을 수 있으니 신중하게 신고해주세요. 자세한 사항은 개발자 문의를 이용해주세요.', + confirm: '신고하기', + children: [ + _choice(myState, '욕설/비방/음담패설'), + _choice(myState, '사행성 게시물'), + _choice(myState, '불법 복제/무단 도용'), + _choice(myState, '게시글/댓글 도배'), + _choice(myState, '기타'), + ], + ); + } + ), + ), + ); + } + + Widget _choice(StateSetter myState, String choice) { + return Builder( + builder: (context) => InkWell( + onTap: () => myState(() => reportReason = choice), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: 24, + child: Text( + choice, + style: TextStyle( + fontSize: 16, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: choice == reportReason + ? GuamColorFamily.purpleCore + : GuamColorFamily.grayscaleGray3, + ), + ), + ), + if (choice == reportReason) + IconButton( + padding: EdgeInsets.only(right: 8), + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/check.svg'), + onPressed: null, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/post_image.dart b/lib/screens/boards/posts/post_image.dart new file mode 100644 index 00000000..aa189e2a --- /dev/null +++ b/lib/screens/boards/posts/post_image.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/image/image_container.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class PostImage extends StatelessWidget { + final String picture; /// 추후 String -> Picture model로... + final bool blur; + final int hiddenImgCnt; + + PostImage({this.picture, this.blur, this.hiddenImgCnt}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container( + height: double.infinity, + width: double.infinity, + child: IgnorePointer( + ignoring: true, + child: ImageThumbnail( + imagePath: picture, /// 추후 picture.urlPath 로... + height: 100, + width: 100, + ), + ), + ), + if (blur) + Container( + decoration: BoxDecoration( + color: Color.fromRGBO(120, 120, 120, 0.5), + borderRadius: BorderRadius.circular(8), + ), + ), + if (blur) + Center( + child: Text( + "+ $hiddenImgCnt", + style: TextStyle( + fontSize: 12, + color: GuamColorFamily.grayscaleWhite, + ), + ), + ) + ], + ); + } +} diff --git a/lib/screens/boards/posts/post_info.dart b/lib/screens/boards/posts/post_info.dart new file mode 100644 index 00000000..5b66cf1b --- /dev/null +++ b/lib/screens/boards/posts/post_info.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/icon_text.dart'; +import 'package:guam_community_client/models/boards/post.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:hexcolor/hexcolor.dart'; +import 'package:provider/provider.dart'; + +import '../../../commons/common_img_nickname.dart'; +import '../../../providers/posts/posts.dart'; + +class PostInfo extends StatefulWidget { + final Post post; + final double iconSize; + final bool showProfile; + final bool profileClickable; + final HexColor iconColor; + + PostInfo({ + this.post, + this.iconSize = 20, + this.showProfile = true, + this.profileClickable = true, + this.iconColor, + }); + + @override + State createState() => _PostInfoState(); +} + +class _PostInfoState extends State { + bool isLiked; + bool isScrapped; + int likeCount; + int scrapCount; + + @override + void initState() { + isLiked = widget.post.isLiked; + isScrapped = widget.post.isScrapped; + likeCount = widget.post.likeCount; + scrapCount = widget.post.scrapCount; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final postsProvider = context.watch(); + + Future likeOrUnlikePost() async { + try { + if (!isLiked) { + return await postsProvider.likePost( + postId: widget.post.id, + ).then((successful) { + if (successful) { + setState(() { + isLiked = true; + likeCount ++; + }); + } else { + return postsProvider.fetchPosts(postsProvider.boardId); + } + }); + } else { + return await postsProvider.unlikePost( + postId: widget.post.id, + ).then((successful) { + if (successful) { + setState(() { + isLiked = !isLiked; + likeCount --; + }); + } else { + return postsProvider.fetchPosts(postsProvider.boardId); + } + }); + } + } catch (e) { + print(e); + } + } + + Future scrapOrUnscrapPost() async { + try { + if (!isScrapped) { + return await postsProvider.scrapPost( + postId: widget.post.id, + ).then((successful) { + if (successful) { + setState(() { + isScrapped = true; + scrapCount ++; + }); + } else { + return postsProvider.fetchPosts(postsProvider.boardId); + } + }); + } else { + return await postsProvider.unscrapPost( + postId: widget.post.id, + ).then((successful) { + if (successful) { + setState(() { + isScrapped = !isScrapped; + scrapCount --; + }); + } else { + return postsProvider.fetchPosts(postsProvider.boardId); + } + }); + } + } catch (e) { + print(e); + } + } + + return Padding( + padding: EdgeInsets.only(top: 8, bottom: 12), + child: Row( + children: [ + if (widget.showProfile) + CommonImgNickname( + imgUrl: widget.post.profile.profileImg, + nickname: widget.post.profile.nickname, + profileClickable: widget.profileClickable, + nicknameColor: GuamColorFamily.grayscaleGray2, + ), + if (widget.showProfile) Spacer(), + Row( + children: [ + IconText( + iconSize: widget.iconSize, + text: likeCount.toString(), + iconPath: isLiked + ? 'assets/icons/like_filled.svg' + : 'assets/icons/like_outlined.svg', + onPressed: likeOrUnlikePost, + iconColor: isLiked + ? GuamColorFamily.redCore + : widget.iconColor, + textColor: widget.iconColor, + ), + IconText( + iconSize: widget.iconSize, + text: widget.post.commentCount.toString(), + iconPath: 'assets/icons/comment.svg', + iconColor: widget.iconColor, + textColor: widget.iconColor, + ), + IconText( + iconSize: widget.iconSize, + text: scrapCount.toString(), + iconPath: isScrapped + ? 'assets/icons/scrap_filled.svg' + : 'assets/icons/scrap_outlined.svg', + onPressed: scrapOrUnscrapPost, + iconColor: isScrapped + ? GuamColorFamily.purpleCore + : widget.iconColor, + textColor: widget.iconColor, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/screens/boards/posts/post_list.dart b/lib/screens/boards/posts/post_list.dart new file mode 100644 index 00000000..9ffae438 --- /dev/null +++ b/lib/screens/boards/posts/post_list.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:guam_community_client/providers/posts/posts.dart'; +import 'package:guam_community_client/models/boards/post.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/screens/boards/posts/preview/post_preview.dart'; + +import '../../../commons/sub_headings.dart'; +import '../../search/search_filter.dart'; + +class PostList extends StatefulWidget { + final List posts; + + PostList(this.posts); + + @override + State createState() => _PostListState(); +} + +class _PostListState extends State { + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration(color: GuamColorFamily.purpleLight3), // backgrounds color + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 24, left: 22, right: 10, bottom: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SubHeadings("특별한 일이 있나요? ✨"), + SearchFilter(provider: context.read()), + ], + ), + ), + Padding( + padding: EdgeInsets.only(bottom: 10), + child: Column( + children: [...widget.posts.map((post) => PostPreview(post))] + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/boards/posts/preview/post_preview.dart b/lib/screens/boards/posts/preview/post_preview.dart new file mode 100644 index 00000000..3befe2cc --- /dev/null +++ b/lib/screens/boards/posts/preview/post_preview.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/guam_progress_indicator.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/models/boards/post.dart'; +import 'package:guam_community_client/providers/messages/messages.dart'; +import 'package:guam_community_client/providers/posts/posts.dart'; +import 'package:guam_community_client/screens/app/tab_item.dart'; +import 'package:guam_community_client/screens/boards/posts/detail/post_detail.dart'; +import 'package:guam_community_client/screens/boards/posts/preview/post_preview_home_tab.dart'; +import 'package:guam_community_client/screens/boards/posts/preview/post_preview_search_tab.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; +import '../../../../providers/home/home_provider.dart'; +import '../../../../providers/user_auth/authenticate.dart'; + +class PostPreview extends StatelessWidget with Toast { + final Post post; + + PostPreview(this.post); + + @override + Widget build(BuildContext context) { + Posts postProvider = context.read(); + Authenticate authProvider = context.read(); + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Container( + padding: EdgeInsets.only(left: 24, right: 24, top: 12), + decoration: BoxDecoration( + color: GuamColorFamily.grayscaleWhite, + borderRadius: BorderRadius.circular(24), + ), + child: InkWell( + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Posts(authProvider)), + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: FutureBuilder( + future: postProvider.getPost(post.id), + builder: (_, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return PostDetail(snapshot.data); + } else if (snapshot.hasError) { + Navigator.pop(context); + postProvider.fetchPosts(0); + showToast(success: false, msg: '게시글을 찾을 수 없습니다.'); + return null; + } else { + return Center(child: guamProgressIndicator()); + } + } + ), + ), + ), + ); + }, + child: TabItem.values[context.read().idx] == TabItem.search + ? PostPreviewSearchTab(post) + : PostPreviewHomeTab(post) + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/preview/post_preview_board_type.dart b/lib/screens/boards/posts/preview/post_preview_board_type.dart new file mode 100644 index 00000000..f3ecc5b4 --- /dev/null +++ b/lib/screens/boards/posts/preview/post_preview_board_type.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class PostPreviewBoardType extends StatelessWidget { + final String boardType; + + PostPreviewBoardType(this.boardType); + + @override + Widget build(BuildContext context) { + return Text( + boardType, + style: TextStyle( + fontSize: 12, + height: 19.2/12, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray4, + ), + ); + } +} diff --git a/lib/screens/boards/posts/preview/post_preview_category.dart b/lib/screens/boards/posts/preview/post_preview_category.dart new file mode 100644 index 00000000..5c811eca --- /dev/null +++ b/lib/screens/boards/posts/preview/post_preview_category.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/color_of_category.dart'; +import 'package:guam_community_client/models/boards/post.dart'; + +class PostPreviewCategory extends StatelessWidget { + final Post post; + + PostPreviewCategory(this.post); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 17, + child: TextButton( + onPressed: null, + child: Text( + "#" + post.category.title, + style: TextStyle( + fontSize: 12, + color: colorOfCategory(post.category.title), + ), + ), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + alignment: Alignment.centerLeft, + ), + ), + ); + } +} diff --git a/lib/screens/boards/posts/preview/post_preview_content.dart b/lib/screens/boards/posts/preview/post_preview_content.dart new file mode 100644 index 00000000..d4e1adf1 --- /dev/null +++ b/lib/screens/boards/posts/preview/post_preview_content.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class PostPreviewContent extends StatelessWidget { + final String content; + + PostPreviewContent(this.content); + + @override + Widget build(BuildContext context) { + return Text( + content, + maxLines: 4, + overflow: TextOverflow.ellipsis, + style: TextStyle( + height: 20.8/13, + fontSize: 13, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray3, + ), + ); + } +} diff --git a/lib/screens/boards/posts/preview/post_preview_home_tab.dart b/lib/screens/boards/posts/preview/post_preview_home_tab.dart new file mode 100644 index 00000000..1779648c --- /dev/null +++ b/lib/screens/boards/posts/preview/post_preview_home_tab.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:guam_community_client/commons/custom_divider.dart'; +import 'package:guam_community_client/models/boards/post.dart'; +import 'package:guam_community_client/screens/boards/posts/preview/post_preview_category.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +import './post_preview_board_type.dart'; +import './post_preview_content.dart'; +import './post_preview_relative_time.dart'; +import './post_preview_title.dart'; +import '../post_info.dart'; + +class PostPreviewHomeTab extends StatelessWidget { + final Post post; + final bool isAnonymous; + + PostPreviewHomeTab(this.post) : this.isAnonymous = post.boardType == '익명게시판'; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + this.isAnonymous + ? PostPreviewBoardType(this.post.boardType) + : post.category != null + ? PostPreviewCategory(post) + : Container(), + Spacer(), + PostPreviewRelativeTime(DateTime.parse(this.post.createdAt)), + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: CustomDivider(color: GuamColorFamily.grayscaleGray7), + ), + Row( + children: [ + if (post.imagePaths.isNotEmpty) + IconButton( + onPressed: null, + padding: EdgeInsets.only(right: 4), + constraints: BoxConstraints(), + icon: SvgPicture.asset( + 'assets/icons/picture.svg', + color: GuamColorFamily.grayscaleGray5, + width: 20, + height: 20, + ), + ), + PostPreviewTitle(this.post.title), + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: PostPreviewContent(this.post.content), + ), + PostInfo( + post: post, + iconColor: GuamColorFamily.grayscaleGray5, + profileClickable: false, + ), + ], + ); + } +} diff --git a/lib/screens/boards/posts/preview/post_preview_relative_time.dart b/lib/screens/boards/posts/preview/post_preview_relative_time.dart new file mode 100644 index 00000000..c586cc0c --- /dev/null +++ b/lib/screens/boards/posts/preview/post_preview_relative_time.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:jiffy/jiffy.dart'; + +class PostPreviewRelativeTime extends StatelessWidget { + final DateTime t; + + PostPreviewRelativeTime(this.t); + + @override + Widget build(BuildContext context) { + return Text( + Jiffy(t).fromNow(), + style: TextStyle( + fontSize: 10, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray4, + ) + ); + } +} diff --git a/lib/screens/boards/posts/preview/post_preview_search_tab.dart b/lib/screens/boards/posts/preview/post_preview_search_tab.dart new file mode 100644 index 00000000..7ca9c818 --- /dev/null +++ b/lib/screens/boards/posts/preview/post_preview_search_tab.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:guam_community_client/commons/custom_divider.dart'; +import 'package:guam_community_client/models/boards/post.dart'; +import 'package:guam_community_client/screens/boards/posts/preview/post_preview_category.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +import '../post_info.dart'; +import './post_preview_board_type.dart'; +import './post_preview_relative_time.dart'; +import './post_preview_content.dart'; +import './post_preview_title.dart'; + +class PostPreviewSearchTab extends StatelessWidget { + final Post post; + final bool isAnonymous; + + PostPreviewSearchTab(this.post): + this.isAnonymous = post.boardType == '익명게시판'; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (post.imagePaths.isNotEmpty) + IconButton( + onPressed: null, + padding: EdgeInsets.only(right: 4), + constraints: BoxConstraints(), + icon: SvgPicture.asset( + 'assets/icons/picture.svg', + color: GuamColorFamily.grayscaleGray5, + width: 20, + height: 20, + ), + ), + PostPreviewTitle(this.post.title), + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: CustomDivider(color: GuamColorFamily.grayscaleGray7), + ), + if (!this.isAnonymous && post.category != null) PostPreviewCategory(post), + Padding( + padding: EdgeInsets.symmetric(vertical: 4), + child: PostPreviewContent(this.post.content), + ), + Row( + children: [ + PostPreviewBoardType(this.post.boardType), + Spacer(), + PostPreviewRelativeTime(DateTime.parse(this.post.createdAt)), + ], + ), + PostInfo( + post: post, + iconColor: GuamColorFamily.grayscaleGray5, + profileClickable: false, + ) + ], + ); + } +} diff --git a/lib/screens/boards/posts/preview/post_preview_title.dart b/lib/screens/boards/posts/preview/post_preview_title.dart new file mode 100644 index 00000000..a342e186 --- /dev/null +++ b/lib/screens/boards/posts/preview/post_preview_title.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class PostPreviewTitle extends StatelessWidget { + final String title; + + PostPreviewTitle(this.title); + + @override + Widget build(BuildContext context) { + return Text( + title, + style: TextStyle(fontSize: 14), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } +} diff --git a/lib/screens/login/login_button.dart b/lib/screens/login/login_button.dart new file mode 100644 index 00000000..d2095610 --- /dev/null +++ b/lib/screens/login/login_button.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:hexcolor/hexcolor.dart'; + +class LoginButton extends StatelessWidget { + final String logo; + final String platform; + final HexColor color; + final Function onTap; + + LoginButton(this.logo, this.platform, this.color, this.onTap); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(bottom: 16), + child: InkWell( + // onTap: () => Navigator.of(context).push( + // MaterialPageRoute(builder: (_) => SignUp()), + // ), + onTap: onTap, + child: Container( + alignment: Alignment.center, + height: 56, + width: MediaQuery.of(context).size.width * 0.9, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.all(Radius.circular(10)), + border: Border.all(color: GuamColorFamily.grayscaleGray6, width: 1), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: null, + padding: EdgeInsets.only(right: 8), + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/logos/$logo.svg'), + ), + Text( + platform, + style: TextStyle( + color: GuamColorFamily.grayscaleGray1, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/login/login_buttons.dart b/lib/screens/login/login_buttons.dart new file mode 100644 index 00000000..c6b1f84d --- /dev/null +++ b/lib/screens/login/login_buttons.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'login_button.dart'; +import '../user_auth/kakao_login.dart'; + +class LoginButtons extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Column( + children: [ + KakaoLogin(), + // LoginButton('google_logo', '구글로 시작하기', GuamColorFamily.grayscaleWhite, () {}), + ], + ); + } +} diff --git a/lib/screens/login/login_page.dart b/lib/screens/login/login_page.dart new file mode 100644 index 00000000..13f8a796 --- /dev/null +++ b/lib/screens/login/login_page.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/helpers/svg_provider.dart'; +import 'package:guam_community_client/screens/app/splash/splash_text.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:hexcolor/hexcolor.dart'; +import 'login_buttons.dart'; + +class LoginPage extends StatelessWidget { + @override Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + alignment: Alignment.topCenter, + children: [ + Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.fitWidth, + alignment: Alignment.bottomCenter, + image: AssetImage('assets/backgrounds/back_0.75x.png'), + ), + ), + ), + Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + image: DecorationImage( + fit: BoxFit.fitWidth, + alignment: Alignment.bottomCenter, + image: AssetImage('assets/backgrounds/front_0.75x.png'), + ), + ), + child: Stack( + children: [ + // 위에서부터 아래로 star 배치 + _star( + width: 26, height: 26, // size of star + top: size.height*0.12, left: size.width*0.24, // position of star (iPhone 13 : 375x812) + color: GuamColorFamily.purpleDark1, + ), + _star( + width: 32, height: 32, + top: size.height*0.19, left: size.width*0.88, + color: GuamColorFamily.purpleCore, + ), + _star( + width: 25, height: 25, + top: size.height*0.32, left: size.width*0.42, + color: GuamColorFamily.purpleLight1, + ), + _star( + width: 25, height: 25, + top: size.height*0.50, left: size.width*0.76, + color: GuamColorFamily.purpleLight2, + ), + _star( + width: 32, height: 32, + top: size.height*0.57, left: size.width*0.16, + color: GuamColorFamily.purpleLight2, + ), + _star( + width: 25, height: 25, + top: size.height*0.68, left: size.width*0.57, + color: GuamColorFamily.purpleLight3, + ), + SplashText(animation: false), + ], + ), + ), + Positioned( + top: size.height*0.75, + child: LoginButtons(), + ), + ], + ), + ); + } + + Widget _star({double top, double left, double width, double height, HexColor color}){ + return Positioned( + top: top, + left: left, + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + image: DecorationImage( + image: SvgProvider( + 'assets/backgrounds/splash/star_splash.svg', + color: color, + ), + ), + ) + ), + ); + } +} diff --git a/lib/screens/login/signup/interest_choice_chip.dart b/lib/screens/login/signup/interest_choice_chip.dart new file mode 100644 index 00000000..287bd539 --- /dev/null +++ b/lib/screens/login/signup/interest_choice_chip.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class InterestChoiceChip extends StatefulWidget { + final List interestOptions; + final List interestList; + final Function(List) onSelectionChanged; + + InterestChoiceChip({this.interestOptions, this.interestList, this.onSelectionChanged}); + + @override + _InterestChoiceChipState createState() => _InterestChoiceChipState(); +} + +class _InterestChoiceChipState extends State { + @override + Widget build(BuildContext context) { + return Wrap( + children: _buildChoiceList(), + ); + } + + _buildChoiceList() { + List choices = []; + List selectedInterests = widget.interestList; + + widget.interestOptions.forEach((interest) { + choices.add( + Padding( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: ChoiceChip( + label: Container( + width: double.infinity, + padding: EdgeInsets.zero, + alignment: Alignment.center, + constraints: BoxConstraints(maxHeight: 40), + child: Text( + interest, + style: TextStyle( + fontSize: 16, + color: selectedInterests.contains(interest) + ? GuamColorFamily.purpleCore + : GuamColorFamily.grayscaleGray2, + ), + ), + ), + selected: selectedInterests.contains(interest), + selectedColor: GuamColorFamily.purpleLight2, + backgroundColor: GuamColorFamily.grayscaleWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + side: BorderSide( + color: selectedInterests.contains(interest) + ? GuamColorFamily.purpleCore + : GuamColorFamily.grayscaleGray6 + ), + ), + onSelected: (selected) { + setState(() { + selectedInterests.contains(interest) + ? selectedInterests.remove(interest) + : selectedInterests.add(interest); + widget.onSelectionChanged(selectedInterests); + }); + }, + ), + ) + ); + }); + return choices; + } +} diff --git a/lib/screens/login/signup/signup.dart b/lib/screens/login/signup/signup.dart new file mode 100644 index 00000000..6f064283 --- /dev/null +++ b/lib/screens/login/signup/signup.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/next_button.dart'; +import 'package:guam_community_client/screens/login/signup/signup_nickname.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; +import '../../../providers/user_auth/authenticate.dart'; + +class SignUp extends StatefulWidget { + @override + _SignUpState createState() => _SignUpState(); +} + +class _SignUpState extends State { + Map input = {}; + int pageIdx = 0; + + + @override + Widget build(BuildContext context) { + List pages = [ + SignupNickname(input) + ]; + + Size size = MediaQuery.of(context).size; + + Future signUp() async { + await context.read().setProfile(fields: input); + } + + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + body: SingleChildScrollView( + child: Container( + width: double.infinity, + height: size.height, + color: GuamColorFamily.grayscaleWhite, + padding: EdgeInsets.only(left: 16, top: size.height*0.11, right: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + pages[pageIdx], + Padding( + padding: EdgeInsets.only(bottom: 40), + child: NextButton( + label: pageIdx < pages.length - 1 ? '다음' : '시작', + onTap: pageIdx < pages.length - 1 ? pageIdx++ : signUp + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/login/signup/signup_interest.dart b/lib/screens/login/signup/signup_interest.dart new file mode 100644 index 00000000..b09e18e9 --- /dev/null +++ b/lib/screens/login/signup/signup_interest.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/screens/login/signup/interest_choice_chip.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + + +class SignupInterest extends StatefulWidget { + final Map input; + + SignupInterest(this.input); + + @override + State createState() => _SignupInterestState(); +} + +class _SignupInterestState extends State { + final List interestOptions = [ + '🛠 개발', '📈 데이터분석', '🎨 디자인','📝 기획/마케팅', '🎁 기타', + ]; + Map results = {}; + + @override + Widget build(BuildContext context) { + final size = MediaQuery.of(context).size; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton( + onPressed: null, + padding: EdgeInsets.all(4.67), + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/baloon.svg'), + ), + Padding( + padding: EdgeInsets.only(top: 10), + child: Text( + '관심사를 모두 선택해주세요.', + style: TextStyle( + height: 1.6, + fontSize: 24, + color: GuamColorFamily.grayscaleGray1, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + '맞춤형 피드를 위해 관심사를 알려주세요.', + style: TextStyle( + height: 1.6, + fontSize: 18, + color: GuamColorFamily.grayscaleGray3, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + _selectInterests(interestOptions, size) + ], + ); + } + + Widget _selectInterests(List interestOptions, Size size) { + return Container( + width: size.width, + padding: EdgeInsets.only(top: 80), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InterestChoiceChip( + interestList: widget.input['interest'].cast(), // convert List to List + interestOptions: interestOptions, + onSelectionChanged: (selectedList) { + setState(() => widget.input['interest'] = selectedList); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/login/signup/signup_nickname.dart b/lib/screens/login/signup/signup_nickname.dart new file mode 100644 index 00000000..244e5976 --- /dev/null +++ b/lib/screens/login/signup/signup_nickname.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class SignupNickname extends StatefulWidget { + final Map input; + + SignupNickname(this.input); + + @override + State createState() => _SignupNicknameState(); +} + +class _SignupNicknameState extends State { + final _nicknameTextFieldController = TextEditingController(); + + @override + void initState() { + _nicknameTextFieldController.text = widget.input['nickname']; + super.initState(); + } + + @override + void dispose() { + _nicknameTextFieldController.dispose(); + super.dispose(); + } + + void _setNickname(String nickname) => + setState(() => widget.input['nickname'] = nickname); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconButton( + onPressed: null, + padding: EdgeInsets.all(4.67), + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/baloon.svg'), + ), + Padding( + padding: EdgeInsets.only(top: 10), + child: Text( + 'IT인들의 괌에\n오신 것을 환영합니다!', + style: TextStyle( + height: 1.6, + fontSize: 24, + color: GuamColorFamily.grayscaleGray1, + ), + ), + ), + Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + '사용하실 닉네임을\n10자 이내로 입력해주세요.', + style: TextStyle( + height: 1.6, + fontSize: 18, + color: GuamColorFamily.grayscaleGray3, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + TextField( + style: TextStyle(fontSize: 16), + keyboardType: TextInputType.name, + controller: _nicknameTextFieldController, + minLines: 1, + maxLength: widget.input['nickname'] == '' ? null : 10, + onChanged: (e) { + _setNickname(_nicknameTextFieldController.text); + // _checkButtonEnable(); + }, + cursorColor: GuamColorFamily.purpleCore, + decoration: InputDecoration( + hintText: "ex) 크로플보다와플", + hintStyle: TextStyle( + fontSize: 16, + color: GuamColorFamily.grayscaleGray4, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + counterStyle: TextStyle( + color: GuamColorFamily.purpleCore, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + fontSize: 12, + ), + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + border: UnderlineInputBorder(borderSide: BorderSide(color: GuamColorFamily.purpleCore)), + focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: GuamColorFamily.purpleCore)), + enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: GuamColorFamily.grayscaleGray5)), + contentPadding: EdgeInsets.only(left: 14, top: 77, bottom: 13), + ), + ), + ], + ); + } +} diff --git a/lib/screens/messages/message_body.dart b/lib/screens/messages/message_body.dart new file mode 100644 index 00000000..21abf7fb --- /dev/null +++ b/lib/screens/messages/message_body.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:guam_community_client/commons/back.dart'; +import 'package:guam_community_client/commons/custom_app_bar.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/providers/messages/messages.dart'; +import 'package:guam_community_client/screens/messages/message_box_edit.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; + +import '../../commons/guam_progress_indicator.dart'; +import '../../providers/user_auth/authenticate.dart'; +import 'message_preview.dart'; + +class MessageBody extends StatefulWidget { + @override + State createState() => _MessageBodyState(); +} + +class _MessageBodyState extends State with Toast { + List _messageBoxes = []; + bool _isFirstLoadRunning = false; + ScrollController _scrollController = ScrollController(); + + void _firstLoad() async { + setState(() => _isFirstLoadRunning = true); + try { + await context.read().fetchMessageBoxes(); + _messageBoxes = context.read().messageBoxes; + } catch (err) { + print('알 수 없는 오류가 발생했습니다.'); + } + setState(() => _isFirstLoadRunning = false); + } + + @override + void initState() { + _firstLoad(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + title: '쪽지함', + leading: Back(), + trailing: Padding( + padding: EdgeInsets.only(right: 14), + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/delete_outlined.svg'), + onPressed: (_messageBoxes == null || _messageBoxes.isEmpty) + ? null + : () => Navigator.of(context, rootNavigator: true).push( + PageRouteBuilder( + pageBuilder: (_, __, ___) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: MessageBoxEdit(onRefresh: _firstLoad), + ), + transitionDuration: Duration(seconds: 0), + ) + ), + ), + ), + ), + body: _isFirstLoadRunning + ? Center(child: guamProgressIndicator()) + : RefreshIndicator( + onRefresh: () async => _firstLoad(), + color: Color(0xF9F8FFF), // GuamColorFamily.purpleLight1 + child: Container( + height: double.infinity, + color: GuamColorFamily.grayscaleWhite, + padding: EdgeInsets.only(top: 18), + child: SingleChildScrollView( + controller: _scrollController, + physics: AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + if (_messageBoxes == null || _messageBoxes.isEmpty) + Center( + child: Padding( + padding: EdgeInsets.only(top: MediaQuery.of(context).size.height * 0.1), + child: Text( + '쪽지함이 비어있습니다.', + style: TextStyle( + fontSize: 16, + color: GuamColorFamily.grayscaleGray4, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + ), + if (_messageBoxes != null && _messageBoxes.isNotEmpty) + ..._messageBoxes.reversed.map((messageBox) => MessagePreview(messageBox, onRefresh: _firstLoad) + ) + ] + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/messages/message_bottom_modal.dart b/lib/screens/messages/message_bottom_modal.dart new file mode 100644 index 00000000..259d31bd --- /dev/null +++ b/lib/screens/messages/message_bottom_modal.dart @@ -0,0 +1,132 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:guam_community_client/commons/image/image_container.dart'; +import 'package:guam_community_client/helpers/pick_image.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:image_picker/image_picker.dart'; + +class MessageBottomModal extends StatefulWidget { + final Map input; + + MessageBottomModal(this.input); + + @override + _MessageBottomModalState createState() => _MessageBottomModalState(); +} + +class _MessageBottomModalState extends State { + final _messageTextFieldController = TextEditingController(); + Map input; + + @override + void initState() { + super.initState(); + input = widget.input; + input['image'] = []; + } + + @override + void dispose() { + input['image'].clear(); + _messageTextFieldController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final double maxImgSize = 80; + + Future setImageFile(PickedFile val) async { + setState(() { + if (val != null) widget.input['image'].add(val); + }); + } + + Future deleteImageFile() async { + setState(() => widget.input['image'].removeAt(0)); + } + + return Container( + decoration: BoxDecoration( + color: GuamColorFamily.grayscaleGray7, + borderRadius: BorderRadius.circular(5), + border: Border.all(color: GuamColorFamily.grayscaleGray6, width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + keyboardType: TextInputType.multiline, + controller: _messageTextFieldController, + onChanged: (e) => widget.input['text'] = e, + maxLines: 4, + maxLength: 200, + style: TextStyle(fontSize: 14, height: 1.6, color: GuamColorFamily.grayscaleGray2), + decoration: InputDecoration( + hintText: "내용을 입력해주세요.", + hintStyle: TextStyle(fontSize: 14, color: GuamColorFamily.grayscaleGray5), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: EdgeInsets.all(16), + ), + ), + Padding( + padding: EdgeInsets.only(left: 16, bottom: 16), + child: widget.input['image'].length == 0 + ? SizedBox( + height: maxImgSize, + width: maxImgSize, + child: Container( + decoration: BoxDecoration( + color: GuamColorFamily.grayscaleGray6, + borderRadius: BorderRadius.circular(8), + ), + child: IconButton( + iconSize: 24, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset( + 'assets/icons/plus.svg', + color: GuamColorFamily.purpleLight1, + ), + onPressed: () => pickImage(type: 'gallery') + .then((img) => setImageFile(img)), + ), + ), + ) + : Stack( + children: [ + Container( + child: ImageThumbnail( + width: maxImgSize, + height: maxImgSize, + image: Image( + image: FileImage(File(widget.input['image'][0].path)), + fit: BoxFit.fill, + ), + ), + ), + Positioned( + top: 2, + right: 2, + child: IconButton( + iconSize: 23, + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/cancel_filled.svg'), + onPressed: () => deleteImageFile(), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/messages/message_box.dart b/lib/screens/messages/message_box.dart new file mode 100644 index 00000000..7e2880f5 --- /dev/null +++ b/lib/screens/messages/message_box.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/providers/messages/messages.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../providers/user_auth/authenticate.dart'; +import 'message_body.dart'; + +class MessageBox extends StatelessWidget { + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: MessageBoxScaffold(), + ); + } +} + +class MessageBoxScaffold extends StatefulWidget { + @override + State createState() => _MessageBoxScaffoldState(); +} + +class _MessageBoxScaffoldState extends State { + final bool newMessage = true; + + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + Messages msgProvider = context.read(); + + return Padding( + padding: EdgeInsets.only(right: 4), + child: IconButton( + icon: SvgPicture.asset( + newMessage + ? 'assets/icons/message_new.svg' + : 'assets/icons/message_default.svg' + ), + onPressed: () async { + await msgProvider.fetchMessageBoxes(); + Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute(builder: (_) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: MessageBody(), + )) + ); + } + ), + ); + } +} diff --git a/lib/screens/messages/message_box_edit.dart b/lib/screens/messages/message_box_edit.dart new file mode 100644 index 00000000..601fecc7 --- /dev/null +++ b/lib/screens/messages/message_box_edit.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/custom_app_bar.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; +import '../../commons/guam_progress_indicator.dart'; +import '../../providers/messages/messages.dart'; +import 'message_preview.dart'; + +class MessageBoxEdit extends StatefulWidget { + final Function onRefresh; + + MessageBoxEdit({this.onRefresh}); + + @override + State createState() => _MessageBoxEditState(); +} + +class _MessageBoxEditState extends State { + List _messageBoxes = []; + bool _isFirstLoadRunning = false; + + void _firstLoad() async { + setState(() => _isFirstLoadRunning = true); + try { + await context.read().fetchMessageBoxes(); + _messageBoxes = context.read().messageBoxes; + } catch (err) { + print(err); + print('알 수 없는 오류가 발생했습니다.'); + } + setState(() => _isFirstLoadRunning = false); + } + + @override + void initState() { + _firstLoad(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + title: '쪽지함', + leading: null, + trailing: Padding( + padding: EdgeInsets.only(right: 11), + child: TextButton( + child: Text( + '완료', + style: TextStyle( + fontSize: 16, + color: GuamColorFamily.purpleCore, + fontFamily: GuamFontFamily.SpoqaHanSansNeoMedium, + ), + ), + onPressed: () { + widget.onRefresh(); + Navigator.pop(context); + }, + ) + ), + ), + body: _isFirstLoadRunning ? Container( + color: GuamColorFamily.grayscaleWhite, + child: Center(child: guamProgressIndicator()), + ) : Container( + color: GuamColorFamily.grayscaleWhite, + padding: EdgeInsets.only(top: 18), + child: Column( + children: [ + ..._messageBoxes.reversed.map((messageBox) => MessagePreview( + messageBox, + onRefresh: widget.onRefresh, + editable: true, + )) + ] + ), + ), + ); + } +} diff --git a/lib/screens/messages/message_detail.dart b/lib/screens/messages/message_detail.dart new file mode 100644 index 00000000..0aa7c0cd --- /dev/null +++ b/lib/screens/messages/message_detail.dart @@ -0,0 +1,191 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_mentions/flutter_mentions.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/commons/back.dart'; +import 'package:guam_community_client/commons/bottom_modal/bottom_modal_default.dart'; +import 'package:guam_community_client/commons/bottom_modal/bottom_modal_with_alert.dart'; +import 'package:guam_community_client/commons/common_text_field.dart'; +import 'package:guam_community_client/commons/custom_app_bar.dart'; +import 'package:guam_community_client/models/messages/message.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; +import 'package:guam_community_client/providers/messages/messages.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +import '../../commons/guam_progress_indicator.dart'; +import '../../providers/user_auth/authenticate.dart'; +import 'message_detail_body.dart'; + +class MessageDetail extends StatefulWidget { + final List messages; + final Profile otherProfile; + final Function onRefresh; + + MessageDetail(this.messages, this.otherProfile, this.onRefresh); + + @override + State createState() => _MessageDetailState(); +} + +class _MessageDetailState extends State { + List _messages; + bool commentImageExist = false; + bool _isFirstLoadRunning = false; + + void addMessageImage() { + setState(() => commentImageExist = true); + } + + void removeMessageImage() { + setState(() => commentImageExist = false); + } + + void _refreshMsg() async { + setState(() => _isFirstLoadRunning = true); + await context.read().getMessages(widget.otherProfile.id); + _messages = context.read().messages; + setState(() => _isFirstLoadRunning = false); + } + + @override + void initState() { + _refreshMsg(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + Messages msgProvider = context.read(); + Authenticate authProvider = context.read(); + + Future sendMessage({Map fields, List files}) async { + bool msgSended = false; + try { + await msgProvider.sendMessage( + fields: fields, + files: files, + ).then((successful) { + if (successful) { + msgSended = true; + _refreshMsg(); /// 쪽지 페이지 갱신 + widget.onRefresh(); /// 쪽지함 페이지 갱신 + } else { + print("Error!"); + } + }); + } catch (e) { + print(e); + } + return msgSended; + } + + return _isFirstLoadRunning ? Container( + color: GuamColorFamily.grayscaleWhite, + child: Center(child: guamProgressIndicator()), + ) : Portal( + child: Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + title: widget.otherProfile.nickname, + leading: Back(), + trailing: Padding( + padding: EdgeInsets.only(right: 11), + child: IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + icon: SvgPicture.asset('assets/icons/more.svg'), + onPressed: () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: GuamColorFamily.grayscaleWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ) + ), + builder: (context) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: Builder( + builder: (context) { + return SingleChildScrollView( + child: Container( + padding: EdgeInsets.only(left: 24, top: 24, bottom: 21), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BottomModalWithAlert( + funcName: '삭제하기', + title: '쪽지함을 삭제하시겠어요?', + body: '삭제된 쪽지는 복원할 수 없습니다.', + func: () async => await context.read() + .deleteMessageBox(widget.otherProfile.id) + .then((successful) async { + if (successful) { + widget.onRefresh(); /// 쪽지함 페이지 갱신 + Navigator.pop(context); + Navigator.pop(context); + Navigator.pop(context); + await context.read().fetchMessageBoxes(); + } + }), + ), + // BottomModalDefault( + // text: '신고하기', + // onPressed: (){}, + // ), + // BottomModalDefault( + // text: '차단하기', + // onPressed: (){}, + // ), + ], + ), + ), + ); + } + ) + ) + ), + ), + ), + ), + body: RefreshIndicator( + color: Color(0xF9F8FFF), // GuamColorFamily.purpleLight1 + onRefresh: () async => _refreshMsg(), + child: Container( + height: double.infinity, + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: Padding( + padding: EdgeInsets.only(bottom: 70), + child: Column( + children: [ + ..._messages.map((msg) => MessageDetailBody(msg, widget.otherProfile)) + ] + ), + ), + ), + ), + ), + bottomSheet: Container( + padding: EdgeInsets.only(bottom: 10), + color: GuamColorFamily.grayscaleWhite, + child: CommonTextField( + messageTo: widget.otherProfile.id, + sendButton: '전송', + onTap: sendMessage, + mentionList: [], + addImage: addMessageImage, + removeImage: removeMessageImage, + editTarget: null, + ), + ), + ), + ); + } +} diff --git a/lib/screens/messages/message_detail_body.dart b/lib/screens/messages/message_detail_body.dart new file mode 100644 index 00000000..812c57df --- /dev/null +++ b/lib/screens/messages/message_detail_body.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/helpers/http_request.dart'; +import 'package:guam_community_client/helpers/svg_provider.dart'; +import 'package:guam_community_client/models/messages/message.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:provider/provider.dart'; + +import '../../commons/custom_divider.dart'; +import '../../commons/image/image_carousel.dart'; +import '../../models/profiles/profile.dart'; +import '../../providers/user_auth/authenticate.dart'; + +class MessageDetailBody extends StatelessWidget { + final Message message; + final Profile otherProfile; + + MessageDetailBody(this.message, this.otherProfile); + + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + bool isMe = message.sentBy == authProvider.me.id; + Profile sentBy = isMe ? authProvider.me : otherProfile; + + return Container( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.cover, + image: sentBy.profileImg != null + ? NetworkImage(HttpRequest().s3BaseAuthority + sentBy.profileImg) + : SvgProvider('assets/icons/profile_image.svg') + ), + ), + ), + Padding( + padding: EdgeInsets.only(left: 6), + child: Text( + isMe ? sentBy.nickname + ' (나)': sentBy.nickname, + style: TextStyle( + fontSize: 12, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: isMe + ? GuamColorFamily.purpleCore + : GuamColorFamily.grayscaleGray2, + ), + ), + ), + Spacer(), + Text( + Jiffy(message.createdAt).fromNow(), + style: TextStyle( + fontSize: 12, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray4, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.only(left: 10, top: 8, bottom: 10), + child: Text( + message.text, + style: TextStyle( + height: 1.6, + fontSize: 13, + color: GuamColorFamily.grayscaleGray2, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + if (message.imagePath != null) + InkWell( + child: Padding( + padding: EdgeInsets.only(left: 5), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + fit: BoxFit.cover, + image: NetworkImage( + HttpRequest().s3BaseAuthority + message.imagePath), + ), + ), + ), + ), + onTap: () { + Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute( + builder: (_) => ImageCarousel( + pictures: [message.imagePath], + initialPage: 0, + showImageCount: false, + showImageActions: true, + ), + ), + ); + }, + ), + Padding( + padding: EdgeInsets.only(top: 8), + child: CustomDivider(color: GuamColorFamily.grayscaleGray7), + ), + ], + ), + ); + } +} diff --git a/lib/screens/messages/message_preview.dart b/lib/screens/messages/message_preview.dart new file mode 100644 index 00000000..72b7b07e --- /dev/null +++ b/lib/screens/messages/message_preview.dart @@ -0,0 +1,196 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/guam_progress_indicator.dart'; +import 'package:guam_community_client/helpers/http_request.dart'; +import 'package:guam_community_client/helpers/svg_provider.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/models/messages/message.dart' as Message; +import 'package:guam_community_client/models/messages/message_box.dart'; +import 'package:guam_community_client/providers/messages/messages.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:provider/provider.dart'; + +import '../../commons/bottom_modal/bottom_modal_with_alert.dart'; +import '../../providers/user_auth/authenticate.dart'; +import 'message_detail.dart'; + +class MessagePreview extends StatefulWidget { + final MessageBox messageBox; + final Function onRefresh; + final bool editable; + + MessagePreview(this.messageBox, {this.onRefresh, this.editable=false}); + + @override + State createState() => _MessagePreviewState(); +} + +class _MessagePreviewState extends State with Toast { + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Card( + elevation: 0, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: GuamColorFamily.grayscaleGray7, width: 1.5), + ), + child: Container( + padding: EdgeInsets.only(left: 12, top: 6, bottom: 6), + child: InkWell( + onTap: !widget.editable ? () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: FutureBuilder( + future: context.read().getMessages(widget.messageBox.otherProfile.id), + builder: (_, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return MessageDetail(snapshot.data, widget.messageBox.otherProfile, widget.onRefresh); + } else if (snapshot.hasError) { + Navigator.pop(context); + showToast(success: false, msg: '해당 쪽지를 찾을 수 없습니다.'); + return null; + } else { + return Container( + color: GuamColorFamily.grayscaleWhite, + child: Center(child: guamProgressIndicator()), + ); + } + }, + ), + ), + ), + ) : null, + child: Row( + children: [ + Stack( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.cover, + image: widget.messageBox.otherProfile.profileImg != null + ? NetworkImage(HttpRequest().s3BaseAuthority + widget.messageBox.otherProfile.profileImg) + : SvgProvider('assets/icons/profile_image.svg') + ), + ), + ), + if (!widget.messageBox.lastLetter.isRead) + Positioned( + top: 0, + child: CircleAvatar( + backgroundColor: GuamColorFamily.fuchsiaCore, + radius: 6, + ) + ), + ], + ), + Expanded( + child: Container( + padding: EdgeInsets.only(left: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.messageBox.otherProfile.nickname, + style: TextStyle( + fontSize: 12, + height: 1.6, + fontFamily: GuamFontFamily.SpoqaHanSansNeoMedium, + color: GuamColorFamily.grayscaleGray2, + ), + ), + Text( + widget.messageBox.lastLetter.text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + height: 1.6, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: widget.messageBox.lastLetter.isRead + ? GuamColorFamily.grayscaleGray4 + : GuamColorFamily.grayscaleGray2, + ), + ), + ], + ), + ), + ), + Spacer(), + if (widget.editable) + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: Builder( + builder: (context) { + return TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size(23, 20), + textStyle: TextStyle( + fontSize: 12, + fontFamily: GuamFontFamily.SpoqaHanSansNeoMedium, + color: GuamColorFamily.redCore, + ), + ), + child: BottomModalWithAlert( + funcName: '삭제', + title: '쪽지함을 삭제하시겠어요?', + body: '삭제된 쪽지는 복원할 수 없습니다.', + func: () async => await context.read() + .deleteMessageBox(widget.messageBox.otherProfile.id) + .then((successful) async { + context.read().fetchMessageBoxes(); + if (successful) { + // Navigator.pop(context); + Navigator.pop(context); + widget.onRefresh(); + await context.read().fetchMessageBoxes(); + } + }) + ), + ); + } + ), + ), + if (!widget.editable) + Padding( + padding: EdgeInsets.only(right: 10, top: 14, bottom: 15), + child: Text( + Jiffy(widget.messageBox.lastLetter.createdAt).fromNow(), + style: TextStyle( + fontSize: 12, + height: 1.6, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray4, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/notifications/notifications_app.dart b/lib/screens/notifications/notifications_app.dart new file mode 100644 index 00000000..41ec209a --- /dev/null +++ b/lib/screens/notifications/notifications_app.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:guam_community_client/commons/custom_divider.dart'; +import 'package:guam_community_client/providers/notifications/notifications.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:provider/provider.dart'; +import '../../providers/posts/posts.dart'; +import '../../providers/user_auth/authenticate.dart'; +import 'notifications_body.dart'; +import '../../commons/custom_app_bar.dart'; + +class NotificationsApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Posts(authProvider)), + ChangeNotifierProvider(create: (_) => Notifications(authProvider)), + ], + child: NotificationsAppScaffold(), + ); + } +} + + +class NotificationsAppScaffold extends StatefulWidget { + @override + State createState() => _NotificationsAppScaffoldState(); +} + +class _NotificationsAppScaffoldState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: NotificationsBody(), + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + title: '알림', + // trailing: Padding( + // padding: EdgeInsets.only(right: 14), + // child: IconButton( + // padding: EdgeInsets.zero, + // constraints: BoxConstraints(), + // icon: SvgPicture.asset('assets/icons/delete_outlined.svg'), + // onPressed: () => showMaterialModalBottomSheet( + // context: context, + // useRootNavigator: true, + // backgroundColor: GuamColorFamily.grayscaleWhite, + // shape: RoundedRectangleBorder( + // borderRadius: BorderRadius.only( + // topLeft: Radius.circular(20), + // topRight: Radius.circular(20), + // ) + // ), + // builder: (context) => SingleChildScrollView( + // child: Container( + // padding: EdgeInsets.only(left: 24, top: 24, right: 14, bottom: 21), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // '알림을 모두 삭제하시겠어요?', + // style: TextStyle( + // fontSize: 18, + // color: GuamColorFamily.grayscaleGray2, + // ), + // ), + // TextButton( + // child: Text( + // '취소', + // style: TextStyle(fontSize: 16, color: GuamColorFamily.purpleCore), + // ), + // style: TextButton.styleFrom( + // padding: EdgeInsets.zero, + // minimumSize: Size(30, 26), + // alignment: Alignment.centerRight, + // ), + // onPressed: () => Navigator.pop(context) + // ), + // ], + // ), + // CustomDivider(color: GuamColorFamily.grayscaleGray7), + // Padding( + // padding: EdgeInsets.symmetric(vertical: 20), + // child: Text( + // '알림을 삭제하면 다시 되돌릴 수 없습니다.', + // style: TextStyle( + // fontSize: 14, + // color: GuamColorFamily.grayscaleGray2, + // fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + // ), + // ), + // ), + // Center( + // child: TextButton( + // onPressed: (){}, + // child: Text( + // '삭제하기', + // style: TextStyle(fontSize: 16, color: GuamColorFamily.redCore), + // ) + // ), + // ), + // ], + // ), + // ), + // ) + // ) + // ), + // ) + ), + ); + } +} diff --git a/lib/screens/notifications/notifications_body.dart b/lib/screens/notifications/notifications_body.dart new file mode 100644 index 00000000..444e378d --- /dev/null +++ b/lib/screens/notifications/notifications_body.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/guam_progress_indicator.dart'; +import 'package:guam_community_client/providers/notifications/notifications.dart'; +import 'package:guam_community_client/screens/notifications/notifications_preview.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; + +class NotificationsBody extends StatefulWidget { + @override + State createState() => _NotificationsBodyState(); +} + +class _NotificationsBodyState extends State { + List _notifications = []; + int _currentPage = 1; + bool _hasNextPage = true; + bool _isFirstLoadRunning = false; + bool _isLoadMoreRunning = false; + ScrollController _scrollController = ScrollController(); + + void _firstLoad() async { + setState(() => _isFirstLoadRunning = true); + try { + await context.read().fetchNotifications(); + _notifications = context.read().notifications; + } catch (err) { + print('알 수 없는 오류가 발생했습니다.'); + } + setState(() => _isFirstLoadRunning = false); + } + + void _loadMore() async { + if (_hasNextPage == true && + _isFirstLoadRunning == false && + _isLoadMoreRunning == false && + _scrollController.position.extentAfter < 300) { + setState(() => _isLoadMoreRunning = true); + try { + _currentPage ++; + final fetchedNotifications = await context.read().addNotifications( + page: _currentPage, + ); + if (fetchedNotifications != null && fetchedNotifications.length > 0) { + setState(() => _notifications.addAll(fetchedNotifications)); + } else { + // This means there is no more data + // and therefore, we will not send another GET request + setState(() => _hasNextPage = false); + } + } catch (err) { + print('알 수 없는 오류가 발생했습니다.'); + } + setState(() => _isLoadMoreRunning = false); + } + } + + @override + void initState() { + _firstLoad(); + _scrollController = ScrollController()..addListener(_loadMore); + super.initState(); + } + + @override + void dispose() { + _scrollController.removeListener(_loadMore); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _isFirstLoadRunning + ? Center(child: guamProgressIndicator()) + : Container( + color: GuamColorFamily.grayscaleWhite, + padding: EdgeInsets.only(top: 18), + child: RefreshIndicator( + color: Color(0xF9F8FFF), // GuamColorFamily.purpleLight1 + onRefresh: () => context.read().fetchNotifications(), + child: Container( + height: double.infinity, + child: SingleChildScrollView( + controller: _scrollController, + physics: AlwaysScrollableScrollPhysics(), + child: Column(children: [ + if (_notifications.isEmpty) + Center( + child: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).size.height * 0.1), + child: Text( + '새로운 알림이 없습니다.', + style: TextStyle( + fontSize: 16, + color: GuamColorFamily.grayscaleGray4, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + ), + if (_notifications.isNotEmpty) + ..._notifications.map((noti) => NotificationsPreview(noti, onRefresh: _firstLoad)), + if (_isLoadMoreRunning == true) + Padding( + padding: EdgeInsets.only(top: 10, bottom: 40), + child: guamProgressIndicator(size: 40), + ), + if (_hasNextPage == false) + Container( + color: GuamColorFamily.purpleLight2, + padding: EdgeInsets.only(top: 10, bottom: 10), + child: Center(child: Text( + '모든 알림을 불러왔습니다!', + style: TextStyle( + fontSize: 13, + color: GuamColorFamily.grayscaleGray2, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + )), + ), + ]), + ), + ), + ), + ); + } +} diff --git a/lib/screens/notifications/notifications_preview.dart b/lib/screens/notifications/notifications_preview.dart new file mode 100644 index 00000000..97c06eaa --- /dev/null +++ b/lib/screens/notifications/notifications_preview.dart @@ -0,0 +1,178 @@ +import 'dart:ffi'; + +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/custom_divider.dart'; +import 'package:guam_community_client/commons/guam_progress_indicator.dart'; +import 'package:guam_community_client/helpers/http_request.dart'; +import 'package:guam_community_client/helpers/svg_provider.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:guam_community_client/models/notification.dart' as Notification; +import 'package:guam_community_client/providers/notifications/notifications.dart'; +import 'package:guam_community_client/screens/notifications/notifications_type.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:provider/provider.dart'; + +import '../../models/boards/post.dart'; +import '../../providers/posts/posts.dart'; +import '../../providers/user_auth/authenticate.dart'; +import '../boards/posts/detail/post_detail.dart'; + +class NotificationsPreview extends StatelessWidget with Toast { + final Notification.Notification notification; + final Function onRefresh; + + NotificationsPreview(this.notification, {this.onRefresh}); + + @override + Widget build(BuildContext context) { + Posts postProvider = context.read(); + Authenticate authProvider = context.read(); + Notifications notiProvider = context.read(); + + void readNotifications() async { + await notiProvider.readNotifications( + userId: authProvider.me.id, + pushEventIds: [notification.id.toString()], + ); + await Future.delayed(Duration(seconds: 1)); + onRefresh(); + } + + return Column( + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + child: Container( + padding: EdgeInsets.only(left: 12, top: 4, bottom: 4), + child: InkWell( + onTap: () { + readNotifications(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Posts(authProvider)), + ], + child: FutureBuilder( + /// todo: linkUrl 을 '/'로 split 하여 postId 추출 => postId 값만 보내주기 + future: postProvider.getPost(int.parse(notification.linkUrl.split('/')[4])), + builder: (_, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return PostDetail(snapshot.data); + } else if (snapshot.hasError) { + Navigator.pop(context); + postProvider.fetchPosts(0); + showToast(success: false, msg: '게시글을 찾을 수 없습니다.'); + return null; + } else { + return Center(child: guamProgressIndicator()); + } + }, + ), + ), + ), + ); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Stack( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + fit: BoxFit.cover, + image: notification.writer.profileImg != null + ? NetworkImage(HttpRequest().s3BaseAuthority + notification.writer.profileImg) + : SvgProvider('assets/icons/profile_image.svg') + ), + ), + ), + if (!notification.isRead) + Positioned( + top: 0, + child: CircleAvatar( + backgroundColor: GuamColorFamily.fuchsiaCore, + radius: 4, + ) + ), + ], + ), + Container( + width: MediaQuery.of(context).size.width * 0.79, + padding: EdgeInsets.only(left: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + notification.writer.nickname, + style: TextStyle( + fontSize: 13, + color: notification.isRead + ? GuamColorFamily.grayscaleGray3 + : GuamColorFamily.grayscaleGray1, + ), + ), + Text( + notificationsType(notification.kind), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: notification.isRead + ? GuamColorFamily.grayscaleGray3 + : GuamColorFamily.grayscaleGray1, + ), + ), + ], + ), + if (notification.body != null) + Text( + notification.body, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + height: 1.6, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: notification.isRead + ? GuamColorFamily.grayscaleGray4 + : GuamColorFamily.grayscaleGray3, + ), + ), + Padding( + padding: EdgeInsets.only(top: 4), + child: Text( + Jiffy(notification.createdAt).fromNow(), + style: TextStyle( + fontSize: 12, + height: 1.6, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray4, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 28), + child: CustomDivider(color: GuamColorFamily.grayscaleGray7), + ) + ], + ); + } +} diff --git a/lib/screens/notifications/notifications_type.dart b/lib/screens/notifications/notifications_type.dart new file mode 100644 index 00000000..b86022a8 --- /dev/null +++ b/lib/screens/notifications/notifications_type.dart @@ -0,0 +1,12 @@ +notificationsType(String kind) { + String description; + switch (kind) { + case 'POST_COMMENT': description = ' 님이 댓글을 남겼습니다.'; break; + case 'POST_COMMENT_MENTION': description = ' 님이 댓글에서 언급했습니다.'; break; + case 'POST_SCRAP': description = ' 님이 게시글을 스크랩했습니다.'; break; + case 'POST_LIKE': description = ' 님이 게시글을 좋아합니다.'; break; + case 'POST_COMMENT_LIKE': description = ' 님이 댓글을 좋아합니다.'; break; + default: description = ''; break; + } + return description; +} diff --git a/lib/screens/profiles/buttons/blacklist_remove_button.dart b/lib/screens/profiles/buttons/blacklist_remove_button.dart new file mode 100644 index 00000000..29282d02 --- /dev/null +++ b/lib/screens/profiles/buttons/blacklist_remove_button.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import '../../../commons/common_text_button.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class BlackListRemoveButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + return CommonTextButton( + text: '해제', + fontSize: 14, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + textColor: GuamColorFamily.grayscaleGray4, + onPressed: () {}, + ); + } +} diff --git a/lib/screens/profiles/buttons/interest_button.dart b/lib/screens/profiles/buttons/interest_button.dart new file mode 100644 index 00000000..f55264c2 --- /dev/null +++ b/lib/screens/profiles/buttons/interest_button.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class InterestButton extends StatelessWidget { + final String interest; + final bool deletable; + + InterestButton(this.interest, {this.deletable = false}); + + @override + Widget build(BuildContext context) { + return Chip( + label: Text('#$interest'), + labelStyle: TextStyle( + fontFamily: deletable ? GuamFontFamily.SpoqaHanSansNeoRegular : GuamFontFamily.SpoqaHanSansNeoMedium, + fontSize: 14, + color: deletable ? GuamColorFamily.grayscaleGray2 : GuamColorFamily.purpleLight1, + height: 22/14, + ), + onDeleted: deletable ? () {} : null, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: GuamColorFamily.purpleLight3, + side: deletable ? BorderSide(width: 0.5, color: GuamColorFamily.grayscaleGray6) : null, + padding: EdgeInsets.symmetric(vertical: 2, horizontal: 8), + ); + } +} diff --git a/lib/screens/profiles/buttons/long_button.dart b/lib/screens/profiles/buttons/long_button.dart new file mode 100644 index 00000000..6090988d --- /dev/null +++ b/lib/screens/profiles/buttons/long_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/icon_text.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class LongButton extends StatelessWidget { + final String label; + final Function onPressed; + + LongButton({this.label, this.onPressed}); + + @override + Widget build(BuildContext context) { + return TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 20, horizontal: 15), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + side: BorderSide(color: GuamColorFamily.grayscaleGray6), + backgroundColor: Colors.transparent, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + fontSize: 14, + color: GuamColorFamily.grayscaleGray2, + ) + ), + IconText( + iconSize: 18, + iconColor: GuamColorFamily.grayscaleGray5, + paddingBtw: 0, + iconPath: 'assets/icons/right.svg', + ) + ], + ), + onPressed: onPressed, + ); + } +} diff --git a/lib/screens/profiles/buttons/message_send_button.dart b/lib/screens/profiles/buttons/message_send_button.dart new file mode 100644 index 00000000..535f54d4 --- /dev/null +++ b/lib/screens/profiles/buttons/message_send_button.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:guam_community_client/commons/bottom_modal/bottom_modal_with_message.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; +import 'package:guam_community_client/providers/messages/messages.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:provider/provider.dart'; + +import '../../../providers/user_auth/authenticate.dart'; + +class MessageSendButton extends StatelessWidget { + final Profile otherProfile; + + MessageSendButton(this.otherProfile); + + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + + return Padding( + padding: EdgeInsets.only(bottom: 24), + child: InkWell( + child: Container( + width: 103, + height: 31, + decoration: BoxDecoration( + color: GuamColorFamily.purpleLight3, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + constraints: BoxConstraints(), + padding: EdgeInsets.only(right: 4), + icon: SvgPicture.asset( + 'assets/icons/message_default.svg', + color: GuamColorFamily.purpleCore, + ), + onPressed: null, + ), + Text( + '쪽지 보내기', + style: TextStyle( + color: GuamColorFamily.purpleCore, + fontSize: 13, + ), + ), + ], + ), + ), + onTap: () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + builder: (context) => MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: SingleChildScrollView( + child: BottomModalWithMessage( + funcName: '보내기', + title: '${otherProfile.nickname} 님에게 쪽지 보내기', + profile: otherProfile, + func: null, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/profiles/buttons/profile_edit_button.dart b/lib/screens/profiles/buttons/profile_edit_button.dart new file mode 100644 index 00000000..de4e0a3d --- /dev/null +++ b/lib/screens/profiles/buttons/profile_edit_button.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/providers/user_auth/authenticate.dart'; +import 'package:provider/provider.dart'; +import 'package:guam_community_client/commons/icon_text.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import '../pages/profiles_edit.dart'; + +class ProfileEditButton extends StatelessWidget { + @override + Widget build(BuildContext context) { + final myProfile = context.read().me; + + return IconText( + iconSize: 18, + iconPath: 'assets/icons/write.svg', + iconColor: GuamColorFamily.purpleLight1, + paddingBtw: 0, + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ProfilesEdit(myProfile) + ) + ), + ); + } +} diff --git a/lib/screens/profiles/buttons/profile_img_edit_button.dart b/lib/screens/profiles/buttons/profile_img_edit_button.dart new file mode 100644 index 00000000..e28d90aa --- /dev/null +++ b/lib/screens/profiles/buttons/profile_img_edit_button.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import '../../../commons/common_text_button.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'dart:io' show Platform; +import '../edit/profile_edit_img_modal.dart'; + +class ProfileImgEditButton extends StatelessWidget { + final Function setImageFile; + final Function resetImageFile; + + ProfileImgEditButton(this.setImageFile, this.resetImageFile); + + @override + Widget build(BuildContext context) { + return CommonTextButton( + text: '프로필 사진 변경', + fontSize: 12, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + textColor: GuamColorFamily.purpleCore, + onPressed: () { + if (Platform.isAndroid) { + showMaterialModalBottomSheet( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(20), + topRight: const Radius.circular(20), + ) + ), + context: context, + useRootNavigator: true, + builder: (_) => ProfileEditImgModal(setImageFile, resetImageFile) + ); + } else { + showCupertinoModalBottomSheet( + context: context, + useRootNavigator: true, + builder: (_) => ProfileEditImgModal(setImageFile, resetImageFile) + ); + } + }, + ); + } +} diff --git a/lib/screens/profiles/buttons/profile_interest_button.dart b/lib/screens/profiles/buttons/profile_interest_button.dart new file mode 100644 index 00000000..82ec7d87 --- /dev/null +++ b/lib/screens/profiles/buttons/profile_interest_button.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; +import '../../../models/profiles/interest.dart'; +import '../../../providers/user_auth/authenticate.dart'; + +class ProfileInterestButton extends StatefulWidget { + final Interest interest; + final int index; + final bool deletable; + final Function removeInterest; + + ProfileInterestButton(this.interest, {this.index, this.removeInterest, this.deletable = false}); + + @override + State createState() => _ProfileInterestButtonState(); +} + +class _ProfileInterestButtonState extends State { + bool sending = false; + + void toggleSending() { + setState(() => sending = !sending); + } + + Future deleteInterest() async { + toggleSending(); + try { + await context.read().deleteInterest( + queryParams: {"name": widget.interest.name}, + ).then((successful) { + toggleSending(); + if (successful) { + widget.removeInterest(widget.index); + } else { + print("Error!"); + } + }); + } catch (e) { + print(e); + } + } + + @override + Widget build(BuildContext context) { + return Chip( + padding: EdgeInsets.all(4), + label: Text(widget.interest.name), + labelStyle: TextStyle( + fontSize: 12, + height: 19.2/12, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: widget.deletable ? GuamColorFamily.grayscaleGray2 : GuamColorFamily.grayscaleGray4, + ), + backgroundColor: GuamColorFamily.grayscaleWhite, + onDeleted: widget.deletable ? deleteInterest : null, + side: BorderSide(color: GuamColorFamily.grayscaleGray6), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + } +} diff --git a/lib/screens/profiles/buttons/web_button.dart b/lib/screens/profiles/buttons/web_button.dart new file mode 100644 index 00000000..9ee4319a --- /dev/null +++ b/lib/screens/profiles/buttons/web_button.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import '../../../commons/icon_text.dart'; + +class WebButton extends StatelessWidget { + final String url; + final String iconPath; + + WebButton(this.url, this.iconPath); + + @override + Widget build(BuildContext context) { + return IconText( + iconSize: 24, + iconPath: iconPath, + iconColor: GuamColorFamily.grayscaleGray5, + paddingBtw: 0, + onPressed: _launchURL, + ); + } + + void _launchURL() async { + if (!await launch(url)) throw 'Could not launch $url'; + } +} \ No newline at end of file diff --git a/lib/screens/profiles/edit/interests/profile_edit_interests.dart b/lib/screens/profiles/edit/interests/profile_edit_interests.dart new file mode 100644 index 00000000..0005c026 --- /dev/null +++ b/lib/screens/profiles/edit/interests/profile_edit_interests.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../../providers/user_auth/authenticate.dart'; +import '../profile_edit_label.dart'; +import '../../../../commons/next.dart'; +import '../../profile/profile_interests.dart'; +import 'profile_edit_interests_detail.dart'; +import '../../../../models/profiles/interest.dart'; + +class ProfileEditInterests extends StatelessWidget { + @override + Widget build(BuildContext context) { + final List interests = context.read().me.interests; + + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ProfileEditLabel('관심사'), + Next(onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ProfileEditInterestsDetail(interests) + ) + )), + ], + ), + if (interests.isNotEmpty) + Padding(padding: EdgeInsets.only(bottom: 8)), + ProfileInterests(interests) + ], + ); + } +} diff --git a/lib/screens/profiles/edit/interests/profile_edit_interests_detail.dart b/lib/screens/profiles/edit/interests/profile_edit_interests_detail.dart new file mode 100644 index 00000000..ef60acb4 --- /dev/null +++ b/lib/screens/profiles/edit/interests/profile_edit_interests_detail.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; +import 'package:guam_community_client/screens/profiles/buttons/profile_interest_button.dart'; +import './profile_edit_interests_label.dart'; +import './profile_edit_interests_textfield.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import '../../../../commons/custom_app_bar.dart'; +import 'package:guam_community_client/commons/back.dart'; +import '../../../../models/profiles/interest.dart'; + +class ProfileEditInterestsDetail extends StatefulWidget { + final List interests; + + ProfileEditInterestsDetail(this.interests); + + @override + State createState() => _ProfileEditInterestsDetailState(); +} + +class _ProfileEditInterestsDetailState extends State { + List _interests; + + @override + void initState() { + _interests = widget.interests; + super.initState(); + } + + void addInterest(String interest) { + setState(() => _interests.add(Interest(name: interest))); + } + + removeInterest(int idx) { + setState(() => _interests.removeAt(idx) + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + leading: Back(), + title: '프로필 수정', + ), + body: SingleChildScrollView( + child: Container( + width: double.infinity, + padding: EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileEditInterestsLabel(_interests.length), + ProfileEditInterestsTextField(addInterest), + Wrap( + spacing: 8, + runSpacing: 5, + children: [..._interests.mapIndexed((idx, i) => ProfileInterestButton( + i, + index: idx, + deletable: true, + removeInterest: removeInterest, + ))], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/profiles/edit/interests/profile_edit_interests_label.dart b/lib/screens/profiles/edit/interests/profile_edit_interests_label.dart new file mode 100644 index 00000000..6328ce04 --- /dev/null +++ b/lib/screens/profiles/edit/interests/profile_edit_interests_label.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class ProfileEditInterestsLabel extends StatelessWidget { + final int nInterests; + + ProfileEditInterestsLabel(this.nInterests); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + '내 관심사', + style: TextStyle( + fontSize: 16, + height: 25.6/16, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray1, + ), + ), + Padding(padding: EdgeInsets.only(right: 4)), + Text( + '$nInterests', + style: TextStyle( + fontSize: 12, + height: 19.2/12, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.purpleCore, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/profiles/edit/interests/profile_edit_interests_textfield.dart b/lib/screens/profiles/edit/interests/profile_edit_interests_textfield.dart new file mode 100644 index 00000000..fba8beae --- /dev/null +++ b/lib/screens/profiles/edit/interests/profile_edit_interests_textfield.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; +import '../../../../commons/button_size_circular_progress_indicator.dart'; +import '../../../../providers/user_auth/authenticate.dart'; + +class ProfileEditInterestsTextField extends StatefulWidget { + final void Function(String interest) addInterest; + + ProfileEditInterestsTextField(this.addInterest); + + @override + State createState() => ProfileEditInterestsTextFieldState(); +} + +class ProfileEditInterestsTextFieldState extends State { + bool sending = false; + final _interestFieldController = TextEditingController(); + + void toggleSending() { + setState(() => sending = !sending); + } + + Future createInterest() async { + toggleSending(); + try { + await context.read().setInterest( + body: {"name": _interestFieldController.text}, + ).then((successful) { + toggleSending(); + if (successful) { + widget.addInterest(_interestFieldController.text); + _interestFieldController.clear(); + } else { + print("Error!"); + } + }); + } catch (e) { + print(e); + } + } + + @override + Widget build(BuildContext context) { + return Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: TextField( + maxLines: 1, + keyboardType: TextInputType.text, + controller: _interestFieldController, + cursorColor: GuamColorFamily.purpleCore, + style: TextStyle( + fontSize: 14, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray1, + height: 22.4/14 + ), + decoration: InputDecoration( + hintText: "관심사를 입력해주세요", + hintStyle: TextStyle(fontSize: 14, color: GuamColorFamily.grayscaleGray5), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + contentPadding: EdgeInsets.symmetric(vertical: 15, horizontal: 16), + ), + ), + ), + !sending ? TextButton( + onPressed: () => createInterest(), + style: TextButton.styleFrom( + padding: EdgeInsets.only(right: 6), + minimumSize: Size(30, 26), + alignment: Alignment.center, + ), + child: Text( + '등록', + style: TextStyle( + color: GuamColorFamily.purpleCore, + fontSize: 14, + ), + ), + ) : ButtonSizeCircularProgressIndicator() + ], + ), + margin: EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all(color: GuamColorFamily.grayscaleGray6, width: 1), + ), + ); + } +} diff --git a/lib/screens/profiles/edit/profile_edit_blog.dart b/lib/screens/profiles/edit/profile_edit_blog.dart new file mode 100644 index 00000000..c3cdff5f --- /dev/null +++ b/lib/screens/profiles/edit/profile_edit_blog.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'profile_edit_label.dart'; +import 'profile_edit_textfield.dart'; + +class ProfileEditBlog extends StatelessWidget { + final String blogUrl; + final Function setInput; + + ProfileEditBlog(this.blogUrl, this.setInput); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(bottom: 24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileEditLabel('블로그'), + ProfileEditTextField(input: blogUrl, isBlogUrl: true, func: setInput, funcKey: 'blogUrl') + ], + ), + ); + } +} diff --git a/lib/screens/profiles/edit/profile_edit_github.dart b/lib/screens/profiles/edit/profile_edit_github.dart new file mode 100644 index 00000000..f996dfd2 --- /dev/null +++ b/lib/screens/profiles/edit/profile_edit_github.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'profile_edit_label.dart'; +import 'profile_edit_textfield.dart'; + +class ProfileEditGithub extends StatelessWidget { + final String githubId; + final Function setInput; + + ProfileEditGithub(this.githubId, this.setInput); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(bottom: 24), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileEditLabel('github ID'), + ProfileEditTextField(input: githubId, func: setInput, funcKey: 'githubId') + ], + ), + ); + } +} diff --git a/lib/screens/profiles/edit/profile_edit_img_modal.dart b/lib/screens/profiles/edit/profile_edit_img_modal.dart new file mode 100644 index 00000000..466aeb2a --- /dev/null +++ b/lib/screens/profiles/edit/profile_edit_img_modal.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:guam_community_client/commons/common_text_button.dart'; + +import '../../../helpers/pick_image.dart'; + +class ProfileEditImgModal extends StatelessWidget { + final Function setImageFile; + final Function resetImageFile; + + const ProfileEditImgModal(this.setImageFile, this.resetImageFile); + + @override + Widget build(BuildContext context) { + return Wrap( + children: [ + Container( + padding: const EdgeInsets.all(24), + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: const Radius.circular(20), + topRight: const Radius.circular(20), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CommonTextButton( + text: '사진 가져오기', + fontSize: 16, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + textColor: GuamColorFamily.grayscaleGray1, + onPressed: () => pickImage(type: 'gallery').then((img) { + setImageFile(img); + }), + ), + Padding(padding: EdgeInsets.only(bottom: 20)), + CommonTextButton( + text: '기본 사진으로 설정', + fontSize: 16, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + textColor: GuamColorFamily.grayscaleGray1, + onPressed: () => resetImageFile(), + ), + ], + ), + ) + ], + ); + } +} diff --git a/lib/screens/profiles/edit/profile_edit_intro.dart b/lib/screens/profiles/edit/profile_edit_intro.dart new file mode 100644 index 00000000..3fc91eab --- /dev/null +++ b/lib/screens/profiles/edit/profile_edit_intro.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'profile_edit_label.dart'; +import 'profile_edit_textfield.dart'; + +class ProfileEditIntro extends StatelessWidget { + final String intro; + final Function setInput; + + ProfileEditIntro(this.intro, this.setInput); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: EdgeInsets.only(bottom: 44), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileEditLabel('소개'), + Padding(padding: EdgeInsets.only(bottom: 16)), + Row( // Essential to fit in the whole space + children: [ + ProfileEditTextField(input: intro, maxLength: 150, func: setInput, funcKey: 'introduction') + ], + ) + ], + ), + ); + } +} diff --git a/lib/screens/profiles/edit/profile_edit_label.dart b/lib/screens/profiles/edit/profile_edit_label.dart new file mode 100644 index 00000000..06b1aff7 --- /dev/null +++ b/lib/screens/profiles/edit/profile_edit_label.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:hexcolor/hexcolor.dart'; + +class ProfileEditLabel extends StatelessWidget { + final String label; + final HexColor textColor; + + ProfileEditLabel(this.label, {this.textColor}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 84, + child: Text( + label, + style: TextStyle( + fontSize: 14, + height: 22/14, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: textColor == null ? GuamColorFamily.grayscaleGray1 : textColor, + ), + ), + ); + } +} diff --git a/lib/screens/profiles/edit/profile_edit_nickname.dart b/lib/screens/profiles/edit/profile_edit_nickname.dart new file mode 100644 index 00000000..07828ecd --- /dev/null +++ b/lib/screens/profiles/edit/profile_edit_nickname.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'profile_edit_label.dart'; +import 'profile_edit_textfield.dart'; + +class ProfileEditNickname extends StatelessWidget { + final String nickname; + final Function setInput; + + ProfileEditNickname(this.nickname, this.setInput); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.fromLTRB(0, 24, 0, 44), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileEditLabel('이름'), + ProfileEditTextField(input: nickname, maxLength: 10, func: setInput, funcKey: 'nickname') + ], + ), + ); + } +} diff --git a/lib/screens/profiles/edit/profile_edit_optional.dart b/lib/screens/profiles/edit/profile_edit_optional.dart new file mode 100644 index 00000000..8ea91532 --- /dev/null +++ b/lib/screens/profiles/edit/profile_edit_optional.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/screens/profiles/edit/interests/profile_edit_interests.dart'; +import 'profile_edit_label.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'profile_edit_github.dart'; +import 'profile_edit_blog.dart'; +import 'interests/profile_edit_interests.dart'; + +class ProfileEditOptional extends StatelessWidget { + final Map input; + final Function setInput; + + ProfileEditOptional(this.input, this.setInput); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ProfileEditLabel( + '선택사항', + textColor: GuamColorFamily.purpleCore, + ), + Padding(padding: EdgeInsets.only(bottom: 8)), + ProfileEditGithub(input['githubId'], setInput), + ProfileEditBlog(input['blogUrl'], setInput), + ProfileEditInterests(), + ], + ); + } +} diff --git a/lib/screens/profiles/edit/profile_edit_textfield.dart b/lib/screens/profiles/edit/profile_edit_textfield.dart new file mode 100644 index 00000000..4b04486e --- /dev/null +++ b/lib/screens/profiles/edit/profile_edit_textfield.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class ProfileEditTextField extends StatefulWidget { + final String input; + final int maxLength; + final bool isBlogUrl; + final Function func; /// request body 에 쓰일 setInput 함수 + final String funcKey; /// nickname, introduction, blogUrl 이 위 func 에 사용되는 argument + + ProfileEditTextField({this.input, this.maxLength, this.isBlogUrl=false, this.func, this.funcKey}); + + @override + State createState() => _ProfileEditTextFieldState(); +} + +class _ProfileEditTextFieldState extends State { + final _textFieldController = TextEditingController(); + + @override + void initState() { + _textFieldController.text = widget.input; + super.initState(); + } + + @override + void dispose() { + _textFieldController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Expanded( + child: TextField( + maxLength: widget.maxLength, + controller: _textFieldController, + onChanged: (e) => widget.func(widget.funcKey, _textFieldController.text), + style: TextStyle( + fontSize: 14, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray1, + height: 22.4/14 + ), + cursorColor: GuamColorFamily.purpleCore, + decoration: InputDecoration( + counterStyle: TextStyle( + color: GuamColorFamily.purpleCore, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + fontSize: 12 + ), + border: UnderlineInputBorder( + borderSide: BorderSide(color: GuamColorFamily.purpleLight2) + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: GuamColorFamily.purpleLight2) + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: GuamColorFamily.purpleLight2) + ), + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.only(bottom: 8), + hintText: widget.isBlogUrl + ? 'https://guam.com' + : '', + hintStyle: TextStyle( + color: GuamColorFamily.grayscaleGray4, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + fontSize: 13, + ), + ), + ) + ); + } +} diff --git a/lib/screens/profiles/my_profiles_body.dart b/lib/screens/profiles/my_profiles_body.dart new file mode 100644 index 00000000..268a03f7 --- /dev/null +++ b/lib/screens/profiles/my_profiles_body.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; +import 'package:guam_community_client/screens/profiles/buttons/profile_edit_button.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_bottom_buttons.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_img.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_intro.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_nickname.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_interests.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_web_buttons.dart'; +import 'package:provider/provider.dart'; +import '../../providers/user_auth/authenticate.dart'; + +class MyProfilesBody extends StatelessWidget { + MyProfilesBody(); + + @override + Widget build(BuildContext context) { + final Profile me = context.read().me; + + return Container( + width: double.infinity, + padding: EdgeInsets.fromLTRB(24, 40, 24, 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ProfileImg(profileImg: me.profileImg, height: 144, width: 144), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ProfileNickname(nickname: me.nickname), + ProfileEditButton(), + ], + ), + ProfileIntro(me.intro), + ProfileWebButtons(githubId: me.githubId, blogUrl: me.blogUrl), + ProfileInterests(me.interests), + ProfileBottomButtons(), + ], + ), + ); + } +} diff --git a/lib/screens/profiles/other_profiles_body.dart b/lib/screens/profiles/other_profiles_body.dart new file mode 100644 index 00000000..38766efe --- /dev/null +++ b/lib/screens/profiles/other_profiles_body.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; +import 'package:guam_community_client/screens/profiles/buttons/message_send_button.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_img.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_intro.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_nickname.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_interests.dart'; +import 'package:guam_community_client/screens/profiles/profile/profile_web_buttons.dart'; +import 'package:provider/provider.dart'; + +import '../../providers/user_auth/authenticate.dart'; + +class OtherProfilesBody extends StatelessWidget { + final Profile profile; + + OtherProfilesBody({this.profile}); + + @override + Widget build(BuildContext context) { + bool isMe = context.read().isMe(profile.id); + + return Container( + width: double.infinity, + padding: EdgeInsets.fromLTRB(24, 40, 24, 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ProfileImg(profileImg: profile.profileImg, height: 144, width: 144), + ProfileNickname(nickname: profile.nickname, editable: false), + ProfileIntro(profile.intro ?? ""), + ProfileWebButtons( + githubId: profile.githubId ?? "", + blogUrl: profile.blogUrl ?? "", + isMe: isMe, + ), + // 추후 MyProfile의 id랑 비교해서 본인임이 확인되면 프로필 탭으로 이동하도록 하겠습니다. + if (!isMe) MessageSendButton(profile), + ProfileInterests(profile.interests), + ], + ), + ); + } +} diff --git a/lib/screens/profiles/pages/blacklist_edit.dart b/lib/screens/profiles/pages/blacklist_edit.dart new file mode 100644 index 00000000..ccf72e65 --- /dev/null +++ b/lib/screens/profiles/pages/blacklist_edit.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/back.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import '../../../commons/custom_app_bar.dart'; +import '../../../commons/common_img_nickname.dart'; +import '../buttons/blacklist_remove_button.dart'; + +class BlackListEdit extends StatelessWidget { + final List blacklist = [ + { + 'id': 4, + 'nickname': 'marcelko', + 'profileImageUrl': 'https://t1.daumcdn.net/cfile/tistory/99A97E4C5D25E9C226', + }, + { + 'id': 5, + 'nickname': 'chadan1', + 'profileImageUrl': 'https://w.namu.la/s/40de86374ddd74756b31d4694a7434ee9398baa51fa5ae72d28f2eeeafdadf0c475c55c58e29a684920e0d6a42602b339f8aaf6d19764b04405a0f8bee7f598d2922db9475579419aac4635d0a71fdb8a4b2343cb550e6ed93e13c1a05cede75', + }, + { + 'id': 6, + 'nickname': 'marcelkor', + 'profileImageUrl': null, + }, + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + leading: Back(), + title: '차단 목록 관리', + ), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24), + child: Wrap( + runSpacing: 12, + children: [...blacklist.map((e) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CommonImgNickname( + userId: e['id'], + imgUrl: e['profileImageUrl'], + nickname: e['nickname'], + ), + BlackListRemoveButton() + ], + ))], + ), + ), + ) + ); + } +} diff --git a/lib/screens/profiles/pages/my_posts.dart b/lib/screens/profiles/pages/my_posts.dart new file mode 100644 index 00000000..dfa41e1a --- /dev/null +++ b/lib/screens/profiles/pages/my_posts.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/back.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import '../../../commons/custom_app_bar.dart'; + + +class MyPosts extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + leading: Back(), + title: '내가 쓴 글', + ), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Column( + children: [] + ), + ), + ) + ); + } +} diff --git a/lib/screens/profiles/pages/profiles_edit.dart b/lib/screens/profiles/pages/profiles_edit.dart new file mode 100644 index 00000000..d78a1b81 --- /dev/null +++ b/lib/screens/profiles/pages/profiles_edit.dart @@ -0,0 +1,219 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:guam_community_client/mixins/toast.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; +import 'package:guam_community_client/screens/profiles/buttons/profile_img_edit_button.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; +import 'package:provider/provider.dart'; +import '../../../commons/custom_app_bar.dart'; +import '../../../commons/common_text_button.dart'; +import '../../../commons/custom_divider.dart'; +import '../../../providers/user_auth/authenticate.dart'; +import '../profile/profile_img.dart'; +import '../edit/profile_edit_nickname.dart'; +import '../edit/profile_edit_intro.dart'; +import '../edit/profile_edit_optional.dart'; + +class ProfilesEdit extends StatefulWidget { + final Profile profile; + + ProfilesEdit(this.profile); + + @override + State createState() => _ProfilesEditState(); +} + +class _ProfilesEditState extends State with Toast { + bool sending = false; + Map input = {}; + List profileImage = []; + String profileImg; + + @override + void initState() { + input['nickname'] = widget.profile.nickname; + input['introduction'] = widget.profile.intro; + input['githubId'] = widget.profile.githubId; + input['blogUrl'] = widget.profile.blogUrl; + profileImg = widget.profile.profileImg; + super.initState(); + } + + void toggleSending() { + setState(() => sending = !sending); + } + + void setInput(String _key, String _value) { + setState(() => input[_key] = _value); + } + + Future setImageFile(PickedFile val) async { + setState(() { + if (profileImage.isNotEmpty) profileImage.clear(); + if (val != null) profileImage.add(val); + }); + } + + Future resetImageFile() async { + setState(() { + profileImg = null; + if (profileImage.isNotEmpty) profileImage.clear(); + }); + } + + /// import the path of images from assets directory + Future getImageFileFromAssets(String path) async { + final byteData = await rootBundle.load('assets/$path'); + final buffer = byteData.buffer; + Directory tempDir = await getTemporaryDirectory(); + var filePath = tempDir.path + '/tmp.png'; /// arbitrary name & extension + return File(filePath).writeAsBytes(buffer.asUint8List( + byteData.offsetInBytes, byteData.lengthInBytes, + )); + } + + Future setProfile() async { + toggleSending(); + try { + return showToast(success: false, msg: '프로필 수정 디버깅 중입니다...'); + + if (input['nickname'] == '') { + return showToast(success: false, msg: '닉네임을 설정해주세요.'); + } + // if (profileImage.isEmpty && profileImg == null) { + // return showToast(success: false, msg: '프로필 사진을 설정해주세요.'); + // } + await context.read().setProfile( + fields: input, + files: profileImage.isNotEmpty + ? [File(profileImage[0].path)] /// 프사 새롭게 추가 + : profileImg != null /// 프사 유지하거나 기본 프사로 바꾸거나 + ? null + : [await getImageFileFromAssets('images/profile_image.png')], + ).then((successful) { + toggleSending(); + if (successful) { + Navigator.maybePop(context); + } else { + print("Error!"); + } + }); + } catch (e) { + print(e); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + leading: IconButton( + icon: SvgPicture.asset('assets/icons/back.svg'), + onPressed: () => showMaterialModalBottomSheet( + context: context, + useRootNavigator: true, + backgroundColor: GuamColorFamily.grayscaleWhite, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + builder: (context) => SingleChildScrollView( + child: Container( + padding: EdgeInsets.only(left: 24, top: 24, right: 18, bottom: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '프로필 수정을 취소하시겠어요?', + style: TextStyle(fontSize: 18, color: GuamColorFamily.grayscaleGray2), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + '돌아가기', + style: TextStyle(fontSize: 16, color: GuamColorFamily.purpleCore, + ), + ), + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size(30, 26), + alignment: Alignment.centerRight, + ), + ), + ], + ), + CustomDivider(color: GuamColorFamily.grayscaleGray7), + Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Text( + '수정된 내용이 사라집니다.', + style: TextStyle( + fontSize: 14, + height: 1.6, + color: GuamColorFamily.grayscaleGray2, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + ), + Center( + child: TextButton( + onPressed: () { + Navigator.pop(context); + Navigator.maybePop(context); + }, + child: Text( + '취소하기', + style: TextStyle(fontSize: 16, color: GuamColorFamily.redCore), + ), + ), + ), + ], + ), + ), + ), + ), + ), + title: '프로필 수정', + trailing: Padding( + padding: EdgeInsets.only(right: 8), + child: CommonTextButton( + text: '완료', + fontSize: 16, + fontFamily: GuamFontFamily.SpoqaHanSansNeoMedium, + textColor: GuamColorFamily.purpleCore, + onPressed: () async => await setProfile(), + ), + ), + ), + body: SingleChildScrollView( + child: Container( + width: double.infinity, + padding: EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ProfileImg(newImage: profileImage, profileImg: profileImg, height: 96, width: 96), + ProfileImgEditButton(setImageFile, resetImageFile), + ProfileEditNickname(input['nickname'], setInput), + ProfileEditIntro(input['introduction'], setInput), + ProfileEditOptional(input, setInput), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/profiles/pages/saved_posts.dart b/lib/screens/profiles/pages/saved_posts.dart new file mode 100644 index 00000000..46818d6b --- /dev/null +++ b/lib/screens/profiles/pages/saved_posts.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/back.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import '../../../commons/custom_app_bar.dart'; + +class SavedPosts extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + leading: Back(), + title: '저장한 글', + ), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Column( + children: [] + ), + ), + ) + ); + } +} diff --git a/lib/screens/profiles/pages/settings.dart b/lib/screens/profiles/pages/settings.dart new file mode 100644 index 00000000..0291c373 --- /dev/null +++ b/lib/screens/profiles/pages/settings.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/back.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; +import '../../../commons/custom_app_bar.dart'; +import '../../../providers/user_auth/authenticate.dart'; +import '../buttons/long_button.dart'; +import 'blacklist_edit.dart'; + +class Settings extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + leading: Back(), + title: '계정 설정', + ), + body: SingleChildScrollView( + child: Padding( + padding: EdgeInsets.all(24), + child: Wrap( + runSpacing: 12, + children: [ + // LongButton( + // label: '차단 목록 관리', + // onPressed: () => Navigator.push( + // context, + // MaterialPageRoute( + // builder: (_) => BlackListEdit() + // ) + // ) + // ), + LongButton( + label: '로그아웃', + onPressed: () async => context.read().signOut() + ), + ], + ), + ), + ) + ); + } +} diff --git a/lib/screens/profiles/profile/profile_bottom_buttons.dart b/lib/screens/profiles/profile/profile_bottom_buttons.dart new file mode 100644 index 00000000..49697a6e --- /dev/null +++ b/lib/screens/profiles/profile/profile_bottom_buttons.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import '../buttons/long_button.dart'; +import '../pages/my_posts.dart'; +import '../pages/saved_posts.dart'; +import '../pages/settings.dart'; + +class ProfileBottomButtons extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 24), + child: Wrap( + runSpacing: 12, + children: [ + // LongButton( + // label: '내가 쓴 글', + // onPressed: () => Navigator.push( + // context, + // MaterialPageRoute( + // builder: (_) => MyPosts() + // ) + // ) + // ), + // LongButton( + // label: '저장한 글', + // onPressed: () => Navigator.push( + // context, + // MaterialPageRoute( + // builder: (_) => SavedPosts() + // ) + // ) + // ), + LongButton( + label: '계정 설정', + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => Settings() + ) + ) + ), + ], + ), + ); + } +} diff --git a/lib/screens/profiles/profile/profile_img.dart b/lib/screens/profiles/profile/profile_img.dart new file mode 100644 index 00000000..7dbae535 --- /dev/null +++ b/lib/screens/profiles/profile/profile_img.dart @@ -0,0 +1,79 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/image/closable_image_expanded.dart'; +import 'package:guam_community_client/helpers/http_request.dart'; +import 'package:guam_community_client/helpers/svg_provider.dart'; +import 'package:transparent_image/transparent_image.dart'; + +import '../../../commons/image/image_container.dart'; + +class ProfileImg extends StatefulWidget { + final String profileImg; + final double height; + final double width; + final List newImage; + + ProfileImg({this.profileImg, this.height, this.width, this.newImage}); + + @override + State createState() => _ProfileImgState(); +} + +class _ProfileImgState extends State { + @override + Widget build(BuildContext context) { + return Container( + height: widget.height, + width: widget.width, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: widget.profileImg != null ? Colors.transparent : Colors.grey, + ), + child: ClipOval( + child: widget.profileImg == null + ? widget.newImage != null && widget.newImage.isNotEmpty + ? Container( /// 프사 설정 안 된 상태에서 사진첩에서 사진 불러옴. + child: ImageThumbnail( + width: widget.width, + height: widget.height, + image: Image( + image: FileImage(File(widget.newImage[0].path)), + fit: BoxFit.fill, + ), + ), + ) + : Image( /// 프사가 아예 없거나 기본 사진으로 설정 버튼 누름. + image: SvgProvider('assets/icons/profile_image.svg'), + width: widget.width, + height: widget.height, + ) + : widget.newImage != null && widget.newImage.isNotEmpty + ? Container( /// 프사 설정된 상태에서 사진첩에서 사진 불러옴. + child: ImageThumbnail( + width: widget.width, + height: widget.height, + image: Image( + image: FileImage(File(widget.newImage[0].path)), + fit: BoxFit.fill, + ), + ), + ) + : InkWell( /// 프사 설정된 상태에서 아무 작업도 안 함. + child: FadeInImage( + placeholder: MemoryImage(kTransparentImage), + image: NetworkImage(HttpRequest().s3BaseAuthority + widget.profileImg), + fit: BoxFit.cover, + ), + onTap: () { + Navigator.of(context, rootNavigator: true).push( + MaterialPageRoute(builder: (_) => ClosableImageExpanded( + imagePath: widget.profileImg, + )), + ); + } + ) + ), + ); + } +} diff --git a/lib/screens/profiles/profile/profile_interests.dart b/lib/screens/profiles/profile/profile_interests.dart new file mode 100644 index 00000000..d79d5b4f --- /dev/null +++ b/lib/screens/profiles/profile/profile_interests.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import '../buttons/profile_interest_button.dart'; +import '../../../models/profiles/interest.dart'; + +class ProfileInterests extends StatelessWidget { + final List interests; + + ProfileInterests(this.interests); + + @override + Widget build(BuildContext context) { + return Wrap( + alignment: WrapAlignment.start, + spacing: 8, + runSpacing: 5, + children: [...interests.map((i) => ProfileInterestButton(i))], + ); + } +} diff --git a/lib/screens/profiles/profile/profile_intro.dart b/lib/screens/profiles/profile/profile_intro.dart new file mode 100644 index 00000000..540d0c66 --- /dev/null +++ b/lib/screens/profiles/profile/profile_intro.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class ProfileIntro extends StatelessWidget { + final String intro; + + ProfileIntro(this.intro); + + @override + Widget build(BuildContext context) { + return Text( + intro ?? '', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + fontSize: 14, + color: GuamColorFamily.grayscaleGray3, + height: 22.4/14 + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/profiles/profile/profile_nickname.dart b/lib/screens/profiles/profile/profile_nickname.dart new file mode 100644 index 00000000..dd98c79f --- /dev/null +++ b/lib/screens/profiles/profile/profile_nickname.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + + +class ProfileNickname extends StatelessWidget { + final String nickname; + final bool editable; + + ProfileNickname({this.nickname, this.editable = true}); + + @override + Widget build(BuildContext context) { + return Padding( + child: Text( + nickname, + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: GuamFontFamily.SpoqaHanSansNeoMedium, + fontSize: 18, + color: GuamColorFamily.grayscaleGray2, + height: 28.8/18 + ), + ), + // left 26 of icon text width to align nickname at center + // 수정 가능한 경우는 edit icon 때문에 padding 조절함. + padding: EdgeInsets.fromLTRB((editable ? 26+8 : 8).toDouble(), 16, 8, 16), + ); + } +} diff --git a/lib/screens/profiles/profile/profile_web_buttons.dart b/lib/screens/profiles/profile/profile_web_buttons.dart new file mode 100644 index 00000000..6ee1ffcc --- /dev/null +++ b/lib/screens/profiles/profile/profile_web_buttons.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import '../buttons/web_button.dart'; + +class ProfileWebButtons extends StatelessWidget { + final String githubId; + final String blogUrl; + final bool isMe; + static const String githubUrl = 'https://github.com/'; + + ProfileWebButtons({this.githubId, this.blogUrl, this.isMe=true}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(8, 5, 0, isMe ? 24 : 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment., + children: [ + if(githubId != null) WebButton(githubUrl+githubId, 'assets/icons/github.svg'), + if(blogUrl != null) WebButton(blogUrl, 'assets/icons/blog.svg'), + ], + ), + ); + } +} diff --git a/lib/screens/profiles/profiles_app.dart b/lib/screens/profiles/profiles_app.dart new file mode 100644 index 00000000..575b93ad --- /dev/null +++ b/lib/screens/profiles/profiles_app.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/back.dart'; +import 'package:guam_community_client/commons/guam_progress_indicator.dart'; +import 'package:guam_community_client/models/profiles/profile.dart'; +import 'package:guam_community_client/providers/messages/messages.dart'; +import 'package:guam_community_client/providers/user_auth/authenticate.dart'; +import 'package:guam_community_client/screens/messages/message_box.dart'; +import 'package:guam_community_client/screens/profiles/other_profiles_body.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; + +import '../../commons/custom_app_bar.dart'; +import 'my_profiles_body.dart'; + +class ProfilesApp extends StatelessWidget { + final int userId; + + ProfilesApp({this.userId}); + + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Messages(authProvider)), + ], + child: ProfilesAppScaffold(userId), + ); + } +} + +class ProfilesAppScaffold extends StatefulWidget { + final int userId; + + ProfilesAppScaffold(this.userId); + + @override + State createState() => _ProfilesAppScaffoldState(); +} + +class _ProfilesAppScaffoldState extends State { + Future otherProfile; + + @override + void initState() { + super.initState(); + if (widget.userId != null) + otherProfile = Future.delayed( + Duration.zero, + () async => context.read().getUserProfile(widget.userId), + ); + } + + @override + Widget build(BuildContext context) { + // 특정 userId를 받아 otherProfile 위젯 호출하는 경우. + if (widget.userId != null){ + return FutureBuilder( + future: otherProfile, + builder: (_, AsyncSnapshot snapshot) { + // FutureBuilder에서 받아오는 otherProfile 존재 여부에 따라 위젯 변경 + if (snapshot.hasData){ + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + title: '프로필', + leading: Back(), + ), + body: SingleChildScrollView( + child: OtherProfilesBody(profile: snapshot.data) + ), + ); + } else if (snapshot.hasError) { + // 에러 메시지 띄워주기 + return Center(child: guamProgressIndicator()); + } else { + return Container( + color: GuamColorFamily.grayscaleWhite, + child: Center( + child: CircularProgressIndicator( + color: GuamColorFamily.purpleCore, + ), + ), + ); + } + } + ); + } else { + // 특정 유저 id를 받지 않아 프로필 탭으로 이동하는 경우 + return Scaffold( + backgroundColor: GuamColorFamily.grayscaleWhite, + appBar: CustomAppBar( + title: '프로필', + trailing: MessageBox(), + ), + body: SingleChildScrollView(child: MyProfilesBody()), + ); + } + } +} diff --git a/lib/screens/search/search_app.dart b/lib/screens/search/search_app.dart new file mode 100644 index 00000000..eb69da0b --- /dev/null +++ b/lib/screens/search/search_app.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/screens/search/search_feed.dart'; +import 'package:guam_community_client/screens/search/search_history.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; +import '../../commons/guam_progress_indicator.dart'; +import '../../providers/posts/posts.dart'; +import '../../providers/user_auth/authenticate.dart'; +import 'search_app_bar.dart'; +import 'search_app_textfield.dart'; +import '../../providers/search/search.dart'; + +class SearchApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + Authenticate authProvider = context.read(); + + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => Search(authProvider)), + ChangeNotifierProvider(create: (_) => Posts(authProvider)), + ], + child: SearchAppScaffold(), + ); + } +} + +class SearchAppScaffold extends StatefulWidget { + @override + State createState() => _SearchAppScaffoldState(); +} + +class _SearchAppScaffoldState extends State { + bool showHistory = true; + + void showSearchHistory(bool) { + setState(() => showHistory = bool); + } + + @override + Widget build(BuildContext context) { + final searchProvider = context.watch(); + + return Scaffold( + backgroundColor: GuamColorFamily.purpleLight3, + appBar: SearchAppBar( + title: SearchAppTextField(showHistory, showSearchHistory), + ), + body: Container( + width: double.infinity, + child: searchProvider.searchedPosts.isEmpty + ? showHistory + ? SearchHistory( + searchList: [...searchProvider.history.reversed], + showSearchHistory: showSearchHistory, + ) + : searchProvider.loading + ? Center(child: guamProgressIndicator()) + : Center(child: Text('검색 결과가 없습니다.', style: TextStyle(fontSize: 16))) + : showHistory + ? SearchHistory( + searchList: [...searchProvider.history.reversed], + showSearchHistory: showSearchHistory, + ) + : searchProvider.loading + ? Center(child: guamProgressIndicator()) + : SearchFeed() + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/search/search_app_bar.dart b/lib/screens/search/search_app_bar.dart new file mode 100644 index 00000000..4504d672 --- /dev/null +++ b/lib/screens/search/search_app_bar.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; + +class SearchAppBar extends StatelessWidget with PreferredSizeWidget { + final Widget title; + final dynamic trailing; + final dynamic bottom; + + SearchAppBar({this.title, this.trailing, this.bottom}); + + @override + Size get preferredSize => Size.fromHeight(AppBar().preferredSize.height); + + @override + Widget build(BuildContext context) { + var iconColor = GuamColorFamily.grayscaleGray1; + + return AppBar( + elevation: 0, + title: title, + automaticallyImplyLeading: false, + bottom: bottom, + actions: trailing == null + ? [] + : [Material(color: Colors.transparent, child: trailing)], + backgroundColor: GuamColorFamily.grayscaleWhite, + iconTheme: IconThemeData( + color: iconColor, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/search/search_app_textfield.dart b/lib/screens/search/search_app_textfield.dart new file mode 100644 index 00000000..bfee6c49 --- /dev/null +++ b/lib/screens/search/search_app_textfield.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:provider/provider.dart'; +import '../../commons/common_text_button.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import '../../providers/search/search.dart'; + +class SearchAppTextField extends StatefulWidget { + final bool showHistory; + final Function showSearchHistory; + + SearchAppTextField(this.showHistory, this.showSearchHistory); + + @override + State createState() => SearchAppTextFieldState(); +} + +class SearchAppTextFieldState extends State { + static final controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + final searchProvider = context.read(); + bool isTextEmpty = controller.text.trim() == ''; + + return Container( + height: 40, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: TextField( + maxLines: 1, + autofocus: false, + controller: controller, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.search, + cursorColor: GuamColorFamily.purpleCore, + style: TextStyle( + fontSize: 14, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + color: GuamColorFamily.grayscaleGray1, + height: 22/14, + ), + decoration: InputDecoration( + hintText: "검색어를 입력해주세요.", + prefixIcon: Icon( + Icons.search_outlined, + color: GuamColorFamily.purpleCore, + size: 20, + ), + suffixIcon: !isTextEmpty + ? IconButton( + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + onPressed: () { + controller.clear(); + isTextEmpty = true; + }, + icon: SvgPicture.asset( + 'assets/icons/cancel_filled_x_transparent.svg', + color: GuamColorFamily.grayscaleGray6, + width: 18, + height: 18, + ), + ) + : null, + contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: GuamColorFamily.purpleLight2), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: GuamColorFamily.purpleLight2), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: GuamColorFamily.purpleCore), + ), + ), + onChanged: (e) => isTextEmpty = controller.text.trim() == '', + onSubmitted: (word) { + if (!isTextEmpty) { + searchProvider.searchPosts(query: word, context: context); + widget.showSearchHistory(false); // 검색 시 히스토리 안보여줌. + searchProvider.saveHistory(word); + FocusScope.of(context).unfocus(); + } + }, + ), + ), + Padding(padding: EdgeInsets.only(right: 4)), + // '취소' 키 누르면 'x'키 및 '취소' 키 사라짐. + if (!isTextEmpty || searchProvider.searchedPosts.isNotEmpty) + CommonTextButton( + text: '취소', + fontSize: 14, + textColor: GuamColorFamily.purpleCore, + onPressed: () { + isTextEmpty = true; + widget.showSearchHistory(true); // 취소하면 history 보여줌. + FocusScope.of(context).unfocus(); + controller.clear(); + } + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/search/search_feed.dart b/lib/screens/search/search_feed.dart new file mode 100644 index 00000000..fb7b3bfc --- /dev/null +++ b/lib/screens/search/search_feed.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/sub_headings.dart'; +import 'package:guam_community_client/screens/boards/posts/preview/post_preview.dart'; +import 'package:guam_community_client/screens/search/search_filter.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; +import '../../providers/search/search.dart'; + +class SearchFeed extends StatelessWidget { + @override + Widget build(BuildContext context) { + final searchProvider = context.watch(); + + return SingleChildScrollView( + child: Container( + color: GuamColorFamily.purpleLight3, + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(24, 12, 24, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SubHeadings( + '검색결과 ${searchProvider.searchedPosts.length}건', + fontColor: GuamColorFamily.grayscaleGray1, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + fontSize: 14, + ), + SearchFilter(provider: context.read()), + ], + ), + ), + Column( + children: [...searchProvider.searchedPosts.map((p) => PostPreview(p))], + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/search/search_filter.dart b/lib/screens/search/search_filter.dart new file mode 100644 index 00000000..2b3d5ced --- /dev/null +++ b/lib/screens/search/search_filter.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/common_text_button.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import '../../providers/search/search.dart'; + +class SearchFilter extends StatefulWidget { + final provider; + + SearchFilter({this.provider}); + + @override + State createState() => SearchFilterState(); +} + +class SearchFilterState extends State { + var filter; + + @override + void initState() { + filter = Search.filters.first; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [...Search.filters.map((f) => CommonTextButton( + text: f.label, + fontSize: 14, + fontFamily: filter == f ? GuamFontFamily.SpoqaHanSansNeoMedium : GuamFontFamily.SpoqaHanSansNeoRegular, + textColor: filter == f ? GuamColorFamily.purpleDark1 : GuamColorFamily.grayscaleGray4, + onPressed: () { + setState(() => filter = f); + widget.provider.sortSearchedPosts(f); + }, + ))], + ); + } +} \ No newline at end of file diff --git a/lib/screens/search/search_history.dart b/lib/screens/search/search_history.dart new file mode 100644 index 00000000..e4d10105 --- /dev/null +++ b/lib/screens/search/search_history.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/custom_divider.dart'; +import 'package:guam_community_client/screens/search/search_word.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import '../../commons/sub_headings.dart'; +import 'package:guam_community_client/styles/fonts.dart'; + +class SearchHistory extends StatelessWidget { + final List searchList; + final Function showSearchHistory; + + SearchHistory({this.searchList, this.showSearchHistory}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + CustomDivider(color: GuamColorFamily.grayscaleGray7), + Container( + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 16), + decoration: BoxDecoration(color: GuamColorFamily.grayscaleGray8), + child: SubHeadings( + '최근 검색어', + fontSize: 14, + fontFamily: GuamFontFamily.SpoqaHanSansNeoMedium, + fontColor: GuamColorFamily.grayscaleGray4, + ), + ), + CustomDivider(color: GuamColorFamily.grayscaleGray7), + if (searchList.isNotEmpty) + Container( + color: GuamColorFamily.grayscaleWhite, + padding: EdgeInsets.symmetric(vertical: 12, horizontal: 24), + child: Column( + children: [...searchList.map((w) => Padding( + child: SearchWord(w, showSearchHistory), + padding: EdgeInsets.only(bottom: 16), + ))], + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/search/search_word.dart b/lib/screens/search/search_word.dart new file mode 100644 index 00000000..8f1beb1d --- /dev/null +++ b/lib/screens/search/search_word.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/commons/icon_text.dart'; +import 'package:guam_community_client/screens/search/search_app_textfield.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:guam_community_client/styles/fonts.dart'; +import 'package:provider/provider.dart'; +import '../../providers/search/search.dart'; + +class SearchWord extends StatelessWidget { + final String word; + final Function showSearchHistory; + + SearchWord(this.word, this.showSearchHistory); + + @override + Widget build(BuildContext context) { + final searchProvider = context.read(); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: InkWell( + child: Text( + word, + style: TextStyle( + fontSize: 14, + height: 22.4/14, + overflow: TextOverflow.ellipsis, + color: GuamColorFamily.grayscaleGray2, + fontFamily: GuamFontFamily.SpoqaHanSansNeoRegular, + ), + ), + onTap: () { + searchProvider.searchPosts(query: word); + showSearchHistory(false); + SearchAppTextFieldState.controller.text = word; + searchProvider.saveHistory(word); + }, + ), + ), + IconText( + iconSize: 18, + iconColor: GuamColorFamily.grayscaleGray6, + iconPath: 'assets/icons/cancel_filled_x_transparent.svg', + paddingBtw: 0, + onPressed: () => searchProvider.removeHistory(word), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/screens/user_auth/auth.dart b/lib/screens/user_auth/auth.dart new file mode 100644 index 00000000..e0c74fd2 --- /dev/null +++ b/lib/screens/user_auth/auth.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/screens/login/signup/signup.dart'; +import 'package:provider/provider.dart'; +import '../../providers/home/home_provider.dart'; +import '../app/app.dart'; +import '../login/login_page.dart'; +import '../../providers/user_auth/authenticate.dart'; + +class Auth extends StatelessWidget { + @override + Widget build(BuildContext context) { + final authProvider = context.watch(); + + return authProvider.userSignedIn() + ? authProvider.profileExists() + ? ChangeNotifierProvider( + create: (_) => HomeProvider(), + child: App(), + ) + : SignUp() + : LoginPage(); + } +} diff --git a/lib/screens/user_auth/kakao_login.dart b/lib/screens/user_auth/kakao_login.dart new file mode 100644 index 00000000..72334e02 --- /dev/null +++ b/lib/screens/user_auth/kakao_login.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:guam_community_client/styles/colors.dart'; +import 'package:kakao_flutter_sdk/all.dart'; +import '../../providers/user_auth/authenticate.dart'; +import 'package:provider/provider.dart'; +import '../login/login_button.dart'; + +class KakaoLogin extends StatefulWidget { + @override + State createState() { + return KakaoLoginState(); + } +} + +class KakaoLoginState extends State { + Authenticate authProvider; + bool _isKakaoTalkInstalled; + + @override + void initState() { + authProvider = context.read(); + KakaoContext.clientId = authProvider.kakaoClientId; + KakaoContext.javascriptClientId = authProvider.kakaoJavascriptClientId; + _initKakaoTalkInstalled(); + super.initState(); + } + + void _initKakaoTalkInstalled() async { + final installed = await isKakaoTalkInstalled(); + setState(() => _isKakaoTalkInstalled = installed); + } + + _issueAccessToken(String authCode) async { + try { + final token = await AuthApi.instance.issueAccessToken(authCode); + final tokenManager = new DefaultTokenManager(); + tokenManager.setToken(token); // Store access token in TokenManager for future API requests. + return token; + } catch (e) { + print(e.toString()); + } + } + + /* + * Kakao login via browser + */ + _loginWithKakao() async { + try { + authProvider.toggleLoading(); + + final authCode = await AuthCodeClient.instance.request(); + final token = await _issueAccessToken(authCode); + await authProvider.kakaoSignIn(token.accessToken); + } on KakaoAuthException catch (e) { + print("Kakao Auth Exception:\n$e"); + } on KakaoClientException catch (e) { + print("Kakao Client Exception:\n$e"); + } catch (e) { + print(e); + } finally { + authProvider.toggleLoading(); + } + } + + /* + * Kakao login via KakaoTalk + */ + _loginWithTalk() async { + try { + authProvider.toggleLoading(); + + final authCode = await AuthCodeClient.instance.requestWithTalk(); + final token = await _issueAccessToken(authCode); + await authProvider.kakaoSignIn(token.accessToken); + } on KakaoAuthException catch (e) { + print("Kakao Auth Exception:\n$e"); + } on KakaoClientException catch (e) { + print("Kakao Client Exception:\n$e"); + } catch (e) { + print(e); + } finally { + authProvider.toggleLoading(); + } + } + + @override + Widget build(BuildContext context) { + return LoginButton( + 'kakao_logo', + '카카오톡으로 시작하기', + GuamColorFamily.kakaoYellow, + () => _isKakaoTalkInstalled ? _loginWithTalk() : _loginWithKakao() + ); + } +} diff --git a/lib/screens/user_auth/sign_in.dart b/lib/screens/user_auth/sign_in.dart new file mode 100644 index 00000000..b91bc49a --- /dev/null +++ b/lib/screens/user_auth/sign_in.dart @@ -0,0 +1,58 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter_form_builder/flutter_form_builder.dart'; +// import '../../commons/custom_app_bar.dart'; +// import 'package:provider/provider.dart'; +// import '../../providers/user_auth/authenticate.dart'; +// +// class SignIn extends StatelessWidget { +// final _formKey = GlobalKey(); +// +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: CustomAppBar(title: '로그인'), +// body: Column( +// children: [ +// FormBuilder( +// key: _formKey, +// child: Column( +// children: [ +// FormBuilderTextField( +// name: 'email', +// decoration: InputDecoration( +// labelText: '이메일', +// ), +// valueTransformer: (text) => text.trim(), +// validator: FormBuilderValidators.compose([ +// FormBuilderValidators.required(context), +// ]), +// keyboardType: TextInputType.text, +// ), +// FormBuilderTextField( +// name: 'password', +// decoration: InputDecoration( +// labelText: '비밀번호', +// ), +// obscureText: true, +// valueTransformer: (text) => text.trim(), +// validator: FormBuilderValidators.compose([ +// FormBuilderValidators.required(context), +// ]), +// keyboardType: TextInputType.text, +// ), +// FlatButton( +// onPressed: () { +// if (_formKey.currentState.saveAndValidate()) { +// // context.read() +// // .signIn(params: _formKey.currentState.value) +// // .then((val) => Navigator.of(context).popUntil((route) => route.isFirst)); +// } +// }, +// child: const Text('확인')) +// ], +// ), +// ) +// ], +// )); +// } +// } diff --git a/lib/screens/user_auth/sign_out.dart b/lib/screens/user_auth/sign_out.dart new file mode 100644 index 00000000..3c604691 --- /dev/null +++ b/lib/screens/user_auth/sign_out.dart @@ -0,0 +1,14 @@ +// import 'package:flutter/material.dart'; +// import 'package:provider/provider.dart'; +// import '../../providers/user_auth/authenticate.dart'; +// import '../../commons/buttons/common_outlined_button.dart'; +// +// class SignOut extends StatelessWidget { +// @override +// Widget build(BuildContext context) { +// return commonOutlinedButton( +// text: "로그아웃", +// onPressed: () => context.read().signOut() +// ); +// } +// } diff --git a/lib/screens/user_auth/sign_up.dart b/lib/screens/user_auth/sign_up.dart new file mode 100644 index 00000000..96271b91 --- /dev/null +++ b/lib/screens/user_auth/sign_up.dart @@ -0,0 +1,70 @@ +// import 'package:flutter/material.dart'; +// import 'package:flutter_form_builder/flutter_form_builder.dart'; +// import '../../commons/custom_app_bar.dart'; +// import 'package:provider/provider.dart'; +// import '../../providers/user_auth/authenticate.dart'; +// +// class SignUp extends StatelessWidget { +// final _formKey = GlobalKey(); +// +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: CustomAppBar(title: '회원가입'), +// body: Column( +// children: [ +// FormBuilder( +// key: _formKey, +// child: Column( +// children: [ +// FormBuilderTextField( +// name: 'name', +// decoration: InputDecoration( +// labelText: '이름', +// ), +// valueTransformer: (text) => text.trim(), +// validator: FormBuilderValidators.compose([ +// FormBuilderValidators.required(context), +// ]), +// keyboardType: TextInputType.text, +// ), +// FormBuilderTextField( +// name: 'email', +// decoration: InputDecoration( +// labelText: '이메일', +// ), +// valueTransformer: (text) => text.trim(), +// validator: FormBuilderValidators.compose([ +// FormBuilderValidators.required(context), +// ]), +// keyboardType: TextInputType.text, +// ), +// FormBuilderTextField( +// name: 'password', +// decoration: InputDecoration( +// labelText: '비밀번호', +// ), +// obscureText: true, +// valueTransformer: (text) => text.trim(), +// validator: FormBuilderValidators.compose([ +// FormBuilderValidators.required(context), +// ]), +// keyboardType: TextInputType.text, +// ), +// FlatButton( +// onPressed: () { +// // if (_formKey.currentState.saveAndValidate()) { +// // context.read().signUp(params: _formKey.currentState.value) +// // .then((val) => Navigator.of(context).popUntil((route) => route.isFirst)); +// // } +// }, +// child: const Text('확인') +// ) +// ], +// ), +// ) +// ], +// ) +// ); +// } +// } diff --git a/lib/styles/colors.dart b/lib/styles/colors.dart new file mode 100644 index 00000000..3f34cef1 --- /dev/null +++ b/lib/styles/colors.dart @@ -0,0 +1,42 @@ +import 'dart:ui'; + +import 'package:hexcolor/hexcolor.dart'; + +class HexColorToColor extends Color { + static int _getColorFromHex(String hexColor) { + return int.parse(hexColor, radix: 16); + } + HexColorToColor(final String hexColor) : super(_getColorFromHex(hexColor)); +} + +class GuamColorFamily { + static HexColor grayscaleGray1 = HexColor('#1D1D1D'); + static HexColor grayscaleGray2 = HexColor('#4E4E4E'); + static HexColor grayscaleGray3 = HexColor('#767676'); + static HexColor grayscaleGray4 = HexColor('#A0A0A0'); + static HexColor grayscaleGray5 = HexColor('#C5C5C5'); + static HexColor grayscaleGray6 = HexColor('#E1E1E1'); + static HexColor grayscaleGray7 = HexColor('#F2F2F2'); + static HexColor grayscaleGray8 = HexColor('#FDFDFD'); + static HexColor grayscaleWhite = HexColor('#FFFFFF'); + + static HexColor purpleCore = HexColor('#6951FF'); + static HexColor purpleDark1 = HexColor('#5038E3'); + static HexColor purpleLight1 = HexColor('#9F8FFF'); + static HexColor purpleLight2 = HexColor('#E5E1FF'); + static HexColor purpleLight3 = HexColor('#F9F8FF'); + + static HexColor blueCore = HexColor('#5483F1'); + + static HexColor pinkCore = HexColor('#E874F2'); + + static HexColor greenCore = HexColor('#75D973'); + + static HexColor orangeCore = HexColor('#F3B962'); + + static HexColor redCore = HexColor('#F37462'); + + static HexColor fuchsiaCore = HexColor('#EF5DA8'); + + static HexColor kakaoYellow = HexColor('#FFCD00'); +} diff --git a/lib/styles/fonts.dart b/lib/styles/fonts.dart new file mode 100644 index 00000000..2cde99d9 --- /dev/null +++ b/lib/styles/fonts.dart @@ -0,0 +1,5 @@ +class GuamFontFamily { + static const SpoqaHanSansNeoMedium = "SpoqaHanSansNeoMedium"; + static const SpoqaHanSansNeoRegular = "SpoqaHanSansNeoRegular"; + static const Poppins = "Poppins"; +} diff --git a/pubspec.lock b/pubspec.lock index 7ffced4e..ce190f05 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -15,20 +15,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + bot_toast: + dependency: "direct main" + description: + name: bot_toast + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.2" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.1" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -36,6 +50,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.17" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "5.5.7" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.6.16" collection: dependency: transitive description: @@ -43,13 +78,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + conditional_builder: + dependency: "direct main" + description: + name: conditional_builder + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + cross_file: + dependency: transitive + description: + name: cross_file + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3+1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" + dio: + dependency: "direct main" + description: + name: dio + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.6" + dotted_border: + dependency: "direct main" + description: + name: dotted_border + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0+1" fake_async: dependency: transitive description: @@ -57,30 +127,376 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + url: "https://pub.dartlang.org" + source: hosted + version: "9.1.9" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.7" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0+14" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.19" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "6.2.7" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.16" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + url: "https://pub.dartlang.org" + source: hosted + version: "1.17.1" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.4" + firebase_messaging: + dependency: "direct main" + description: + name: firebase_messaging + url: "https://pub.dartlang.org" + source: hosted + version: "11.4.1" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.1" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_form_builder: + dependency: "direct main" + description: + name: flutter_form_builder + url: "https://pub.dartlang.org" + source: hosted + version: "6.2.0" + flutter_linkify: + dependency: "direct main" + description: + name: flutter_linkify + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.2" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_mentions: + dependency: "direct main" + description: + name: flutter_mentions + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + flutter_portal: + dependency: "direct main" + description: + name: flutter_portal + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "0.23.0+1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + hexcolor: + dependency: "direct main" + description: + name: hexcolor + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.7" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + image_picker: + dependency: "direct main" + description: + name: image_picker + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.5+4" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.8" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.0" + intl: + dependency: "direct main" + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + jiffy: + dependency: "direct main" + description: + name: jiffy + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.5.0" + kakao_flutter_sdk: + dependency: "direct main" + description: + name: kakao_flutter_sdk + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.0" + linkify: + dependency: transitive + description: + name: linkify + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted + version: "1.7.0" + modal_bottom_sheet: + dependency: "direct main" + description: + name: modal_bottom_sheet + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + package_info: + dependency: "direct main" + description: + name: package_info + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + package_info_plus_linux: + dependency: transitive + description: + name: package_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_info_plus_macos: + dependency: transitive + description: + name: package_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted version: "1.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + package_info_plus_web: + dependency: transitive + description: + name: package_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_info_plus_windows: + dependency: transitive + description: + name: package_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" path: dependency: transitive description: @@ -88,6 +504,160 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.1+1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.10" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.14" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.6" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + platform: + dependency: "direct main" + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.15" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.12" + shared_preferences_ios: + dependency: transitive + description: + name: shared_preferences_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + shared_preferences_macos: + dependency: transitive + description: + name: shared_preferences_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" sky_engine: dependency: transitive description: flutter @@ -134,7 +704,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.4.8" + transparent_image: + dependency: "direct main" + description: + name: transparent_image + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" typed_data: dependency: transitive description: @@ -142,12 +719,90 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "2.5.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+1" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.1" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.16.0 <3.0.0" + flutter: ">=2.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 066484de..b7373d6c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: guam_community_client -description: Connect Developers +description: We connect developers. # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. @@ -15,19 +15,52 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.0.0+1 +version: 1.0.3 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.10.0 <3.0.0" dependencies: flutter: sdk: flutter + http: ^0.13.1 + provider: ^5.0.0 + cupertino_icons: ^1.0.2 + hexcolor: ^2.0.3 + modal_bottom_sheet: ^2.0.0 + conditional_builder: ^1.0.2 + flutter_form_builder: ^6.0.0-nullsafety.1 + intl: ^0.17.0 + bot_toast: ^4.0.1 + dotted_border: ^2.0.0+1 + url_launcher: ^6.0.5 + flutter_secure_storage: ^5.0.2 + flutter_portal: ^0.4.0 + flutter_mentions: 2.0.1 + jiffy: ^5.0.0 + flutter_linkify: ^5.0.2 + path_provider: ^2.0.9 + # Image + flutter_svg: ^0.23.0+1 + transparent_image: ^2.0.0 + image_picker: ^0.7.4 + carousel_slider: ^4.0.0 - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + # FlutterFire + firebase_core: ^1.12.0 + firebase_analytics: ^9.1.0 + firebase_auth: ^3.3.6 + cloud_firestore: ^3.1.7 + firebase_messaging: ^11.2.6 + + # Kakao Login Dependency + kakao_flutter_sdk: ^0.9.0 + dio: ^4.0.4 + json_annotation: ^4.4.0 + shared_preferences: ^2.0.13 + platform: ^3.1.0 + package_info: ^2.0.2 dev_dependencies: flutter_test: @@ -38,17 +71,14 @@ dev_dependencies: # The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - + assets: + - assets/icons/ + - assets/logos/ + - assets/images/ + - assets/gifs/ + - assets/backgrounds/ + - assets/backgrounds/splash/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. @@ -60,17 +90,15 @@ flutter: # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 + fonts: + - family: SpoqaHanSansNeoMedium + fonts: + - asset: assets/fonts/SpoqaHanSansNeo-Medium.ttf + weight: 400 + - family: SpoqaHanSansNeoRegular + fonts: + - asset: assets/fonts/SpoqaHanSansNeo-Regular.ttf + weight: 400 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages