diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..995c43e
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+Dico Story
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..7643783
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..61a9130
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 0000000..6bcf72c
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..5227d61
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..e34606c
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..abd6de5
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..97205fe
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..e286393
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,79 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+}
+
+android {
+ compileSdkVersion 31
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ applicationId "com.aprianto.dicostory"
+ minSdkVersion 21
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation 'androidx.core:core-ktx:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+
+ implementation 'androidx.annotation:annotation:1.3.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ testImplementation 'junit:junit:4.+'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+ // layout resources
+ implementation "com.airbnb.android:lottie:5.0.3"
+ implementation 'com.github.bumptech.glide:glide:4.11.0'
+ implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
+
+ // retrofit2 API library
+ implementation 'com.squareup.retrofit2:retrofit:2.9.0'
+ implementation "com.squareup.retrofit2:converter-gson:2.9.0"
+ implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"
+
+ // Android Jetpack library
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
+ implementation "androidx.activity:activity-ktx:1.4.0"
+ implementation "androidx.fragment:fragment-ktx:1.4.1"
+ implementation 'androidx.preference:preference:1.1.1'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ implementation "androidx.datastore:datastore-preferences:1.0.0"
+
+ // camera X
+ def camerax_version = "1.1.0-beta02"
+ implementation "androidx.camera:camera-camera2:${camerax_version}"
+ implementation "androidx.camera:camera-lifecycle:${camerax_version}"
+ implementation "androidx.camera:camera-view:${camerax_version}"
+
+
+
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/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/app/src/androidTest/java/com/aprianto/dicostory/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/aprianto/dicostory/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..daa9bd0
--- /dev/null
+++ b/app/src/androidTest/java/com/aprianto/dicostory/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.aprianto.dicostory
+
+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.aprianto.dicostory", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..23bfe79
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000..74403fc
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/com/aprianto/dicostory/data/model/Login.kt b/app/src/main/java/com/aprianto/dicostory/data/model/Login.kt
new file mode 100644
index 0000000..0ca4e7d
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/model/Login.kt
@@ -0,0 +1,15 @@
+package com.aprianto.dicostory.data.model
+
+import com.google.gson.annotations.SerializedName
+
+data class Login(
+
+ @field:SerializedName("loginResult")
+ val loginResult: User,
+
+ @field:SerializedName("error")
+ val error: Boolean,
+
+ @field:SerializedName("message")
+ val message: String
+)
diff --git a/app/src/main/java/com/aprianto/dicostory/data/model/Register.kt b/app/src/main/java/com/aprianto/dicostory/data/model/Register.kt
new file mode 100644
index 0000000..5adff91
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/model/Register.kt
@@ -0,0 +1,12 @@
+package com.aprianto.dicostory.data.model
+
+import com.google.gson.annotations.SerializedName
+
+data class Register(
+
+ @field:SerializedName("error")
+ val error: Boolean,
+
+ @field:SerializedName("message")
+ val message: String
+)
diff --git a/app/src/main/java/com/aprianto/dicostory/data/model/Story.kt b/app/src/main/java/com/aprianto/dicostory/data/model/Story.kt
new file mode 100644
index 0000000..66b393c
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/model/Story.kt
@@ -0,0 +1,29 @@
+package com.aprianto.dicostory.data.model
+
+import com.google.gson.annotations.SerializedName
+
+data class Story(
+
+ @field:SerializedName("photoUrl")
+ val photoUrl: String,
+
+ @field:SerializedName("createdAt")
+ val createdAt: String,
+
+ @field:SerializedName("name")
+ val name: String,
+
+ @field:SerializedName("description")
+ val description: String,
+
+ @field:SerializedName("lon")
+ val lon: Any,
+
+ @field:SerializedName("id")
+ val id: String,
+
+ @field:SerializedName("lat")
+ val lat: Any
+
+
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/data/model/StoryList.kt b/app/src/main/java/com/aprianto/dicostory/data/model/StoryList.kt
new file mode 100644
index 0000000..6b1bf0a
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/model/StoryList.kt
@@ -0,0 +1,15 @@
+package com.aprianto.dicostory.data.model
+
+import com.google.gson.annotations.SerializedName
+
+data class StoryList(
+
+ @field:SerializedName("listStory")
+ val listStory: List,
+
+ @field:SerializedName("error")
+ val error: Boolean,
+
+ @field:SerializedName("message")
+ val message: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/data/model/StoryUpload.kt b/app/src/main/java/com/aprianto/dicostory/data/model/StoryUpload.kt
new file mode 100644
index 0000000..91dfa71
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/model/StoryUpload.kt
@@ -0,0 +1,11 @@
+package com.aprianto.dicostory.data.model
+
+import com.google.gson.annotations.SerializedName
+
+data class StoryUpload(
+ @field:SerializedName("error")
+ val error: Boolean,
+
+ @field:SerializedName("message")
+ val message: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/data/model/User.kt b/app/src/main/java/com/aprianto/dicostory/data/model/User.kt
new file mode 100644
index 0000000..0abf816
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/model/User.kt
@@ -0,0 +1,14 @@
+package com.aprianto.dicostory.data.model
+
+import com.google.gson.annotations.SerializedName
+
+data class User(
+ @field:SerializedName("name")
+ val name: String,
+
+ @field:SerializedName("userId")
+ val userId: String,
+
+ @field:SerializedName("token")
+ val token: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/data/repository/remote/ApiConfig.kt b/app/src/main/java/com/aprianto/dicostory/data/repository/remote/ApiConfig.kt
new file mode 100644
index 0000000..b21570e
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/repository/remote/ApiConfig.kt
@@ -0,0 +1,28 @@
+package com.aprianto.dicostory.data.repository.remote
+
+import androidx.viewbinding.BuildConfig
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+
+class ApiConfig {
+ companion object{
+ fun getApiService(): ApiService {
+ val loggingInterceptor = if(BuildConfig.DEBUG) {
+ HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
+ } else {
+ HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
+ }
+ val client = OkHttpClient.Builder()
+ .addInterceptor(loggingInterceptor)
+ .build()
+ val retrofit = Retrofit.Builder()
+ .baseUrl("https://story-api.dicoding.dev/v1/")
+ .addConverterFactory(GsonConverterFactory.create())
+ .client(client)
+ .build()
+ return retrofit.create(ApiService::class.java)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/data/repository/remote/ApiService.kt b/app/src/main/java/com/aprianto/dicostory/data/repository/remote/ApiService.kt
new file mode 100644
index 0000000..707221b
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/repository/remote/ApiService.kt
@@ -0,0 +1,42 @@
+package com.aprianto.dicostory.data.repository.remote
+
+import com.aprianto.dicostory.data.model.Login
+import com.aprianto.dicostory.data.model.Register
+import com.aprianto.dicostory.data.model.StoryList
+import com.aprianto.dicostory.data.model.StoryUpload
+import okhttp3.MultipartBody
+import okhttp3.RequestBody
+import retrofit2.Call
+import retrofit2.http.*
+
+
+interface ApiService {
+ @POST("login")
+ @FormUrlEncoded
+ fun doLogin(
+ @Field("email") email: String,
+ @Field("password") password: String
+ ): Call
+
+ @POST("register")
+ @FormUrlEncoded
+ fun doRegister(
+ @Field("name") name: String,
+ @Field("email") email: String,
+ @Field("password") password: String
+ ): Call
+
+ @GET("stories")
+ fun getStoryList(
+ @Header("Authorization") token:String,
+ @Query("size") size:Int
+ ): Call
+
+ @Multipart
+ @POST("stories")
+ fun doUploadImage(
+ @Header("Authorization") token:String,
+ @Part file: MultipartBody.Part,
+ @Part("description") description: RequestBody,
+ ): Call
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/data/viewmodel/AuthViewModel.kt b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/AuthViewModel.kt
new file mode 100644
index 0000000..bf95269
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/AuthViewModel.kt
@@ -0,0 +1,74 @@
+package com.aprianto.dicostory.data.viewmodel
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.aprianto.dicostory.R
+import com.aprianto.dicostory.data.model.Login
+import com.aprianto.dicostory.data.model.Register
+import com.aprianto.dicostory.data.repository.remote.ApiConfig
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+
+
+class AuthViewModel(val context: Context) : ViewModel() {
+
+ var loading = MutableLiveData(View.GONE)
+ val error = MutableLiveData("")
+ val tempEmail = MutableLiveData("") // hold email to saved with user preferences
+
+ /* for handle API response */
+ val loginResult = MutableLiveData()
+ val registerResult = MutableLiveData()
+
+ private val TAG = AuthViewModel::class.simpleName
+
+ fun login(email: String, password: String) {
+ tempEmail.postValue(email) // temporary hold email for save user preferences
+ loading.postValue(View.VISIBLE)
+ val client = ApiConfig.getApiService().doLogin(email, password)
+ client.enqueue(object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ // parsing manual error code API
+ when (response.code()) {
+ 400 -> error.postValue(context.getString(R.string.API_error_email_invalid))
+ 401 -> error.postValue(context.getString(R.string.API_error_unauthorized))
+ 200 -> loginResult.postValue(response.body())
+ else -> error.postValue("ERROR ${response.code()} : ${response.message()}")
+ }
+ loading.postValue(View.GONE)
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ loading.postValue(View.GONE)
+ Log.e(TAG, "onFailure Call: ${t.message}")
+ error.postValue(t.message)
+ }
+ })
+ }
+
+ fun register(name: String, email: String, password: String) {
+ loading.postValue(View.VISIBLE)
+ val client = ApiConfig.getApiService().doRegister(name, email, password)
+ client.enqueue(object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ // parsing manual error code API
+ when (response.code()) {
+ 400 -> error.postValue(context.getString(R.string.API_error_email_invalid))
+ 201 -> registerResult.postValue(response.body())
+ else -> error.postValue("ERROR ${response.code()} : ${response.errorBody()}")
+ }
+ loading.postValue(View.GONE)
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ loading.postValue(View.GONE)
+ Log.e(TAG, "onFailure Call: ${t.message}")
+ error.postValue(t.message)
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/data/viewmodel/SettingViewModel.kt b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/SettingViewModel.kt
new file mode 100644
index 0000000..9b58ad9
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/SettingViewModel.kt
@@ -0,0 +1,38 @@
+package com.aprianto.dicostory.data.viewmodel
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.asLiveData
+import androidx.lifecycle.viewModelScope
+import com.aprianto.dicostory.utils.Constanta
+import com.aprianto.dicostory.utils.SettingPreferences
+import kotlinx.coroutines.launch
+
+class SettingViewModel(private val pref: SettingPreferences): ViewModel() {
+
+ // simplify single invocation of preferences with property params
+ fun getUserPreferences(property:String): LiveData {
+ return when(property){
+ Constanta.UserPreferences.UserUID.name -> pref.getUserUid().asLiveData()
+ Constanta.UserPreferences.UserToken.name -> pref.getUserToken().asLiveData()
+ Constanta.UserPreferences.UserName.name -> pref.getUserName().asLiveData()
+ Constanta.UserPreferences.UserEmail.name -> pref.getUserEmail().asLiveData()
+ Constanta.UserPreferences.UserLastLogin.name -> pref.getUserLastLogin().asLiveData()
+ else -> pref.getUserUid().asLiveData()
+ }
+ }
+
+ fun setUserPreferences(userToken: String, userUid: String, userName:String, userEmail: String) {
+ viewModelScope.launch {
+ pref.saveLoginSession(userToken,userUid,userName,userEmail)
+ }
+ }
+
+ fun clearUserPreferences() {
+ viewModelScope.launch {
+ pref.clearLoginSession()
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/data/viewmodel/StoryViewModel.kt b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/StoryViewModel.kt
new file mode 100644
index 0000000..396e154
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/StoryViewModel.kt
@@ -0,0 +1,82 @@
+package com.aprianto.dicostory.data.viewmodel
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import androidx.lifecycle.*
+import com.aprianto.dicostory.R
+import com.aprianto.dicostory.data.model.Story
+import com.aprianto.dicostory.data.model.StoryList
+import com.aprianto.dicostory.data.model.StoryUpload
+import com.aprianto.dicostory.data.repository.remote.ApiConfig
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.MultipartBody
+import okhttp3.RequestBody.Companion.asRequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import java.io.File
+
+class StoryViewModel(val context: Context) : ViewModel() {
+ var loading = MutableLiveData(View.GONE)
+ var isSuccessUploadStory = MutableLiveData(false)
+ val storyList = MutableLiveData>()
+ var error = MutableLiveData("")
+ private val TAG = StoryViewModel::class.simpleName
+
+ fun loadStoryData(token: String) {
+ loading.postValue(View.VISIBLE)
+ val client = ApiConfig.getApiService().getStoryList(token, 30)
+ client.enqueue(object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ if (response.isSuccessful) {
+ storyList.postValue(response.body()?.listStory)
+ } else {
+ error.postValue("ERROR ${response.code()} : ${response.message()}")
+ }
+ loading.postValue(View.GONE)
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ loading.postValue(View.GONE)
+ Log.e(TAG, "onFailure Call: ${t.message}")
+ error.postValue("${context.getString(R.string.API_error_fetch_data)} : ${t.message}")
+ }
+ })
+ }
+
+ fun uploadNewStory(token: String, image: File, description: String) {
+ loading.postValue(View.VISIBLE)
+ "${image.length() / 1024 / 1024} MB" // manual parse from bytes to Mega Bytes
+ val storyDescription = description.toRequestBody("text/plain".toMediaType())
+ val requestImageFile = image.asRequestBody("image/jpeg".toMediaTypeOrNull())
+ val imageMultipart: MultipartBody.Part = MultipartBody.Part.createFormData(
+ "photo",
+ image.name,
+ requestImageFile
+ )
+ val client =
+ ApiConfig.getApiService().doUploadImage(token, imageMultipart, storyDescription)
+ client.enqueue(object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ when (response.code()) {
+ 401 -> error.postValue(context.getString(R.string.API_error_header_token))
+ 413 -> error.postValue(context.getString(R.string.API_error_large_payload))
+ 201 -> isSuccessUploadStory.postValue(true)
+ else -> error.postValue("Error ${response.code()} : ${response.message()}")
+ }
+ loading.postValue(View.GONE)
+ }
+
+ override fun onFailure(call: Call, t: Throwable) {
+ loading.postValue(View.GONE)
+ Log.e(TAG, "onFailure Call: ${t.message}")
+ error.postValue("${context.getString(R.string.API_error_send_payload)} : ${t.message}")
+ }
+ })
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/data/viewmodel/ViewModelGeneralFactory.kt b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/ViewModelGeneralFactory.kt
new file mode 100644
index 0000000..488b148
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/ViewModelGeneralFactory.kt
@@ -0,0 +1,19 @@
+package com.aprianto.dicostory.data.viewmodel
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+
+class ViewModelGeneralFactory(private val context: Context) :
+ ViewModelProvider.NewInstanceFactory() {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(AuthViewModel::class.java)) {
+ return AuthViewModel(context) as T
+ } else if (modelClass.isAssignableFrom(StoryViewModel::class.java)) {
+ return StoryViewModel(context) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class: " + modelClass.name)
+ }
+}
diff --git a/app/src/main/java/com/aprianto/dicostory/data/viewmodel/ViewModelSettingFactory.kt b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/ViewModelSettingFactory.kt
new file mode 100644
index 0000000..2f2b75d
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/data/viewmodel/ViewModelSettingFactory.kt
@@ -0,0 +1,17 @@
+package com.aprianto.dicostory.data.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.aprianto.dicostory.utils.SettingPreferences
+
+class ViewModelSettingFactory(private val pref: SettingPreferences) :
+ ViewModelProvider.NewInstanceFactory() {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(SettingViewModel::class.java)) {
+ return SettingViewModel(pref) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class: " + modelClass.name)
+ }
+}
diff --git a/app/src/main/java/com/aprianto/dicostory/ui/auth/AuthActivity.kt b/app/src/main/java/com/aprianto/dicostory/ui/auth/AuthActivity.kt
new file mode 100644
index 0000000..18ee1fd
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/ui/auth/AuthActivity.kt
@@ -0,0 +1,42 @@
+package com.aprianto.dicostory.ui.auth
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.appcompat.app.AppCompatActivity
+import com.aprianto.dicostory.R
+import com.aprianto.dicostory.databinding.ActivityAuthBinding
+import com.aprianto.dicostory.ui.dashboard.MainActivity
+
+class AuthActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityAuthBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityAuthBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ @Suppress("DEPRECATION")
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
+ )
+ if (savedInstanceState == null) {
+ supportFragmentManager
+ .beginTransaction()
+ .add(R.id.container, LoginFragment.newInstance())
+ .commit()
+ }
+ }
+
+ fun routeToMainActivity() {
+ startActivity(Intent(this@AuthActivity, MainActivity::class.java))
+ }
+
+ override fun onBackPressed() {
+ super.onBackPressed()
+ finishAffinity()
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/ui/auth/LoginFragment.kt b/app/src/main/java/com/aprianto/dicostory/ui/auth/LoginFragment.kt
new file mode 100644
index 0000000..96ad0fc
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/ui/auth/LoginFragment.kt
@@ -0,0 +1,119 @@
+package com.aprianto.dicostory.ui.auth
+
+import android.os.Bundle
+import android.transition.TransitionInflater
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import com.aprianto.dicostory.R
+import com.aprianto.dicostory.data.viewmodel.AuthViewModel
+import com.aprianto.dicostory.data.viewmodel.SettingViewModel
+import com.aprianto.dicostory.data.viewmodel.ViewModelGeneralFactory
+import com.aprianto.dicostory.data.viewmodel.ViewModelSettingFactory
+import com.aprianto.dicostory.databinding.FragmentLoginBinding
+import com.aprianto.dicostory.utils.Constanta
+import com.aprianto.dicostory.utils.Helper
+import com.aprianto.dicostory.utils.SettingPreferences
+import com.aprianto.dicostory.utils.dataStore
+
+
+class LoginFragment : Fragment() {
+ companion object {
+ fun newInstance() = LoginFragment()
+ }
+
+ private var viewModel: AuthViewModel? = null
+ private lateinit var binding: FragmentLoginBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val animation = TransitionInflater.from(requireContext())
+ .inflateTransition(android.R.transition.move)
+ sharedElementEnterTransition = animation
+ sharedElementReturnTransition = animation
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentLoginBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val pref = SettingPreferences.getInstance((activity as AuthActivity).dataStore)
+ val settingViewModel =
+ ViewModelProvider(this, ViewModelSettingFactory(pref))[SettingViewModel::class.java]
+ viewModel = ViewModelProvider(
+ this,
+ ViewModelGeneralFactory((activity as AuthActivity))
+ )[AuthViewModel::class.java]
+ viewModel?.let { vm ->
+ vm.loginResult.observe(viewLifecycleOwner) { login ->
+ // success login process triggered -> save preferences
+ settingViewModel.setUserPreferences(
+ login.loginResult.token,
+ login.loginResult.userId,
+ login.loginResult.name,
+ viewModel!!.tempEmail.value ?: Constanta.preferenceDefaultValue
+ )
+ }
+ vm.error.observe(viewLifecycleOwner) { error ->
+ if (error.isNotEmpty()) {
+ Helper.showDialogInfo(requireContext(), error)
+ }
+ }
+ vm.loading.observe(viewLifecycleOwner) { state ->
+ binding.loading.root.visibility = state
+ }
+ }
+ settingViewModel.getUserPreferences(Constanta.UserPreferences.UserToken.name)
+ .observe(viewLifecycleOwner) { token ->
+ // token changes -> redirect to Main Activity
+ if (token != Constanta.preferenceDefaultValue) (activity as AuthActivity).routeToMainActivity()
+ }
+ binding.btnAction.setOnClickListener {
+ val email = binding.edEmail.text.toString()
+ val password = binding.edPassword.text.toString()
+ when {
+ email.isEmpty() or password.isEmpty() -> {
+ Helper.showDialogInfo(
+ requireContext(),
+ getString(R.string.UI_validation_empty_email_password)
+ )
+ }
+ !email.matches(Constanta.emailPattern) -> {
+ Helper.showDialogInfo(
+ requireContext(),
+ getString(R.string.UI_validation_invalid_email)
+ )
+ }
+ password.length <= 6 -> {
+ Helper.showDialogInfo(
+ requireContext(),
+ getString(R.string.UI_validation_password_rules)
+ )
+ }
+ else -> {
+ viewModel?.login(email, password)
+ }
+ }
+ }
+ binding.btnRegister.setOnClickListener {
+ parentFragmentManager.beginTransaction().apply {
+ replace(R.id.container, RegisterFragment(), RegisterFragment::class.java.simpleName)
+ /* shared element transition to main activity */
+ addSharedElement(binding.labelAuth, "auth")
+ addSharedElement(binding.edEmail, "email")
+ addSharedElement(binding.edPassword, "password")
+ addSharedElement(binding.containerMisc, "misc")
+ commit()
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aprianto/dicostory/ui/auth/RegisterFragment.kt b/app/src/main/java/com/aprianto/dicostory/ui/auth/RegisterFragment.kt
new file mode 100644
index 0000000..95945bf
--- /dev/null
+++ b/app/src/main/java/com/aprianto/dicostory/ui/auth/RegisterFragment.kt
@@ -0,0 +1,114 @@
+package com.aprianto.dicostory.ui.auth
+
+import android.os.Bundle
+import android.transition.TransitionInflater
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import com.aprianto.dicostory.R
+import com.aprianto.dicostory.data.viewmodel.AuthViewModel
+import com.aprianto.dicostory.data.viewmodel.ViewModelGeneralFactory
+import com.aprianto.dicostory.databinding.FragmentRegisterBinding
+import com.aprianto.dicostory.utils.Constanta
+import com.aprianto.dicostory.utils.Helper
+
+class RegisterFragment : Fragment() {
+
+ private var viewModel: AuthViewModel? = null
+ private lateinit var binding: FragmentRegisterBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val animation = TransitionInflater.from(requireContext())
+ .inflateTransition(android.R.transition.move)
+ sharedElementEnterTransition = animation
+ sharedElementReturnTransition = animation
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentRegisterBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel = ViewModelProvider(
+ this,
+ ViewModelGeneralFactory((activity as AuthActivity))
+ )[AuthViewModel::class.java]
+ viewModel?.let { vm ->
+ vm.registerResult.observe(viewLifecycleOwner) { register ->
+ if (!register.error) {
+ val dialog = Helper.dialogInfoBuilder(
+ (activity as AuthActivity),
+ getString(R.string.UI_info_successful_register_user)
+ )
+ val btnOk = dialog.findViewById