diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..7bae7a0 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# FILLIN Android Team +* @TeamFILL-IN/android \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..c5c7c0b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,16 @@ +--- +name: Feature request +about: FILL-IN Issue Template +title: '' +labels: '' +assignees: '' + +--- + + +# πŸ“ DESCRIPTION +### Describe issue here + +# β˜‘οΈ TODO +- [ ] TODO 1 +- [ ] TODO 2 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3823552 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +## Related Issues + + +## What Did You Do? +- [x] + + +## Reference +- [x] diff --git a/.github/workflows/debug_builder.yml b/.github/workflows/debug_builder.yml new file mode 100644 index 0000000..ff38f89 --- /dev/null +++ b/.github/workflows/debug_builder.yml @@ -0,0 +1,61 @@ +name: FILLIN Push Builder + +on: + push: + branches: [ develop, master ] + +defaults: + run: + shell: bash + working-directory: . + +jobs: + build: + name: APK Builder When Push + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Gradle cache + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Change gradlew permissions + run: chmod +x ./gradlew + + - name: Build debug APK + run: ./gradlew assembleDebug --stacktrace + + - name: On Success + if: ${{ success() }} + uses: MeilCli/slack-upload-file@v1 + with: + slack_token: ${{ secrets.SLACK_BOT_TOKEN}} + channels: ${{ secrets.SLACK_CHANNEL_ID }} + file_path: 'app/build/outputs/apk/debug/app-debug.apk' + file_name: 'FILLIN.apk' + file_type: 'apk' + initial_comment: 'μ°°μΉ΅ πŸ“Έ FILL-IN이 μŠ¬λž™μœΌλ‘œ λ“€μ–΄μ˜€λŠ” μž₯면을 μ°μ–΄λ³΄κ² μŠ΅λ‹ˆλ‹€' + + - name: On Failed, Notify in Slack + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: '#ff0000' + SLACK_ICON: https://user-images.githubusercontent.com/54518925/148585882-ee5c6dc5-6789-4b90-9fd0-f7382275e974.jpeg?size=48 + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_TITLE: 'FILLIN/Android Debug build Fail❌' + MSG_MINIMAL: true + SLACK_USERNAME: FILLIN AND-BOT + SLACK_MESSAGE: 'APK 생성 쀑 μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. μ—λŸ¬λ₯Ό ν™•μΈν•΄μ£Όμ„Έμš”' diff --git a/.github/workflows/pr_checker.yml b/.github/workflows/pr_checker.yml new file mode 100644 index 0000000..6581d76 --- /dev/null +++ b/.github/workflows/pr_checker.yml @@ -0,0 +1,62 @@ +name: FILLIN PR Checker + +on: + pull_request: + branches: [ develop, master ] + +defaults: + run: + shell: bash + working-directory: . + +jobs: + build: + name: PR Checker + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Gradle cache + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Change gradlew permissions + run: chmod +x ./gradlew + + - name: Build debug APK + run: ./gradlew assembleDebug --stacktrace + + - name: On Success + if: ${{ success() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: '#53A551' + SLACK_ICON: https://user-images.githubusercontent.com/54518925/148585882-ee5c6dc5-6789-4b90-9fd0-f7382275e974.jpeg?size=48 + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_TITLE: 'FILLIN/PR Check S.U.C.C.E.S.S πŸŽ‰πŸŽ‰πŸŽ‰' + MSG_MINIMAL: true + SLACK_USERNAME: FILLIN AND-BOT + SLACK_MESSAGE: 'P R μ„± 곡!!! πŸŽ‰πŸŽ‰πŸŽ‰' + + - name: On Failed, Notify in Slack + if: ${{ failure() }} + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: '#ff0000' + SLACK_ICON: https://user-images.githubusercontent.com/54518925/148585882-ee5c6dc5-6789-4b90-9fd0-f7382275e974.jpeg?size=48 + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_TITLE: 'FILLIN/Android Debug build Fail❌' + MSG_MINIMAL: true + SLACK_USERNAME: FILLIN AND-BOT + SLACK_MESSAGE: 'μ—λŸ¬λ₯Ό ν™•μΈν•΄μ£Όμ„Έμš”' diff --git a/.gitignore b/.gitignore index a421e68..ee24cc8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,10 +24,6 @@ output.json *.iml .idea/ -# Keystore files -*.jks -*.keystore - # Google Services (e.g. APIs or Firebase) google-services.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f4b726 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Fill-Android + +필름 ν˜„μƒμ†Œ, 필름 μ •λ³΄μ œκ³΅ μ„œλΉ„μŠ€ +λ‹Ήμ‹ μ˜ μΆ”μ–΅μœΌλ‘œ FILL-IN

+ + +## 기술 μŠ€νƒ πŸ’» +- Architecture: MVC + MVVM +- Android Jetpack + - Lifecycle + - AAC + - Dagger-Hilt + - App Startup + - DataBinding/ViewBinding + - Security(EncryptedSharedPreference) + - Paging3 + - SplashScreen Core +- Modern Kotlin + - Coroutines + Flow +- CI/CD + - Github Action + - Slack +- Glide +- Retrofit/Okhttp +- Kakao SDK +- NaverMap SDK +- Timber +- Flipper + +## μ—­ν•  πŸ“Έ +- 이강민 : 지도 λ·°, 지도 검색, 지도 상세정보 +- μœ€ν˜„μ§€ : λ§ˆμ΄νŽ˜μ΄μ§€, μ‚¬μ§„μ—…λ‘œλ“œ 및 μŠ€νŠœλ””μ˜€ν•„λ¦„ 선택 +- κΉ€μˆ˜λΉˆ : 메인 ν™ˆ, 필름둀 , 사진상세보기(λͺ¨λ‹¬μ°½) +- μ΄ν˜„μš° : μ†Œμ…œ 둜그인, ν”„λ‘œμ νŠΈ μ„€μ •, μœ ν‹Έ, κ·Έ μ™Έ μž‘μ—… + +## 폴더링 ꡬ쑰 πŸ“‚ +
+
+### package name은 λ°˜λ“œμ‹œ μ†Œλ¬Έμžλ‘œ μž‘μ„± +- presentation -> λ·° κ΄€λ ¨ μž‘μ—… +- di -> μ˜μ‘΄μ„± μ£Όμž… κ΄€λ ¨ λͺ¨λ“ˆ +- data -> μ„œλ²„, 데이터 κ΄€λ ¨ μž‘μ—… +- core -> util ν™•μž₯ν•¨μˆ˜ λͺ¨λ“ˆ + +## μ»¨λ²€μ…˜ 🎞 +- Coding Convention: +- Branch μ „λž΅: +- Github Convention: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4fbb912..e726b3e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,28 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + signingConfigs { + getByName("debug") { + keyAlias = "androiddebugkey" + keyPassword = "android" + storeFile = File("${project.rootDir.absolutePath}/keystore/debug.keystore") + storePassword = "android" + } + create("release") { + keyAlias = "fillin" + keyPassword = "fillinandroid" + storeFile = File("${project.rootDir.absolutePath}/keystore/releasekey.jks") + storePassword = "fillinandroid" + } + } + buildTypes { + getByName("debug") { + buildConfigField("String", "KAKAO_AUTH", "\"a7ddbcd24d7fff22320cc13a1e534104\"") + } + getByName("release") { + signingConfig = signingConfigs.getByName("release") isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), @@ -47,6 +67,8 @@ android { } dependencies { + implementation(project(":core")) + // Kotlin implementation(KotlinDependencies.kotlin) @@ -54,6 +76,7 @@ dependencies { implementation(AndroidXDependencies.coreKtx) implementation(AndroidXDependencies.appCompat) implementation(AndroidXDependencies.constraintLayout) + implementation(AndroidXDependencies.startup) implementation(AndroidXDependencies.hilt) kapt(KaptDependencies.hiltCompiler) implementation(AndroidXDependencies.fragment) @@ -62,6 +85,8 @@ dependencies { implementation(AndroidXDependencies.coroutines) implementation(AndroidXDependencies.lifeCycleKtx) implementation(AndroidXDependencies.lifecycleJava8) + implementation(AndroidXDependencies.pagingRuntime) + implementation(AndroidXDependencies.splashScreen) // Third-Party implementation(ThirdPartyDependencies.glide) @@ -74,10 +99,23 @@ dependencies { implementation(ThirdPartyDependencies.retrofitGsonConverter) implementation(ThirdPartyDependencies.timber) implementation(ThirdPartyDependencies.ossLicense) + implementation(ThirdPartyDependencies.kakaoLogin) + implementation(ThirdPartyDependencies.naverMap) + implementation(ThirdPartyDependencies.mapLocation) + implementation(ThirdPartyDependencies.dotsIndicator) // Material Design implementation(MaterialDesignDependencies.materialDesign) + // Flipper + debugImplementation(ThirdPartyDependencies.flipper) + debugImplementation(ThirdPartyDependencies.flipperNetwork) { + exclude("com.squareup.okhttp3", "okhttp") + } + debugImplementation(ThirdPartyDependencies.flipperLeakCanary) + debugImplementation(ThirdPartyDependencies.leakCanary) + debugImplementation(ThirdPartyDependencies.soloader) + // Test Dependency testImplementation(TestDependencies.jUnit) androidTestImplementation(TestDependencies.androidTest) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ff59496..d1e1531 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -18,4 +18,105 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile + +##---------------Begin: Kakao ---------- +-keep class com.kakao.sdk.**.model.* { ; } +-keep class * extends com.google.gson.TypeAdapter + +##---------------End: Kakao ---------- + +##---------------Begin: proguard configuration for Gson ---------- +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Application classes that will be serialized/deserialized over Gson +-keep class com.google.gson.examples.android.model.** { ; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken + +##---------------End: proguard configuration for Gson ---------- + +##---------------Begin: proguard configuration for Glide ---------- +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep class * extends com.bumptech.glide.module.AppGlideModule { + (...); +} +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} +-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder { + *** rewind(); +} +##---------------End: proguard configuration for Glide ---------- + +##---------------Begin: proguard configuration for Retrofit ---------- +# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and +# EnclosingMethod is required to use InnerClasses. +-keepattributes Signature, InnerClasses, EnclosingMethod + +# Retrofit does reflection on method and parameter annotations. +-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations + +# Keep annotation default values (e.g., retrofit2.http.Field.encoded). +-keepattributes AnnotationDefault + +# Retain service method parameters when optimizing. +-keepclassmembers,allowshrinking,allowobfuscation interface * { + @retrofit2.http.* ; +} + +# Ignore annotation used for build tooling. +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement + +# Ignore JSR 305 annotations for embedding nullability information. +-dontwarn javax.annotation.** + +# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath. +-dontwarn kotlin.Unit + +# Top-level functions that can only be used by Kotlin. +-dontwarn retrofit2.KotlinExtensions + +# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy +# and replaces all potential values with null. Explicitly keeping the interfaces prevents this. +-if interface * { @retrofit2.http.* ; } +-keep,allowobfuscation interface <1> + +# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items). +-keep,allowobfuscation,allowshrinking interface retrofit2.Call +-keep,allowobfuscation,allowshrinking class retrofit2.Response + +# With R8 full mode generic signatures are stripped for classes that are not +# kept. Suspend functions are wrapped in continuations where the type argument +# is used. +-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation +##---------------End: proguard configuration for Retrofit ---------- + +##---------------Begin: proguard configuration for Flipper ---------- +-keep class com.facebook.jni.** { *; } +-keep class com.facebook.flipper.** { *; } +##---------------End: proguard configuration for Flipper ---------- diff --git a/app/src/debug/java/com/teamfillin/fillin/FlipperInitializer.kt b/app/src/debug/java/com/teamfillin/fillin/FlipperInitializer.kt new file mode 100644 index 0000000..e235b8c --- /dev/null +++ b/app/src/debug/java/com/teamfillin/fillin/FlipperInitializer.kt @@ -0,0 +1,37 @@ +package com.teamfillin.fillin + +import android.app.Application +import com.facebook.flipper.android.AndroidFlipperClient +import com.facebook.flipper.plugins.inspector.DescriptorMapping +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin +import com.facebook.flipper.plugins.leakcanary2.FlipperLeakListener +import com.facebook.flipper.plugins.leakcanary2.LeakCanary2FlipperPlugin +import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor +import com.facebook.flipper.plugins.network.NetworkFlipperPlugin +import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin +import com.facebook.soloader.SoLoader +import com.teamfillin.fillin.data.local.FILE_NAME +import leakcanary.LeakCanary +import okhttp3.OkHttpClient + +object FlipperInitializer { + private val flipperNetworkPlugin = NetworkFlipperPlugin() + + fun init(app: Application) { + LeakCanary.config = LeakCanary.config.copy( + onHeapAnalyzedListener = FlipperLeakListener() + ) + SoLoader.init(app, false) + val client = AndroidFlipperClient.getInstance(app) + client.addPlugin(InspectorFlipperPlugin(app, DescriptorMapping.withDefaults())) + client.addPlugin(InspectorFlipperPlugin(app, DescriptorMapping.withDefaults())) + client.addPlugin(flipperNetworkPlugin) + client.addPlugin(SharedPreferencesFlipperPlugin(app, FILE_NAME)) + client.addPlugin(LeakCanary2FlipperPlugin()) + client.start() + } + + fun addFlipperNetworkPlguin(builder: OkHttpClient.Builder): OkHttpClient.Builder { + return builder.addNetworkInterceptor(FlipperOkhttpInterceptor(flipperNetworkPlugin)) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8f584fb..3a119f1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,13 +1,100 @@ + + + + + android:supportsRtl="false" + android:theme="@style/Theme.FillInAndroid" + tools:replace="android:supportsRtl"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_close22-playstore.png b/app/src/main/ic_close22-playstore.png new file mode 100644 index 0000000..9f4afb7 Binary files /dev/null and b/app/src/main/ic_close22-playstore.png differ diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..0164feb Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/teamfillin/fillin/App.kt b/app/src/main/java/com/teamfillin/fillin/App.kt new file mode 100644 index 0000000..1e66740 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/App.kt @@ -0,0 +1,24 @@ +package com.teamfillin.fillin + +import android.app.Application +import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO +import com.teamfillin.fillin.design.ResolutionMetrics +import dagger.hilt.android.HiltAndroidApp +import javax.inject.Inject + +@HiltAndroidApp +class App : Application() { + @Inject + lateinit var metrics: ResolutionMetrics + override fun onCreate() { + super.onCreate() + AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_NO) + FlipperInitializer.init(this) + App.resolutionMetrics = metrics + } + + companion object { + lateinit var resolutionMetrics: ResolutionMetrics + } +} diff --git a/app/src/main/java/com/teamfillin/fillin/config/di/LoginModule.kt b/app/src/main/java/com/teamfillin/fillin/config/di/LoginModule.kt new file mode 100644 index 0000000..cbdc401 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/config/di/LoginModule.kt @@ -0,0 +1,25 @@ +package com.teamfillin.fillin.config.di + +import android.content.Context +import com.kakao.sdk.user.UserApiClient +import com.teamfillin.fillin.presentation.login.KakaoAuthService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +object LoginModule { + @Provides + @ActivityScoped + fun provideUserApiClient(): UserApiClient = UserApiClient.instance + + @Provides + fun provideKakaoAuthService( + @ActivityContext context: Context, + client: UserApiClient + ) = KakaoAuthService(context, client) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/config/di/RepositoryModule.kt b/app/src/main/java/com/teamfillin/fillin/config/di/RepositoryModule.kt new file mode 100644 index 0000000..dca3487 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/config/di/RepositoryModule.kt @@ -0,0 +1,31 @@ +package com.teamfillin.fillin.config.di + +import com.teamfillin.fillin.data.repository.AuthRepositoryImpl +import com.teamfillin.fillin.data.repository.PhotoPagingRepository +import com.teamfillin.fillin.data.service.PagingService +import com.teamfillin.fillin.domain.repository.AuthRepository +import com.teamfillin.fillin.presentation.filmroll.add.repository.AddPhotoRepository +import com.teamfillin.fillin.presentation.filmroll.add.repository.AddPhotoRepositoryImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RepositoryModule { + @Provides + @Singleton + fun provideAuthRepository(authRepository: AuthRepositoryImpl): AuthRepository = authRepository + + @Provides + @Singleton + fun provideAddPhotoRepository(repository: AddPhotoRepositoryImpl): AddPhotoRepository = + repository + + @Provides + @Singleton + fun providePhotoPagingRepository(service: PagingService): PhotoPagingRepository = + PhotoPagingRepository(service) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/config/di/RetrofitModule.kt b/app/src/main/java/com/teamfillin/fillin/config/di/RetrofitModule.kt new file mode 100644 index 0000000..cde0d16 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/config/di/RetrofitModule.kt @@ -0,0 +1,57 @@ +package com.teamfillin.fillin.config.di + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.teamfillin.fillin.FlipperInitializer +import com.teamfillin.fillin.data.interceptor.AuthInterceptor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RetrofitModule { + @Provides + @Singleton + fun provideAuthInterceptor(authInterceptor: AuthInterceptor): Interceptor = authInterceptor + + @Provides + @Singleton + fun provideOkHttpInterceptor( + authInterceptor: Interceptor + ): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + ) + .addInterceptor(authInterceptor) + .apply { FlipperInitializer.addFlipperNetworkPlguin(this).build() } + .build() + } + + @Provides + @Singleton + fun provideGson(): Gson = GsonBuilder().setLenient().create() + + @Provides + @Singleton + fun provideRetrofit( + client: OkHttpClient, + gson: Gson + ): Retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + + const val BASE_URL = "https://asia-northeast3-fill-in-13efb.cloudfunctions.net/app/api/" +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/config/di/ServiceModule.kt b/app/src/main/java/com/teamfillin/fillin/config/di/ServiceModule.kt new file mode 100644 index 0000000..b1304f3 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/config/di/ServiceModule.kt @@ -0,0 +1,54 @@ +package com.teamfillin.fillin.config.di + +import com.teamfillin.fillin.data.service.* +import com.teamfillin.fillin.data.service.PagingService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ServiceModule { + @Provides + @Singleton + fun provideAuthService(retrofit: Retrofit): AuthService = + retrofit.create(AuthService::class.java) + + @Provides + @Singleton + fun provideStudioService(retrofit: Retrofit): StudioService = + retrofit.create(StudioService::class.java) + + @Provides + @Singleton + fun provideNewPhotoService(retrofit: Retrofit): HomeService = + retrofit.create(HomeService::class.java) + + @Provides + @Singleton + fun provideFilmStyleService(retrofit: Retrofit): FilmStyleService = + retrofit.create(FilmStyleService::class.java) + + @Provides + @Singleton + fun provideUserInfoService(retrofit: Retrofit): UserService= + retrofit.create(UserService::class.java) + + @Provides + @Singleton + fun provideUserPhotoService(retrofit:Retrofit): MyPagePhotoService= + retrofit.create(MyPagePhotoService::class.java) + + @Provides + @Singleton + fun provideFillRollService(retrofit: Retrofit): FilmRollService = + retrofit.create(FilmRollService::class.java) + + @Provides + @Singleton + fun providePagingService(retrofit: Retrofit): PagingService = + retrofit.create(PagingService::class.java) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/config/di/SingletonModule.kt b/app/src/main/java/com/teamfillin/fillin/config/di/SingletonModule.kt new file mode 100644 index 0000000..990ee3c --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/config/di/SingletonModule.kt @@ -0,0 +1,30 @@ +package com.teamfillin.fillin.config.di + +import android.app.Application +import android.content.Context +import com.teamfillin.fillin.data.local.FillInDataStore +import com.teamfillin.fillin.design.ResolutionMetrics +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object SingletonModule { + @Provides + @Singleton + @ApplicationContext + fun provideApplication(application: Application) = application + + @Provides + @Singleton + fun provideResolutionMetrics(@ApplicationContext context: Application) = + ResolutionMetrics(context) + + @Provides + @Singleton + fun provideFillInDataStore(@ApplicationContext context: Context) = FillInDataStore(context) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/config/initializer/KakaoAuthInitializer.kt b/app/src/main/java/com/teamfillin/fillin/config/initializer/KakaoAuthInitializer.kt new file mode 100644 index 0000000..3a79bbd --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/config/initializer/KakaoAuthInitializer.kt @@ -0,0 +1,16 @@ +package com.teamfillin.fillin.config.initializer + +import android.content.Context +import androidx.startup.Initializer +import com.kakao.sdk.common.KakaoSdk +import com.teamfillin.fillin.BuildConfig + +class KakaoAuthInitializer : Initializer { + override fun create(context: Context) { + KakaoSdk.init(context, BuildConfig.KAKAO_AUTH) + } + + override fun dependencies(): MutableList>> { + return mutableListOf(TimberInitializer::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/config/initializer/TimberInitializer.kt b/app/src/main/java/com/teamfillin/fillin/config/initializer/TimberInitializer.kt new file mode 100644 index 0000000..10e94cc --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/config/initializer/TimberInitializer.kt @@ -0,0 +1,21 @@ +package com.teamfillin.fillin.config.initializer + +import android.content.Context +import androidx.startup.Initializer +import timber.log.Timber + +class TimberInitializer : Initializer { + override fun create(context: Context) { + Timber.plant(FillInDebugTree()) + } + + private class FillInDebugTree : Timber.DebugTree() { + override fun createStackElementTag(element: StackTraceElement): String? { + return "${element.className} ${element.methodName} ${element.lineNumber}" + } + } + + override fun dependencies(): MutableList>> { + return mutableListOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/interceptor/AuthInterceptor.kt b/app/src/main/java/com/teamfillin/fillin/data/interceptor/AuthInterceptor.kt new file mode 100644 index 0000000..de24e13 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/interceptor/AuthInterceptor.kt @@ -0,0 +1,54 @@ +package com.teamfillin.fillin.data.interceptor + +import com.google.gson.Gson +import com.teamfillin.fillin.config.di.RetrofitModule.BASE_URL +import com.teamfillin.fillin.data.local.FillInDataStore +import com.teamfillin.fillin.data.response.ResponseRefresh +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import javax.inject.Inject + +class AuthInterceptor @Inject constructor( + private val localStorage: FillInDataStore, + private val gson: Gson +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest = chain.request() + val authRequest = if (isLogin(originalRequest)) originalRequest else + originalRequest.newBuilder().addHeader("token", localStorage.userToken).build() + val response = chain.proceed(authRequest) + when (response.code) { + 401 -> { + val refreshTokenRequest = originalRequest.newBuilder().get() + .url("${BASE_URL}auth/token") + .addHeader("accesstoken", localStorage.userToken) + .addHeader("refreshtoken", localStorage.refreshToken) + .build() + val refreshTokenResponse = chain.proceed(refreshTokenRequest) + + if (refreshTokenResponse.isSuccessful) { + val refreshToken = gson.fromJson( + refreshTokenResponse.body?.string(), + ResponseRefresh::class.java + ) + with(localStorage) { + userToken = refreshToken.tokens.newAccessToken + this.refreshToken = refreshToken.tokens.refreshToken + } + val newRequest = originalRequest.newBuilder() + .addHeader("token", localStorage.userToken) + .build() + return chain.proceed(newRequest) + } + } + } + return response + } + + private fun isLogin(originalRequest: Request) = + !originalRequest.url.encodedPath.contains("token") && + originalRequest.url.encodedPath.contains("auth") + +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/local/FillInDataStore.kt b/app/src/main/java/com/teamfillin/fillin/data/local/FillInDataStore.kt new file mode 100644 index 0000000..359e182 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/local/FillInDataStore.kt @@ -0,0 +1,41 @@ +package com.teamfillin.fillin.data.local + +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys +import com.teamfillin.fillin.BuildConfig +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +const val FILE_NAME = "FILLINDATASTORE" + +class FillInDataStore @Inject constructor( + @ApplicationContext private val context: Context +) { + private val dataStore: SharedPreferences = + if (BuildConfig.DEBUG) context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) + else EncryptedSharedPreferences.create( + FILE_NAME, + MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC), + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + var userToken: String + set(value) = dataStore.edit { putString("USER_TOKEN", value) } + get() = dataStore.getString("USER_TOKEN", "") ?: "" + + var refreshToken: String + set(value) = dataStore.edit { putString("REFRESH_TOKEN", value) } + get() = dataStore.getString("REFRESH_TOKEN", "") ?: "" + + var nickname: String + set(value) = dataStore.edit { putString("NICKNAME", value) } + get() = dataStore.getString("NICKNAME", "") ?: "" + var isOnboardingShown: Boolean + set(value) = dataStore.edit { putBoolean("ONBOARDING", value) } + get() = dataStore.getBoolean("ONBOARDING", false) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/remote/CategoryPhotosPagingSource.kt b/app/src/main/java/com/teamfillin/fillin/data/remote/CategoryPhotosPagingSource.kt new file mode 100644 index 0000000..6ae6c8e --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/remote/CategoryPhotosPagingSource.kt @@ -0,0 +1,39 @@ +package com.teamfillin.fillin.data.remote + +import com.teamfillin.fillin.core.base.OffsetPagingSource +import com.teamfillin.fillin.core.base.START_POSITION_INDEX +import com.teamfillin.fillin.data.response.experimental.CategoryPhotoDto +import com.teamfillin.fillin.data.service.PagingService +import timber.log.Timber + +class CategoryPhotosPagingSource( + private val service: PagingService, + private val styleId: Int, + private val filmId: Int +) : OffsetPagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + val currentPosition = params.key ?: START_POSITION_INDEX + + val response = runCatching { + if (styleId != -1) service.retrievePhotosByStyle(styleId, currentPosition) + else service.retrievePhotosByFilm(filmId, currentPosition) + }.getOrElse { + Timber.e(it) + return LoadResult.Error(it) + } + + val nextPositon = if (response.data.photos.isEmpty()) null else currentPosition + 1 + val previousPosition = + if (currentPosition != START_POSITION_INDEX) null else currentPosition - 1 + return runCatching { + LoadResult.Page( + data = response.data.photos, + prevKey = previousPosition, + nextKey = nextPositon + ) + }.getOrElse { + Timber.e(it) + LoadResult.Error(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/remote/PhotosPagingSource.kt b/app/src/main/java/com/teamfillin/fillin/data/remote/PhotosPagingSource.kt new file mode 100644 index 0000000..f706641 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/remote/PhotosPagingSource.kt @@ -0,0 +1,28 @@ +package com.teamfillin.fillin.data.remote + +import com.teamfillin.fillin.core.base.OffsetPagingSource +import com.teamfillin.fillin.core.base.START_POSITION_INDEX +import com.teamfillin.fillin.data.response.experimental.PhotoDto +import com.teamfillin.fillin.data.service.PagingService + +class PhotosPagingSource( + private val service: PagingService +) : OffsetPagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + val currentPosition = params.key ?: START_POSITION_INDEX + + val response = runCatching { service.retrievePhotos(currentPosition) } + .getOrElse { return LoadResult.Error(it) } + + val nextPositon = currentPosition + 1 + val previousPosition = + if (currentPosition != START_POSITION_INDEX) null else currentPosition - 1 + return runCatching { + LoadResult.Page( + data = response.data.photos, + prevKey = previousPosition, + nextKey = nextPositon + ) + }.getOrElse { LoadResult.Error(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/teamfillin/fillin/data/repository/AuthRepositoryImpl.kt new file mode 100644 index 0000000..a2d56c8 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/repository/AuthRepositoryImpl.kt @@ -0,0 +1,28 @@ +package com.teamfillin.fillin.data.repository + +import com.teamfillin.fillin.data.local.FillInDataStore +import com.teamfillin.fillin.data.response.toEntity +import com.teamfillin.fillin.data.service.AuthService +import com.teamfillin.fillin.domain.entity.Auth +import com.teamfillin.fillin.domain.repository.AuthRepository +import retrofit2.await +import timber.log.Timber +import javax.inject.Inject + +class AuthRepositoryImpl @Inject constructor( + private val dataStore: FillInDataStore, + private val service: AuthService +) : AuthRepository { + override suspend fun login(token: String, id: String) { + runCatching { + service.login(token, id = id).await() + }.fold({ + val auth = it.data.toEntity() + with(dataStore) { + userToken = auth.accessToken + refreshToken = auth.refreshToken + } + if (auth is Auth.SignUp) dataStore.nickname = auth.nickName + }, Timber::e) + } +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/repository/PhotoPagingRepository.kt b/app/src/main/java/com/teamfillin/fillin/data/repository/PhotoPagingRepository.kt new file mode 100644 index 0000000..bcfaf8c --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/repository/PhotoPagingRepository.kt @@ -0,0 +1,16 @@ +package com.teamfillin.fillin.data.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import com.teamfillin.fillin.data.remote.PhotosPagingSource +import com.teamfillin.fillin.data.service.PagingService +import javax.inject.Inject + +class PhotoPagingRepository @Inject constructor( + private val service: PagingService +) { + fun retrievePager() = Pager( + config = PagingConfig(10), + pagingSourceFactory = { PhotosPagingSource(service) } + ).flow +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/BaseResponse.kt b/app/src/main/java/com/teamfillin/fillin/data/response/BaseResponse.kt new file mode 100644 index 0000000..4a90b78 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/BaseResponse.kt @@ -0,0 +1,8 @@ +package com.teamfillin.fillin.data.response + +data class BaseResponse( + val status: Int, + val success: Boolean, + val message: String, + val data: T +) diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseAuth.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseAuth.kt new file mode 100644 index 0000000..40ffe5b --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseAuth.kt @@ -0,0 +1,15 @@ +package com.teamfillin.fillin.data.response + +import com.teamfillin.fillin.domain.entity.Auth + +data class ResponseAuth( + val email: String?, + val nickName: String? = null, + val accessToken: String, + val refreshToken: String +) + +fun ResponseAuth.toEntity(): Auth = when (nickName) { + null -> Auth.Login(email, accessToken, refreshToken) + else -> Auth.SignUp(email, nickName, accessToken, refreshToken) +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseFilmRoll.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseFilmRoll.kt new file mode 100644 index 0000000..87bdb48 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseFilmRoll.kt @@ -0,0 +1,24 @@ +package com.teamfillin.fillin.data.response + +data class ResponseFilmRoll( + val photos: List, + val curation: Curation +) { + data class FilmPhotoInfo( + val nickname: String, + val userImageUrl: String, + val photoId: Int, + val imageUrl: String, + val filmId: Int, + val filmName: String, + val likeCount: Int, + val isLiked: Boolean + ) + + data class Curation( + val id: Int, + val title: String, + val photolist: String + ) +} + diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseFilmStyleInfo.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseFilmStyleInfo.kt new file mode 100644 index 0000000..e187497 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseFilmStyleInfo.kt @@ -0,0 +1,16 @@ +package com.teamfillin.fillin.data.response + +data class ResponseFilmStyleInfo( + val photos: List +) { + data class FIlmStyleInfo( + val nickname: String, + val userImageUrl: String, + val photoId: Int, + val imageUrl: String, + val filmId: Int, + val filmName: String, + val likeCount: Int, + val isLiked: Boolean + ) +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseFilmType.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseFilmType.kt new file mode 100644 index 0000000..8384004 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseFilmType.kt @@ -0,0 +1,11 @@ +package com.teamfillin.fillin.data.response + +data class ResponseFilmType( + val films: List +) { + data class Films( + val id: Int, + val name: String, + val styleId: Int + ) +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseLocationInfo.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseLocationInfo.kt new file mode 100644 index 0000000..d70afdd --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseLocationInfo.kt @@ -0,0 +1,6 @@ +package com.teamfillin.fillin.data.response + +data class ResponseLocationInfo( + val name: String, + val location: String +) \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseNewPhotoInfo.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseNewPhotoInfo.kt new file mode 100644 index 0000000..ed62af5 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseNewPhotoInfo.kt @@ -0,0 +1,15 @@ +package com.teamfillin.fillin.data.response + +data class ResponseNewPhotoInfo( + val photos: List, +) { + data class Photo( + val nickname: String, + val userImageUrl: String, + val photoId: Int, + val imageUrl: String, + val filmId: Int, + val filmName: String, + val likeCount: Int + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseRefresh.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseRefresh.kt new file mode 100644 index 0000000..cb4fd6d --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseRefresh.kt @@ -0,0 +1,16 @@ +package com.teamfillin.fillin.data.response + +import com.google.gson.annotations.SerializedName + +data class ResponseRefresh( + val status: Int, + val success: Boolean, + val message: String, + val tokens: AuthToken +) { + data class AuthToken( + val newAccessToken: String, + @SerializedName("refreshtoken") + val refreshToken: String + ) +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseSearch.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseSearch.kt new file mode 100644 index 0000000..17ae985 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseSearch.kt @@ -0,0 +1,11 @@ +package com.teamfillin.fillin.data.response + +data class ResponseSearch( + val studios: List +) { + data class StudioResponse( + val id: Int, + val name: String, + val address: String + ) +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseStudio.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseStudio.kt new file mode 100644 index 0000000..c3ab597 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseStudio.kt @@ -0,0 +1,21 @@ +package com.teamfillin.fillin.data.response + +data class ResponseStudio( + val studio: Studio +) { + data class Studio( + val id: Int, + val name: String, + val address: String, + val price: String, + val time: String, + val tel: String, + val lati: Double, + val long: Double, + val etc: String, + val isDeleted: Boolean, + val site: String + ) +} + + diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseStudioLocation.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseStudioLocation.kt new file mode 100644 index 0000000..3c71596 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseStudioLocation.kt @@ -0,0 +1,11 @@ +package com.teamfillin.fillin.data.response + +data class ResponseStudioLocation( + val studios: List +) { + data class StudioLocation( + val id: Int, + val lati: Double, + val long: Double + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseStudioPhoto.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseStudioPhoto.kt new file mode 100644 index 0000000..4e03811 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseStudioPhoto.kt @@ -0,0 +1,16 @@ +package com.teamfillin.fillin.data.response + +data class ResponseStudioPhoto( + val photos: List +) { + data class StudioPhoto( + val studioName: String, + val nickname: String, + val userImageUrl: String, + val photoId: Int, + val imageUrl: String, + val filmId: Int, + val filmName: String, + val likeCount: Int + ) +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseUserInfo.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseUserInfo.kt new file mode 100644 index 0000000..11d7ae9 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseUserInfo.kt @@ -0,0 +1,18 @@ +package com.teamfillin.fillin.data.response + +data class ResponseUserInfo( + val user: User +) { + data class User( + val createdAt: String, + val email: String, + val id: Int, + val idKey: String, + val imageUrl: String, + val isDeleted: Boolean, + val nickname: String, + val refreshToken: String, + val social: String, + val updatedAt: String + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/ResponseUserPhotoInfo.kt b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseUserPhotoInfo.kt new file mode 100644 index 0000000..45aee57 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/ResponseUserPhotoInfo.kt @@ -0,0 +1,15 @@ +package com.teamfillin.fillin.data.response + +data class ResponseUserPhotoInfo( + val photos:List +) { + data class Photo( + val filmId: Int, + val filmName: String, + val imageUrl: String, + val likeCount: Int, + val nickname: String, + val photoId: Int, + val userImageUrl: String + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/response/experimental/PhotoDto.kt b/app/src/main/java/com/teamfillin/fillin/data/response/experimental/PhotoDto.kt new file mode 100644 index 0000000..214ba70 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/response/experimental/PhotoDto.kt @@ -0,0 +1,50 @@ +package com.teamfillin.fillin.data.response.experimental + +import com.teamfillin.fillin.domain.entity.CategoryPhoto +import com.teamfillin.fillin.domain.entity.Photo + +data class ResponseAllPhoto( + val photos: List +) + +data class ResponsePhotoByCategory( + val photos: List +) + +data class PhotoDto( + val nickname: String, + val userImageUrl: String, + val photoId: Int, + val imageUrl: String, + val filmId: Int, + val filmName: String, + val likeCount: Int, + val isLiked: Boolean +) { + fun toPhoto() = + Photo(nickname, userImageUrl, photoId, imageUrl, filmId, filmName, likeCount, isLiked) +} + +data class CategoryPhotoDto( + val nickname: String, + val userImageUrl: String, + val photoId: Int, + val imageUrl: String, + val filmId: Int, + val filmName: String, + val likeCount: Int, + val isLiked: Boolean, + val isGaro: Boolean +) { + fun toPhoto() = CategoryPhoto( + nickname, + userImageUrl, + photoId, + imageUrl, + filmId, + filmName, + likeCount, + isLiked, + isGaro + ) +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/service/AuthService.kt b/app/src/main/java/com/teamfillin/fillin/data/service/AuthService.kt new file mode 100644 index 0000000..41e5a93 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/service/AuthService.kt @@ -0,0 +1,23 @@ +package com.teamfillin.fillin.data.service + +import com.teamfillin.fillin.data.response.BaseResponse +import com.teamfillin.fillin.data.response.ResponseAuth +import com.teamfillin.fillin.data.response.ResponseRefresh +import retrofit2.Call +import retrofit2.http.* + +interface AuthService { + @FormUrlEncoded + @POST("auth") + fun login( + @Field("token") token: String, + @Field("social") social: String = "kakao", + @Field("idKey") id: String + ): Call> + + @GET("auth/token") + fun refresh( + @Header("accesstoken") accessToken: String, + @Header("refreshtoken") refreshToken: String + ): Call> +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/service/FilmRollService.kt b/app/src/main/java/com/teamfillin/fillin/data/service/FilmRollService.kt new file mode 100644 index 0000000..2b9713e --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/service/FilmRollService.kt @@ -0,0 +1,29 @@ +package com.teamfillin.fillin.data.service + +import com.teamfillin.fillin.data.response.BaseResponse +import com.teamfillin.fillin.data.response.ResponseFilmRoll +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.Call +import retrofit2.http.* + +interface FilmRollService { + @GET("photo/film/{filmId}") + fun getFilmPhoto(@Path("filmId") filmId: Int): Call> + + @GET("curation") + fun getCuration(): Call> + + @GET("photo/style/{styleId}") + fun getFilmStyle(@Path("styleId") styleId: Int): Call> + + @GET("film/{styleId}") + fun getFilmCategory(@Path("styleId") styleId: Int): Call> + + @Multipart + @POST("photo") + fun postImage( + @PartMap body: HashMap, + @Part file: MultipartBody.Part? + ): Call> +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/service/FilmStyleService.kt b/app/src/main/java/com/teamfillin/fillin/data/service/FilmStyleService.kt new file mode 100644 index 0000000..5c00187 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/service/FilmStyleService.kt @@ -0,0 +1,16 @@ +package com.teamfillin.fillin.data.service + +import com.teamfillin.fillin.data.response.BaseResponse +import com.teamfillin.fillin.data.response.ResponseFilmStyleInfo +import com.teamfillin.fillin.data.response.ResponseFilmType +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +interface FilmStyleService { + @GET("film/{styleId}") + fun getFilmType( + @Path("styleId") styleId: Int + ): Call> + +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/service/HomeService.kt b/app/src/main/java/com/teamfillin/fillin/data/service/HomeService.kt new file mode 100644 index 0000000..d9395ef --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/service/HomeService.kt @@ -0,0 +1,15 @@ +package com.teamfillin.fillin.data.service + +import com.teamfillin.fillin.data.response.BaseResponse +import com.teamfillin.fillin.data.response.ResponseNewPhotoInfo +import com.teamfillin.fillin.data.response.ResponseUserInfo +import retrofit2.Call +import retrofit2.http.GET + +interface HomeService { + @GET("photo/latest") + fun getNewPhoto(): Call> + + @GET("user") + fun getUser(): Call> +} diff --git a/app/src/main/java/com/teamfillin/fillin/data/service/MyPagePhotoService.kt b/app/src/main/java/com/teamfillin/fillin/data/service/MyPagePhotoService.kt new file mode 100644 index 0000000..95b7db8 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/service/MyPagePhotoService.kt @@ -0,0 +1,11 @@ +package com.teamfillin.fillin.data.service + +import com.teamfillin.fillin.data.response.BaseResponse +import com.teamfillin.fillin.data.response.ResponseUserPhotoInfo +import retrofit2.Call +import retrofit2.http.GET + +interface MyPagePhotoService { + @GET("photo/user") + fun getUserPhotos() : Call> +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/service/PagingService.kt b/app/src/main/java/com/teamfillin/fillin/data/service/PagingService.kt new file mode 100644 index 0000000..b550cf0 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/service/PagingService.kt @@ -0,0 +1,25 @@ +package com.teamfillin.fillin.data.service + +import com.teamfillin.fillin.data.response.BaseResponse +import com.teamfillin.fillin.data.response.experimental.ResponseAllPhoto +import com.teamfillin.fillin.data.response.experimental.ResponsePhotoByCategory +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface PagingService { + @GET("photo") + suspend fun retrievePhotos(@Query("pageNum") pageNum: Int): BaseResponse + + @GET("photopaging/style/{styleId}") + suspend fun retrievePhotosByStyle( + @Path("styleId") styleId: Int, + @Query("pageNum") pageNum: Int + ): BaseResponse + + @GET("photopaging/film/{filmId}") + suspend fun retrievePhotosByFilm( + @Path("filmId") filmId: Int, + @Query("pageNum") pageNum: Int + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/service/StudioService.kt b/app/src/main/java/com/teamfillin/fillin/data/service/StudioService.kt new file mode 100644 index 0000000..d21f7fd --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/service/StudioService.kt @@ -0,0 +1,27 @@ +package com.teamfillin.fillin.data.service + +import com.teamfillin.fillin.data.response.* +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface StudioService { + @GET("studio") + fun getWholeStudio(): Call> + + @GET("studio/search") + fun getSearchInfo( + @Query("keyword") keyword: String + ): Call> + + @GET("studio/detail/{studioId}") + fun getStudioDetail( + @Path("studioId") studioId: Int + ): Call> + + @GET("photo/studio/{studioId}") + fun getStudioPhoto( + @Path("studioId") studioId: Int + ): Call> +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/data/service/UserService.kt b/app/src/main/java/com/teamfillin/fillin/data/service/UserService.kt new file mode 100644 index 0000000..02d3576 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/data/service/UserService.kt @@ -0,0 +1,12 @@ +package com.teamfillin.fillin.data.service + +import com.teamfillin.fillin.data.response.BaseResponse +import com.teamfillin.fillin.data.response.ResponseUserInfo +import retrofit2.Call +import retrofit2.http.GET + +interface UserService { + @GET("user") + fun getUserInfo() : Call> + +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/design/ResolutionMetrics.kt b/app/src/main/java/com/teamfillin/fillin/design/ResolutionMetrics.kt new file mode 100644 index 0000000..75c969d --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/design/ResolutionMetrics.kt @@ -0,0 +1,43 @@ +package com.teamfillin.fillin.design + +import android.app.Application +import androidx.annotation.Px +import com.teamfillin.fillin.App +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlin.math.roundToInt + +class ResolutionMetrics @Inject constructor( + @ApplicationContext private val application: Application +) { + private val displayMetrics + get() = application.resources.displayMetrics + + val screenWidth + get() = displayMetrics.widthPixels + + val screenHeight + get() = displayMetrics.heightPixels + + val screenShort + get() = screenWidth.coerceAtMost(screenHeight) + + val screenLong + get() = screenWidth.coerceAtLeast(screenHeight) + + @Px + fun toPixel(dp: Int) = (dp * displayMetrics.density).roundToInt() + fun toDP(@Px pixel: Int) = (pixel / displayMetrics.density).roundToInt() +} + +val Number.pixel: Int + @Px get() = App.resolutionMetrics.toDP(this.toInt()) + +val Number.dp: Int + get() = App.resolutionMetrics.toPixel(this.toInt()) + +val Number.pixelFloat: Float + @Px get() = App.resolutionMetrics.toDP(this.toInt()).toFloat() + +val Number.dpFloat: Float + get() = App.resolutionMetrics.toPixel(this.toInt()).toFloat() diff --git a/app/src/main/java/com/teamfillin/fillin/domain/entity/Auth.kt b/app/src/main/java/com/teamfillin/fillin/domain/entity/Auth.kt new file mode 100644 index 0000000..bde3d12 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/domain/entity/Auth.kt @@ -0,0 +1,20 @@ +package com.teamfillin.fillin.domain.entity + +sealed class Auth( + open val email: String?, + open val accessToken: String, + open val refreshToken: String +) { + data class Login( + override val email: String?, + override val accessToken: String, + override val refreshToken: String + ) : Auth(email, accessToken, refreshToken) + + data class SignUp( + override val email: String?, + val nickName: String, + override val accessToken: String, + override val refreshToken: String + ) : Auth(email, accessToken, refreshToken) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/domain/entity/Photo.kt b/app/src/main/java/com/teamfillin/fillin/domain/entity/Photo.kt new file mode 100644 index 0000000..2e192e5 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/domain/entity/Photo.kt @@ -0,0 +1,24 @@ +package com.teamfillin.fillin.domain.entity + +data class Photo( + val nickname: String, + val userImageUrl: String, + val photoId: Int, + val imageUrl: String, + val filmId: Int, + val fileName: String, + val likeCount: Int, + val isLiked: Boolean +) + +data class CategoryPhoto( + val nickname: String, + val userImageUrl: String, + val photoId: Int, + val imageUrl: String, + val filmId: Int, + val fileName: String, + val likeCount: Int, + val isLiked: Boolean, + val isGaro: Boolean +) diff --git a/app/src/main/java/com/teamfillin/fillin/domain/repository/AuthRepository.kt b/app/src/main/java/com/teamfillin/fillin/domain/repository/AuthRepository.kt new file mode 100644 index 0000000..8bf5e2b --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/domain/repository/AuthRepository.kt @@ -0,0 +1,5 @@ +package com.teamfillin.fillin.domain.repository + +interface AuthRepository { + suspend fun login(token: String, id: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/category/FilmCategoryAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/category/FilmCategoryAdapter.kt new file mode 100644 index 0000000..c53adac --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/category/FilmCategoryAdapter.kt @@ -0,0 +1,60 @@ +package com.teamfillin.fillin.presentation.category + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.teamfillin.fillin.data.response.ResponseFilmType +import com.teamfillin.fillin.databinding.ItemCategoryInfoBinding + + +class FilmCategoryAdapter(private val listener: ItemClickListener) : + ListAdapter(DIFFUTIL) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): FilmListViewHolder { + val binding = + ItemCategoryInfoBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return FilmListViewHolder(binding, listener) + } + + override fun onBindViewHolder(holder: FilmListViewHolder, position: Int) { + holder.onBind(getItem(position)) + } + + fun interface ItemClickListener { + fun onClick(data: ResponseFilmType.Films) + } + + class FilmListViewHolder( + private val binding: ItemCategoryInfoBinding, + private val listener: ItemClickListener + ) : RecyclerView.ViewHolder(binding.root) { + fun onBind(fileType: ResponseFilmType.Films) { + binding.category = fileType + binding.root.setOnClickListener { + listener.onClick(fileType) + } + } + } + + companion object { + val DIFFUTIL = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ResponseFilmType.Films, + newItem: ResponseFilmType.Films + ): Boolean { + return oldItem.id == newItem.id + } + override fun areContentsTheSame( + oldItem: ResponseFilmType.Films, + newItem: ResponseFilmType.Films + ): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/category/FilmRollCategoryActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/category/FilmRollCategoryActivity.kt new file mode 100644 index 0000000..32017a0 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/category/FilmRollCategoryActivity.kt @@ -0,0 +1,79 @@ +package com.teamfillin.fillin.presentation.category + +import android.app.Activity +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import androidx.lifecycle.lifecycleScope +import com.google.android.material.tabs.TabLayout +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.data.service.FilmStyleService +import com.teamfillin.fillin.databinding.ActivityFilmRollCategoryBinding +import com.teamfillin.fillin.presentation.map.CustomDecoration +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import retrofit2.await +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class FilmRollCategoryActivity : + BindingActivity(R.layout.activity_film_roll_category) { + + @Inject + lateinit var service: FilmStyleService + private val filmCategoryAdapter = FilmCategoryAdapter { + val intent = Intent().apply { + putExtra("film", it.name) + putExtra("id", it.id) + putExtra("styleId", it.styleId) + } + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setFilmCategoryAdapter() + tabSelectEvent() + } + + private fun setFilmCategoryAdapter() { + binding.rvCategory.adapter = filmCategoryAdapter + val customDecoration = CustomDecoration(2f, 10f, Color.DKGRAY) + binding.rvCategory.addItemDecoration(customDecoration) + getFilmCategoryList(1) + } + + private fun tabSelectEvent() { + binding.filmtab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab?) { + Timber.d("kangmin") + when (tab!!.position) { + 0 -> getFilmCategoryList(1) + 1 -> getFilmCategoryList(2) + 2 -> getFilmCategoryList(3) + else -> getFilmCategoryList(4) + } + } + + override fun onTabUnselected(tab: TabLayout.Tab?) { + } + + override fun onTabReselected(tab: TabLayout.Tab?) { + } + } + ) + } + + private fun getFilmCategoryList(styleId: Int) { + lifecycleScope.launch { + runCatching { + service.getFilmType(styleId).await() + }.onSuccess { + filmCategoryAdapter.submitList(it.data.films) + }.onFailure(Timber::e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/dialog/PhotoDialogFragment.kt b/app/src/main/java/com/teamfillin/fillin/presentation/dialog/PhotoDialogFragment.kt new file mode 100644 index 0000000..ea9414d --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/dialog/PhotoDialogFragment.kt @@ -0,0 +1,56 @@ +package com.teamfillin.fillin.presentation.dialog + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import androidx.fragment.app.DialogFragment +import com.bumptech.glide.Glide +import com.teamfillin.fillin.databinding.FragmentPhotoDialogBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class PhotoDialogFragment : DialogFragment() { + private var _binding: FragmentPhotoDialogBinding? = null + private val binding: FragmentPhotoDialogBinding + get() = requireNotNull(_binding) + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentPhotoDialogBinding.inflate(layoutInflater, container, false) + + binding.btnClose.setOnClickListener { + dismiss() + } + binding.heart.setOnClickListener { + binding.number.text = ( + binding.number.text.toString() + .toInt() + (if (!binding.heart.isSelected) 1 else -1) + ).toString() + binding.heart.isSelected = !binding.heart.isSelected + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val photoUrl = arguments?.getString("photoUrl") + Glide.with(requireActivity()).load(photoUrl).into(binding.ivPhoto) + } + + //νœ΄λŒ€ν° 크기 맞좰 μžλ™ 쑰절 λ‹€μ΄μ–Όλ‘œκ·Έ + override fun onResume() { + super.onResume() + dialog?.window?.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT + ) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/CurationAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/CurationAdapter.kt new file mode 100644 index 0000000..bce1ae3 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/CurationAdapter.kt @@ -0,0 +1,110 @@ +package com.teamfillin.fillin.presentation.filmroll + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.content.receive +import com.teamfillin.fillin.core.context.drawableOf +import com.teamfillin.fillin.core.view.setOnSingleClickListener +import com.teamfillin.fillin.data.response.ResponseFilmRoll +import com.teamfillin.fillin.databinding.ItemCurationBinding +import com.teamfillin.fillin.databinding.ItemCurationFirstBinding +import timber.log.Timber + +private const val CURATION_INFO_TYPE = 1 +private const val CURATION_TYPE = 2 + +class CurationAdapter(private val listener: ItemClickListener) : + ListAdapter(CurationDiffUtil()) { + + override fun getItemViewType(position: Int): Int { + return when (position) { + 0 -> CURATION_INFO_TYPE + else -> CURATION_TYPE + } + } + + class CurationInfoViewHolder( + private val binding: ItemCurationFirstBinding + ) : + RecyclerView.ViewHolder(binding.root) { + fun bind() { + binding.tvCuration.text = "λ”°λœ»ν•œ 사진을 \nμ›ν•œλ‹€λ©΄" + } + } + + class CurationImageViewHolder( + private val binding: ItemCurationBinding, + private val listener: ItemClickListener + ) : + RecyclerView.ViewHolder(binding.root) { + fun bind(film: ResponseFilmRoll.FilmPhotoInfo) { + Glide.with(itemView.context) + .load(film.imageUrl) + .placeholder(itemView.context.drawableOf(R.drawable.ic_launcher_foreground)) + .into(binding.ivCuration) + binding.root.setOnClickListener { + listener.onClick(film) + } + binding.btnLike.setOnSingleClickListener { + binding.btnLike.isSelected = !binding.btnLike.isSelected + } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return when (viewType) { + CURATION_INFO_TYPE -> { + val binding = ItemCurationFirstBinding.inflate( + LayoutInflater.from(parent.context), + parent, false + ) + CurationInfoViewHolder(binding) + } + else -> { + val binding = ItemCurationBinding.inflate( + LayoutInflater.from(parent.context), + parent, false + ) + CurationImageViewHolder(binding, listener) + } + } + } + + fun interface ItemClickListener { + fun onClick(data: ResponseFilmRoll.FilmPhotoInfo) + } + + override fun getItemCount(): Int = currentList.size + 1 + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + + when (position == 0) { + true -> { + (holder as CurationInfoViewHolder).bind() + } + else -> { + (holder as CurationImageViewHolder).bind(getItem(position - 1)) + } + } + } + + private class CurationDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ResponseFilmRoll.FilmPhotoInfo, + newItem: ResponseFilmRoll.FilmPhotoInfo + ) = oldItem.imageUrl == newItem.imageUrl + + override fun areContentsTheSame( + oldItem: ResponseFilmRoll.FilmPhotoInfo, + newItem: ResponseFilmRoll.FilmPhotoInfo + ) = oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/CurationInfo.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/CurationInfo.kt new file mode 100644 index 0000000..8a24da0 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/CurationInfo.kt @@ -0,0 +1,7 @@ +package com.teamfillin.fillin.presentation.filmroll + +data class CurationInfo( + val image: Int, + val type: Int, + val text : String, +) diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmCategoryImageActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmCategoryImageActivity.kt new file mode 100644 index 0000000..1ddf07f --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmCategoryImageActivity.kt @@ -0,0 +1,40 @@ +package com.teamfillin.fillin.presentation.filmroll + +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.core.view.setOnSingleClickListener +import com.teamfillin.fillin.databinding.ActivityCategoryImageBinding +import com.teamfillin.fillin.presentation.filmroll.add.AddPhotoActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class FilmCategoryImageActivity : + BindingActivity(R.layout.activity_category_image) { + + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // click() + val filmImageAdapter = FilmImageAdapter() + binding.rvCategoryImage.adapter = filmImageAdapter + lifecycleScope.launch { + viewModel.photos.flowWithLifecycle(lifecycle) + .collect(filmImageAdapter::submitData) + } + } + + private fun click() { + binding.fabAddPhoto.setOnSingleClickListener { + val intent = Intent(this, AddPhotoActivity::class.java) + startActivity(intent) + } + binding.btnBack.setOnSingleClickListener { finish() } + } +} diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmCategoryViewModel.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmCategoryViewModel.kt new file mode 100644 index 0000000..15d401e --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmCategoryViewModel.kt @@ -0,0 +1,24 @@ +package com.teamfillin.fillin.presentation.filmroll + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.map +import com.teamfillin.fillin.data.repository.PhotoPagingRepository +import com.teamfillin.fillin.data.response.experimental.PhotoDto +import com.teamfillin.fillin.presentation.filmroll.model.toUiModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class FilmCategoryViewModel @Inject constructor( + private val repository: PhotoPagingRepository +) : ViewModel() { + val photos + get() = repository.retrievePager() + .map { pagingData -> + pagingData.map { it.toPhoto().toUiModel() } + }.cachedIn(viewModelScope) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmImageAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmImageAdapter.kt new file mode 100644 index 0000000..671ca46 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmImageAdapter.kt @@ -0,0 +1,51 @@ +package com.teamfillin.fillin.presentation.filmroll + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.teamfillin.fillin.databinding.ItemCategoryImageBinding +import com.teamfillin.fillin.presentation.filmroll.model.PhotoUiModel + +private val DIFF_UTIL = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: PhotoUiModel, newItem: PhotoUiModel): Boolean { + return oldItem.imageUrl == newItem.imageUrl + } + + override fun areContentsTheSame(oldItem: PhotoUiModel, newItem: PhotoUiModel): Boolean { + return oldItem == newItem + } +} + +class FilmImageAdapter : + PagingDataAdapter(DIFF_UTIL) { + class FilmImageViewHolder(private val binding: ItemCategoryImageBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(film: PhotoUiModel) { + Glide.with(binding.root) + .load(film.imageUrl) + .into(binding.ivFilmroll) +// binding.btnLike.setOnSingleClickListener { +// binding.btnLike.isSelected = !binding.btnLike.isSelected +// } + + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): FilmImageViewHolder { + val binding = ItemCategoryImageBinding.inflate( + LayoutInflater.from(parent.context), + parent, false + ) + return FilmImageViewHolder(binding) + } + + override fun onBindViewHolder(holder: FilmImageViewHolder, position: Int) { + getItem(position)?.let { holder.bind(it) } + } +} diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmImageInfo.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmImageInfo.kt new file mode 100644 index 0000000..6f43b61 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmImageInfo.kt @@ -0,0 +1,5 @@ +package com.teamfillin.fillin.presentation.filmroll + +data class FilmImageInfo( + val image : Int +) diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollActivity.kt new file mode 100644 index 0000000..5a7e5e0 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollActivity.kt @@ -0,0 +1,161 @@ +package com.teamfillin.fillin.presentation.filmroll + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.paging.PagingData +import com.google.android.material.tabs.TabLayout +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.core.content.receive +import com.teamfillin.fillin.core.view.setOnSingleClickListener +import com.teamfillin.fillin.data.service.FilmRollService +import com.teamfillin.fillin.databinding.ActivityFilmRollBinding +import com.teamfillin.fillin.presentation.filmroll.add.AddPhotoActivity +import com.teamfillin.fillin.presentation.category.FilmRollCategoryActivity +import com.teamfillin.fillin.presentation.dialog.PhotoDialogFragment +import com.teamfillin.fillin.presentation.filmroll.add.AddCompleteDialog +import com.teamfillin.fillin.presentation.map.SpaceDecoration +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class FilmRollActivity : BindingActivity(R.layout.activity_film_roll) { + @Inject + lateinit var service: FilmRollService + private val viewModel by viewModels() + private val filmRollPagingAdapter = FilmRollPagingAdapter { + val dialog = PhotoDialogFragment() + val bundle = Bundle().apply { putString("photoUrl", it.imageUrl) } + dialog.apply { + arguments = bundle + show(supportFragmentManager, "dialog") + } + } + private var curationAdapter = CurationAdapter { + val dialog = PhotoDialogFragment() + val bundle = Bundle().apply { putString("photoUrl", it.imageUrl) } + dialog.apply { + arguments = bundle + show(supportFragmentManager, "dialog") + } + } + private val addPhotoLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + AddCompleteDialog(this).showDialog() + } + } + private lateinit var resultLauncher: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + clickListener() + setCurationAdapter() + setFilmRollAdapter() + setResultFilmchoice() + lifecycleScope.launch { + viewModel.getCategoryFilm(1, -1) + .flowWithLifecycle(lifecycle) + .collectLatest { + Timber.d("Nunu filmId paging: $it") + filmRollPagingAdapter.submitData(it) + } + } + } + + private fun setCurationAdapter() { + binding.rvCuration.adapter = curationAdapter + addCurationList() + } + + private fun addCurationList() { + service.getCuration().receive({ + curationAdapter.submitList(it.data.photos) + }, { + Timber.d("Error $it") + }) + } + + private fun setFilmRollAdapter() { + binding.rvFilmroll.adapter = filmRollPagingAdapter + binding.rvFilmroll.addItemDecoration(SpaceDecoration(12)) + binding.filmtab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabReselected(tab: TabLayout.Tab?) { + } + + override fun onTabUnselected(tab: TabLayout.Tab?) { + } + + override fun onTabSelected(tab: TabLayout.Tab?) { + when (tab!!.position) { + 0 -> retrieveCategoryPhotoList(1, -1) + 1 -> retrieveCategoryPhotoList(2, -1) + 2 -> retrieveCategoryPhotoList(3, -1) + 3 -> retrieveCategoryPhotoList(4, -1) + } + } + }) + } + + private fun retrieveCategoryPhotoList(tabPosition: Int, filmId: Int) { + if (filmId == -1) { + Timber.d("Nunu tabPosition: $tabPosition") + binding.tvFilmchoice.text = "필름 μ’…λ₯˜λ₯Ό μ„ νƒν•˜μ„Έμš”" + lifecycleScope.launch { + filmRollPagingAdapter.submitData(PagingData.empty()) + viewModel.getCategoryFilm(tabPosition, -1) + .flowWithLifecycle(lifecycle) + .collectLatest { + Timber.d("Nunu tabPosition paging: $it") + filmRollPagingAdapter.submitData(it) + } + } + } else { + Timber.d("Nunu filmId: $filmId") + lifecycleScope.launch { + filmRollPagingAdapter.submitData(PagingData.empty()) + viewModel.getCategoryFilm(-1, filmId) + .flowWithLifecycle(lifecycle) + .collectLatest { + Timber.d("Nunu filmId paging: $it") + filmRollPagingAdapter.submitData(it) + } + } + } + } + + private fun setResultFilmchoice() { + resultLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val film = result.data?.getStringExtra("film") ?: "" + binding.tvFilmchoice.text = film + val id = result.data?.getIntExtra("id", -1) ?: -1 + retrieveCategoryPhotoList(-1, id) + } + } + } + + private fun clickListener() { + binding.fabAddPhoto.setOnSingleClickListener { + val intent = Intent(this, AddPhotoActivity::class.java) + addPhotoLauncher.launch(intent) + } + binding.tvFilmchoice.setOnSingleClickListener { + val intent = Intent(this, FilmRollCategoryActivity::class.java) + resultLauncher.launch(intent) + } + binding.btnBack.setOnSingleClickListener { + finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollAdapter.kt new file mode 100644 index 0000000..9bf46cc --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollAdapter.kt @@ -0,0 +1,55 @@ +package com.teamfillin.fillin.presentation.filmroll + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.teamfillin.fillin.core.view.setOnSingleClickListener +import com.teamfillin.fillin.databinding.ItemFilmRollBinding + +class FilmRollAdapter : + ListAdapter(FilmRollDiffUtil()) { + + class FilmRollViewHolder(private val binding: ItemFilmRollBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(film: FilmrollInfo) { + Glide.with(binding.root) + .load(film.image) + .into(binding.ivItemFilmroll) + binding.btnLike.setOnSingleClickListener { + binding.btnLike.isSelected = !binding.btnLike.isSelected + } + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): FilmRollViewHolder { + val binding = ItemFilmRollBinding.inflate( + LayoutInflater.from(parent.context), + parent, false + ) + return FilmRollViewHolder(binding) + } + + override fun onBindViewHolder(holder: FilmRollViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + private class FilmRollDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: FilmrollInfo, + newItem: FilmrollInfo + ) = + (oldItem.image == newItem.image) + + override fun areContentsTheSame( + oldItem: FilmrollInfo, + newItem: FilmrollInfo + ) = + (oldItem == newItem) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollCategoryActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollCategoryActivity.kt new file mode 100644 index 0000000..012bd35 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollCategoryActivity.kt @@ -0,0 +1,13 @@ +package com.teamfillin.fillin.presentation.filmroll + +import android.os.Bundle +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.databinding.ActivityFilmRollCategoryBinding + +class FilmRollCategoryActivity: BindingActivity(R.layout.activity_film_roll_category) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollPagingAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollPagingAdapter.kt new file mode 100644 index 0000000..6642464 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollPagingAdapter.kt @@ -0,0 +1,53 @@ +package com.teamfillin.fillin.presentation.filmroll + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.teamfillin.fillin.core.view.ItemDiffCallback +import com.teamfillin.fillin.core.view.setOnSingleClickListener +import com.teamfillin.fillin.databinding.ItemFilmRollBinding +import com.teamfillin.fillin.domain.entity.CategoryPhoto + +class FilmRollPagingAdapter( + private val itemClickListener: ItemClickListener +) : PagingDataAdapter( + ItemDiffCallback( + { old, new -> old.photoId == new.photoId }, + { old, new -> old == new } + ) +) { + fun interface ItemClickListener { + fun onClick(data: CategoryPhoto) + } + + class FilmViewHolder( + private val binding: ItemFilmRollBinding, + private val itemClickListener: ItemClickListener + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(film: CategoryPhoto) { + Glide.with(itemView.context) + .load(film.imageUrl) + .into(binding.ivItemFilmroll) + binding.root.setOnSingleClickListener { + itemClickListener.onClick(film) + } + binding.btnLike.setOnSingleClickListener { + binding.btnLike.isSelected = !binding.btnLike.isSelected + } + } + } + + override fun onBindViewHolder(holder: FilmViewHolder, position: Int) { + getItem(position)?.let { holder.bind(it) } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FilmViewHolder { + val binding = ItemFilmRollBinding.inflate( + LayoutInflater.from(parent.context), + parent, false + ) + return FilmViewHolder(binding, itemClickListener) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollViewModel.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollViewModel.kt new file mode 100644 index 0000000..70c04d4 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmRollViewModel.kt @@ -0,0 +1,28 @@ +package com.teamfillin.fillin.presentation.filmroll + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import androidx.paging.map +import com.teamfillin.fillin.data.remote.CategoryPhotosPagingSource +import com.teamfillin.fillin.data.service.PagingService +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@HiltViewModel +class FilmRollViewModel @Inject constructor( + private val pagingService: PagingService +) : ViewModel() { + fun getCategoryFilm( + styleId: Int = -1, + filmId: Int = -1 + ) = Pager( + config = PagingConfig(10), + pagingSourceFactory = { CategoryPhotosPagingSource(pagingService, styleId, filmId) } + ).flow.map { pagingData -> + pagingData.map { it.toPhoto() } + }.cachedIn(viewModelScope) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmrollInfo.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmrollInfo.kt new file mode 100644 index 0000000..5b1ee8a --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/FilmrollInfo.kt @@ -0,0 +1,5 @@ +package com.teamfillin.fillin.presentation.filmroll + +data class FilmrollInfo( + val image: Int +) diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddCancelDialog.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddCancelDialog.kt new file mode 100644 index 0000000..a8f90c5 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddCancelDialog.kt @@ -0,0 +1,43 @@ +package com.teamfillin.fillin.presentation.filmroll.add + +import android.app.Dialog +import android.content.Context +import android.view.WindowManager +import androidx.appcompat.widget.AppCompatButton +import com.teamfillin.fillin.R + +class AddCancelDialog(context: Context) { + + private val dialog = Dialog(context) + private lateinit var onClickListener: OnDialogClickListener + + fun setOnClickListener(listener: OnDialogClickListener) { + onClickListener = listener + } + + fun showDialog() { + dialog.setContentView(R.layout.dialog_addphoto_cancel) + dialog.window!!.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT + ) + dialog.setCanceledOnTouchOutside(true) + dialog.setCancelable(true) + dialog.show() + + val btnCancel = dialog.findViewById(R.id.btn_cancel) + val btnContinue = dialog.findViewById(R.id.btn_continue) + + btnCancel.setOnClickListener { + onClickListener.onClicked() + } + + btnContinue.setOnClickListener { + dialog.dismiss() + } + } + + fun interface OnDialogClickListener { + fun onClicked() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddCompleteDialog.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddCompleteDialog.kt new file mode 100644 index 0000000..0caab62 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddCompleteDialog.kt @@ -0,0 +1,29 @@ +package com.teamfillin.fillin.presentation.filmroll.add + +import android.app.Dialog +import android.content.Context +import android.view.WindowManager +import android.widget.ImageButton +import com.teamfillin.fillin.R + +class AddCompleteDialog(context: Context) { + private val dialog = Dialog(context) + + + fun showDialog() { + dialog.setContentView(R.layout.dialog_addphoto_complete) + dialog.window!!.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT + ) + dialog.setCanceledOnTouchOutside(true) + dialog.setCancelable(true) + dialog.show() + + val btnExit = dialog.findViewById(R.id.btn_exit) + + btnExit.setOnClickListener { + dialog.dismiss() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddPhotoActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddPhotoActivity.kt new file mode 100644 index 0000000..cdac315 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddPhotoActivity.kt @@ -0,0 +1,165 @@ +package com.teamfillin.fillin.presentation.filmroll.add + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.core.content.ContextCompat +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.Glide +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.core.content.ContentUriRequestBody +import com.teamfillin.fillin.core.context.colorOf +import com.teamfillin.fillin.core.view.setOnSingleClickListener +import com.teamfillin.fillin.databinding.ActivityAddPhotoBinding +import com.teamfillin.fillin.presentation.category.FilmRollCategoryActivity +import com.teamfillin.fillin.presentation.map.MapSearchActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import timber.log.Timber + +@AndroidEntryPoint +class AddPhotoActivity : BindingActivity(R.layout.activity_add_photo) { + // @Inject +// lateinit var service: FilmRollService +// var uri: Uri? = null + private lateinit var filmSelectLauncher: ActivityResultLauncher + private lateinit var studioSelectLauncher: ActivityResultLauncher + private lateinit var imagePickerLauncher: ActivityResultLauncher + private lateinit var permissionLauncher: ActivityResultLauncher + private val viewModel by viewModels() + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initResultLauncher() + initView() + } + + private fun initView() { + binding.btnAddPhoto.setOnClickListener { + viewModel.registerPhoto() + } + + binding.tvFilm.setOnSingleClickListener { + filmSelectLauncher.launch(Intent(this, FilmRollCategoryActivity::class.java)) + } + + binding.tvStudio.setOnSingleClickListener { + studioSelectLauncher.launch(Intent(this, MapSearchActivity::class.java)) + } + + binding.btnBack.setOnClickListener { + val addCancelDialog = AddCancelDialog(this) + addCancelDialog.setOnClickListener { finish() } + addCancelDialog.showDialog() + } + + binding.ivAddphoto.setOnClickListener { + checkPermission() + } + + lifecycleScope.launch { + viewModel.isClickable.flowWithLifecycle(lifecycle) + .collect { + with(binding.btnAddPhoto) { + isClickable = it + setBackgroundColor( + if (it) colorOf(R.color.fillin_red) else Color.parseColor("#474645") + ) + setTextColor( + if (it) colorOf(R.color.fillin_black) else Color.parseColor("#6F6F6F") + ) + } + } + } + + lifecycleScope.launch { + viewModel.registerSuccess.flowWithLifecycle(lifecycle) + .collect { + setResult(Activity.RESULT_OK) + finish() + } + } + } + + private fun initResultLauncher() { + filmSelectLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val film = result.data?.getStringExtra("film") ?: "" + val styleId = result.data?.getIntExtra("id", 0) ?: 0 + binding.tvFilm.text = film + viewModel.setFilmMetaData(styleId, film) + } + } + studioSelectLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + val locationId = it.data?.getIntExtra("id", 0) ?: 0 + val studioName = it.data?.getStringExtra("name") ?: "" + viewModel.setStudio(locationId, studioName) + binding.tvStudio.text = studioName + } + } + permissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> + when (isGranted) { + true -> startImagePicker() + false -> Toast.makeText(this, "가러리 κΆŒν•œμ„ ν—ˆμš©ν•΄μ£Όμ„Έμš”.", Toast.LENGTH_SHORT).show() + } + } + imagePickerLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + Timber.d("Nunu data ${result.data?.data}") + val uri = result.data?.data //Intentλ₯Ό λ°˜ν™˜->Intentμ—μ„œ Uri둜 getν•˜κΈ° + Glide.with(this).load(uri).into(binding.ivAddphoto) + if (uri != null) { + val requestBody = ContentUriRequestBody(this@AddPhotoActivity, uri) + viewModel.setUri(requestBody) + } + } + } + } + + private fun checkPermission() { + val cameraPermission = + ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + if (cameraPermission == PackageManager.PERMISSION_GRANTED) { + //ν”„λ‘œκ·Έλž¨ 진행 + startImagePicker() + } else { + //κΆŒν•œμš”μ²­ + requestPermission() + } + } + + override fun onBackPressed() { + AddCancelDialog(this).apply { + setOnClickListener { super.onBackPressed() } + }.showDialog() + } + + private fun startImagePicker() { + val intent = Intent().apply { + type = "image/*" + action = Intent.ACTION_GET_CONTENT + } + imagePickerLauncher.launch(intent) + } + + + private fun requestPermission() { + permissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddPhotoViewModel.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddPhotoViewModel.kt new file mode 100644 index 0000000..ffca476 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/AddPhotoViewModel.kt @@ -0,0 +1,91 @@ +package com.teamfillin.fillin.presentation.filmroll.add + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.teamfillin.fillin.core.content.ContentUriRequestBody +import com.teamfillin.fillin.presentation.filmroll.add.repository.AddPhotoRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class AddPhotoViewModel @Inject constructor( + private val repository: AddPhotoRepository +) : ViewModel() { + private val _film: MutableStateFlow = MutableStateFlow(FilmUiState.NotDetermined) + val film: StateFlow = _film.asStateFlow() + private val _studio: MutableStateFlow = + MutableStateFlow(StudioUiState.NotDetermined) + val studio: StateFlow = _studio.asStateFlow() + private val _image: MutableStateFlow = MutableStateFlow(ImageUiState.Init) + val image: StateFlow = _image.asStateFlow() + val isClickable: Flow = + combine(film, image, studio) { filmUiState, imageUiState, studioUiState -> + filmUiState != FilmUiState.NotDetermined && imageUiState != ImageUiState.Init && studioUiState != StudioUiState.NotDetermined + } + private val _registerSuccess = MutableSharedFlow(replay = 0) + val registerSuccess = _registerSuccess.asSharedFlow() + + fun setFilmMetaData(id: Int, name: String) { + _film.value = FilmUiModel(id, name).toUiState() + } + + fun setStudio(id: Int, name: String) { + _studio.value = StudioUiModel(id, name).toUiState() + } + + fun setUri(requestBody: ContentUriRequestBody) { + _image.value = requestBody.toUiState() + } + + fun registerPhoto() { + viewModelScope.launch { + runCatching { + repository.registerPhoto( + hashMapOf( + "studioId" to (studio.value as StudioUiState.Determined).studio.id.toRequestBody(), + "filmId" to (film.value as FilmUiState.Determined).film.id.toRequestBody() + ), + (image.value as ImageUiState.Selected).image.toFormData() + ) + }.onSuccess { _registerSuccess.emit(Unit) }.onFailure(Timber::e) + } + } + + private fun Int.toRequestBody() = toString().toRequestBody("text/plain".toMediaTypeOrNull()) + + private fun ContentUriRequestBody.toUiState() = ImageUiState.Selected(this) + + sealed class ImageUiState { + object Init : ImageUiState() + data class Selected(val image: ContentUriRequestBody) : ImageUiState() + } + + sealed class FilmUiState { + object NotDetermined : FilmUiState() + data class Determined(val film: FilmUiModel) : FilmUiState() + } + + sealed class StudioUiState { + object NotDetermined : StudioUiState() + data class Determined(val studio: StudioUiModel) : StudioUiState() + } +} + +data class FilmUiModel( + val id: Int, + val name: String +) { + fun toUiState() = AddPhotoViewModel.FilmUiState.Determined(this) +} + +data class StudioUiModel( + val id: Int, + val name: String +) { + fun toUiState() = AddPhotoViewModel.StudioUiState.Determined(this) +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/repository/AddPhotoRepository.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/repository/AddPhotoRepository.kt new file mode 100644 index 0000000..30ea27d --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/repository/AddPhotoRepository.kt @@ -0,0 +1,12 @@ +package com.teamfillin.fillin.presentation.filmroll.add.repository + +import com.teamfillin.fillin.data.response.BaseResponse +import okhttp3.MultipartBody +import okhttp3.RequestBody + +interface AddPhotoRepository { + suspend fun registerPhoto( + photoMetaData: HashMap, + image: MultipartBody.Part + ): BaseResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/repository/AddPhotoRepositoryImpl.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/repository/AddPhotoRepositoryImpl.kt new file mode 100644 index 0000000..efe4aec --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/add/repository/AddPhotoRepositoryImpl.kt @@ -0,0 +1,19 @@ +package com.teamfillin.fillin.presentation.filmroll.add.repository + +import com.teamfillin.fillin.data.response.BaseResponse +import com.teamfillin.fillin.data.service.FilmRollService +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.await +import javax.inject.Inject + +class AddPhotoRepositoryImpl @Inject constructor( + private val service: FilmRollService +) : AddPhotoRepository { + override suspend fun registerPhoto( + photoMetaData: HashMap, + image: MultipartBody.Part + ): BaseResponse { + return service.postImage(photoMetaData, image).await() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/model/PhotoUiModel.kt b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/model/PhotoUiModel.kt new file mode 100644 index 0000000..17d8045 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/filmroll/model/PhotoUiModel.kt @@ -0,0 +1,9 @@ +package com.teamfillin.fillin.presentation.filmroll.model + +import com.teamfillin.fillin.domain.entity.Photo + +data class PhotoUiModel( + val imageUrl: String +) + +fun Photo.toUiModel() = PhotoUiModel(imageUrl) diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/home/HomeActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/home/HomeActivity.kt new file mode 100644 index 0000000..15cd8d0 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/home/HomeActivity.kt @@ -0,0 +1,222 @@ +package com.teamfillin.fillin.presentation.home + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import com.naver.maps.geometry.LatLng +import com.naver.maps.map.LocationTrackingMode +import com.naver.maps.map.NaverMap +import com.naver.maps.map.OnMapReadyCallback +import com.naver.maps.map.overlay.Marker +import com.naver.maps.map.overlay.OverlayImage +import com.naver.maps.map.util.FusedLocationSource +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.core.content.receive +import com.teamfillin.fillin.core.context.toast +import com.teamfillin.fillin.core.view.setOnSingleClickListener +import com.teamfillin.fillin.data.service.HomeService +import com.teamfillin.fillin.data.service.StudioService +import com.teamfillin.fillin.databinding.ActivityHomeBinding +import com.teamfillin.fillin.presentation.filmroll.add.AddPhotoActivity +import com.teamfillin.fillin.presentation.dialog.PhotoDialogFragment +import com.teamfillin.fillin.presentation.filmroll.FilmRollActivity +import com.teamfillin.fillin.presentation.map.SpaceDecoration +import com.teamfillin.fillin.presentation.map.StudioMapActivity +import com.teamfillin.fillin.presentation.my.MyPageActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import retrofit2.await +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class HomeActivity : BindingActivity(R.layout.activity_home), + OnMapReadyCallback { + @Inject + lateinit var service: HomeService + + @Inject + lateinit var studioService: StudioService + private lateinit var newPhotosAdapter: NewPhotosAdapter + private lateinit var fusedLocationSource: FusedLocationSource + private var activityNaverMap: NaverMap? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding.mapMain.onCreate(savedInstanceState) + fusedLocationSource = + FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE) + binding.mapMain.getMapAsync(this) + initDatas() + initNewPhotoRecyclerView() + clickListener() + popup() + } + + private fun initDatas() { + lifecycleScope.launch { + delay(300L) + runCatching { + service.getUser().await() + }.onSuccess { + binding.tvIntro.text = "${it.data.user.nickname}λ‹˜, 쒋은 μ•„μΉ¨μž…λ‹ˆλ‹€." + }.onFailure(Timber::e) + } + } + + private fun clickListener() { + binding.btnAddphoto.setOnSingleClickListener { + val intent = Intent(this, AddPhotoActivity::class.java) + startActivity(intent) + } + binding.btnFilmroll.setOnSingleClickListener { + val intent = Intent(this, FilmRollActivity::class.java) + startActivity(intent) + } + binding.btnStudiomap.setOnSingleClickListener { + val intent = Intent(this, StudioMapActivity::class.java) + startActivity(intent) + } + binding.btnMypage.setOnSingleClickListener { + val intent = Intent(this, MyPageActivity::class.java) + startActivity(intent) + } + } + + private fun initNewPhotoRecyclerView() { + newPhotosAdapter = NewPhotosAdapter { + val dialog = PhotoDialogFragment() + val bundle = Bundle().apply { putString("photoUrl", it.imageUrl) } + dialog.apply { + arguments = bundle + show(supportFragmentManager, "dialog") + } + } + binding.rvNewPhotos.adapter = newPhotosAdapter + binding.rvNewPhotos.addItemDecoration(NewPhotosDecoration()) + lifecycleScope.launch { + delay(400L) + runCatching { + service.getNewPhoto().await() + }.onSuccess { + newPhotosAdapter.replaceList(it.data.photos) + }.onFailure(Timber::e) + } + } + + private fun popup() { + binding.apply { + btnClose.setOnClickListener { + clPopup.isVisible = !clPopup.isVisible + } + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (fusedLocationSource.onRequestPermissionsResult( + requestCode, + permissions, + grantResults + ) + ) { + if (!fusedLocationSource.isActivated) { + activityNaverMap?.locationTrackingMode = LocationTrackingMode.None + } else activityNaverMap?.locationTrackingMode = LocationTrackingMode.Follow + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun onMapReady(naverMap: NaverMap) { + activityNaverMap = naverMap.apply { + mapType = NaverMap.MapType.Navi + setLayerGroupEnabled(NaverMap.LAYER_GROUP_TRANSIT, false) + setLayerGroupEnabled(NaverMap.LAYER_GROUP_BUILDING, true) + isNightModeEnabled = true + locationSource = fusedLocationSource + locationTrackingMode = LocationTrackingMode.Follow + minZoom = 6.0 + maxZoom = 18.0 + uiSettings.run { + isCompassEnabled = false + isScaleBarEnabled = false + isZoomControlEnabled = false + isLocationButtonEnabled = false + } + } + markerLocationEvent() + activityNaverMap?.setOnMapClickListener { pointF, latLng -> + val intent = Intent(this, StudioMapActivity::class.java) + startActivity(intent) + } + } + + private fun markerLocationEvent() { + lifecycleScope.launch { + runCatching { + studioService.getWholeStudio().await() + }.onSuccess { + it.data.studios.forEach { + Marker().apply { + position = LatLng(it.lati, it.long) + icon = OverlayImage.fromResource(R.drawable.ic_place_select) + this.map = activityNaverMap + } + } + }.onFailure(Timber::e) + } + } + + override fun onStart() { + super.onStart() + binding.mapMain.onStart() + } + + override fun onResume() { + super.onResume() + binding.mapMain.onResume() + } + + override fun onPause() { + super.onPause() + binding.mapMain.onPause() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + binding.mapMain.onSaveInstanceState(outState) + } + + override fun onStop() { + super.onStop() + binding.mapMain.onStop() + } + + override fun onDestroy() { + super.onDestroy() + binding.mapMain.onDestroy() + } + + override fun onLowMemory() { + super.onLowMemory() + binding.mapMain.onLowMemory() + } + + companion object { + @JvmStatic + fun getIntent(context: Context) = Intent(context, HomeActivity::class.java).apply { + flags = + Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + } + + private const val LOCATION_PERMISSION_REQUEST_CODE = 1000 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/home/HomeMultiMode.kt b/app/src/main/java/com/teamfillin/fillin/presentation/home/HomeMultiMode.kt new file mode 100644 index 0000000..9e5035c --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/home/HomeMultiMode.kt @@ -0,0 +1,4 @@ +package com.teamfillin.fillin.presentation.home + +const val NEW_PHOTOS_TYPE = 1 +const val NEXT_BUTTON_TYPE = 2 \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/home/NewPhotosAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/home/NewPhotosAdapter.kt new file mode 100644 index 0000000..9cb9401 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/home/NewPhotosAdapter.kt @@ -0,0 +1,98 @@ +package com.teamfillin.fillin.presentation.home + +import android.content.Intent +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.teamfillin.fillin.core.view.setOnSingleClickListener +import com.teamfillin.fillin.data.response.ResponseNewPhotoInfo +import com.teamfillin.fillin.databinding.ItemNewPhotosListBinding +import com.teamfillin.fillin.databinding.ItemNextButtonBinding +import com.teamfillin.fillin.presentation.filmroll.FilmRollActivity +import timber.log.Timber + +class NewPhotosAdapter( + private val listener: ItemClickListener +) : RecyclerView.Adapter() { + + private var photolist = listOf() + + override fun getItemViewType(position: Int): Int { + return when { + position < photolist.size -> NEW_PHOTOS_TYPE + else -> NEXT_BUTTON_TYPE + } + } + + override fun getItemCount(): Int = photolist.size + 1 + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return when (viewType) { + NEW_PHOTOS_TYPE -> { + val binding = ItemNewPhotosListBinding.inflate( + LayoutInflater.from(parent.context), + parent, false + ) + NewPhotosViewHolder(binding, listener) + } + else -> { + val binding = ItemNextButtonBinding.inflate( + LayoutInflater.from(parent.context), + parent, false + ) + NextButtonViewHolder(binding) + } + } + + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (position < photolist.size) { + true -> { + Timber.d("Data ${photolist[position]}") + (holder as NewPhotosViewHolder).bind(photolist[position]) + } + else -> { + (holder as NextButtonViewHolder).bind() + } + } + } + + fun replaceList(newList: List) { + photolist = newList.toList() + notifyDataSetChanged() + } + + fun interface ItemClickListener { + fun onClick(data: ResponseNewPhotoInfo.Photo) + } + + + class NewPhotosViewHolder( + private val binding: ItemNewPhotosListBinding, + private val listener: ItemClickListener + ) : + RecyclerView.ViewHolder(binding.root) { + fun bind(data: ResponseNewPhotoInfo.Photo) { + Glide.with(itemView.context) + .load(data.imageUrl) + .into(binding.ivNewPhoto) + binding.root.setOnSingleClickListener { + listener.onClick(data) + } + } + } + + class NextButtonViewHolder(private val binding: ItemNextButtonBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind() { + binding.root.setOnSingleClickListener { + val intent = Intent(itemView.context, FilmRollActivity::class.java) + itemView.context.startActivity(intent) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/home/NewPhotosData.kt b/app/src/main/java/com/teamfillin/fillin/presentation/home/NewPhotosData.kt new file mode 100644 index 0000000..44e73ac --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/home/NewPhotosData.kt @@ -0,0 +1,6 @@ +package com.teamfillin.fillin.presentation.home + +data class NewPhotosData( + var image: Int, + val type: Int +) diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/home/NewPhotosDecoration.kt b/app/src/main/java/com/teamfillin/fillin/presentation/home/NewPhotosDecoration.kt new file mode 100644 index 0000000..dc92c38 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/home/NewPhotosDecoration.kt @@ -0,0 +1,26 @@ +package com.teamfillin.fillin.presentation.home + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.teamfillin.fillin.design.dp + +class NewPhotosDecoration : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + if (parent.getChildAdapterPosition(view) == 0) { + outRect.left = 18.dp + outRect.right = 4.dp + } else { + with(outRect) { + left = 4.dp + right = 4.dp + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/login/KakaoAuthService.kt b/app/src/main/java/com/teamfillin/fillin/presentation/login/KakaoAuthService.kt new file mode 100644 index 0000000..6efc5bc --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/login/KakaoAuthService.kt @@ -0,0 +1,54 @@ +package com.teamfillin.fillin.presentation.login + +import android.content.Context +import com.kakao.sdk.auth.model.OAuthToken +import com.kakao.sdk.user.UserApiClient +import dagger.hilt.android.qualifiers.ActivityContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber +import javax.inject.Inject + +class KakaoAuthService @Inject constructor( + @ActivityContext private val context: Context, + private val client: UserApiClient +) { + val isKakaoTalkLoginAvailable: Boolean + get() = client.isKakaoTalkLoginAvailable(context) + private val _loginState: MutableStateFlow = MutableStateFlow(LoginState.Init) + val loginState: StateFlow = _loginState.asStateFlow() + private val kakaoAuthCallback: (OAuthToken?, Throwable?) -> Unit = { token, error -> + error?.let(::handleLoginError) + token?.let(::handleLoginSuccess) + } + + + fun loginByKakaoTalk() { + client.loginWithKakaoTalk(context, callback = kakaoAuthCallback) + } + + fun loginByKakaoAccount() { + client.loginWithKakaoAccount(context, callback = kakaoAuthCallback) + } + + private fun handleLoginError(throwable: Throwable) { + _loginState.value = LoginState.Failure(throwable) + } + + private fun handleLoginSuccess(oAuthToken: OAuthToken) { + client.me { user, _ -> + _loginState.value = LoginState.Success(oAuthToken.accessToken, user?.id.toString()) + } + } + + fun logout() { + client.logout(Timber::e) + } + + sealed class LoginState { + object Init : LoginState() + data class Success(val token: String, val id: String) : LoginState() + data class Failure(val error: Throwable) : LoginState() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/login/LoginActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/login/LoginActivity.kt new file mode 100644 index 0000000..23cfb25 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/login/LoginActivity.kt @@ -0,0 +1,70 @@ +package com.teamfillin.fillin.presentation.login + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.core.context.toast +import com.teamfillin.fillin.databinding.ActivityLoginBinding +import com.teamfillin.fillin.presentation.home.HomeActivity +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class LoginActivity : BindingActivity(R.layout.activity_login) { + @Inject + lateinit var kakaoAuthService: KakaoAuthService + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + super.onCreate(savedInstanceState) + binding.containerLoginKakao.setOnClickListener { + if (kakaoAuthService.isKakaoTalkLoginAvailable) { + kakaoAuthService.loginByKakaoTalk() + } else { + kakaoAuthService.loginByKakaoAccount() + } + } + + lifecycleScope.launch { + kakaoAuthService.loginState + .flowWithLifecycle(lifecycle) + .collect { + when (it) { + is KakaoAuthService.LoginState.Success -> viewModel.login(it.token, it.id) + is KakaoAuthService.LoginState.Failure -> Timber.d("Kakao Login Failed ${it.error}") + else -> Timber.d("Kakao INIT") + } + } + } + + lifecycleScope.launch { + viewModel.loginResult + .flowWithLifecycle(lifecycle) + .collect { + when (it) { + is LoginViewModel.InHouseLoginState.Success -> { + startActivity(HomeActivity.getIntent(this@LoginActivity)) + } + is LoginViewModel.InHouseLoginState.Failure -> toast(it.message) + } + } + } + } + + companion object { + @JvmStatic + fun getIntent(context: Context) = Intent(context, LoginActivity::class.java).apply { + flags = + Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/login/LoginViewModel.kt b/app/src/main/java/com/teamfillin/fillin/presentation/login/LoginViewModel.kt new file mode 100644 index 0000000..597a002 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/login/LoginViewModel.kt @@ -0,0 +1,39 @@ +package com.teamfillin.fillin.presentation.login + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.teamfillin.fillin.domain.repository.AuthRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val authRepository: AuthRepository +) : ViewModel() { + private val _loginResult: MutableStateFlow = + MutableStateFlow(InHouseLoginState.Init) + val loginResult: StateFlow = _loginResult.asStateFlow() + + fun login(token: String, id: String) { + viewModelScope.launch { + runCatching { authRepository.login(token, id) } + .fold({ + _loginResult.value = InHouseLoginState.Success + }, { + _loginResult.value = InHouseLoginState.Failure(it.message ?: "λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€") + Timber.e(it) + }) + } + } + + sealed class InHouseLoginState { + object Init : InHouseLoginState() + object Success : InHouseLoginState() + data class Failure(val message: String) : InHouseLoginState() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/map/CustomDecoration.kt b/app/src/main/java/com/teamfillin/fillin/presentation/map/CustomDecoration.kt new file mode 100644 index 0000000..0224f88 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/map/CustomDecoration.kt @@ -0,0 +1,35 @@ +package com.teamfillin.fillin.presentation.map + +import android.graphics.Canvas +import android.graphics.Paint +import androidx.annotation.ColorInt +import androidx.recyclerview.widget.RecyclerView + +class CustomDecoration( + private val height: Float, + private val padding: Float, + @ColorInt private val color: Int +) : RecyclerView.ItemDecoration() { + private val paint = Paint() + + init { + paint.color = color + } + + override fun onDrawOver( + c: Canvas, + parent: RecyclerView, + state: RecyclerView.State + ) { + val left = parent.paddingStart + padding + val right = parent.width - parent.paddingEnd - padding + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + val params = child.layoutParams as RecyclerView.LayoutParams + val top = (child.bottom + params.bottomMargin).toFloat() + val bottom = top + height + c.drawRect(left, top, right, bottom, paint) + } + } +} + diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/map/MapSearchActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/map/MapSearchActivity.kt new file mode 100644 index 0000000..6f3348a --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/map/MapSearchActivity.kt @@ -0,0 +1,106 @@ +package com.teamfillin.fillin.presentation.map + +import android.app.Activity +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.view.MenuItem +import android.view.inputmethod.EditorInfo +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.lifecycleScope +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.data.service.StudioService +import com.teamfillin.fillin.databinding.ActivityMapSearchBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import retrofit2.await +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class MapSearchActivity : BindingActivity(R.layout.activity_map_search) { + @Inject + lateinit var service: StudioService + private val searchListAdapter = SearchListAdapter { + val intent = Intent().apply { + putExtra("id", it.id) + putExtra("name", it.name) + } + setResult(Activity.RESULT_OK, intent) + finish() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + toolbarEvent() + editTextIconEvent() + setLocationListAdapter() + } + + private fun toolbarEvent() { + setSupportActionBar(binding.tbTitle) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowTitleEnabled(false) + setHomeAsUpIndicator(R.drawable.ic_back) + } + } + + private fun editTextIconEvent() { + binding.editSearch.doAfterTextChanged { + editTextBlankCheck() + } + binding.editSearch.setOnEditorActionListener { _, id, _ -> + var handled = false + if (id == EditorInfo.IME_ACTION_SEARCH) { + binding.ivSearch.performClick() + handled = true + } + handled + } + } + + private fun setLocationListAdapter() { + binding.rvLocationInfo.adapter = searchListAdapter + val customDecoration = CustomDecoration(1f, 10f, Color.GRAY) + binding.rvLocationInfo.addItemDecoration(customDecoration) + locationSearchEvent() + } + + private fun locationSearchEvent() { + binding.ivSearch.setOnClickListener { + lifecycleScope.launch { + runCatching { + service.getSearchInfo(binding.editSearch.text.toString()).await() + }.onSuccess { + binding.rvLocationInfo.isVisible = it.data.studios.isNotEmpty() + binding.clNoResult.isVisible = it.data.studios.isEmpty() + if (it.data.studios.isNotEmpty()) { + searchListAdapter.submitList(it.data.studios) + } + }.onFailure(Timber::e) + } + } + + binding.ivClear.setOnClickListener { + binding.editSearch.text.clear() + } + } + + private fun editTextBlankCheck() { + binding.ivClear.isVisible = !binding.editSearch.text.isNullOrEmpty() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + finish() + return true + } + } + return super.onOptionsItemSelected(item) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/map/PhotoReviewListAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/map/PhotoReviewListAdapter.kt new file mode 100644 index 0000000..ba4c813 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/map/PhotoReviewListAdapter.kt @@ -0,0 +1,60 @@ +package com.teamfillin.fillin.presentation.map + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.teamfillin.fillin.data.response.ResponseStudioPhoto +import com.teamfillin.fillin.databinding.ItemPhotoReviewBinding + +class PhotoReviewListAdapter(private val listener: ItemClickListener) : + ListAdapter( + DIFFUTIL + ) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoReviewListViewHolder { + val binding = + ItemPhotoReviewBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return PhotoReviewListViewHolder(binding, listener) + } + + override fun onBindViewHolder(holder: PhotoReviewListViewHolder, position: Int) { + holder.onBind(getItem(position)) + } + + fun interface ItemClickListener { + fun onClick(data: ResponseStudioPhoto.StudioPhoto) + } + + class PhotoReviewListViewHolder( + private val binding: ItemPhotoReviewBinding, + private val listener: ItemClickListener + ) : RecyclerView.ViewHolder(binding.root) { + fun onBind(studioPhoto: ResponseStudioPhoto.StudioPhoto) { + Glide.with(itemView.context).load(studioPhoto.imageUrl).into(binding.ivPhoto) + binding.root.setOnClickListener { + listener.onClick(studioPhoto) + } + } + } + + companion object { + val DIFFUTIL = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ResponseStudioPhoto.StudioPhoto, + newItem: ResponseStudioPhoto.StudioPhoto + ): Boolean { + return oldItem.photoId == newItem.photoId + } + + override fun areContentsTheSame( + oldItem: ResponseStudioPhoto.StudioPhoto, + newItem: ResponseStudioPhoto.StudioPhoto + ): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/map/SearchListAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/map/SearchListAdapter.kt new file mode 100644 index 0000000..0b2f4cb --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/map/SearchListAdapter.kt @@ -0,0 +1,59 @@ +package com.teamfillin.fillin.presentation.map + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.teamfillin.fillin.data.response.ResponseSearch +import com.teamfillin.fillin.databinding.ItemLocationInfoBinding + +class SearchListAdapter(private val listener: ItemClickListener) : + ListAdapter( + DIFFUTIL + ) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchListViewHolder { + val binding = + ItemLocationInfoBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return SearchListViewHolder(binding, listener) + } + + override fun onBindViewHolder(holder: SearchListViewHolder, position: Int) { + holder.onBind(getItem(position)) + } + + fun interface ItemClickListener { + fun onClick(data: ResponseSearch.StudioResponse) + } + + class SearchListViewHolder( + private val binding: ItemLocationInfoBinding, + private val listener: ItemClickListener + ) : RecyclerView.ViewHolder(binding.root) { + fun onBind(searchInfo: ResponseSearch.StudioResponse) { + binding.location = searchInfo + binding.root.setOnClickListener { + listener.onClick(searchInfo) + } + } + } + + companion object { + val DIFFUTIL = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ResponseSearch.StudioResponse, + newItem: ResponseSearch.StudioResponse + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: ResponseSearch.StudioResponse, + newItem: ResponseSearch.StudioResponse + ): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/map/SpaceDecoration.kt b/app/src/main/java/com/teamfillin/fillin/presentation/map/SpaceDecoration.kt new file mode 100644 index 0000000..684eee5 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/map/SpaceDecoration.kt @@ -0,0 +1,21 @@ +package com.teamfillin.fillin.presentation.map + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.* +import com.teamfillin.fillin.design.dp + +class SpaceDecoration(private val margin: Int) : ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: State + ) { + with(outRect) { + right = margin.dp + bottom = margin.dp + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/map/StudioMapActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/map/StudioMapActivity.kt new file mode 100644 index 0000000..55fedab --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/map/StudioMapActivity.kt @@ -0,0 +1,270 @@ +package com.teamfillin.fillin.presentation.map + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.view.MenuItem +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.widget.NestedScrollView +import androidx.lifecycle.lifecycleScope +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.naver.maps.geometry.LatLng +import com.naver.maps.map.CameraUpdate +import com.naver.maps.map.LocationTrackingMode +import com.naver.maps.map.NaverMap +import com.naver.maps.map.OnMapReadyCallback +import com.naver.maps.map.overlay.Marker +import com.naver.maps.map.overlay.OverlayImage +import com.naver.maps.map.util.FusedLocationSource +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.data.service.StudioService +import com.teamfillin.fillin.databinding.ActivityStudioMapBinding +import com.teamfillin.fillin.presentation.dialog.PhotoDialogFragment +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import retrofit2.await +import timber.log.Timber +import javax.inject.Inject + + +@AndroidEntryPoint +class StudioMapActivity : BindingActivity(R.layout.activity_studio_map), + OnMapReadyCallback { + @Inject + lateinit var service: StudioService + private lateinit var behavior: BottomSheetBehavior + private lateinit var fusedLocationSource: FusedLocationSource + private var activityNaverMap: NaverMap? = null + private val photoReviewAdapter = PhotoReviewListAdapter { + val dialog = PhotoDialogFragment() + val bundle = Bundle().apply { putString("photoUrl", it.imageUrl) } + dialog.apply { + arguments = bundle + show(supportFragmentManager, "dialog") + } + } + private val studioIdHash = HashMap() + private val locationHash = HashMap() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding.mapMain.onCreate(savedInstanceState) + behavior = BottomSheetBehavior.from(binding.clBottomSheet) + fusedLocationSource = + FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE) + binding.mapMain.getMapAsync(this) + behavior.state = BottomSheetBehavior.STATE_HIDDEN + toolbarEvent() + searchClickEvent() + initAdapter() + } + + private fun toolbarEvent() { + setSupportActionBar(binding.tbTitle) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowTitleEnabled(false) + } + } + + private fun searchClickEvent() { + binding.clSearch.setOnClickListener { + val intent = Intent(this, MapSearchActivity::class.java) + mapSearchActivityLauncher.launch(intent) + } + } + + private fun initAdapter() { + binding.rvPhotoReview.adapter = photoReviewAdapter + val customDecoration = SpaceDecoration(7) + binding.rvPhotoReview.addItemDecoration(customDecoration) + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (fusedLocationSource.onRequestPermissionsResult( + requestCode, + permissions, + grantResults + ) + ) { + if (!fusedLocationSource.isActivated) { + activityNaverMap?.locationTrackingMode = LocationTrackingMode.None + } else activityNaverMap?.locationTrackingMode = LocationTrackingMode.Follow + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun onMapReady(naverMap: NaverMap) { + activityNaverMap = naverMap.apply { + mapType = NaverMap.MapType.Navi + setLayerGroupEnabled(NaverMap.LAYER_GROUP_TRANSIT, false) + setLayerGroupEnabled(NaverMap.LAYER_GROUP_BUILDING, true) + isNightModeEnabled = true + locationSource = fusedLocationSource + locationTrackingMode = LocationTrackingMode.Follow + minZoom = 6.0 + maxZoom = 18.0 + uiSettings.run { + isCompassEnabled = false + isScaleBarEnabled = false + isZoomControlEnabled = false + isLocationButtonEnabled = false + } + } + + binding.clBottomSheet.visibility = View.VISIBLE + markerLocationEvent() + binding.btnLocation.map = naverMap + activityNaverMap?.setOnMapClickListener { _, _ -> + behavior.state = BottomSheetBehavior.STATE_HIDDEN + } + } + + private fun markerLocationEvent() { + + lifecycleScope.launch { + runCatching { + service.getWholeStudio().await() + }.onSuccess { + it.data.studios.forEach { + Marker().apply { + position = LatLng(it.lati, it.long) + icon = OverlayImage.fromResource(R.drawable.ic_place_select) + this.map = activityNaverMap + studioIdHash[LatLng(it.lati, it.long)] = it.id + locationHash[it.id] = LatLng(it.lati, it.long) + setOnClickListener { + studioIdHash[position]?.let { + getStudioDetail(it) + getStudioPhoto(it) + } + true + } + } + } + }.onFailure(Timber::e) + } + } + + private fun getStudioDetail(position: Int) { + lifecycleScope.launch { + runCatching { + service.getStudioDetail(position).await() + }.onSuccess { + val studio = it.data.studio + binding.apply { + tvLocationName.text = studio.name + tvLocationDetail.text = studio.address + tvTimeDetail.text = studio.time + tvCallDetail.text = studio.tel + tvPriceDetail.text = studio.price + behavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + val cameraUpdate = CameraUpdate.scrollTo(locationHash[position]!!) + activityNaverMap?.moveCamera(cameraUpdate) + if (studio.site.isNullOrEmpty()) binding.tvLink.visibility = View.GONE + else { + binding.tvLink.visibility = View.VISIBLE + linkText(studio.site) + } + linkText(studio.site) + }.onFailure(Timber::e) + } + } + + private fun getStudioPhoto(position: Int) { + lifecycleScope.launch { + runCatching { + service.getStudioPhoto(position).await() + }.onSuccess { + photoReviewAdapter.submitList(it.data.photos) + }.onFailure(Timber::e) + } + } + + private fun linkText(site: String) { + val spannable = SpannableStringBuilder("μ›Ή μ‚¬μ΄νŠΈλ‘œ 이동") + spannable.setSpan(object : ClickableSpan() { + override fun onClick(p0: View) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(site)) + startActivity(intent) + } + }, 0, 9, Spanned.SPAN_EXCLUSIVE_INCLUSIVE) + binding.tvLink.text = spannable + binding.tvLink.movementMethod = LinkMovementMethod.getInstance() + } + + private val mapSearchActivityLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + val locationId = it.data?.getIntExtra("id", 0) + locationId?.let { + getStudioDetail(it) + getStudioPhoto(it) + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + finish() + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onStart() { + super.onStart() + binding.mapMain.onStart() + } + + override fun onResume() { + super.onResume() + binding.mapMain.onResume() + } + + override fun onPause() { + super.onPause() + binding.mapMain.onPause() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + binding.mapMain.onSaveInstanceState(outState) + } + + override fun onStop() { + super.onStop() + binding.mapMain.onStop() + } + + override fun onDestroy() { + super.onDestroy() + binding.mapMain.onDestroy() + } + + override fun onLowMemory() { + super.onLowMemory() + binding.mapMain.onLowMemory() + } + + companion object { + private const val LOCATION_PERMISSION_REQUEST_CODE = 1000 + var photoUrl = "" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/my/MyPageActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/my/MyPageActivity.kt new file mode 100644 index 0000000..5e4bbc3 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/my/MyPageActivity.kt @@ -0,0 +1,75 @@ +package com.teamfillin.fillin.presentation.my + +import android.os.Bundle +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.Glide +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.data.service.MyPagePhotoService +import com.teamfillin.fillin.data.service.UserService +import com.teamfillin.fillin.databinding.ActivityMyPageBinding +import com.teamfillin.fillin.presentation.dialog.PhotoDialogFragment +import com.teamfillin.fillin.presentation.map.SpaceDecoration +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import retrofit2.await +import timber.log.Timber +import javax.inject.Inject + +@AndroidEntryPoint +class MyPageActivity : BindingActivity(R.layout.activity_my_page) { + @Inject + lateinit var userService: UserService + @Inject + lateinit var myPhotoService: MyPagePhotoService + private val adapter = MyPagePhotoRecyclerViewAdapter{ + val dialog = PhotoDialogFragment() + Timber.d("mypagekangmin") + val bundle = Bundle().apply { putString("photoUrl", it.imageUrl) } + dialog.apply { + arguments = bundle + show(supportFragmentManager, "dialog") + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding.btnBackHome.setOnClickListener { + finish() + } + + initAdapter() + showUserInfo() + showMyPhoto() + } + + private fun initAdapter() { + binding.rvMyPage.adapter = adapter + val spaceDecoration = SpaceDecoration(7) + binding.rvMyPage.addItemDecoration(spaceDecoration) + } + private fun showUserInfo() { + lifecycleScope.launch { + runCatching { + userService.getUserInfo().await() + }.onSuccess { + binding.tvNickname.text = it.data.user.nickname + Glide.with(this@MyPageActivity) + .load(it.data.user.imageUrl) + .circleCrop() + .into(binding.ivProfile) + }.onFailure(Timber::e) + } + } + + private fun showMyPhoto() { + lifecycleScope.launch { + runCatching { + myPhotoService.getUserPhotos().await() + }.onSuccess { + adapter.replaceList(it.data.photos) + }.onFailure(Timber::e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/my/MyPagePhotoRecyclerViewAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/my/MyPagePhotoRecyclerViewAdapter.kt new file mode 100644 index 0000000..df88dea --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/my/MyPagePhotoRecyclerViewAdapter.kt @@ -0,0 +1,52 @@ +package com.teamfillin.fillin.presentation.my + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.teamfillin.fillin.data.response.ResponseUserPhotoInfo +import com.teamfillin.fillin.databinding.ItemMyPageBinding + +class MyPagePhotoRecyclerViewAdapter(private val listener: ItemClickListener) : + RecyclerView.Adapter() { + private var photoList = listOf() + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): ViewHolder { + val binding = ItemMyPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding, listener) + } + + override fun onBindViewHolder( + holder: ViewHolder, + position: Int + ) { + holder.bind(photoList[position]) + } + + override fun getItemCount() = photoList.size + + fun replaceList(newList: List) { + photoList = newList.toList() + notifyDataSetChanged() + } + + fun interface ItemClickListener { + fun onClick(data: ResponseUserPhotoInfo.Photo) + } + + class ViewHolder( + private val binding: ItemMyPageBinding, + private val listener: ItemClickListener + ) : + RecyclerView.ViewHolder(binding.root) { + fun bind(photo: ResponseUserPhotoInfo.Photo) { + Glide.with(itemView.context).load(photo.imageUrl).into(binding.ivPhoto) + binding.root.setOnClickListener { + listener.onClick(photo) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/onboarding/OnboardingActivity.kt b/app/src/main/java/com/teamfillin/fillin/presentation/onboarding/OnboardingActivity.kt new file mode 100644 index 0000000..9633034 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/onboarding/OnboardingActivity.kt @@ -0,0 +1,57 @@ +package com.teamfillin.fillin.presentation.onboarding + +import android.os.Bundle +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.isVisible +import androidx.viewpager2.widget.ViewPager2 +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.base.BindingActivity +import com.teamfillin.fillin.core.view.setOnSingleClickListener +import com.teamfillin.fillin.data.local.FillInDataStore +import com.teamfillin.fillin.databinding.ActivityOnboardingBinding +import com.teamfillin.fillin.presentation.home.HomeActivity +import com.teamfillin.fillin.presentation.login.LoginActivity +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class OnboardingActivity : + BindingActivity(R.layout.activity_onboarding) { + @Inject + lateinit var dataStore: FillInDataStore + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + super.onCreate(savedInstanceState) + if (dataStore.isOnboardingShown) + startActivity(LoginActivity.getIntent(this@OnboardingActivity)) + val adapter = OnboardingAdapter(this) + binding.viewpagerOnboaring.adapter = adapter + binding.dotOnboarding.setViewPager2(binding.viewpagerOnboaring) + binding.viewpagerOnboaring.registerOnPageChangeCallback( + object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + binding.txtOnbardingSkip.isVisible = position != 2 + binding.txtOnbardingStart.isVisible = position == 2 + binding.txtOnbardingNext.isVisible = position != 2 + } + } + ) + with(binding) { + txtOnbardingNext.setOnSingleClickListener { + binding.viewpagerOnboaring.setCurrentItem( + binding.viewpagerOnboaring.currentItem + 1, + true + ) + } + txtOnbardingStart.setOnSingleClickListener { + dataStore.isOnboardingShown = true + startActivity(LoginActivity.getIntent(this@OnboardingActivity)) + } + txtOnbardingSkip.setOnSingleClickListener { + dataStore.isOnboardingShown = true + startActivity(LoginActivity.getIntent(this@OnboardingActivity)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/teamfillin/fillin/presentation/onboarding/OnboardingAdapter.kt b/app/src/main/java/com/teamfillin/fillin/presentation/onboarding/OnboardingAdapter.kt new file mode 100644 index 0000000..0d97517 --- /dev/null +++ b/app/src/main/java/com/teamfillin/fillin/presentation/onboarding/OnboardingAdapter.kt @@ -0,0 +1,63 @@ +package com.teamfillin.fillin.presentation.onboarding + +import android.content.Context +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.teamfillin.fillin.R +import com.teamfillin.fillin.core.context.drawableOf +import com.teamfillin.fillin.core.context.stringOf +import com.teamfillin.fillin.databinding.ItemOnboardingBinding + +class OnboardingAdapter( + context: Context +) : RecyclerView.Adapter() { + private val inflater = LayoutInflater.from(context) + + class OnboardingViewHolder( + private val binding: ItemOnboardingBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun onBind(onboarding: Onboarding) { + binding.onboarding = onboarding + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OnboardingViewHolder { + val binding = ItemOnboardingBinding.inflate(inflater, parent, false) + return OnboardingViewHolder(binding) + } + + override fun onBindViewHolder(holder: OnboardingViewHolder, position: Int) { + holder.onBind(Onboarding.pages(holder.itemView.context)[position]) + } + + override fun getItemCount() = 3 + + + data class Onboarding( + val title: String, + val content: String, + val imageResId: Drawable? + ) { + companion object { + fun pages(context: Context) = listOf( + Onboarding( + context.stringOf(R.string.onboarding_title_1), + context.stringOf(R.string.onboarding_content_1), + context.drawableOf(R.drawable.ic_onboarding_1) + ), + Onboarding( + context.stringOf(R.string.onboarding_title_2), + context.stringOf(R.string.onboarding_content_2), + context.drawableOf(R.drawable.ic_onboarding_2) + ), + Onboarding( + context.stringOf(R.string.onboarding_title_3), + context.stringOf(R.string.onboarding_content_3), + context.drawableOf(R.drawable.ic_onboarding_3) + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/and_card_img.png b/app/src/main/res/drawable-hdpi/and_card_img.png new file mode 100644 index 0000000..0ca8cc4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/and_card_img.png differ diff --git a/app/src/main/res/drawable-hdpi/and_card_long_img.png b/app/src/main/res/drawable-hdpi/and_card_long_img.png new file mode 100644 index 0000000..4aee527 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/and_card_long_img.png differ diff --git a/app/src/main/res/drawable-hdpi/and_photo_rectangle.png b/app/src/main/res/drawable-hdpi/and_photo_rectangle.png new file mode 100644 index 0000000..15c4b73 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/and_photo_rectangle.png differ diff --git a/app/src/main/res/drawable-hdpi/and_photoframe.png b/app/src/main/res/drawable-hdpi/and_photoframe.png new file mode 100644 index 0000000..bf61b6d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/and_photoframe.png differ diff --git a/app/src/main/res/drawable-hdpi/profile.png b/app/src/main/res/drawable-hdpi/profile.png new file mode 100644 index 0000000..18321e5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/profile.png differ diff --git a/app/src/main/res/drawable-mdpi/and_card_img.png b/app/src/main/res/drawable-mdpi/and_card_img.png new file mode 100644 index 0000000..972c660 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/and_card_img.png differ diff --git a/app/src/main/res/drawable-mdpi/and_card_long_img.png b/app/src/main/res/drawable-mdpi/and_card_long_img.png new file mode 100644 index 0000000..efd07c4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/and_card_long_img.png differ diff --git a/app/src/main/res/drawable-mdpi/and_photo_rectangle.png b/app/src/main/res/drawable-mdpi/and_photo_rectangle.png new file mode 100644 index 0000000..5c2c87f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/and_photo_rectangle.png differ diff --git a/app/src/main/res/drawable-mdpi/and_photoframe.png b/app/src/main/res/drawable-mdpi/and_photoframe.png new file mode 100644 index 0000000..19cb172 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/and_photoframe.png differ diff --git a/app/src/main/res/drawable-mdpi/profile.png b/app/src/main/res/drawable-mdpi/profile.png new file mode 100644 index 0000000..c334bbf Binary files /dev/null and b/app/src/main/res/drawable-mdpi/profile.png differ diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/like.png b/app/src/main/res/drawable-v24/like.png new file mode 100644 index 0000000..9e0e89e Binary files /dev/null and b/app/src/main/res/drawable-v24/like.png differ diff --git a/app/src/main/res/drawable-v24/unlike.png b/app/src/main/res/drawable-v24/unlike.png new file mode 100644 index 0000000..c724b82 Binary files /dev/null and b/app/src/main/res/drawable-v24/unlike.png differ diff --git a/app/src/main/res/drawable-xhdpi/and_card_img.png b/app/src/main/res/drawable-xhdpi/and_card_img.png new file mode 100644 index 0000000..9c9fa11 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/and_card_img.png differ diff --git a/app/src/main/res/drawable-xhdpi/and_card_long_img.png b/app/src/main/res/drawable-xhdpi/and_card_long_img.png new file mode 100644 index 0000000..68d9a90 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/and_card_long_img.png differ diff --git a/app/src/main/res/drawable-xhdpi/and_photo_rectangle.png b/app/src/main/res/drawable-xhdpi/and_photo_rectangle.png new file mode 100644 index 0000000..0cf7e58 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/and_photo_rectangle.png differ diff --git a/app/src/main/res/drawable-xhdpi/and_photoframe.png b/app/src/main/res/drawable-xhdpi/and_photoframe.png new file mode 100644 index 0000000..84a2af8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/and_photoframe.png differ diff --git a/app/src/main/res/drawable-xhdpi/profile.png b/app/src/main/res/drawable-xhdpi/profile.png new file mode 100644 index 0000000..1417ce3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/profile.png differ diff --git a/app/src/main/res/drawable-xxhdpi/and_card_img.png b/app/src/main/res/drawable-xxhdpi/and_card_img.png new file mode 100644 index 0000000..0a7d82a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/and_card_img.png differ diff --git a/app/src/main/res/drawable-xxhdpi/and_card_long_img.png b/app/src/main/res/drawable-xxhdpi/and_card_long_img.png new file mode 100644 index 0000000..591d4bb Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/and_card_long_img.png differ diff --git a/app/src/main/res/drawable-xxhdpi/and_photo_rectangle.png b/app/src/main/res/drawable-xxhdpi/and_photo_rectangle.png new file mode 100644 index 0000000..882000e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/and_photo_rectangle.png differ diff --git a/app/src/main/res/drawable-xxhdpi/and_photoframe.png b/app/src/main/res/drawable-xxhdpi/and_photoframe.png new file mode 100644 index 0000000..bb40853 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/and_photoframe.png differ diff --git a/app/src/main/res/drawable-xxhdpi/profile.png b/app/src/main/res/drawable-xxhdpi/profile.png new file mode 100644 index 0000000..8463728 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/profile.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/and_card_img.png b/app/src/main/res/drawable-xxxhdpi/and_card_img.png new file mode 100644 index 0000000..d5942e0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/and_card_img.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/and_card_long_img.png b/app/src/main/res/drawable-xxxhdpi/and_card_long_img.png new file mode 100644 index 0000000..20d2ca8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/and_card_long_img.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/and_photo_rectangle.png b/app/src/main/res/drawable-xxxhdpi/and_photo_rectangle.png new file mode 100644 index 0000000..0858d4c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/and_photo_rectangle.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/and_photoframe.png b/app/src/main/res/drawable-xxxhdpi/and_photoframe.png new file mode 100644 index 0000000..d69d2a3 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/and_photoframe.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/profile.png b/app/src/main/res/drawable-xxxhdpi/profile.png new file mode 100644 index 0000000..990d06f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/profile.png differ diff --git a/app/src/main/res/drawable/buttonline.xml b/app/src/main/res/drawable/buttonline.xml new file mode 100644 index 0000000..70a6166 --- /dev/null +++ b/app/src/main/res/drawable/buttonline.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/heartbutton.xml b/app/src/main/res/drawable/heartbutton.xml new file mode 100644 index 0000000..e68a8c3 --- /dev/null +++ b/app/src/main/res/drawable/heartbutton.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_addphoto.xml b/app/src/main/res/drawable/ic_addphoto.xml new file mode 100644 index 0000000..fb8ac11 --- /dev/null +++ b/app/src/main/res/drawable/ic_addphoto.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_addphoto_default.xml b/app/src/main/res/drawable/ic_addphoto_default.xml new file mode 100644 index 0000000..92173ac --- /dev/null +++ b/app/src/main/res/drawable/ic_addphoto_default.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..0eb159b --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_basic_profile.xml b/app/src/main/res/drawable/ic_basic_profile.xml new file mode 100644 index 0000000..f8aff02 --- /dev/null +++ b/app/src/main/res/drawable/ic_basic_profile.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_btn_addphoto.xml b/app/src/main/res/drawable/ic_btn_addphoto.xml new file mode 100644 index 0000000..c4aee5c --- /dev/null +++ b/app/src/main/res/drawable/ic_btn_addphoto.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_btn_like.xml b/app/src/main/res/drawable/ic_btn_like.xml new file mode 100644 index 0000000..28e62be --- /dev/null +++ b/app/src/main/res/drawable/ic_btn_like.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_btn_unlike.xml b/app/src/main/res/drawable/ic_btn_unlike.xml new file mode 100644 index 0000000..599c09b --- /dev/null +++ b/app/src/main/res/drawable/ic_btn_unlike.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_call.xml b/app/src/main/res/drawable/ic_call.xml new file mode 100644 index 0000000..7ab1753 --- /dev/null +++ b/app/src/main/res/drawable/ic_call.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml new file mode 100644 index 0000000..482bbb5 --- /dev/null +++ b/app/src/main/res/drawable/ic_camera.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_card_curation.xml b/app/src/main/res/drawable/ic_card_curation.xml new file mode 100644 index 0000000..d802609 --- /dev/null +++ b/app/src/main/res/drawable/ic_card_curation.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_category.xml b/app/src/main/res/drawable/ic_category.xml new file mode 100644 index 0000000..e22811f --- /dev/null +++ b/app/src/main/res/drawable/ic_category.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..5fe1c0a --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_curation_cover.xml b/app/src/main/res/drawable/ic_curation_cover.xml new file mode 100644 index 0000000..386842c --- /dev/null +++ b/app/src/main/res/drawable/ic_curation_cover.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_dialog_clear.xml b/app/src/main/res/drawable/ic_dialog_clear.xml new file mode 100644 index 0000000..771b708 --- /dev/null +++ b/app/src/main/res/drawable/ic_dialog_clear.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000..158a63e --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_fillin_logo.xml b/app/src/main/res/drawable/ic_fillin_logo.xml new file mode 100644 index 0000000..843d9bc --- /dev/null +++ b/app/src/main/res/drawable/ic_fillin_logo.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_filmroll.xml b/app/src/main/res/drawable/ic_filmroll.xml new file mode 100644 index 0000000..c675ee0 --- /dev/null +++ b/app/src/main/res/drawable/ic_filmroll.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_filmroll_24.xml b/app/src/main/res/drawable/ic_filmroll_24.xml new file mode 100644 index 0000000..e23d676 --- /dev/null +++ b/app/src/main/res/drawable/ic_filmroll_24.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_filmroll_small.xml b/app/src/main/res/drawable/ic_filmroll_small.xml new file mode 100644 index 0000000..ce44909 --- /dev/null +++ b/app/src/main/res/drawable/ic_filmroll_small.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 0000000..3e3b7ad --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_addphoto.xml b/app/src/main/res/drawable/ic_home_addphoto.xml new file mode 100644 index 0000000..9c272ce --- /dev/null +++ b/app/src/main/res/drawable/ic_home_addphoto.xml @@ -0,0 +1,37 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_icn_close.xml b/app/src/main/res/drawable/ic_icn_close.xml new file mode 100644 index 0000000..c5fca87 --- /dev/null +++ b/app/src/main/res/drawable/ic_icn_close.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_icn_heart.xml b/app/src/main/res/drawable/ic_icn_heart.xml new file mode 100644 index 0000000..3e3b7ad --- /dev/null +++ b/app/src/main/res/drawable/ic_icn_heart.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_icn_kakao.xml b/app/src/main/res/drawable/ic_icn_kakao.xml new file mode 100644 index 0000000..ce71efc --- /dev/null +++ b/app/src/main/res/drawable/ic_icn_kakao.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_img_photo.xml b/app/src/main/res/drawable/ic_img_photo.xml new file mode 100644 index 0000000..8afad2f --- /dev/null +++ b/app/src/main/res/drawable/ic_img_photo.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_img_trash.xml b/app/src/main/res/drawable/ic_img_trash.xml new file mode 100644 index 0000000..61d99cc --- /dev/null +++ b/app/src/main/res/drawable/ic_img_trash.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..eaf1ed3 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml new file mode 100644 index 0000000..935be69 --- /dev/null +++ b/app/src/main/res/drawable/ic_link.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_logo.xml b/app/src/main/res/drawable/ic_logo.xml new file mode 100644 index 0000000..55e2dc3 --- /dev/null +++ b/app/src/main/res/drawable/ic_logo.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_mypage.xml b/app/src/main/res/drawable/ic_mypage.xml new file mode 100644 index 0000000..a437743 --- /dev/null +++ b/app/src/main/res/drawable/ic_mypage.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_next.xml b/app/src/main/res/drawable/ic_next.xml new file mode 100644 index 0000000..f49fd91 --- /dev/null +++ b/app/src/main/res/drawable/ic_next.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_no_result.xml b/app/src/main/res/drawable/ic_no_result.xml new file mode 100644 index 0000000..9203f81 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_result.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_onboarding_1.xml b/app/src/main/res/drawable/ic_onboarding_1.xml new file mode 100644 index 0000000..c91b26c --- /dev/null +++ b/app/src/main/res/drawable/ic_onboarding_1.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_onboarding_2.xml b/app/src/main/res/drawable/ic_onboarding_2.xml new file mode 100644 index 0000000..2d4d51d --- /dev/null +++ b/app/src/main/res/drawable/ic_onboarding_2.xml @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_onboarding_3.xml b/app/src/main/res/drawable/ic_onboarding_3.xml new file mode 100644 index 0000000..b2ef12f --- /dev/null +++ b/app/src/main/res/drawable/ic_onboarding_3.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_photo.xml b/app/src/main/res/drawable/ic_photo.xml new file mode 100644 index 0000000..f04ec11 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_place_big.xml b/app/src/main/res/drawable/ic_place_big.xml new file mode 100644 index 0000000..7e37062 --- /dev/null +++ b/app/src/main/res/drawable/ic_place_big.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_place_select.xml b/app/src/main/res/drawable/ic_place_select.xml new file mode 100644 index 0000000..0a9d16d --- /dev/null +++ b/app/src/main/res/drawable/ic_place_select.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_place_small.xml b/app/src/main/res/drawable/ic_place_small.xml new file mode 100644 index 0000000..1d9c4ef --- /dev/null +++ b/app/src/main/res/drawable/ic_place_small.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_price.xml b/app/src/main/res/drawable/ic_price.xml new file mode 100644 index 0000000..5fa42bf --- /dev/null +++ b/app/src/main/res/drawable/ic_price.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_red_heart.xml b/app/src/main/res/drawable/ic_red_heart.xml new file mode 100644 index 0000000..f32c828 --- /dev/null +++ b/app/src/main/res/drawable/ic_red_heart.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_scrap.xml b/app/src/main/res/drawable/ic_scrap.xml new file mode 100644 index 0000000..d2e6ff0 --- /dev/null +++ b/app/src/main/res/drawable/ic_scrap.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_stduiomap.xml b/app/src/main/res/drawable/ic_stduiomap.xml new file mode 100644 index 0000000..562f836 --- /dev/null +++ b/app/src/main/res/drawable/ic_stduiomap.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_tab_indicator_default.xml b/app/src/main/res/drawable/ic_tab_indicator_default.xml new file mode 100644 index 0000000..c8eb5b4 --- /dev/null +++ b/app/src/main/res/drawable/ic_tab_indicator_default.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_tab_indicator_selected.xml b/app/src/main/res/drawable/ic_tab_indicator_selected.xml new file mode 100644 index 0000000..139d78b --- /dev/null +++ b/app/src/main/res/drawable/ic_tab_indicator_selected.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_time.xml b/app/src/main/res/drawable/ic_time.xml new file mode 100644 index 0000000..1003026 --- /dev/null +++ b/app/src/main/res/drawable/ic_time.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/icn_arrow.xml b/app/src/main/res/drawable/icn_arrow.xml new file mode 100644 index 0000000..9621e17 --- /dev/null +++ b/app/src/main/res/drawable/icn_arrow.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/icn_search.xml b/app/src/main/res/drawable/icn_search.xml new file mode 100644 index 0000000..733a9dd --- /dev/null +++ b/app/src/main/res/drawable/icn_search.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/layout_line.xml b/app/src/main/res/drawable/layout_line.xml new file mode 100644 index 0000000..f9f4b6c --- /dev/null +++ b/app/src/main/res/drawable/layout_line.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/likebutton.xml b/app/src/main/res/drawable/likebutton.xml new file mode 100644 index 0000000..a1c8dd4 --- /dev/null +++ b/app/src/main/res/drawable/likebutton.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/linearline.xml b/app/src/main/res/drawable/linearline.xml new file mode 100644 index 0000000..c473bb5 --- /dev/null +++ b/app/src/main/res/drawable/linearline.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/map_search_background.xml b/app/src/main/res/drawable/map_search_background.xml new file mode 100644 index 0000000..4c82d08 --- /dev/null +++ b/app/src/main/res/drawable/map_search_background.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/nextbutton.xml b/app/src/main/res/drawable/nextbutton.xml new file mode 100644 index 0000000..e05f389 --- /dev/null +++ b/app/src/main/res/drawable/nextbutton.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/popup.xml b/app/src/main/res/drawable/popup.xml new file mode 100644 index 0000000..47aeef5 --- /dev/null +++ b/app/src/main/res/drawable/popup.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_tab_indicator.xml b/app/src/main/res/drawable/selector_tab_indicator.xml new file mode 100644 index 0000000..046f006 --- /dev/null +++ b/app/src/main/res/drawable/selector_tab_indicator.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_btn_darkgrey2.xml b/app/src/main/res/drawable/shape_btn_darkgrey2.xml new file mode 100644 index 0000000..d025100 --- /dev/null +++ b/app/src/main/res/drawable/shape_btn_darkgrey2.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_btn_fillinred.xml b/app/src/main/res/drawable/shape_btn_fillinred.xml new file mode 100644 index 0000000..c6ee61f --- /dev/null +++ b/app/src/main/res/drawable/shape_btn_fillinred.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_kakao_background.xml b/app/src/main/res/drawable/shape_kakao_background.xml new file mode 100644 index 0000000..7b86543 --- /dev/null +++ b/app/src/main/res/drawable/shape_kakao_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_mypage.xml b/app/src/main/res/drawable/shape_mypage.xml new file mode 100644 index 0000000..62dbaf0 --- /dev/null +++ b/app/src/main/res/drawable/shape_mypage.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_mypage_text.xml b/app/src/main/res/drawable/shape_mypage_text.xml new file mode 100644 index 0000000..9da5e78 --- /dev/null +++ b/app/src/main/res/drawable/shape_mypage_text.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/futura_std.xml b/app/src/main/res/font/futura_std.xml new file mode 100644 index 0000000..df89757 --- /dev/null +++ b/app/src/main/res/font/futura_std.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/futura_std_bold.otf b/app/src/main/res/font/futura_std_bold.otf new file mode 100644 index 0000000..00f1d70 Binary files /dev/null and b/app/src/main/res/font/futura_std_bold.otf differ diff --git a/app/src/main/res/font/futura_std_book.otf b/app/src/main/res/font/futura_std_book.otf new file mode 100644 index 0000000..71e0bc0 Binary files /dev/null and b/app/src/main/res/font/futura_std_book.otf differ diff --git a/app/src/main/res/font/noto_sans_kr.xml b/app/src/main/res/font/noto_sans_kr.xml new file mode 100644 index 0000000..a1440aa --- /dev/null +++ b/app/src/main/res/font/noto_sans_kr.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/noto_sans_kr_black.otf b/app/src/main/res/font/noto_sans_kr_black.otf new file mode 100644 index 0000000..0cfa4bb Binary files /dev/null and b/app/src/main/res/font/noto_sans_kr_black.otf differ diff --git a/app/src/main/res/font/noto_sans_kr_bold.otf b/app/src/main/res/font/noto_sans_kr_bold.otf new file mode 100644 index 0000000..7f4131c Binary files /dev/null and b/app/src/main/res/font/noto_sans_kr_bold.otf differ diff --git a/app/src/main/res/font/noto_sans_kr_light.otf b/app/src/main/res/font/noto_sans_kr_light.otf new file mode 100644 index 0000000..03f948c Binary files /dev/null and b/app/src/main/res/font/noto_sans_kr_light.otf differ diff --git a/app/src/main/res/font/noto_sans_kr_medium.otf b/app/src/main/res/font/noto_sans_kr_medium.otf new file mode 100644 index 0000000..d8cacce Binary files /dev/null and b/app/src/main/res/font/noto_sans_kr_medium.otf differ diff --git a/app/src/main/res/font/noto_sans_kr_regular.otf b/app/src/main/res/font/noto_sans_kr_regular.otf new file mode 100644 index 0000000..e26c1cd Binary files /dev/null and b/app/src/main/res/font/noto_sans_kr_regular.otf differ diff --git a/app/src/main/res/font/noto_sans_kr_thin.otf b/app/src/main/res/font/noto_sans_kr_thin.otf new file mode 100644 index 0000000..6bc00c4 Binary files /dev/null and b/app/src/main/res/font/noto_sans_kr_thin.otf differ diff --git a/app/src/main/res/icn_edit.svg b/app/src/main/res/icn_edit.svg new file mode 100644 index 0000000..647779d --- /dev/null +++ b/app/src/main/res/icn_edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/layout/activity_add_photo.xml b/app/src/main/res/layout/activity_add_photo.xml new file mode 100644 index 0000000..1c20604 --- /dev/null +++ b/app/src/main/res/layout/activity_add_photo.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_category_image.xml b/app/src/main/res/layout/activity_category_image.xml new file mode 100644 index 0000000..e93c98a --- /dev/null +++ b/app/src/main/res/layout/activity_category_image.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_film_roll.xml b/app/src/main/res/layout/activity_film_roll.xml new file mode 100644 index 0000000..54dd5b1 --- /dev/null +++ b/app/src/main/res/layout/activity_film_roll.xml @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_film_roll_category.xml b/app/src/main/res/layout/activity_film_roll_category.xml new file mode 100644 index 0000000..f535b76 --- /dev/null +++ b/app/src/main/res/layout/activity_film_roll_category.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml new file mode 100644 index 0000000..86575f0 --- /dev/null +++ b/app/src/main/res/layout/activity_home.xml @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..9541d4d --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_map_search.xml b/app/src/main/res/layout/activity_map_search.xml new file mode 100644 index 0000000..a93f23b --- /dev/null +++ b/app/src/main/res/layout/activity_map_search.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_my_page.xml b/app/src/main/res/layout/activity_my_page.xml new file mode 100644 index 0000000..049894c --- /dev/null +++ b/app/src/main/res/layout/activity_my_page.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml new file mode 100644 index 0000000..004a44e --- /dev/null +++ b/app/src/main/res/layout/activity_onboarding.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_studio_map.xml b/app/src/main/res/layout/activity_studio_map.xml new file mode 100644 index 0000000..4cc6b59 --- /dev/null +++ b/app/src/main/res/layout/activity_studio_map.xml @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_addphoto_cancel.xml b/app/src/main/res/layout/dialog_addphoto_cancel.xml new file mode 100644 index 0000000..205bd0d --- /dev/null +++ b/app/src/main/res/layout/dialog_addphoto_cancel.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_addphoto_complete.xml b/app/src/main/res/layout/dialog_addphoto_complete.xml new file mode 100644 index 0000000..6019b0d --- /dev/null +++ b/app/src/main/res/layout/dialog_addphoto_complete.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_photo_dialog.xml b/app/src/main/res/layout/fragment_photo_dialog.xml new file mode 100644 index 0000000..ac014b0 --- /dev/null +++ b/app/src/main/res/layout/fragment_photo_dialog.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_category_image.xml b/app/src/main/res/layout/item_category_image.xml new file mode 100644 index 0000000..1095554 --- /dev/null +++ b/app/src/main/res/layout/item_category_image.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_category_info.xml b/app/src/main/res/layout/item_category_info.xml new file mode 100644 index 0000000..78d081c --- /dev/null +++ b/app/src/main/res/layout/item_category_info.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_curation.xml b/app/src/main/res/layout/item_curation.xml new file mode 100644 index 0000000..baa56e8 --- /dev/null +++ b/app/src/main/res/layout/item_curation.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_curation_first.xml b/app/src/main/res/layout/item_curation_first.xml new file mode 100644 index 0000000..92eccd5 --- /dev/null +++ b/app/src/main/res/layout/item_curation_first.xml @@ -0,0 +1,29 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_film_roll.xml b/app/src/main/res/layout/item_film_roll.xml new file mode 100644 index 0000000..a94cc20 --- /dev/null +++ b/app/src/main/res/layout/item_film_roll.xml @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_film_roll_pic.xml b/app/src/main/res/layout/item_film_roll_pic.xml new file mode 100644 index 0000000..9666773 --- /dev/null +++ b/app/src/main/res/layout/item_film_roll_pic.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_location_info.xml b/app/src/main/res/layout/item_location_info.xml new file mode 100644 index 0000000..87d535c --- /dev/null +++ b/app/src/main/res/layout/item_location_info.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_my_page.xml b/app/src/main/res/layout/item_my_page.xml new file mode 100644 index 0000000..372c42f --- /dev/null +++ b/app/src/main/res/layout/item_my_page.xml @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_new_photos_list.xml b/app/src/main/res/layout/item_new_photos_list.xml new file mode 100644 index 0000000..beddbed --- /dev/null +++ b/app/src/main/res/layout/item_new_photos_list.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_next_button.xml b/app/src/main/res/layout/item_next_button.xml new file mode 100644 index 0000000..b75618d --- /dev/null +++ b/app/src/main/res/layout/item_next_button.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_onboarding.xml b/app/src/main/res/layout/item_onboarding.xml new file mode 100644 index 0000000..11a5977 --- /dev/null +++ b/app/src/main/res/layout/item_onboarding.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_photo_review.xml b/app/src/main/res/layout/item_photo_review.xml new file mode 100644 index 0000000..2fbc379 --- /dev/null +++ b/app/src/main/res/layout/item_photo_review.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cf..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cf..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..eb0cdfd Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..aa8c478 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..b71458b Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..f350ccf Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..9ea5fb4 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..ee2ab21 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..a4b591e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..c98ab15 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..ca5b570 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a3b6048 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index ee669e6..5ff85f3 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -13,4 +13,12 @@ ?attr/colorPrimaryVariant + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..6901397 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -6,5 +6,24 @@ #FF03DAC5 #FF018786 #FF000000 - #FFFFFFFF + #dc4f1c + #010101 + #fdfaf9 + #8b8b8b + #1c1b1b + #555555 + #232222 + #ff9416 + #faff00 + #df1d1d + #0078d4 + #6f6f6f + #474645 + #a5a5a5 + #ffffff + #FEE500 + #000000 + + #d56c46 + #e1dedd \ No newline at end of file diff --git a/app/src/main/res/values/fonts.xml b/app/src/main/res/values/fonts.xml new file mode 100644 index 0000000..b01bdc4 --- /dev/null +++ b/app/src/main/res/values/fonts.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..f001f3e --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #010101 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a4508dc..1efae51 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,33 @@ - FillInAndroid + FILL-IN + μ°°μΉ΅μ°°μΉ΅ 필린이 + Fuji film X 1200 + Fill-in your Feel\nFill-in your film- + ν•„λ¦°μ—μ„œ μ†Œμ€‘ν•œ 좔얡을\nFILL-IN ν•΄λ³΄μ„Έμš” + 카카였 둜그인 + κ±΄λ„ˆλ›°κΈ° + λ‹€μŒ + μ‹œμž‘ν•˜κΈ° + 필름을 μŠ€μΊ” ν•  ν˜„μƒμ†Œ 정보λ₯Ό μ°Ύκ³  κ³„μ‹ κ°€μš”? + μ–΄λ–€ 필름을 μ‚¬μš© ν•΄ 볼지 κ³ λ―Ό ν•˜κ³  κ³„μ‹ κ°€μš”? + 직접 찍은 필름 μ‚¬μ§„μ΄λ‚˜ 정보λ₯Ό κ³΅μœ ν•˜κ³  μ‹Άλ‚˜μš”? + μ£Όλ³€ ν˜„μƒμ†Œ 정보λ₯Ό\nν•„λ¦°μ—μ„œ μ°Ύμ•„λ³΄μ„Έμš”! + λ‹€μ–‘ν•œ 필름 사진을\nν•„λ¦°μ—μ„œ λ§Œλ‚˜λ³΄μ„Έμš”! + λ‚˜λ§Œμ˜ 필름 사진을\nν•„λ¦°μ—μ„œ κ³΅μœ ν•΄λ³΄μ„Έμš”! + λ‚˜λ§Œ μ•„λŠ” 필름과 ν˜„μƒμ†Œλ₯Ό\n필린에 μ•Œλ €μ£Όμ„Έμš”! + MY PHOTOS + Film + Studio + 사진좔가λ₯Ό μ·¨μ†Œν• κΉŒμš”?\nμž…λ ₯ν•˜μ‹  정보가 λͺ¨λ‘ μ‚­μ œλ©λ‹ˆλ‹€. + 사진 μΆ”κ°€λ₯Ό μ™„λ£Œν–ˆμ–΄μš”! + λ§ˆμ΄νŽ˜μ΄μ§€μ—μ„œ λ‚΄κ°€ 올린 사진듀을\n확인할 수 μžˆμ–΄μš”. + Add Photo + + 좔얡을 ν˜„μƒν•  ν˜„μƒμ†Œλ₯Ό κ²€μƒ‰ν•΄λ³΄μ„Έμš” + μ›Ή μ‚¬μ΄νŠΈλ‘œ 이동 + PHOTO REVIEW + λ‚˜λ§Œ μ•„λŠ” ν˜„μƒμ†Œλ₯Ό 필린에 μ•Œλ €μ£Όμ„Έμš”! + Hello blank fragment + 검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€. + 검색어λ₯Ό λ°”λ₯΄κ²Œ μž…λ ₯ν–ˆλŠ”μ§€ ν™•μΈν•΄λ³΄κ±°λ‚˜,\nμ£Όμ†Œλ‘œ λ‹€μ‹œ κ²€μƒ‰ν•΄λ³΄μ„Έμš”. \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 5d6034e..73d0d64 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,16 +1,30 @@ - + + \ No newline at end of file diff --git a/app/src/release/java/com/teamfillin/fillin/FlipperInitializer.kt b/app/src/release/java/com/teamfillin/fillin/FlipperInitializer.kt new file mode 100644 index 0000000..c280544 --- /dev/null +++ b/app/src/release/java/com/teamfillin/fillin/FlipperInitializer.kt @@ -0,0 +1,9 @@ +package com.teamfillin.fillin + +import android.app.Application +import okhttp3.OkHttpClient + +object FlipperInitializer { + fun init(app: Application) {} + fun addFlipperNetworkPlguin(builder: OkHttpClient.Builder) = builder +} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 876c922..04155e0 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -3,5 +3,5 @@ plugins { } repositories { - mavenCentral() + mavenCentral() } diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index f05c213..a15b9af 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -3,6 +3,6 @@ object Constants { const val compileSdk = 31 const val minSdk = 26 const val targetSdk = 30 - const val versionCode = 1 - const val versionName = "1.0" + const val versionCode = 2 + const val versionName = "1.0.1" } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 0ec1ece..70ff479 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -7,6 +7,7 @@ object AndroidXDependencies { const val appCompat = "androidx.appcompat:appcompat:${Versions.appCompatVersion}" const val constraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.constraintLayoutVersion}" + const val startup = "androidx.startup:startup-runtime:${Versions.appStartUpVersion}" const val hilt = "com.google.dagger:hilt-android:${Versions.hiltVersion}" const val fragment = "androidx.fragment:fragment-ktx:${Versions.fragmentKtxVersion}" const val legacy = "androidx.legacy:legacy-support-v4:${Versions.legacySupportVersion}" @@ -16,6 +17,8 @@ object AndroidXDependencies { const val lifeCycleKtx = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycleVersion}" const val lifecycleJava8 = "androidx.lifecycle:lifecycle-common-java8:${Versions.lifecycleVersion}" + const val splashScreen = "androidx.core:core-splashscreen:${Versions.splashVersion}" + const val pagingRuntime = "androidx.paging:paging-runtime:${Versions.pagingVersion}" } object TestDependencies { @@ -48,8 +51,17 @@ object ThirdPartyDependencies { const val soloader = "com.facebook.soloader:soloader:${Versions.soloaderVersion}" const val flipperNetwork = "com.facebook.flipper:flipper-network-plugin:${Versions.flipperVersion}" + const val flipperLeakCanary = + "com.facebook.flipper:flipper-leakcanary2-plugin:${Versions.flipperVersion}" + const val leakCanary = + "com.squareup.leakcanary:leakcanary-android:${Versions.leakCanaryVersion}" const val ossLicense = "com.google.android.gms:play-services-oss-licenses:${Versions.ossVersion}" + const val kakaoLogin = "com.kakao.sdk:v2-user:${Versions.kakaoVersion}" + const val naverMap = "com.naver.maps:map-sdk:${Versions.naverVersion}" + const val mapLocation = + "com.google.android.gms:play-services-location:${Versions.locationVersion}" + const val dotsIndicator = "com.tbuonomo:dotsindicator:${Versions.dotsIndicatorVersion}" } object ClassPathPlugins { diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 6244dd1..c81dcc3 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -6,14 +6,17 @@ object Versions { const val appCompatVersion = "1.4.0" const val materialDesignVersion = "1.4.0" const val constraintLayoutVersion = "2.1.0" + const val appStartUpVersion = "1.1.0" const val legacySupportVersion = "1.0.0" const val hiltVersion = "2.38.1" - const val fragmentKtxVersion = "1.3.4" - const val coroutinesAndroidVersion = "1.4.3" + const val fragmentKtxVersion = "1.4.0" + const val coroutinesAndroidVersion = "1.6.0" + const val pagingVersion = "3.1.0" const val securityVersion = "1.0.0" - const val lifecycleVersion = "2.3.1" + const val lifecycleVersion = "2.4.0" const val ossPluginVersion = "0.10.4" const val ossVersion = "17.0.0" + const val splashVersion = "1.0.0-beta01" const val gradleVersion = "7.0.4" @@ -21,14 +24,19 @@ object Versions { const val retrofitVersion = "2.9.0" const val okHttpVersion = "4.9.1" const val gsonVersion = "2.8.6" - const val timberVersion = "4.7.1" + const val timberVersion = "5.0.1" + const val kakaoVersion = "2.8.4" + const val naverVersion = "3.13.0" + const val locationVersion = "19.0.0" + const val dotsIndicatorVersion = "4.2" const val junitVersion = "4.13.2" const val espressoVersion = "3.3.0" const val androidTestVersion = "1.1.2" - const val flipperVersion = "0.96.1" - const val soloaderVersion = "0.10.1" + const val flipperVersion = "0.128.4" + const val soloaderVersion = "0.10.3" + const val leakCanaryVersion = "2.6" val javaVersion = JavaVersion.VERSION_11 const val jvmVersion = "11" diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..c445099 --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") +} + +android { + compileSdk = 31 + + defaultConfig { + minSdk = 26 + targetSdk = 30 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = Versions.javaVersion + targetCompatibility = Versions.javaVersion + } + + kotlinOptions { + jvmTarget = Versions.jvmVersion + } + + buildFeatures { + dataBinding = true + viewBinding = true + } +} + +dependencies { + // Kotlin + implementation(KotlinDependencies.kotlin) + + // AndroidX + implementation(AndroidXDependencies.coreKtx) + implementation(AndroidXDependencies.appCompat) + implementation(AndroidXDependencies.constraintLayout) + implementation(AndroidXDependencies.coroutines) + implementation(AndroidXDependencies.fragment) + implementation(AndroidXDependencies.pagingRuntime) + + // Material Design + implementation(MaterialDesignDependencies.materialDesign) + + // Third-Party + implementation(ThirdPartyDependencies.glide) + kapt(KaptDependencies.glide) + implementation(ThirdPartyDependencies.gson) + implementation(platform(ThirdPartyDependencies.okHttpBom)) + implementation(ThirdPartyDependencies.okHttp) + implementation(ThirdPartyDependencies.retrofit) + + // Test Dependency + testImplementation(TestDependencies.jUnit) + androidTestImplementation(TestDependencies.androidTest) + androidTestImplementation(TestDependencies.espresso) +} \ No newline at end of file diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/src/androidTest/java/com/teamfillin/fillin/core/ExampleInstrumentedTest.kt b/core/src/androidTest/java/com/teamfillin/fillin/core/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..eef23c1 --- /dev/null +++ b/core/src/androidTest/java/com/teamfillin/fillin/core/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.teamfillin.fillin.core + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.teamfillin.fillin.core.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c5e62ca --- /dev/null +++ b/core/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/core/src/main/java/com/teamfillin/fillin/core/base/BindingActivity.kt b/core/src/main/java/com/teamfillin/fillin/core/base/BindingActivity.kt new file mode 100644 index 0000000..61fe4ac --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/base/BindingActivity.kt @@ -0,0 +1,24 @@ +package com.teamfillin.fillin.core.base + +import android.os.Bundle +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.viewbinding.ViewBinding + +/* +* Created By Nunu Lee +* at 2022.01.09 +* */ +abstract class BindingActivity( + @LayoutRes private val layoutResId: Int +) : AppCompatActivity() { + protected lateinit var binding: T + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, layoutResId) + binding.lifecycleOwner = this + } +} \ No newline at end of file diff --git a/core/src/main/java/com/teamfillin/fillin/core/base/BindingFragment.kt b/core/src/main/java/com/teamfillin/fillin/core/base/BindingFragment.kt new file mode 100644 index 0000000..9823f3a --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/base/BindingFragment.kt @@ -0,0 +1,38 @@ +package com.teamfillin.fillin.core.base + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment + +/* +* Created By Nunu Lee +* at 2022.01.09 +* */ +abstract class BindingFragment( + @LayoutRes private val layoutResId: Int +) : Fragment() { + private var _binding: T? = null + protected val binding: T + get() = requireNotNull(_binding) { "binding object is not initialized" } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = DataBindingUtil.inflate(inflater, layoutResId, container, false) + binding.lifecycleOwner = viewLifecycleOwner + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + // Nunu: μ•ˆλ“œλ‘œμ΄λ“œ κ³΅μ‹λ¬Έμ„œμ—μ„œ _binding κ°μ²΄λŠ” super.onDestroyView() 이후에 ν•˜λŠ” 것이라고 λͺ…μ‹œλ˜μ–΄μžˆμŠ΅λ‹ˆλ‹€. + _binding = null + } +} \ No newline at end of file diff --git a/core/src/main/java/com/teamfillin/fillin/core/base/OffsetPagingSource.kt b/core/src/main/java/com/teamfillin/fillin/core/base/OffsetPagingSource.kt new file mode 100644 index 0000000..92bf750 --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/base/OffsetPagingSource.kt @@ -0,0 +1,15 @@ +package com.teamfillin.fillin.core.base + +import androidx.paging.PagingSource +import androidx.paging.PagingState + +const val START_POSITION_INDEX = 1 + +abstract class OffsetPagingSource : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + val anchorPage = state.closestPageToPosition(anchorPosition) + anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/teamfillin/fillin/core/content/BitmapRequestBody.kt b/core/src/main/java/com/teamfillin/fillin/core/content/BitmapRequestBody.kt new file mode 100644 index 0000000..1b3e223 --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/content/BitmapRequestBody.kt @@ -0,0 +1,21 @@ +package com.teamfillin.fillin.core.content + +import android.graphics.Bitmap +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody +import okio.BufferedSink + +/* +*.Created by Nunu Lee +* at 2022.1.10 +* +* Bitmap을 RequestBody에 λ‹΄μ•„μ„œ Multipart μ„œλ²„ν†΅μ‹ μ„ ν•΄μ•Όν•  λ•Œ μ‚¬μš©λ˜λŠ” ν΄λž˜μŠ€μž…λ‹ˆλ‹€. + */ +class BitmapRequestBody(private val bitmap: Bitmap) : RequestBody() { + override fun contentType(): MediaType = "image/jpeg".toMediaType() + + override fun writeTo(sink: BufferedSink) { + bitmap.compress(Bitmap.CompressFormat.JPEG, 99, sink.outputStream()) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/teamfillin/fillin/core/content/ContentUriRequestBody.kt b/core/src/main/java/com/teamfillin/fillin/core/content/ContentUriRequestBody.kt new file mode 100644 index 0000000..cc66a59 --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/content/ContentUriRequestBody.kt @@ -0,0 +1,52 @@ +package com.teamfillin.fillin.core.content + +import android.content.Context +import android.net.Uri +import android.provider.MediaStore +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okio.BufferedSink +import okio.source + +class ContentUriRequestBody( + context: Context, + private val uri: Uri +) : RequestBody() { + private val contentResolver = context.contentResolver + + private var fileName = "" + private var size = -1L + + init { + contentResolver.query( + uri, + arrayOf(MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DISPLAY_NAME), + null, + null, + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)) + fileName = + cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)) + } + } + } + + fun getFileName() = fileName + + override fun contentLength(): Long = size + + override fun contentType(): MediaType? = + contentResolver.getType(uri)?.toMediaTypeOrNull() + + override fun writeTo(sink: BufferedSink) { + contentResolver.openInputStream(uri)?.source()?.use { source -> + sink.writeAll(source) + } + } + + fun toFormData() = MultipartBody.Part.createFormData("image", getFileName(), this) +} \ No newline at end of file diff --git a/core/src/main/java/com/teamfillin/fillin/core/content/RetrofitUtil.kt b/core/src/main/java/com/teamfillin/fillin/core/content/RetrofitUtil.kt new file mode 100644 index 0000000..9f1ee46 --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/content/RetrofitUtil.kt @@ -0,0 +1,25 @@ +package com.teamfillin.fillin.core.content + +import android.util.Log +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +fun Call.receive( + onSuccess: (T) -> Unit, + onError: ((stateCode: Int) -> Unit)? = null +) { + this.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + onSuccess.invoke(response.body() ?: return) + } else { + onError?.invoke(response.code()) + } + } + + override fun onFailure(call: Call, t: Throwable) { + Log.d("NetworkTest", "error: $t") + } + }) +} \ No newline at end of file diff --git a/core/src/main/java/com/teamfillin/fillin/core/context/ContextExt.kt b/core/src/main/java/com/teamfillin/fillin/core/context/ContextExt.kt new file mode 100644 index 0000000..e078d82 --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/context/ContextExt.kt @@ -0,0 +1,28 @@ +package com.teamfillin.fillin.core.context + +import android.content.Context +import android.view.View +import android.widget.Toast +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import com.google.android.material.snackbar.Snackbar + +fun Context.toast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} + +fun Context.longToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() +} + +fun Context.snackBar(anchorView: View, message: () -> String) { + Snackbar.make(anchorView, message(), Snackbar.LENGTH_SHORT).show() +} + +fun Context.stringOf(@StringRes resId: Int) = getString(resId) + +fun Context.colorOf(@ColorRes resId: Int) = ContextCompat.getColor(this, resId) + +fun Context.drawableOf(@DrawableRes resId: Int) = ContextCompat.getDrawable(this, resId) diff --git a/core/src/main/java/com/teamfillin/fillin/core/design/StatusBar.kt b/core/src/main/java/com/teamfillin/fillin/core/design/StatusBar.kt new file mode 100644 index 0000000..df5b637 --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/design/StatusBar.kt @@ -0,0 +1,14 @@ +package com.teamfillin.fillin.core.design + +import android.app.Activity +import androidx.annotation.ColorRes +import androidx.fragment.app.Fragment +import com.teamfillin.fillin.core.context.colorOf + +fun Activity.statusBarColorOf(@ColorRes resId: Int) { + window?.statusBarColor = colorOf(resId) +} + +fun Fragment.statusBarColorOf(@ColorRes resId: Int) { + requireActivity().statusBarColorOf(resId) +} diff --git a/core/src/main/java/com/teamfillin/fillin/core/fragment/FragmentExt.kt b/core/src/main/java/com/teamfillin/fillin/core/fragment/FragmentExt.kt new file mode 100644 index 0000000..0d7b4dc --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/fragment/FragmentExt.kt @@ -0,0 +1,28 @@ +package com.teamfillin.fillin.core.fragment + +import android.view.View +import android.widget.Toast +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import com.google.android.material.snackbar.Snackbar + +fun Fragment.toast(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() +} + +fun Fragment.longToast(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() +} + +fun Fragment.snackBar(anchorView: View, message: () -> String) { + Snackbar.make(anchorView, message(), Snackbar.LENGTH_SHORT).show() +} + +fun Fragment.stringOf(@StringRes resId: Int) = getString(resId) + +fun Fragment.colorOf(@ColorRes resId: Int) = ContextCompat.getColor(requireContext(), resId) + +fun Fragment.drawableOf(@DrawableRes resId: Int) = ContextCompat.getDrawable(requireContext(), resId) diff --git a/core/src/main/java/com/teamfillin/fillin/core/intent/ArgsExt.kt b/core/src/main/java/com/teamfillin/fillin/core/intent/ArgsExt.kt new file mode 100644 index 0000000..a720c2f --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/intent/ArgsExt.kt @@ -0,0 +1,25 @@ +package com.teamfillin.fillin.core.intent + +import android.os.Parcelable +import androidx.fragment.app.Fragment +import kotlin.properties.ReadOnlyProperty + +fun intArgs() = ReadOnlyProperty { thisRef, property -> + thisRef.requireArguments().getInt(property.name) +} + +fun longArgs() = ReadOnlyProperty { thisRef, property -> + thisRef.requireArguments().getLong(property.name) +} + +fun boolArgs() = ReadOnlyProperty { thisRef, property -> + thisRef.requireArguments().getBoolean(property.name) +} + +fun stringExtra() = ReadOnlyProperty { thisRef, property -> + thisRef.requireArguments().getString(property.name, "") +} + +fun

parcelableExtra() = ReadOnlyProperty { thisRef, property -> + thisRef.requireArguments().getParcelable(property.name) +} diff --git a/core/src/main/java/com/teamfillin/fillin/core/intent/ExtraExt.kt b/core/src/main/java/com/teamfillin/fillin/core/intent/ExtraExt.kt new file mode 100644 index 0000000..2b693fb --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/intent/ExtraExt.kt @@ -0,0 +1,38 @@ +package com.teamfillin.fillin.core.intent + +import android.app.Activity +import android.os.Parcelable +import kotlin.properties.ReadOnlyProperty + +fun intExtra(defaultValue: Int = -1) = ReadOnlyProperty { thisRef, property -> + thisRef.intent.extras?.getInt( + property.name, + defaultValue + ) ?: defaultValue +} + +fun longExtra(defaultValue: Long = -1) = ReadOnlyProperty { thisRef, property -> + thisRef.intent.extras?.getLong( + property.name, + defaultValue + ) ?: defaultValue +} + +fun boolExtra(defaultValue: Boolean = false) = + ReadOnlyProperty { thisRef, property -> + thisRef.intent.extras?.getBoolean( + property.name, + defaultValue + ) ?: defaultValue + } + +fun stringExtra(defaultValue: String? = null) = + ReadOnlyProperty { thisRef, property -> + if (defaultValue == null) thisRef.intent.extras?.getString(property.name) + else thisRef.intent.extras?.getString(property.name, defaultValue) + } + +fun

parcelableExtra(defaultValue: P? = null) = + ReadOnlyProperty { thisRef, property -> + thisRef.intent.extras?.getParcelable

(property.name) ?: defaultValue + } diff --git a/core/src/main/java/com/teamfillin/fillin/core/view/ViewExt.kt b/core/src/main/java/com/teamfillin/fillin/core/view/ViewExt.kt new file mode 100644 index 0000000..3ecbccc --- /dev/null +++ b/core/src/main/java/com/teamfillin/fillin/core/view/ViewExt.kt @@ -0,0 +1,49 @@ +package com.teamfillin.fillin.core.view + +import android.view.View +import android.widget.ImageView +import androidx.annotation.DrawableRes +import androidx.recyclerview.widget.DiffUtil +import com.bumptech.glide.Glide + +inline fun View.setOnSingleClickListener( + delay: Long = 500L, + crossinline block: (View) -> Unit +) { + var previousClickedTime = 0L + setOnClickListener { view -> + val clickedTime = System.currentTimeMillis() + if (clickedTime - previousClickedTime >= delay) { + block(view) + previousClickedTime = clickedTime + } + } +} + +fun ImageView.load( + @DrawableRes placeHolder: Int = -1, + url: String +) { + if (placeHolder == -1) + Glide.with(context) + .load(url) + .into(this) + else + Glide.with(context) + .load(url) + .placeholder(placeHolder) + .into(this) +} + +class ItemDiffCallback( + val onItemsTheSame: (T, T) -> Boolean, + val onContentsTheSame: (T, T) -> Boolean +) : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: T, newItem: T + ): Boolean = onItemsTheSame(oldItem, newItem) + + override fun areContentsTheSame( + oldItem: T, newItem: T + ): Boolean = onContentsTheSame(oldItem, newItem) +} diff --git a/core/src/test/java/com/teamfillin/fillin/core/ExampleUnitTest.kt b/core/src/test/java/com/teamfillin/fillin/core/ExampleUnitTest.kt new file mode 100644 index 0000000..d72cfd7 --- /dev/null +++ b/core/src/test/java/com/teamfillin/fillin/core/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.teamfillin.fillin.core + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 86d389f..a64e221 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,5 @@ org.gradle.parallel=true org.gradle.daemon=true org.gradle.caching=true android.useAndroidX=true -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official +android.enableJetifier=true \ No newline at end of file diff --git a/keystore/debug.keystore b/keystore/debug.keystore new file mode 100644 index 0000000..4c6f183 Binary files /dev/null and b/keystore/debug.keystore differ diff --git a/settings.gradle b/settings.gradle index f9db699..9e87114 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,8 +3,11 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/'} + maven { url 'https://naver.jfrog.io/artifactory/maven/' } jcenter() // Warning: this repository is going to shut down soon } } rootProject.name = "FillInAndroid" include ':app' +include ':core'